From da5fdf3c3e3cfa04405b73de9aee6ae6487650fd Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Oct 2026 02:48:00 +0530 Subject: [PATCH 1/8] Fix Connections regressions: Manage settings route, suggest link to Concierge, disconnect flash, and Back after refresh on settings pages Signed-off-by: krishna2323 --- .../WorkspaceMoreFeaturesPage/index.tsx | 3 ++- .../advanced/BusinessCentralAdvancedPage.tsx | 4 ++++ .../export/BusinessCentralExportPage.tsx | 4 ++++ .../import/BusinessCentralImportPage.tsx | 5 +++++ .../advanced/CampfireAdvancedPage.tsx | 4 ++++ .../campfire/export/CampfireExportPage.tsx | 4 ++++ .../campfire/import/CampfireImportPage.tsx | 5 +++++ .../certinia/import/CertiniaImportPage.tsx | 4 ++++ .../advanced/DualEntryAdvancedPage.tsx | 4 ++++ .../dualentry/export/DualEntryExportPage.tsx | 4 ++++ .../dualentry/import/DualEntryImportPage.tsx | 5 +++++ .../advanced/SageIntacctAdvancedPage.tsx | 5 ++++- .../intacct/import/SageIntacctImportPage.tsx | 4 ++++ .../advanced/NetSuiteAdvancedPage.tsx | 4 ++++ .../netsuite/import/NetSuiteImportPage.tsx | 4 ++++ .../import/QuickbooksDesktopImportPage.tsx | 5 ++++- .../qbo/advanced/QuickbooksAdvancedPage.tsx | 4 ++++ .../qbo/import/QuickbooksImportPage.tsx | 4 ++++ .../reconciliation/CardReconciliationPage.tsx | 8 ++++++- .../rillet/advanced/RilletAdvancedPage.tsx | 4 ++++ .../rillet/export/RilletExportPage.tsx | 4 ++++ .../rillet/import/RilletImportPage.tsx | 5 +++++ .../useConnectedAccountingIntegration.tsx | 12 +++++++---- .../accounting/xero/XeroImportPage.tsx | 4 ++++ .../xero/advanced/XeroAdvancedPage.tsx | 4 ++++ .../connections/ConnectionsAccountingPage.tsx | 2 +- .../connections/ConnectionsMergePageBase.tsx | 2 +- .../ConnectionsMergeProviderCard.tsx | 4 ++-- .../ConnectionsReceiptPartnersPage.tsx | 2 +- .../connections/WorkspaceConnectionsPage.tsx | 21 +------------------ src/pages/workspace/connections/utils.ts | 2 +- .../merge/useMergeProviderCardDetails.tsx | 12 +++++++---- .../useReceiptPartnersSettings.ts | 12 +++++++---- 33 files changed, 132 insertions(+), 42 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 53afa54bbcea..28079f9de2bb 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -53,6 +53,7 @@ import { import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -218,7 +219,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + Navigation.navigate(getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); }; const warnVendorsManagedByAccounting = async () => { diff --git a/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx b/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx index e91c73e7e4ed..da34005bf9be 100644 --- a/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx @@ -6,6 +6,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,6 +23,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -39,6 +41,7 @@ function BusinessCentralAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const bankAccounts = policy?.connections?.businessCentral?.data?.bankAccounts; const autoSync = businessCentralConfig?.autoSync?.enabled ?? false; @@ -61,6 +64,7 @@ function BusinessCentralAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="BusinessCentralAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx b/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx index 9265ce487c2c..aea10b2bdf2d 100644 --- a/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx @@ -4,12 +4,14 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; @@ -33,6 +35,7 @@ function BusinessCentralExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const businessCentralData = policy?.connections?.businessCentral?.data; const exportConfig = businessCentralConfig?.export; @@ -116,6 +119,7 @@ function BusinessCentralExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="BusinessCentralExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx b/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx index e9410ef2b8f9..423b0ae41ebb 100644 --- a/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx @@ -6,6 +6,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,8 +19,10 @@ import { updateBusinessCentralSyncTaxRates, } from '@libs/actions/connections/BusinessCentral'; import {getLatestErrorField} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -75,6 +78,7 @@ function BusinessCentralImportPage({policy}: WithPolicyConnectionsProps) { const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const businessCentralData = policy?.connections?.businessCentral?.data; @@ -104,6 +108,7 @@ function BusinessCentralImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="BusinessCentralImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx b/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx index 4f58b2557a22..cd6f91e9afef 100644 --- a/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx +++ b/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx @@ -5,6 +5,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -23,6 +24,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -38,6 +40,7 @@ function CampfireAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const campfireConfig = policy?.connections?.campfire?.config; const campfireData = policy?.connections?.campfire?.data; const autoSync = campfireConfig?.autoSync?.enabled ?? false; @@ -66,6 +69,7 @@ function CampfireAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CampfireAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx b/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx index 08423428d65d..1a6ad429d275 100644 --- a/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx +++ b/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx @@ -9,6 +9,7 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,6 +19,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -32,6 +34,7 @@ function CampfireExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -57,6 +60,7 @@ function CampfireExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CampfireExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx b/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx index 87422746ba73..e41ffb7b4c23 100644 --- a/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx +++ b/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx @@ -1,13 +1,16 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearCampfireErrorField, updateCampfireEnableNewCategories, updateCampfireFieldMapping, updateCampfireSyncTaxRates} from '@libs/actions/connections/Campfire'; import {getLatestErrorField} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -21,6 +24,7 @@ function CampfireImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const campfireConfig = policy?.connections?.campfire?.config; const campfireData = policy?.connections?.campfire?.data; const enableNewCategories = campfireConfig?.enableNewCategories ?? false; @@ -30,6 +34,7 @@ function CampfireImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CampfireImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx b/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx index 123cfa2ca67f..a2d6db80522f 100644 --- a/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx +++ b/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,6 +13,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {CERTINIA_DIMENSION_PARAMS, getDimensionLabel, getDisplayTypeLabel, getParentTagMappingLabel} from '@pages/workspace/accounting/certinia/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -26,12 +28,14 @@ function CertiniaImportPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.CERTINIA]?.config; const coding = config?.coding; const hasPSA = !!config?.hasPSA; return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CertiniaImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx b/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx index 8f9f6967e2fd..3fe2c35e460f 100644 --- a/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx +++ b/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx @@ -7,6 +7,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -25,6 +26,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -42,6 +44,7 @@ function DualEntryAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const dualentryConfig = policy?.connections?.dualEntry?.config; const dualentryData = policy?.connections?.dualEntry?.data; const autoSync = dualentryConfig?.autoSync?.enabled ?? false; @@ -72,6 +75,7 @@ function DualEntryAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="DualEntryAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx b/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx index ef836006d27b..79f3970f9014 100644 --- a/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx +++ b/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx @@ -8,6 +8,7 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,6 +18,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -31,6 +33,7 @@ function DualEntryExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -56,6 +59,7 @@ function DualEntryExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="DualEntryExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx b/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx index 1ae94ff7499b..f0c78422b749 100644 --- a/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx +++ b/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx @@ -1,13 +1,16 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDualEntryErrorField, updateDualEntryEnableNewCategories, updateDualEntryFieldMapping, updateDualEntrySyncTaxRates} from '@libs/actions/connections/DualEntry'; import {getLatestErrorField} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -21,6 +24,7 @@ function DualEntryImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const dualentryConfig = policy?.connections?.dualEntry?.config; const dualentryData = policy?.connections?.dualEntry?.data; const enableNewCategories = dualentryConfig?.enableNewCategories ?? false; @@ -29,6 +33,7 @@ function DualEntryImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="DualEntryImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx index 61e9cc8b3939..d739c638074c 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -6,6 +6,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendi import createDynamicRoute from '@navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@navigation/Navigation'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -44,6 +46,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const styles = useThemeStyles(); const {importEmployees, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {}; @@ -108,7 +111,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { contentContainerStyle={styles.pb2} titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} - onBackButtonPress={() => Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} > Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_AUTO_SYNC.path)))}> diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx index 3623440c9bbf..4c99a0f0a93e 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +12,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -51,6 +53,7 @@ function SageIntacctImportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const sageIntacctConfig = policy?.connections?.intacct?.config; const sageIntacctData = policy?.connections?.intacct?.data; @@ -72,6 +75,7 @@ function SageIntacctImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="SageIntacctImportPage" headerTitle="workspace.accounting.import" headerSubtitle={getCurrentSageIntacctEntityName(policy, translate('workspace.common.topLevel'))} diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx index 9c91f4f919f8..5c668f21de14 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx @@ -5,6 +5,7 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -30,6 +31,7 @@ import { shouldHideReimbursedReportsSection, shouldHideReportsExportTo, } from '@pages/workspace/accounting/netsuite/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -50,6 +52,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id ?? CONST.DEFAULT_NUMBER_ID.toString(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.netsuite?.options?.config; const autoSyncConfig = policy?.connections?.netsuite?.config; @@ -284,6 +287,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="NetSuiteAdvancedPage" headerTitle="workspace.accounting.advanced" headerSubtitle={config?.subsidiary ?? ''} diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx index e2b4880f73f9..15272dcdecf5 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx @@ -4,6 +4,7 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import {canUseTaxNetSuite, getCustomersOrJobsLabelNetSuite, getNetSuiteImportCus import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getImportCustomFieldsSettings} from '@pages/workspace/accounting/netsuite/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -32,12 +34,14 @@ function NetSuiteImportPage({policy}: WithPolicyConnectionsProps) { const {isBetaEnabled} = usePermissions(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.netsuite?.options?.config; const {subsidiaryList} = policy?.connections?.netsuite?.options?.data ?? {}; const selectedSubsidiary = useMemo(() => (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID), [subsidiaryList, config?.subsidiaryID]); return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="NetSuiteImportPage" headerTitle="workspace.accounting.import" headerSubtitle={config?.subsidiary ?? ''} diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx index 080d2da301b3..c4b5c44c6cf4 100644 --- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx +++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,6 +11,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -29,6 +31,7 @@ function QuickbooksDesktopImportPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {mappings, pendingFields, errorFields} = policy?.connections?.quickbooksDesktop?.config ?? {}; const sections: QBDSectionType[] = [ @@ -68,7 +71,7 @@ function QuickbooksDesktopImportPage({policy}: WithPolicyProps) { contentContainerStyle={styles.pb2} titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD} - onBackButtonPress={() => Navigation.goBack()} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} > {sections.map((section) => ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="QuickbooksAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx index a8b8595c95bb..8e0e488a99a9 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import {shouldSwitchLocationsToReportFields} from '@pages/workspace/accounting/qbo/utils'; import {getQuickbooksOnlineIntegrationName, isIntuitEnterpriseSuiteConnection} from '@pages/workspace/accounting/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -34,6 +36,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { const integrationName = getQuickbooksOnlineIntegrationName(policy, translate); const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const qboConfig = policy?.connections?.quickbooksOnline?.config; const {syncClasses, syncCustomers, syncLocations, syncItems, syncTax, syncCustomDimensions, pendingFields, errorFields} = qboConfig ?? {}; const isIntuitEnterpriseSuite = isIntuitEnterpriseSuiteConnection(policy); @@ -106,6 +109,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="QuickbooksImportPage" headerTitle="workspace.accounting.import" title="workspace.qbo.importDescription" diff --git a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx index e6cb49ff4fb3..5f1fd43c5d23 100644 --- a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx @@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useEnvironment from '@hooks/useEnvironment'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReconciliationFundID from '@hooks/useReconciliationFundID'; @@ -25,6 +26,7 @@ import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -53,6 +55,7 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) { const {translate} = useLocalize(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {environmentURL} = useEnvironment(); // Continuous Reconciliation is configured per card feed, and this workspace can sit on more than one: its own @@ -145,7 +148,10 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) { shouldEnableMaxHeight testID="CardReconciliationPage" > - + Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + /> Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="RilletAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx b/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx index a4ad95b60ae4..4f2182c02874 100644 --- a/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx +++ b/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx @@ -8,6 +8,7 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,6 +18,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -32,6 +34,7 @@ function RilletExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -56,6 +59,7 @@ function RilletExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="RilletExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx index 8d88050ba609..a246e41caa3d 100644 --- a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx +++ b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx @@ -1,13 +1,16 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearRilletErrorField, updateRilletEnableNewCategories, updateRilletFieldMapping, updateRilletSyncTaxRates} from '@libs/actions/connections/Rillet'; import {getLatestErrorField} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -21,6 +24,7 @@ function RilletImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const rilletConfig = policy?.connections?.rillet?.config; const rilletData = policy?.connections?.rillet?.data; const enableNewCategories = rilletConfig?.enableNewCategories ?? false; @@ -29,6 +33,7 @@ function RilletImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="RilletImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx index 8b27816e7eb4..c0377f33adb3 100644 --- a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx +++ b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx @@ -64,8 +64,8 @@ type UseConnectedAccountingIntegrationOptions = { /** Style applied to the settings rows, which the Accounting page lays out as cards */ menuItemWrapperStyle?: StyleProp; - /** Runs once the user confirms disconnecting, before the connection is removed */ - onDisconnectConfirmed?: () => void; + /** Runs once the user confirms disconnecting and takes over calling `disconnect`, so the panel can close before the connection is removed */ + onDisconnectConfirmed?: (disconnect: () => void) => void; }; /** @@ -236,8 +236,12 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW if (action !== ModalActions.CONFIRM || !connectedIntegration || !policyID) { return; } - onDisconnectConfirmed?.(); - removePolicyConnection(policy, connectedIntegration); + const disconnect = () => removePolicyConnection(policy, connectedIntegration); + if (onDisconnectConfirmed) { + onDisconnectConfirmed(disconnect); + return; + } + disconnect(); }); }, shouldCallAfterModalHide: true, diff --git a/src/pages/workspace/accounting/xero/XeroImportPage.tsx b/src/pages/workspace/accounting/xero/XeroImportPage.tsx index 819f94371c93..2480cf38eb6b 100644 --- a/src/pages/workspace/accounting/xero/XeroImportPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroImportPage.tsx @@ -4,6 +4,7 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +12,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {getCurrentXeroOrganizationName} from '@libs/PolicyUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -26,6 +28,7 @@ function XeroImportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {importCustomers, importTaxRates, importTrackingCategories, pendingFields, errorFields} = policy?.connections?.xero?.config ?? {}; const currentXeroOrganizationName = useMemo(() => getCurrentXeroOrganizationName(policy ?? undefined), [policy]); @@ -67,6 +70,7 @@ function XeroImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="XeroImportPage" headerTitle="workspace.accounting.import" headerSubtitle={currentXeroOrganizationName} diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index ab0ff226bd49..7476bd10da6a 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -7,6 +7,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getCurrentXeroOrganizationName, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -35,6 +37,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const xeroConfig = policy?.connections?.xero?.config; const {pendingFields, errorFields, sync} = xeroConfig ?? {}; const {bankAccounts, expenseAccounts} = policy?.connections?.xero?.data ?? {}; @@ -63,6 +66,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="XeroAdvancedPage" headerTitle="workspace.accounting.advanced" headerSubtitle={currentXeroOrganizationName} diff --git a/src/pages/workspace/connections/ConnectionsAccountingPage.tsx b/src/pages/workspace/connections/ConnectionsAccountingPage.tsx index a2a1a7f2bf2d..73f45a49a233 100644 --- a/src/pages/workspace/connections/ConnectionsAccountingPage.tsx +++ b/src/pages/workspace/connections/ConnectionsAccountingPage.tsx @@ -53,7 +53,7 @@ function ConnectionsAccountingPage({policy}: PolicyAccountingPageProps) { oldDotPolicyConnectionsURL, } = useConnectedAccountingIntegration(policy, { // These settings have nothing to show once the integration is gone - onDisconnectConfirmed: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), + onDisconnectConfirmed: (disconnect) => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID), {afterTransition: disconnect}), }); const connectionsMenuItems = connectionDetails?.settingsMenuItems ?? []; diff --git a/src/pages/workspace/connections/ConnectionsMergePageBase.tsx b/src/pages/workspace/connections/ConnectionsMergePageBase.tsx index 6d9d785f3bb2..77565ccd0547 100644 --- a/src/pages/workspace/connections/ConnectionsMergePageBase.tsx +++ b/src/pages/workspace/connections/ConnectionsMergePageBase.tsx @@ -114,7 +114,7 @@ function ConnectionsMergePageBaseContent({policyID, category, cards}: Connection card={connectedCard} policy={policy} handleConnect={() => handleReconnect(connectedCard)} - onDisconnect={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))} + onDisconnect={(disconnect) => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID), {afterTransition: disconnect})} onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))} canWriteMoreFeatures={canWriteMoreFeatures} showReadOnlyModal={showReadOnlyModal} diff --git a/src/pages/workspace/connections/ConnectionsMergeProviderCard.tsx b/src/pages/workspace/connections/ConnectionsMergeProviderCard.tsx index f5c53f726f37..09d904901708 100644 --- a/src/pages/workspace/connections/ConnectionsMergeProviderCard.tsx +++ b/src/pages/workspace/connections/ConnectionsMergeProviderCard.tsx @@ -35,8 +35,8 @@ type ConnectionsMergeProviderCardProps = { /** Callback invoked when the user taps the "Connect" or "Reconnect" button. */ handleConnect: () => void; - /** Called once the user confirms disconnecting, right before the connection is removed */ - onDisconnect?: () => void; + /** Called once the user confirms disconnecting and takes over calling `disconnect` */ + onDisconnect?: (disconnect: () => void) => void; /** Called when the header's back button is pressed. */ onBackButtonPress: () => void; diff --git a/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx b/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx index 5024beb810e2..56e25c7d6f53 100644 --- a/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx +++ b/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx @@ -56,7 +56,7 @@ function ConnectionsReceiptPartnersPage({route}: ConnectionsReceiptPartnersPageP getOverflowMenu, } = useReceiptPartnersSettings(policyID, { // These settings have nothing to show once Uber is disconnected - onDisconnectConfirmed: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), + onDisconnectConfirmed: (disconnect) => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID), {afterTransition: disconnect}), }); useWorkspaceDocumentTitle(policy?.name, 'workspace.common.receiptPartners'); const isLoading = policy?.isLoading; diff --git a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx index 7c1dba68a69d..e5457ecfc520 100644 --- a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx +++ b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx @@ -22,7 +22,6 @@ import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBe import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; import usePermissions from '@hooks/usePermissions'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; @@ -50,12 +49,8 @@ import variables from '@styles/variables'; import {openPolicyReceiptPartnersPage} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Account} from '@src/types/onyx'; import type {ConnectionName} from '@src/types/onyx/Policy'; -import type {OnyxEntry} from 'react-native-onyx'; - import {useFocusEffect, useRoute} from '@react-navigation/native'; import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -83,8 +78,6 @@ type MergeSetupFlow = { }; /** HR and recruiting providers are Control-only, so their data is only requested for Control workspaces */ -const accountManagerReportIDSelector = (account: OnyxEntry) => (account?.accountManagerAccountID ? account.accountManagerReportID : undefined); - function fetchConnectionsData(policyID: string | undefined, canUseMergeConnections: boolean, isRecruitingBetaEnabled: boolean) { if (!policyID) { return; @@ -111,7 +104,6 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { const [isSearchFocused, setIsSearchFocused] = useState(false); const {isBetaEnabled} = usePermissions(); const {openConciergeAnywhere} = useOpenConciergeAnywhere(); - const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT, {selector: accountManagerReportIDSelector}); const {startIntegrationFlow} = useAccountingActions(); const {canWrite: canWriteAccounting} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.ACCOUNTING); const [lastSelectedTab, setLastSelectedTab] = useState(CONST.TAB.CONNECTIONS.ALL); @@ -160,18 +152,7 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { // A tab also ends up empty once everything in it is connected const shouldShowEmptyState = !visibleListings.length; - // Suggestions go to the person who looks after this workspace, like the old Accounting page did - const openSuggestIntegration = () => { - if (policy?.chatReportIDAdmins) { - openConciergeAnywhere({reportID: String(policy.chatReportIDAdmins)}); - return; - } - if (accountManagerReportID) { - openConciergeAnywhere({reportID: accountManagerReportID}); - return; - } - openConciergeAnywhere({forceConcierge: true}); - }; + const openSuggestIntegration = () => openConciergeAnywhere({forceConcierge: true}); // Upgrade and setup flows that leave the app come back here with the integration to connect in the route params. // `startIntegrationFlow` changes identity whenever `policy` does, and the params are only cleared in a later render, diff --git a/src/pages/workspace/connections/utils.ts b/src/pages/workspace/connections/utils.ts index 225087142eaa..eba5318849b9 100644 --- a/src/pages/workspace/connections/utils.ts +++ b/src/pages/workspace/connections/utils.ts @@ -53,7 +53,7 @@ function getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled: boolean, } /** The page an accounting integration's settings open from, which is the Connections accounting panel while the unified Connections beta is on */ -function getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled: boolean, policyID: string) { +function getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled: boolean, policyID: string | undefined) { if (isUnifiedConnectionsBetaEnabled) { return ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); } diff --git a/src/pages/workspace/merge/useMergeProviderCardDetails.tsx b/src/pages/workspace/merge/useMergeProviderCardDetails.tsx index e9300582f215..831157a088a6 100644 --- a/src/pages/workspace/merge/useMergeProviderCardDetails.tsx +++ b/src/pages/workspace/merge/useMergeProviderCardDetails.tsx @@ -28,8 +28,8 @@ type UseMergeProviderCardDetailsParams = { canWriteMoreFeatures: boolean; showReadOnlyModal: () => void; - /** Runs once the user confirms disconnecting, before the connection is removed */ - onDisconnect?: () => void; + /** Runs once the user confirms disconnecting and takes over calling `disconnect`, so the panel can close before the connection is removed */ + onDisconnect?: (disconnect: () => void) => void; }; /** The status text, sync error and overflow menu of a Merge provider, shared by the HR and Recruiting cards and their Connections panel */ @@ -148,8 +148,12 @@ function useMergeProviderCardDetails({card, policy, handleConnect, canWriteMoreF if (result?.action !== ModalActions.CONFIRM || !policy) { return; } - onDisconnect?.(); - removePolicyConnection(policy, card.connectionName); + const disconnect = () => removePolicyConnection(policy, card.connectionName); + if (onDisconnect) { + onDisconnect(disconnect); + return; + } + disconnect(); }); }, shouldCallAfterModalHide: true, diff --git a/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts b/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts index 278d2114752b..8198698fb49f 100644 --- a/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts +++ b/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts @@ -19,8 +19,8 @@ import type {ValueOf} from 'type-fest'; import {useEffect} from 'react'; type UseReceiptPartnersSettingsOptions = { - /** Runs once the user confirms disconnecting, before the connection is removed */ - onDisconnectConfirmed?: () => void; + /** Runs once the user confirms disconnecting and takes over calling `disconnect`, so the panel can close before the connection is removed */ + onDisconnectConfirmed?: (disconnect: () => void) => void; }; /** State and actions for the workspace's receipt partner connections, shared by the Receipt partners page and its Connections panel */ @@ -107,8 +107,12 @@ function useReceiptPartnersSettings(policyID: string, {onDisconnectConfirmed}: U if (action !== ModalActions.CONFIRM) { return; } - onDisconnectConfirmed?.(); - disconnectPartner(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER); + const disconnect = () => disconnectPartner(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER); + if (onDisconnectConfirmed) { + onDisconnectConfirmed(disconnect); + return; + } + disconnect(); }); }, shouldCallAfterModalHide: true, From 3847d51b9cef8a978153653c391df65fc4bf0a2f Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Oct 2026 03:23:36 +0530 Subject: [PATCH 2/8] Keep the Connections page from turning into a loader when connecting an integration turns Connections on Signed-off-by: krishna2323 --- src/libs/actions/Policy/Policy.ts | 5 +++++ tests/actions/PolicyTest.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 88e9ea3ecc5b..19f30907477e 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -5160,6 +5160,11 @@ function enablePolicyFeatureForConnection(policy: OnyxEntry, featureName if (!policy?.id || PolicyUtils.isPolicyFeatureEnabled(policy, featureName)) { return; } + // A workspace can't be connected while Connections is off, so there is nothing to fetch. Without this, the open + // Connections page would be replaced by a loader. It's set first so the page never sees the feature on without it. + if (featureName === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED && isEmptyObject(policy.connections)) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policy.id}`, true); + } ENABLE_CONNECTION_FEATURE[featureName](policy.id, true, false); } diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 85482a4d1a45..d88efc22f740 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -5803,6 +5803,21 @@ describe('actions/Policy', () => { expect(apiWriteSpy).not.toHaveBeenCalled(); apiWriteSpy.mockRestore(); }); + + it('should mark the connections as fetched when it turns Connections on for a workspace with no connections', async () => { + // Given a workspace with Connections off, which means it has no connections to fetch + const policy: PolicyType = {...createRandomPolicy(20, CONST.POLICY.TYPE.CORPORATE), areConnectionsEnabled: false, connections: undefined}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + mockFetch?.pause?.(); + + // When connecting an integration from the Connections page turns Connections on + Policy.enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + await waitForBatchedUpdates(); + + // Then the open Connections page has nothing to fetch, so it keeps showing instead of a loader + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policy.id}`)).toBe(true); + expect((await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`))?.areConnectionsEnabled).toBe(true); + }); }); describe('enablePolicyTaxes', () => { From 49a85b4c0dc86e1829c154fe47766879d32df999 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Oct 2026 04:02:05 +0530 Subject: [PATCH 3/8] remove the unifiedConnections beta and the pages it replaced Signed-off-by: krishna2323 --- src/CONST/index.ts | 1 - src/ROUTES.ts | 5 + .../ConnectToSageIntacctFlow/index.tsx | 6 +- .../SearchRouter/useNavigationSuggestions.tsx | 14 - src/components/SearchBar/CompactSearchBar.tsx | 45 -- .../useIsUnifiedConnectionsBetaEnabled.ts | 11 - src/languages/de.ts | 39 +- src/languages/el.ts | 42 +- src/languages/en.ts | 41 +- src/languages/es.ts | 37 +- src/languages/fr.ts | 39 +- src/languages/it.ts | 37 +- src/languages/ja.ts | 39 +- src/languages/nl.ts | 37 +- src/languages/pl.ts | 37 +- src/languages/pt-BR.ts | 37 +- src/languages/zh-hans.ts | 37 +- .../ModalStackNavigators/index.tsx | 2 +- .../Navigators/WorkspaceSplitNavigator.tsx | 10 +- src/libs/Navigation/types.ts | 1 + src/libs/PolicyUtils/features.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Policy/Policy.ts | 2 - src/pages/workspace/WorkspaceInitialPage.tsx | 4 - .../WorkspaceMoreFeaturesPage/index.tsx | 240 +--------- .../accounting/AccountingContext/index.tsx | 45 +- .../workspace/accounting/ClaimOfferPage.tsx | 15 +- .../accounting/PolicyAccountingPage.tsx | 432 ------------------ .../advanced/BusinessCentralAdvancedPage.tsx | 5 +- .../export/BusinessCentralExportPage.tsx | 5 +- .../import/BusinessCentralImportPage.tsx | 6 +- .../advanced/CampfireAdvancedPage.tsx | 5 +- .../campfire/export/CampfireExportPage.tsx | 5 +- .../campfire/import/CampfireImportPage.tsx | 6 +- .../certinia/import/CertiniaImportPage.tsx | 5 +- .../advanced/DualEntryAdvancedPage.tsx | 5 +- .../dualentry/export/DualEntryExportPage.tsx | 5 +- .../dualentry/import/DualEntryImportPage.tsx | 6 +- .../advanced/SageIntacctAdvancedPage.tsx | 5 +- .../intacct/import/SageIntacctImportPage.tsx | 5 +- .../NetSuiteSetupPage/index.native.tsx | 7 +- .../NetSuiteExistingConnectionsPage.tsx | 7 +- .../advanced/NetSuiteAdvancedPage.tsx | 5 +- ...eProvincialTaxPostingAccountSelectPage.tsx | 5 +- .../NetSuiteTaxPostingAccountSelectPage.tsx | 5 +- ...NetSuiteTravelBillingConfigurationPage.tsx | 5 +- .../netsuite/import/NetSuiteImportPage.tsx | 5 +- .../QuickBooksDesktopSetupFlowSyncPage.tsx | 7 +- .../QuickbooksDesktopAutoSyncPageBase.tsx | 9 +- .../import/QuickbooksDesktopImportPage.tsx | 5 +- .../qbo/advanced/QuickbooksAdvancedPage.tsx | 5 +- .../qbo/import/QuickbooksImportPage.tsx | 5 +- .../reconciliation/CardReconciliationPage.tsx | 5 +- .../rillet/advanced/RilletAdvancedPage.tsx | 5 +- .../rillet/export/RilletExportPage.tsx | 5 +- .../rillet/import/RilletImportPage.tsx | 6 +- .../useConnectedAccountingIntegration.tsx | 3 - src/pages/workspace/accounting/utils.tsx | 27 +- .../accounting/xero/XeroImportPage.tsx | 5 +- .../xero/advanced/XeroAdvancedPage.tsx | 5 +- .../connections/ConnectionsAccountingPage.tsx | 4 +- .../connections/ConnectionsMergePageBase.tsx | 4 +- .../ConnectionsReceiptPartnersPage.tsx | 4 +- .../connections/WorkspaceConnectionsPage.tsx | 11 +- .../useAccountingConnectionListings.ts | 3 - src/pages/workspace/connections/utils.ts | 26 +- .../withUnifiedConnectionsBeta.tsx | 28 -- src/pages/workspace/getWorkspaceMenuItems.ts | 98 +--- src/pages/workspace/hr/WorkspaceHRPage.tsx | 55 --- .../hr/gusto/GustoFinalApproverPage.tsx | 6 +- .../hr/merge/MergeHRFinalApproverPage.tsx | 6 +- .../hr/zenefits/ZenefitsFinalApproverPage.tsx | 6 +- src/pages/workspace/mcp/WorkspaceMCPPage.tsx | 129 ------ .../merge/MergeConnectionsPageBase.tsx | 261 ----------- .../workspace/merge/MergeProviderCard.tsx | 139 ------ .../WorkspaceReceiptPartnersPage.tsx | 205 --------- .../connectionRows/UberConnectionRow.tsx | 182 -------- .../receiptPartners/connectionRows/types.ts | 16 - .../recruiting/WorkspaceRecruitingPage.tsx | 60 --- .../upgrade/WorkspaceUpgradePage.tsx | 10 +- tests/actions/PolicyTest.ts | 1 - tests/ui/WorkspaceMoreFeaturesPageTest.tsx | 101 +--- .../AccountingConnectionConfirmationTest.tsx | 112 ++--- ...tionsAccountingPageQBOTokenExpiryTest.tsx} | 3 - tests/unit/ConnectionsPanelGatesTest.tsx | 39 +- .../PolicyAccountingPageAutoStartFlowTest.tsx | 261 ----------- tests/unit/SearchRouterNavigationTest.ts | 19 +- ...kspaceConnectionsPageAutoStartFlowTest.tsx | 36 +- tests/unit/getWorkspaceMenuItemsTest.ts | 149 +----- .../netsuite/NetSuiteSetupPageTest.tsx | 4 +- .../useAccountingConnectionListingsTest.ts | 28 +- tests/unit/withUnifiedConnectionsBetaTest.tsx | 53 --- 92 files changed, 248 insertions(+), 3264 deletions(-) delete mode 100644 src/components/SearchBar/CompactSearchBar.tsx delete mode 100644 src/hooks/useIsUnifiedConnectionsBetaEnabled.ts delete mode 100644 src/pages/workspace/accounting/PolicyAccountingPage.tsx delete mode 100644 src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx delete mode 100644 src/pages/workspace/hr/WorkspaceHRPage.tsx delete mode 100644 src/pages/workspace/mcp/WorkspaceMCPPage.tsx delete mode 100644 src/pages/workspace/merge/MergeConnectionsPageBase.tsx delete mode 100644 src/pages/workspace/merge/MergeProviderCard.tsx delete mode 100644 src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx delete mode 100644 src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx delete mode 100644 src/pages/workspace/receiptPartners/connectionRows/types.ts delete mode 100644 src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx rename tests/unit/{PolicyAccountingPageQBOTokenExpiryTest.tsx => ConnectionsAccountingPageQBOTokenExpiryTest.tsx} (96%) delete mode 100644 tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx delete mode 100644 tests/unit/withUnifiedConnectionsBetaTest.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a295f031849a..e2397ec93233 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1206,7 +1206,6 @@ const CONST = { INSIGHTS_PAGE: 'insightsPage', INSIGHTS_COMPARE: 'insightsCompare', PAYMENT_HISTORY: 'paymentHistory', - UNIFIED_CONNECTIONS: 'unifiedConnections', SPEND_FOOTER_SELECTORS: 'spendFooterSelectors', SUPPORT_TICKET: 'supportTicket', ANCHORED_FIELD_DROPDOWNS: 'anchoredFieldDropdowns', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 51eba403a336..c18d4cd7be50 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3609,11 +3609,16 @@ const ROUTES = { integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean, isIntuitEnterpriseSuite?: boolean, + tab?: ValueOf, ) => { if (!policyID) { Log.warn('Invalid policyID is used to build the WORKSPACE_CONNECTIONS route'); } + if (tab) { + return `workspaces/${policyID}/connections?tab=${tab}` as const; + } + let queryParams = ''; if (newConnectionName) { queryParams += `?newConnectionName=${newConnectionName}`; diff --git a/src/components/ConnectToSageIntacctFlow/index.tsx b/src/components/ConnectToSageIntacctFlow/index.tsx index 73ff7e5d307e..a471bb264049 100644 --- a/src/components/ConnectToSageIntacctFlow/index.tsx +++ b/src/components/ConnectToSageIntacctFlow/index.tsx @@ -1,12 +1,9 @@ import useHasReusablePoliciesConnectedTo from '@hooks/useHasReusablePoliciesConnectedTo'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useOnyx from '@hooks/useOnyx'; import {isAuthenticationError} from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; -import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; - import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -21,7 +18,6 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) { const hasReusablePoliciesConnectedToSageIntacct = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, policyID); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const shouldGoToEnterCredentials = isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); useEffect(() => { if (shouldGoToEnterCredentials) { @@ -29,7 +25,7 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) { return; } if (!hasReusablePoliciesConnectedToSageIntacct) { - Navigation.navigate(`${getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)}/${DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path}`); + Navigation.navigate(`${ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)}/${DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path}`); return; } Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.getRoute(policyID)); diff --git a/src/components/Search/SearchRouter/useNavigationSuggestions.tsx b/src/components/Search/SearchRouter/useNavigationSuggestions.tsx index a521e6b12fe9..d0dca34054d5 100644 --- a/src/components/Search/SearchRouter/useNavigationSuggestions.tsx +++ b/src/components/Search/SearchRouter/useNavigationSuggestions.tsx @@ -10,7 +10,6 @@ import TextWithTooltip from '@components/TextWithTooltip'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -142,15 +141,9 @@ type BuildWorkspaceNavigationItemsParams = { isVendorMatchingBetaEnabled: boolean; - /** Whether the Merge ATS beta gating the Recruiting feature is enabled. */ - isRecruitingBetaEnabled: boolean; - /** Whether the company agents beta is enabled. */ isCompanyAgentsBetaEnabled: boolean; - /** Whether the unified Connections beta replaces the separate connection pages. */ - isUnifiedConnectionsBetaEnabled: boolean; - /** Whether navigation should use the narrow-layout Workspace flow. */ shouldUseNarrowLayout: boolean; @@ -298,9 +291,7 @@ function buildWorkspaceNavigationItems({ icons, isOffline, isVendorMatchingBetaEnabled, - isRecruitingBetaEnabled, isCompanyAgentsBetaEnabled, - isUnifiedConnectionsBetaEnabled, shouldUseNarrowLayout, convertToDisplayString, getItemText, @@ -319,9 +310,7 @@ function buildWorkspaceNavigationItems({ icons, policyCategories: policyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`], isVendorMatchingBetaEnabled, - isRecruitingBetaEnabled, isCompanyAgentsBetaEnabled, - isUnifiedConnectionsBetaEnabled, convertToDisplayString, }); @@ -402,7 +391,6 @@ function useNavigationSuggestions(query: string, shouldWatchForApprovals = true) const {convertToDisplayString} = useCurrencyListActions(); const {isOffline} = useNetwork(); const {isBetaEnabled} = usePermissions(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const icons = useMemoizedLazyExpensifyIcons(SEARCH_ROUTER_ICON_NAMES); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -457,9 +445,7 @@ function useNavigationSuggestions(query: string, shouldWatchForApprovals = true) icons, isOffline: !!isOffline, isVendorMatchingBetaEnabled: isBetaEnabled(CONST.BETAS.VENDOR_MATCHING), - isRecruitingBetaEnabled: isBetaEnabled(CONST.BETAS.MERGE_ATS), isCompanyAgentsBetaEnabled: isBetaEnabled(CONST.BETAS.COMPANY_AGENTS), - isUnifiedConnectionsBetaEnabled, shouldUseNarrowLayout, convertToDisplayString, getItemText: (item) => translate(item.translationKey), diff --git a/src/components/SearchBar/CompactSearchBar.tsx b/src/components/SearchBar/CompactSearchBar.tsx deleted file mode 100644 index c623e6196f2c..000000000000 --- a/src/components/SearchBar/CompactSearchBar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import React from 'react'; - -import type {SharedSearchBarProps} from './BaseSearchBar'; - -import BaseSearchBar from './BaseSearchBar'; - -type CompactSearchBarProps = SharedSearchBarProps; - -/** - * The compact list-style search input (placeholder instead of a floating label), matching the one used above selection lists. - */ -function CompactSearchBar({ref, label, style, inputValue, onChangeText, onSubmitEditing, shouldShowEmptyState, emptyStateContainerStyle}: CompactSearchBarProps) { - const styles = useThemeStyles(); - const theme = useTheme(); - // The compact search input is sized by the physical device width (`isSmallScreenWidth`), not `shouldUseNarrowLayout`, so - // it stays compact when rendered inside an RHP/narrow pane on web/desktop and only grows to the tall size on mobile. - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth} = useResponsiveLayout(); - - return ( - - ); -} - -export default CompactSearchBar; diff --git a/src/hooks/useIsUnifiedConnectionsBetaEnabled.ts b/src/hooks/useIsUnifiedConnectionsBetaEnabled.ts deleted file mode 100644 index e45dd043e441..000000000000 --- a/src/hooks/useIsUnifiedConnectionsBetaEnabled.ts +++ /dev/null @@ -1,11 +0,0 @@ -import CONST from '@src/CONST'; - -import usePermissions from './usePermissions'; - -/** Whether the unified Connections page replaces the separate Accounting, HR, Recruiting, Receipt partners and MCP pages */ -function useIsUnifiedConnectionsBetaEnabled(): boolean { - const {isBetaEnabled} = usePermissions(); - return isBetaEnabled(CONST.BETAS.UNIFIED_CONNECTIONS); -} - -export default useIsUnifiedConnectionsBetaEnabled; diff --git a/src/languages/de.ts b/src/languages/de.ts index 78c2ea71d2e8..d33047608d23 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3624,7 +3624,7 @@ ${amount} für ${merchant} – ${date}`, 1. Klicke auf *Workspaces*. 2. Wähle deinen Workspace aus. - 3. Klicke auf *Accounting*. + 3. Klicke auf *Verknüpfungen*. 4. Suche nach ${integrationName}. 5. Klicke auf *Connect*. @@ -5032,22 +5032,14 @@ ${amount} für ${merchant} – ${date}`, description: 'Großartige Neuigkeiten 🎉. Kontaktiere uns, wenn sie Hilfe bei der Einrichtung benötigen.', }, mcp: { - connectors: 'Konnektoren', - connectorsSubtitle: 'Verbinde einen KI-Assistenten mit deinem Expensify-Konto.', - connect: 'Verbinden', - helpPrompt: 'Brauchst du Hilfe beim Verbinden?', - helpLink: 'Lies unsere Anleitung.', claude: { title: 'Claude', - subtitle: 'von Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'von Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'von OpenAI', }, }, connections: { @@ -6674,10 +6666,6 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU title: 'Organisieren', subtitle: 'Ausgaben gruppieren und analysieren, jede gezahlte Steuer erfassen.', }, - integrateSection: { - title: 'Integrieren', - subtitle: 'Verbinde Expensify mit beliebten Finanzprodukten.', - }, distanceRates: { title: 'Kilometersätze', subtitle: 'Sätze Tarife fest, aktualisiere sie und setze sie durch.', @@ -6906,23 +6894,18 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU }, mcp: { title: 'MCP', - subtitle: 'Verbinde einen KI-Assistenten mit deinem Expensify-Konto.', }, receiptPartners: { title: 'Belegpartner', - subtitle: 'Belege automatisch importieren.', }, connectionsWarningModal: { featureEnabledTitle: 'Nicht so schnell ...', featureEnabledText: 'Um diese Funktion zu aktivieren oder zu deaktivieren, musst du deine Einstellungen für den Buchhaltungsimport ändern.', - disconnectText: 'Um die Buchhaltung zu deaktivieren, musst du die Buchhaltungsanbindung von deinem Workspace trennen.', manageSettings: 'Einstellungen verwalten', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Uber-Verknüpfung trennen', - disconnectText: 'Um diese Funktion zu deaktivieren, trenne bitte zuerst die Uber for Business-Integration.', description: 'Möchtest du diese Integration wirklich trennen?', - confirmText: 'Verstanden', }, workflowWarningModal: { featureEnabledTitle: 'Nicht so schnell ...', @@ -6939,16 +6922,12 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU subtitle: 'Legen Sie einen abrechnungsfähigen Stundensatz für die Zeiterfassung fest.', 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.', disabledTitle: 'Nicht so schnell...', disabledMessage: 'Um diese Funktion zu aktivieren oder zu deaktivieren, müssen Sie Ihre Buchhaltungsimporteinstellungen ändern.', }, - recruitingWarningModal: { - disconnectText: ({integration}: {integration: string}) => `Um Recruiting zu deaktivieren, trennen Sie bitte zuerst die Verbindung von ${integration} mit diesem Workspace.`, - }, }, reports: { reportsCustomTitleExamples: 'Beispiele:', @@ -7417,7 +7396,6 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, accounting: { settings: 'Einstellungen', title: 'Verknüpfungen', - subtitle: 'Verbinden Sie Ihre Buchhaltungssoftware für eine automatische Synchronisierung.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7429,10 +7407,6 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Chatten Sie mit Ihrer Kundenbetreuung.', - talkYourAccountManager: 'Chatte mit deiner/deinem Account Manager/in.', - talkToConcierge: 'Chatte mit Concierge.', - needAnotherAccounting: 'Benötigen Sie eine weitere Buchhaltungssoftware?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7473,7 +7447,6 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, disconnect: 'Trennen', reinstall: 'Connector neu installieren', disconnectTitle: (connectionName = 'Integration') => `${connectionName} trennen`, - connectTitle: (connectionName) => `${connectionName} verbinden`, syncError: (connectionName) => `Verbindung mit ${connectionName} nicht möglich`, accounts: 'Kontenplan', taxes: 'Steuern', @@ -7490,12 +7463,10 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Standardmäßige NetSuite-Mitarbeiterperson', }, disconnectPrompt: (connectionName = 'diese Integration') => `Möchtest du ${connectionName} wirklich trennen?`, - connectPrompt: (connectionName) => `Sind Sie sicher, dass Sie ${connectionName} verbinden möchten? Dadurch werden alle bestehenden Buchhaltungsverbindungen entfernt.`, enterCredentials: 'Gib deine Anmeldedaten ein', reconnect: 'Erneut verbinden', updateCredentials: 'Anmeldedaten aktualisieren', claimOffer: { - badgeText: 'Angebot verfügbar!', xero: { headline: 'Hol dir Xero 6 Monate lang kostenlos!', description: 'Neu bei Xero? Expensify-Kund*innen erhalten 6 Monate kostenlos. Fordern Sie unten Ihr Angebot an.', @@ -8968,10 +8939,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc subscriptions: 'Abonnements', }, hr: { - title: 'Personalwesen', connectionsSubtitle: 'Verbinden Sie sich mit Ihrem HR-System, um Mitarbeiterdaten zu synchronisieren, Erstattungen automatisch den richtigen Personen zuzuordnen und die Ausgaben Ihres Teams ohne manuellen Aufwand korrekt zu halten.', - subtitle: 'HR-Tools verbinden und Mitarbeitergenehmigungen synchron halten.', alreadyConnectedTitle: 'Es kann keine Verbindung zu mehreren HR-Plattformen hergestellt werden', alreadyConnectedPrompt: 'Sie müssen Ihre aktuelle HR-Plattform trennen, bevor Sie eine andere verbinden.', connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`, @@ -9034,7 +9003,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, }, recruiting: { - title: 'Personalbeschaffung', connectionsSubtitle: 'Verbinden Sie Ihr Recruiting-System, um Kandidatendaten zu synchronisieren, Erstattungen automatisch den richtigen Personen zuzuordnen und die Ausgaben Ihres Teams ohne manuelle Arbeit korrekt zu halten.', alreadyConnectedTitle: 'Es kann keine Verbindung zu mehreren ATS-Plattformen hergestellt werden', @@ -9042,7 +9010,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc syncing: 'Kandidaten werden synchronisiert', setupIncomplete: (setupLink: string | undefined) => `Verbunden. ${setupLink ? `Einrichtung abschließen` : 'Einrichtung abschließen'} zum Importieren von Kandidaten.`, - dontSeeYourATS: `Sie sehen Ihr ATS hier nicht? Fragen Sie Concierge, und wir können es hinzufügen.`, importSettings: 'Import-Einstellungen', defaultApprover: 'Standardgenehmiger', approverFields: {recruiter: 'Personalvermittler', recruitingCoordinator: 'Recruiting-Koordinator'}, @@ -9068,7 +9035,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, enableJobStagesOrTags: 'Wählen Sie mindestens eine Jobphase oder ein Tag aus, um fortzufahren', }, - subtitle: 'Verknüpfen Sie Recruiting-Tools und halten Sie Kandidatengenehmigungen synchron.', syncResults: { importedCount: () => ({ one: '1 Kandidat', @@ -9088,9 +9054,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, }, merge: { - connections: 'Verbindungen', - connect: 'Verbinden', - findIntegration: 'Integration finden', syncNow: 'Jetzt synchronisieren', disconnect: 'Trennen', disconnectTitle: (providerName: string) => `${providerName} trennen`, diff --git a/src/languages/el.ts b/src/languages/el.ts index 098778ccaa3c..b7b5634228ad 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -3672,7 +3672,7 @@ ${amount} για ${merchant} - ${date}`, 1. Κάντε κλικ στο *χώρο εργασίας*. 2. Επιλέξτε τον χώρο εργασίας σας. - 3. Κάντε κλικ στο *λογιστική*. + 3. Κάντε κλικ στις *Συνδέσεις*. 4. Βρείτε το ${integrationName}. 5. Κάντε κλικ στο *σύνδεση*. @@ -5106,22 +5106,14 @@ ${amount} για ${merchant} - ${date}`, description: 'Καλά νέα 🎉. Επικοινωνήστε μαζί μας αν χρειαστούν βοήθεια με τη ρύθμιση.', }, mcp: { - connectors: 'Συνδέσεις', - connectorsSubtitle: 'Συνδέστε έναν βοηθό AI με τον λογαριασμό σας στο Expensify.', - connect: 'Σύνδεση', - helpPrompt: 'Χρειάζεστε βοήθεια με τη σύνδεση;', - helpLink: 'Διαβάστε τον οδηγό μας.', claude: { title: 'Claude', - subtitle: 'από την Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'από την Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'από την OpenAI', }, }, connections: { @@ -6784,10 +6776,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ title: 'Οργανώστε', subtitle: 'Ομαδοποιήστε και αναλύστε τις δαπάνες, καταγράψτε κάθε φόρο που πληρώθηκε.', }, - integrateSection: { - title: 'Ενσωμάτωση', - subtitle: 'Συνδέστε το Expensify με δημοφιλή χρηματοοικονομικά προϊόντα.', - }, distanceRates: { title: 'Τιμές χιλιομετρικής αποζημίωσης', subtitle: 'Προσθέστε, ενημερώστε και εφαρμόστε τιμές.', @@ -7027,26 +7015,18 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, mcp: { title: 'MCP', - subtitle: 'Συνδέστε έναν βοηθό AI με τον λογαριασμό σας στο Expensify.', }, receiptPartners: { title: 'Συνεργάτες αποδείξεων', - subtitle: 'Αυτόματη εισαγωγή αποδείξεων.', }, connectionsWarningModal: { featureEnabledTitle: 'Όχι και τόσο γρήγορα...', featureEnabledText: 'Για να ενεργοποιήσετε ή να απενεργοποιήσετε αυτή τη λειτουργία, θα χρειαστεί να αλλάξετε τις ρυθμίσεις εισαγωγής λογιστικής.', - disconnectText: 'Για να απενεργοποιήσετε τη λογιστική, θα χρειαστεί να αποσυνδέσετε τη λογιστική σύνδεση από τον χώρο εργασίας σας.', manageSettings: 'Διαχείριση ρυθμίσεων', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Αποσύνδεση του Uber', - disconnectText: 'Για να απενεργοποιήσετε αυτή τη λειτουργία, αποσυνδέστε πρώτα την ενσωμάτωση Uber for Business.', description: 'Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε αυτήν την ενοποίηση;', - confirmText: 'Εντάξει', - }, - hrWarningModal: { - disconnectText: ({integration}: {integration: string}) => `Για να απενεργοποιήσετε το HR, αποσυνδέστε πρώτα το ${integration} από αυτόν τον χώρο εργασίας.`, }, workflowWarningModal: { featureEnabledTitle: 'Όχι και τόσο γρήγορα...', @@ -7069,10 +7049,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ disabledTitle: 'Όχι τόσο γρήγορα...', disabledMessage: 'Για να ενεργοποιήσετε ή να απενεργοποιήσετε αυτήν τη λειτουργία, θα πρέπει να αλλάξετε τις ρυθμίσεις εισαγωγής λογιστικής.', }, - recruitingWarningModal: { - disconnectText: ({integration}: {integration: string}) => - `Για να απενεργοποιήσετε την προσέλκυση προσωπικού, αποσυνδέστε πρώτα το ${integration} από αυτόν τον χώρο εργασίας.`, - }, }, reports: { reportsCustomTitleExamples: 'Παραδείγματα:', @@ -7550,7 +7526,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ accounting: { settings: 'ρυθμίσεις', title: 'Συνδέσεις', - subtitle: 'Συνδέστε το λογιστικό σας λογισμικό για αυτόματο συγχρονισμό.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7562,10 +7537,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Συνομιλήστε με τον υπεύθυνο λογαριασμού σας.', - talkYourAccountManager: 'Συνομιλήστε με τον υπεύθυνο λογαριασμού σας.', - talkToConcierge: 'Συνομιλήστε με το Concierge.', - needAnotherAccounting: 'Χρειάζεστε άλλο λογιστικό λογισμικό;', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7606,7 +7577,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ disconnect: 'Αποσύνδεση', reinstall: 'Επανεγκατάσταση συνδέτη', disconnectTitle: (connectionName = 'ενσωμάτωση') => `Αποσύνδεση ${connectionName}`, - connectTitle: (connectionName) => `Συνδέστε ${connectionName}`, syncError: (connectionName) => `Δεν είναι δυνατή η σύνδεση με το ${connectionName}`, accounts: 'Λογιστικό σχέδιο', taxes: 'Φόροι', @@ -7623,12 +7593,10 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Προεπιλογή εργαζομένου NetSuite', }, disconnectPrompt: (connectionName = 'αυτή η ενοποίηση') => `Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε το ${connectionName};`, - connectPrompt: (connectionName) => `Είστε βέβαιοι ότι θέλετε να συνδέσετε το ${connectionName}; Αυτό θα αφαιρέσει τυχόν υπάρχουσες λογιστικές συνδέσεις.`, reconnect: 'Επανασύνδεση', enterCredentials: 'Εισαγάγετε τα διαπιστευτήριά σας', updateCredentials: 'Ενημέρωση διαπιστευτηρίων', claimOffer: { - badgeText: 'Προσφορά διαθέσιμη!', xero: { headline: 'Αποκτήστε το Xero δωρεάν για 6 μήνες!', description: @@ -7879,10 +7847,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ qboConnectionExpired: ({date}: {date: string}) => `Η σύνδεσή σας με το QuickBooks Online έληξε στις ${date}.`, }, hr: { - title: 'HR', connectionsSubtitle: 'Συνδεθείτε με το σύστημα HR σας για να συγχρονίζετε τα στοιχεία των εργαζομένων, να αντιστοιχίζετε αυτόματα τις αποζημιώσεις στα σωστά άτομα και να διατηρείτε τα έξοδα της ομάδας σας ακριβή χωρίς χειροκίνητη εργασία.', - subtitle: 'Συνδέστε εργαλεία HR και διατηρήστε τις εγκρίσεις εργαζομένων συγχρονισμένες.', alreadyConnectedTitle: 'Δεν είναι δυνατή η σύνδεση σε πολλές πλατφόρμες HR', alreadyConnectedPrompt: 'Πρέπει να αποσυνδέσετε την τρέχουσα πλατφόρμα HR προτού συνδέσετε μια άλλη.', connectionDescription: (providerName: string) => `Συνδέστε το ${providerName} για να διατηρείτε τις εγκρίσεις εργαζομένων συγχρονισμένες με τον χώρο εργασίας σας.`, @@ -7945,7 +7911,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, }, recruiting: { - title: 'Προσλήψεις', connectionsSubtitle: 'Συνδεθείτε με το σύστημα προσλήψεών σας για να συγχρονίζετε τα δεδομένα υποψηφίων, να αντιστοιχίζετε αυτόματα τις αποζημιώσεις στα σωστά άτομα και να διατηρείτε τις δαπάνες της ομάδας σας ακριβείς χωρίς χειροκίνητη εργασία.', alreadyConnectedTitle: 'Αδυναμία σύνδεσης σε πολλές πλατφόρμες ATS', @@ -7953,7 +7918,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ syncing: 'Γίνεται συγχρονισμός υποψηφίων', setupIncomplete: (setupLink: string | undefined) => `Συνδέθηκε. ${setupLink ? `Ολοκληρώστε τη ρύθμιση` : 'Ολοκληρώστε τη ρύθμιση'} για εισαγωγή υποψηφίων.`, - dontSeeYourATS: `Δεν βλέπετε εδώ το ATS σας; Ρωτήστε το Concierge και μπορούμε να το προσθέσουμε.`, importSettings: 'Ρυθμίσεις εισαγωγής', defaultApprover: 'Προεπιλεγμένος εγκρίνων', approverFields: {recruiter: 'Στρατολογητής', recruitingCoordinator: 'Συντονιστής προσλήψεων'}, @@ -7979,7 +7943,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, enableJobStagesOrTags: 'Επιλέξτε τουλάχιστον ένα στάδιο εργασίας ή ετικέτα για να συνεχίσετε', }, - subtitle: 'Συνδέστε τα εργαλεία προσλήψεων και διατηρήστε τις εγκρίσεις υποψηφίων συγχρονισμένες.', syncResults: { importedCount: () => ({ one: '1 υποψήφιος', @@ -7999,9 +7962,6 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, }, merge: { - connections: 'Συνδέσεις', - connect: 'Σύνδεση', - findIntegration: 'Βρείτε ενοποίηση', syncNow: 'Συγχρονισμός τώρα', disconnect: 'Αποσύνδεση', disconnectTitle: (providerName: string) => `Αποσύνδεση ${providerName}`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 1b1bd7a97e23..78ae176660c4 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3769,7 +3769,7 @@ const translations = { 1. Click *Workspaces*. 2. Select your workspace. - 3. Click *Accounting*. + 3. Click *Connections*. 4. Find ${integrationName}. 5. Click *Connect*. @@ -5218,22 +5218,14 @@ const translations = { description: 'Great news 🎉. Reach out to us if they need any help with the setup.', }, mcp: { - connectors: 'Connectors', - connectorsSubtitle: 'Connect an AI assistant to your Expensify account.', - connect: 'Connect', - helpPrompt: 'Need help connecting?', - helpLink: 'Read our guide.', claude: { title: 'Claude', - subtitle: 'by Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'by Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'by OpenAI', }, }, connections: { @@ -7053,10 +7045,6 @@ const translations = { title: 'Organize', subtitle: 'Group and analyze spend, record every tax paid.', }, - integrateSection: { - title: 'Integrate', - subtitle: 'Connect Expensify to popular financial products.', - }, distanceRates: { title: 'Distance rates', subtitle: 'Add, update, and enforce rates.', @@ -7295,29 +7283,18 @@ const translations = { }, mcp: { title: 'MCP', - subtitle: 'Connect an AI assistant to your Expensify account.', }, receiptPartners: { title: 'Receipt partners', - subtitle: 'Automatically import receipts.', }, connectionsWarningModal: { featureEnabledTitle: 'Not so fast...', featureEnabledText: "To enable or disable this feature, you'll need to change your accounting import settings.", - disconnectText: "To disable accounting, you'll need to disconnect your accounting connection from your workspace.", manageSettings: 'Manage settings', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Disconnect Uber', - disconnectText: 'To disable this feature, please disconnect the Uber for Business integration first.', description: 'Are you sure you want to disconnect this integration?', - confirmText: 'Got it', - }, - hrWarningModal: { - disconnectText: ({integration}: {integration: string}) => `To disable HR, please disconnect ${integration} from this workspace first.`, - }, - recruitingWarningModal: { - disconnectText: ({integration}: {integration: string}) => `To disable Recruiting, please disconnect ${integration} from this workspace first.`, }, workflowWarningModal: { featureEnabledTitle: 'Not so fast...', @@ -7812,7 +7789,6 @@ const translations = { accounting: { settings: 'settings', title: 'Connections', - subtitle: 'Connect your accounting software for an automatic sync.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7827,10 +7803,6 @@ const translations = { sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Chat with your account executive.', - talkYourAccountManager: 'Chat with your account manager.', - talkToConcierge: 'Chat with Concierge.', - needAnotherAccounting: 'Need another accounting software? ', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7871,7 +7843,6 @@ const translations = { disconnect: 'Disconnect', reinstall: 'Reinstall connector', disconnectTitle: (connectionName = 'integration') => `Disconnect ${connectionName}`, - connectTitle: (connectionName: string) => `Connect ${connectionName}`, syncError: (connectionName: string) => `Can't connect to ${connectionName}`, accounts: 'Chart of accounts', @@ -7889,14 +7860,12 @@ const translations = { [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite employee default', }, disconnectPrompt: (connectionName = 'this integration') => `Are you sure you want to disconnect ${connectionName}?`, - connectPrompt: (connectionName: string) => `Are you sure you want to connect ${connectionName}? This will remove any existing accounting connections.`, reconnect: 'Reconnect', enterCredentials: 'Enter your credentials', updateCredentials: 'Update credentials', qboConnectionExpiring: ({date}: {date: string}) => `Your QuickBooks Online connection expires on ${date}.`, qboConnectionExpired: ({date}: {date: string}) => `Your QuickBooks Online connection expired on ${date}.`, claimOffer: { - badgeText: 'Offer available!', xero: { headline: 'Get Xero free for 6 months!', description: 'New to Xero? Expensify customers get 6 months free. Claim your offer below.', @@ -8144,10 +8113,8 @@ const translations = { }, }, hr: { - title: 'HR', connectionsSubtitle: "Connect to your HR system to sync employee data, auto-match reimbursements to the right people, and keep your team's expenses accurate without the manual work.", - subtitle: 'Connect HR tools and keep employee approvals in sync.', alreadyConnectedTitle: 'Cannot connect to multiple HR platforms', alreadyConnectedPrompt: 'You must disconnect your current HR platform before connecting another.', connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`, @@ -8209,8 +8176,6 @@ const translations = { }, }, recruiting: { - title: 'Recruiting', - subtitle: 'Connect Recruiting tools and keep candidate approvals in sync.', connectionsSubtitle: "Connect to your recruiting system to sync candidate data, auto-match reimbursements to the right people, and keep your team's expenses accurate without the manual work.", alreadyConnectedTitle: 'Cannot connect to multiple ATS platforms', @@ -8224,7 +8189,6 @@ const translations = { }, setupIncomplete: (setupLink: string | undefined) => `Connected. ${setupLink ? `Complete setup` : 'Complete setup'} to import candidates.`, - dontSeeYourATS: `Don't see your ATS here? Ask Concierge and we can add it.`, importSettings: 'Import settings', defaultApprover: 'Default approver', approverField: `First approver`, @@ -8265,9 +8229,6 @@ const translations = { }, }, merge: { - connections: 'Connections', - connect: 'Connect', - findIntegration: 'Find integration', syncNow: 'Sync now', disconnect: 'Disconnect', disconnectTitle: (providerName: string) => `Disconnect ${providerName}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 61d8e76d105a..eb7dace5ab72 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3521,7 +3521,7 @@ ${amount} para ${merchant} - ${date}`, 1. Haz clic en *Espacios de trabajo*. 2. Selecciona tu espacio de trabajo. - 3. Haz clic en *Contabilidad*. + 3. Haz clic en *Conexiones*. 4. Busca ${integrationName}. 5. Haz clic en *Conectar*. @@ -4956,22 +4956,14 @@ ${amount} para ${merchant} - ${date}`, description: 'Excelentes noticias 🎉. Contáctanos si necesitan ayuda con la configuración.', }, mcp: { - connectors: 'Conectores', - connectorsSubtitle: 'Conecta un asistente de IA a tu cuenta de Expensify.', - connect: 'Conectar', - helpPrompt: '¿Necesitas ayuda para conectar?', - helpLink: 'Lee nuestra guía.', claude: { title: 'Claude', - subtitle: 'de Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'de Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'de OpenAI', }, }, connections: { @@ -6529,10 +6521,6 @@ ${amount} para ${merchant} - ${date}`, title: 'Organizar', subtitle: 'Agrupa y analiza el gasto, registra cada impuesto pagado.', }, - integrateSection: { - title: 'Integrar', - subtitle: 'Conecta Expensify a otros productos financieros populares.', - }, distanceRates: { title: 'Tasas de distancia', subtitle: 'Añade, actualiza y haz cumplir las tasas.', @@ -6774,25 +6762,19 @@ ${amount} para ${merchant} - ${date}`, }, mcp: { title: 'MCP', - subtitle: 'Conecta un asistente de IA a tu cuenta de Expensify.', }, receiptPartners: { title: 'Socios de recibos', - subtitle: 'Importación automática de recibos.', }, connectionsWarningModal: { featureEnabledTitle: 'No tan rápido...', featureEnabledText: 'Para activar o desactivar esta función, cambia la configuración de importación contable.', - disconnectText: 'Para desactivar la contabilidad, desconecta tu conexión contable del espacio de trabajo.', manageSettings: 'Gestionar la configuración', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Desconectar Uber', - disconnectText: 'Para desactivar esta función, desconecta primero la integración de Uber for Business.', description: '¿Está seguro de que desea desconectar esta integración?', - confirmText: 'Entendido', }, - hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para deshabilitar RR. HH., primero desconecta ${integration} de este espacio de trabajo.`}, workflowWarningModal: { featureEnabledTitle: 'No tan rápido...', featureEnabledText: @@ -6808,7 +6790,6 @@ ${amount} para ${merchant} - ${date}`, subtitle: 'Establecer una tarifa por hora facturable para el seguimiento de tiempo.', defaultHourlyRate: 'Tarifa por hora predeterminada', }, - recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para desactivar Recruiting, primero desconecta ${integration} de este espacio de trabajo.`}, }, reports: { reportsCustomTitleExamples: 'Ejemplos:', @@ -7215,7 +7196,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, accounting: { settings: 'configuración', title: 'Conexiones', - subtitle: 'Conecta tu software de contabilidad para una sincronización automática.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7227,10 +7207,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Chatea con tu ejecutivo de cuenta.', - talkYourAccountManager: 'Chatea con tu gestor de cuenta.', - talkToConcierge: 'Chatear con Concierge.', - needAnotherAccounting: '¿Necesitas otro software de contabilidad? ', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7271,7 +7247,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, disconnect: 'Desconectar', reinstall: 'Reinstalar el conector', disconnectTitle: (connectionName = 'integración') => `Desconectar ${connectionName}`, - connectTitle: (connectionName) => `Conectar ${connectionName}`, syncError: (connectionName) => `No se puede conectar a ${connectionName}`, accounts: 'Plan de cuentas', taxes: 'Impuestos', @@ -7288,12 +7263,10 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Predeterminado del empleado NetSuite', }, disconnectPrompt: (connectionName = 'integración') => `¿Estás seguro de que quieres desconectar ${connectionName}?`, - connectPrompt: (connectionName) => `¿Estás seguro de que quieres conectar a ${connectionName}? Esto eliminará cualquier conexión contable existente.`, enterCredentials: 'Ingresa tus credenciales', reconnect: 'Reconectar', updateCredentials: 'Actualizar credenciales', claimOffer: { - badgeText: '¡Oferta disponible!', xero: { headline: '¡Obtén Xero gratis por 6 meses!', description: @@ -7608,10 +7581,8 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, }, hr: { - title: 'HR', connectionsSubtitle: 'Conéctate a tu sistema de RR. HH. para sincronizar los datos de empleados, emparejar automáticamente los reembolsos con las personas correctas y mantener los gastos de tu equipo precisos sin trabajo manual.', - subtitle: 'Conecta herramientas de HR y mantén sincronizadas las aprobaciones de empleados.', alreadyConnectedTitle: 'No se puede conectar a varias plataformas de RR. HH.', alreadyConnectedPrompt: 'Debes desconectar tu plataforma de RR. HH. actual antes de conectar otra.', connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`, @@ -7674,7 +7645,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, }, recruiting: { - title: 'Reclutamiento', connectionsSubtitle: 'Conéctate a tu sistema de selección para sincronizar los datos de los candidatos, vincular automáticamente los reembolsos con las personas correctas y mantener los gastos de tu equipo precisos sin trabajo manual.', alreadyConnectedTitle: 'No se puede conectar a varias plataformas ATS', @@ -7682,7 +7652,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, syncing: 'Sincronizando candidatos', setupIncomplete: (setupLink: string | undefined) => `Conectado. ${setupLink ? `Completar configuración` : 'Completar configuración'} para importar candidatos.`, - dontSeeYourATS: `¿No ves tu ATS aquí? Pregunta a Concierge y podremos añadirlo.`, importSettings: 'Importar ajustes', defaultApprover: 'Aprobador predeterminado', approverFields: {recruiter: 'Reclutador', recruitingCoordinator: 'Coordinador de selección'}, @@ -7708,7 +7677,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, enableJobStagesOrTags: 'Selecciona al menos una etapa de trabajo o etiqueta para continuar', }, - subtitle: 'Conecta herramientas de selección y mantén sincronizadas las aprobaciones de candidatos.', syncResults: { importedCount: () => ({ one: '1 candidato', @@ -7727,9 +7695,6 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, }, merge: { - connections: 'Conexiones', - connect: 'Conectar', - findIntegration: 'Buscar integración', syncNow: 'Sincronizar ahora', disconnect: 'Desconectar', disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index db786756547a..e5f0b61132a2 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3635,7 +3635,7 @@ ${amount} pour ${merchant} - ${date}`, 1. Cliquez sur *Espaces de travail*. 2. Sélectionnez votre espace de travail. - 3. Cliquez sur *Comptabilité*. + 3. Cliquez sur *Connexions*. 4. Recherchez ${integrationName}. 5. Cliquez sur *Connecter*. @@ -5042,22 +5042,14 @@ ${amount} pour ${merchant} - ${date}`, description: "Excellente nouvelle 🎉. Contactez-nous si votre client a besoin d'aide pour la configuration.", }, mcp: { - connectors: 'Connecteurs', - connectorsSubtitle: 'Connectez un assistant IA à votre compte Expensify.', - connect: 'Connecter', - helpPrompt: "Besoin d'aide pour la connexion ?", - helpLink: 'Consultez notre guide.', claude: { title: 'Claude', - subtitle: 'par Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'par Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'par OpenAI', }, }, connections: { @@ -6685,10 +6677,6 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. title: 'Organiser', subtitle: 'Regroupez et analysez les dépenses, enregistrez chaque taxe payée.', }, - integrateSection: { - title: 'Intégrer', - subtitle: 'Connectez Expensify à des produits financiers populaires.', - }, distanceRates: { title: 'Taux de distance', subtitle: 'Ajoutez, mettez à jour et appliquez les taux.', @@ -6927,23 +6915,18 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. }, mcp: { title: 'MCP', - subtitle: 'Connectez un assistant IA à votre compte Expensify.', }, receiptPartners: { title: 'Partenaires de reçus', - subtitle: 'Importer automatiquement les reçus.', }, connectionsWarningModal: { featureEnabledTitle: 'Pas si vite…', featureEnabledText: 'Pour activer ou désactiver cette fonctionnalité, vous devez modifier vos paramètres d’import comptable.', - disconnectText: 'Pour désactiver la comptabilité, vous devez déconnecter votre connexion comptable de votre espace de travail.', manageSettings: 'Gérer les paramètres', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Dissocier Uber', - disconnectText: 'Pour désactiver cette fonctionnalité, veuillez d’abord déconnecter l’intégration Uber for Business.', description: 'Voulez-vous vraiment déconnecter cette intégration ?', - confirmText: 'Compris', }, workflowWarningModal: { featureEnabledTitle: 'Pas si vite…', @@ -6960,16 +6943,12 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. subtitle: 'Définissez un taux horaire facturable pour le suivi du temps.', 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é.', disabledTitle: 'Pas si vite...', disabledMessage: 'Pour activer ou désactiver cette fonctionnalité, vous devrez modifier vos paramètres d’importation comptable.', }, - recruitingWarningModal: { - disconnectText: ({integration}: {integration: string}) => `Pour désactiver le recrutement, veuillez d’abord déconnecter ${integration} de cet espace de travail.`, - }, }, reports: { reportsCustomTitleExamples: 'Exemples :', @@ -7441,7 +7420,6 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, accounting: { settings: 'paramètres', title: 'Connexions', - subtitle: 'Connectez votre logiciel de comptabilité pour une synchronisation automatique.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7453,10 +7431,6 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Discutez avec votre chargé de compte.', - talkYourAccountManager: 'Discuter avec votre gestionnaire de compte.', - talkToConcierge: 'Discuter avec Concierge.', - needAnotherAccounting: 'Besoin d’un autre logiciel comptable ?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7497,7 +7471,6 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, disconnect: 'Déconnecter', reinstall: 'Réinstaller le connecteur', disconnectTitle: (connectionName = 'intégration') => `Déconnecter ${connectionName}`, - connectTitle: (connectionName) => `Connecter ${connectionName}`, syncError: (connectionName) => `Impossible de se connecter à ${connectionName}`, accounts: 'Plan comptable', taxes: 'Taxes', @@ -7514,12 +7487,10 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Par défaut employé NetSuite', }, disconnectPrompt: (connectionName = 'cette intégration') => `Voulez-vous vraiment déconnecter ${connectionName} ?`, - connectPrompt: (connectionName) => `Voulez-vous vraiment connecter ${connectionName} ? Cette action supprimera toutes les connexions comptables existantes.`, enterCredentials: 'Saisissez vos identifiants', reconnect: 'Reconnecter', updateCredentials: 'Mettre à jour les identifiants', claimOffer: { - badgeText: 'Offre disponible !', xero: { headline: 'Obtenez Xero gratuitement pendant 6 mois !', description: @@ -8991,10 +8962,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e subscriptions: 'Abonnements', }, hr: { - title: 'RH', connectionsSubtitle: 'Connectez votre système RH pour synchroniser les données des employé·es, faire correspondre automatiquement les remboursements aux bonnes personnes et garder les dépenses de votre équipe exactes, sans travail manuel.', - subtitle: 'Connectez vos outils RH et gardez les approbations des employés synchronisées.', alreadyConnectedTitle: 'Impossible de se connecter à plusieurs plateformes RH', alreadyConnectedPrompt: 'Vous devez déconnecter votre plateforme RH actuelle avant d’en connecter une autre.', connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`, @@ -9057,7 +9026,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, }, recruiting: { - title: 'Recrutement', connectionsSubtitle: 'Connectez votre système de recrutement pour synchroniser les données des candidats, faire correspondre automatiquement les remboursements aux bonnes personnes et maintenir des dépenses d’équipe exactes sans travail manuel.', alreadyConnectedTitle: 'Impossible de se connecter à plusieurs plateformes ATS', @@ -9065,7 +9033,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e syncing: 'Synchronisation des candidats', setupIncomplete: (setupLink: string | undefined) => `Connecté. ${setupLink ? `Terminer la configuration` : 'Terminer la configuration'} pour importer des candidats.`, - dontSeeYourATS: `Vous ne voyez pas votre ATS ici ? Demandez à Concierge et nous pourrons l’ajouter.`, importSettings: 'Paramètres d’importation', defaultApprover: 'Approbateur par défaut', approverFields: {recruiter: 'Recruteur', recruitingCoordinator: 'Coordinateur recrutement'}, @@ -9092,7 +9059,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, enableJobStagesOrTags: 'Sélectionnez au moins une étape de poste ou un tag pour continuer', }, - subtitle: 'Connectez les outils de recrutement et synchronisez les validations de candidats.', syncResults: { importedCount: () => ({ one: '1 candidat', @@ -9112,9 +9078,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, }, merge: { - connections: 'Connexions', - connect: 'Connecter', - findIntegration: 'Rechercher une intégration', syncNow: 'Synchroniser maintenant', disconnect: 'Déconnecter', disconnectTitle: (providerName: string) => `Déconnecter ${providerName}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 6af0639dac18..30dc3ad3531e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3608,7 +3608,7 @@ ${amount} per ${merchant} - ${date}`, 1. Fai clic su *Spazi di lavoro*. 2. Seleziona il tuo spazio di lavoro. - 3. Fai clic su *Contabilità*. + 3. Fai clic su *Connessioni*. 4. Trova ${integrationName}. 5. Fai clic su *Connetti*. @@ -5000,22 +5000,14 @@ ${amount} per ${merchant} - ${date}`, description: 'Ottime notizie 🎉. Contattaci se hanno bisogno di aiuto con la configurazione.', }, mcp: { - connectors: 'Connettori', - connectorsSubtitle: 'Collega un assistente IA al tuo account Expensify.', - connect: 'Connetti', - helpPrompt: 'Hai bisogno di aiuto con la connessione?', - helpLink: 'Leggi la nostra guida.', claude: { title: 'Claude', - subtitle: 'di Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'di Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'di OpenAI', }, }, connections: { @@ -6626,10 +6618,6 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. title: 'Organizza', subtitle: 'Raggruppa e analizza le spese, registra ogni imposta pagata.', }, - integrateSection: { - title: 'Integra', - subtitle: 'Collega Expensify ai più diffusi prodotti finanziari.', - }, distanceRates: { title: 'Tariffe chilometriche', subtitle: 'Aggiungi, aggiorna e applica le tariffe.', @@ -6867,23 +6855,18 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. }, mcp: { title: 'MCP', - subtitle: 'Collega un assistente IA al tuo account Expensify.', }, receiptPartners: { title: 'Partner ricevute', - subtitle: 'Importa automaticamente le ricevute.', }, connectionsWarningModal: { featureEnabledTitle: 'Non così in fretta...', featureEnabledText: 'Per attivare o disattivare questa funzionalità, devi modificare le impostazioni di importazione contabile.', - disconnectText: 'Per disattivare la contabilità, devi disconnettere la connessione contabile dal tuo spazio di lavoro.', manageSettings: 'Gestisci impostazioni', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Disconnetti Uber', - disconnectText: 'Per disattivare questa funzionalità, disconnetti prima l’integrazione Uber for Business.', description: 'Sei sicuro di voler disconnettere questa integrazione?', - confirmText: 'Ho capito', }, workflowWarningModal: { featureEnabledTitle: 'Non così in fretta...', @@ -6900,14 +6883,12 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. subtitle: 'Imposta una tariffa oraria fatturabile per il rilevamento del tempo.', 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à.', disabledTitle: 'Non così in fretta...', disabledMessage: 'Per abilitare o disabilitare questa funzione, dovrai modificare le impostazioni di importazione della contabilità.', }, - recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Per disattivare Recruiting, scollega prima ${integration} da questo spazio di lavoro.`}, }, reports: { reportsCustomTitleExamples: 'Esempi:', @@ -7374,7 +7355,6 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, accounting: { settings: 'impostazioni', title: 'Connessioni', - subtitle: 'Collega il tuo software di contabilità per una sincronizzazione automatica.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7386,10 +7366,6 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Chatta con il tuo account executive.', - talkYourAccountManager: 'Chatta con il tuo account manager.', - talkToConcierge: 'Chatta con Concierge.', - needAnotherAccounting: 'Ti serve un altro software di contabilità?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7430,7 +7406,6 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, disconnect: 'Disconnetti', reinstall: 'Reinstalla connettore', disconnectTitle: (connectionName = 'integrazione') => `Disconnetti ${connectionName}`, - connectTitle: (connectionName) => `Collega ${connectionName}`, syncError: (connectionName) => `Impossibile connettersi a ${connectionName}`, accounts: 'Piano dei conti', taxes: 'Tasse', @@ -7447,12 +7422,10 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Impostazione predefinita dipendente NetSuite', }, disconnectPrompt: (connectionName = 'questa integrazione') => `Sei sicuro di voler disconnettere ${connectionName}?`, - connectPrompt: (connectionName) => `Sei sicuro di voler collegare ${connectionName}? Questo rimuoverà tutte le connessioni contabili esistenti.`, enterCredentials: 'Inserisci le tue credenziali', reconnect: 'Riconnetti', updateCredentials: 'Aggiorna credenziali', claimOffer: { - badgeText: 'Offerta disponibile!', xero: { headline: 'Ottieni Xero gratis per 6 mesi!', description: 'Nuovo su Xero? I clienti Expensify ricevono 6 mesi gratis. Richiedi la tua offerta qui sotto.', @@ -8911,10 +8884,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, subscriptions: 'Abbonamenti', }, hr: { - title: 'Risorse umane', connectionsSubtitle: 'Collega il tuo sistema HR per sincronizzare i dati dei dipendenti, abbinare automaticamente i rimborsi alle persone giuste e mantenere le spese del tuo team accurate senza lavoro manuale.', - subtitle: 'Collega gli strumenti HR e mantieni sincronizzate le approvazioni dei dipendenti.', alreadyConnectedTitle: 'Impossibile connettersi a più piattaforme HR', alreadyConnectedPrompt: 'Devi disconnettere la tua attuale piattaforma HR prima di collegarne un’altra.', connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`, @@ -8977,7 +8948,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, }, recruiting: { - title: 'Assunzioni', connectionsSubtitle: 'Connetti il tuo sistema di recruiting per sincronizzare i dati dei candidati, abbinare automaticamente i rimborsi alle persone giuste e mantenere le spese del team accurate senza lavoro manuale.', alreadyConnectedTitle: 'Impossibile connettersi a più piattaforme ATS', @@ -8985,7 +8955,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, syncing: 'Sincronizzazione candidati', setupIncomplete: (setupLink: string | undefined) => `Connesso. ${setupLink ? `Completa la configurazione` : 'Completa configurazione'} per importare i candidati.`, - dontSeeYourATS: `Non vedi il tuo ATS qui? Chiedi a Concierge e possiamo aggiungerlo.`, importSettings: 'Impostazioni di importazione', defaultApprover: 'Approvatore predefinito', approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coordinatore recruiting'}, @@ -9011,7 +8980,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, enableJobStagesOrTags: 'Seleziona almeno una fase del lavoro o un tag per continuare', }, - subtitle: 'Collega gli strumenti di recruiting e mantieni sincronizzate le approvazioni dei candidati.', syncResults: { importedCount: () => ({ one: '1 candidato', @@ -9031,9 +8999,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, }, merge: { - connections: 'Connessioni', - connect: 'Connetti', - findIntegration: 'Trova integrazione', syncNow: 'Sincronizza ora', disconnect: 'Disconnetti', disconnectTitle: (providerName: string) => `Disconnetti ${providerName}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 693bc3953bff..000c9598d0d1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3580,7 +3580,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' 1.「*ワークスペース*」をクリックします。 2. ワークスペースを選択します。 - 3.「*会計*」をクリックします。 + 3.「*連携*」をクリックします。 4. ${integrationName} を探します。 5.「*接続*」をクリックします。 @@ -4965,22 +4965,14 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' description: '素晴らしいニュースです 🎉。セットアップにサポートが必要な場合はお問い合わせください。', }, mcp: { - connectors: 'コネクタ', - connectorsSubtitle: 'AIアシスタントをExpensifyアカウントに接続します。', - connect: '接続', - helpPrompt: '接続にお困りですか?', - helpLink: 'ガイドをご覧ください。', claude: { title: 'Claude', - subtitle: 'Anthropic 提供', }, cursor: { title: 'Cursor', - subtitle: 'Anysphere 提供', }, chatgpt: { title: 'ChatGPT', - subtitle: 'OpenAI 提供', }, }, connections: { @@ -6550,10 +6542,6 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO title: '整理', subtitle: '支出をグループ化して分析し、支払ったすべての税金を記録します。', }, - integrateSection: { - title: '連携する', - subtitle: 'Expensify を人気の金融サービスに接続しましょう。', - }, distanceRates: { title: '距離単価', subtitle: 'レートを追加、更新し、適用します。', @@ -6776,23 +6764,18 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO }, mcp: { title: 'MCP', - subtitle: 'AIアシスタントをExpensifyアカウントに接続します。', }, receiptPartners: { title: '領収書パートナー', - subtitle: '領収書を自動で取り込みます。', }, connectionsWarningModal: { featureEnabledTitle: 'ちょっと待ってください…', featureEnabledText: 'この機能を有効または無効にするには、会計インポート設定を変更する必要があります。', - disconnectText: '会計機能を無効にするには、ワークスペースから会計連携を切断する必要があります。', manageSettings: '設定を管理', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Uber との連携を解除', - disconnectText: 'この機能を無効にするには、まず Uber for Business の連携を解除してください。', description: 'この連携を解除してもよろしいですか?', - confirmText: '了解しました', }, workflowWarningModal: { featureEnabledTitle: 'ちょっと待ってください…', @@ -6809,16 +6792,12 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO subtitle: '時間追跡用の請求可能な時間単価を設定します。', defaultHourlyRate: 'デフォルトの時給率', }, - hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `HR を無効にするには、まずこのワークスペースから ${integration} を切断してください。`}, vendors: { title: 'ベンダー', subtitle: '会計ソフトからインポートした取引先にカード経費を照合します。', disabledTitle: 'ちょっと待ってください...', disabledMessage: 'この機能を有効または無効にするには、会計インポート設定を変更する必要があります。', }, - recruitingWarningModal: { - disconnectText: ({integration}: {integration: string}) => `採用機能を無効にするには、まずこのワークスペースから ${integration} の連携を解除してください。`, - }, }, reports: { reportsCustomTitleExamples: '例:', @@ -7283,7 +7262,6 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から accounting: { settings: '設定', title: '接続', - subtitle: '会計ソフトを接続して、自動同期を行いましょう。', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7295,10 +7273,6 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'アカウントエグゼクティブとチャットします。', - talkYourAccountManager: 'アカウントマネージャーとチャットする', - talkToConcierge: 'Conciergeとチャットする', - needAnotherAccounting: 'ほかの会計ソフトが必要ですか?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7339,7 +7313,6 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から disconnect: '切断', reinstall: 'コネクタを再インストール', disconnectTitle: (connectionName = '連携') => `${connectionName}の接続を解除`, - connectTitle: (connectionName) => `${connectionName} を接続`, syncError: (connectionName) => `${connectionName} に接続できません`, accounts: '勘定科目表', taxes: '税金', @@ -7356,12 +7329,10 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite 従業員のデフォルト', }, disconnectPrompt: (connectionName = 'この連携') => `${connectionName} の接続を本当に解除しますか?`, - connectPrompt: (connectionName) => `${connectionName} を接続してもよろしいですか?これにより、既存の会計連携はすべて削除されます。`, enterCredentials: '認証情報を入力してください', reconnect: '再接続', updateCredentials: '認証情報を更新', claimOffer: { - badgeText: 'オファーをご利用いただけます!', xero: { headline: 'Xero を6か月間無料で利用しましょう!', description: @@ -8800,9 +8771,7 @@ ${reportName}`, subscriptions: 'サブスクリプション', }, hr: { - title: '人事', connectionsSubtitle: '人事システムと連携して従業員データを同期し、精算を自動で正しい担当者に紐づけることで、手作業なしでチームの経費を正確に管理できます。', - subtitle: '人事ツールを連携して、従業員の承認を常に同期させます。', alreadyConnectedTitle: '複数の人事プラットフォームには接続できません', alreadyConnectedPrompt: '別の人事プラットフォームに接続する前に、現在の人事プラットフォームとの接続を解除する必要があります。', connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`, @@ -8864,14 +8833,12 @@ ${reportName}`, }, }, recruiting: { - title: '採用', connectionsSubtitle: '採用システムに接続して候補者データを同期し、払い戻しを自動的に適切な人に紐づけることで、手作業なしでチームの経費精度を保てます。', alreadyConnectedTitle: '複数の ATS プラットフォームには接続できません', alreadyConnectedPrompt: '別の ATS を接続する前に、現在の ATS を必ず切断する必要があります。', syncing: '候補者を同期しています', setupIncomplete: (setupLink: string | undefined) => `接続済み。${setupLink ? `セットアップを完了` : '設定を完了'} をインポート候補として追加します。`, - dontSeeYourATS: `ご利用のATSが見つかりませんか?Concierge にお問い合わせいただければ、追加できます。`, importSettings: 'インポート設定', defaultApprover: 'デフォルト承認者', approverFields: {recruiter: '採用担当者', recruitingCoordinator: '採用コーディネーター'}, @@ -8892,7 +8859,6 @@ ${reportName}`, offices: {title: 'オフィス', description: 'このワークスペースと同期したい候補者のオフィスを選択してください', toggleTitle: 'オフィス', allSelected: 'すべてのオフィス'}, enableJobStagesOrTags: '続行するには、少なくとも 1 つのジョブのステージまたはタグを選択してください', }, - subtitle: '採用ツールを連携して、候補者の承認を常に同期させます。', syncResults: { importedCount: () => ({ one: '1 名の候補者', @@ -8912,9 +8878,6 @@ ${reportName}`, }, }, merge: { - connections: '接続', - connect: '接続', - findIntegration: '連携を検索', syncNow: '今すぐ同期', disconnect: '切断', disconnectTitle: (providerName: string) => `${providerName}を切断`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5ffd40e382e3..570b3e58978c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3611,7 +3611,7 @@ ${amount} voor ${merchant} - ${date}`, 1. Klik op *Workspaces*. 2. Selecteer je workspace. - 3. Klik op *Accounting*. + 3. Klik op *Verbindingen*. 4. Zoek ${integrationName}. 5. Klik op *Connect*. @@ -5002,22 +5002,14 @@ ${amount} voor ${merchant} - ${date}`, description: 'Geweldig nieuws 🎉. Neem contact met ons op als ze hulp nodig hebben bij de configuratie.', }, mcp: { - connectors: 'Connectoren', - connectorsSubtitle: 'Verbind een AI-assistent met je Expensify-account.', - connect: 'Verbinden', - helpPrompt: 'Hulp nodig bij het verbinden?', - helpLink: 'Lees onze handleiding.', claude: { title: 'Claude', - subtitle: 'van Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'van Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'van OpenAI', }, }, connections: { @@ -6618,10 +6610,6 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ title: 'Organiseren', subtitle: 'Groepeer en analyseer uitgaven, leg elke betaalde belasting vast.', }, - integrateSection: { - title: 'Integreren', - subtitle: 'Koppel Expensify aan populaire financiële producten.', - }, distanceRates: { title: 'Kilometertarieven', subtitle: 'Tarieven toevoegen, bijwerken en afdwingen.', @@ -6851,23 +6839,18 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ }, mcp: { title: 'MCP', - subtitle: 'Verbind een AI-assistent met je Expensify-account.', }, receiptPartners: { title: 'Bonpartners', - subtitle: 'Automatisch bonnetjes importeren.', }, connectionsWarningModal: { featureEnabledTitle: 'Niet zo snel...', featureEnabledText: 'Om deze functie in of uit te schakelen, moet je je instellingen voor boekhoudkundige import wijzigen.', - disconnectText: 'Om de boekhouding uit te schakelen, moet je de boekhoudkoppeling van je werkruimte loskoppelen.', manageSettings: 'Instellingen beheren', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Verbinding met Uber verbreken', - disconnectText: 'Om deze functie uit te schakelen, verbreek eerst de koppeling met de Uber for Business-integratie.', description: 'Weet je zeker dat je deze integratie wilt ontkoppelen?', - confirmText: 'Begrepen', }, workflowWarningModal: { featureEnabledTitle: 'Niet zo snel...', @@ -6884,14 +6867,12 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ subtitle: 'Stel een factureerbaar uurtarief in voor tijdregistratie.', 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.', disabledTitle: 'Niet zo snel...', disabledMessage: 'Om deze functie in of uit te schakelen, moet je je boekhoudimportinstellingen wijzigen.', }, - recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Om Werving uit te schakelen, koppel ${integration} eerst los van deze workspace.`}, }, reports: { reportsCustomTitleExamples: 'Voorbeelden:', @@ -7357,7 +7338,6 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, accounting: { settings: 'instellingen', title: 'Verbindingen', - subtitle: 'Verbind je boekhoudsoftware voor een automatische synchronisatie.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7369,10 +7349,6 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Chat met je accountmanager.', - talkYourAccountManager: 'Chat met je accountmanager.', - talkToConcierge: 'Chat met Concierge.', - needAnotherAccounting: 'Nog een boekhoudprogramma nodig?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7413,7 +7389,6 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, disconnect: 'Verbinding verbreken', reinstall: 'Connector opnieuw installeren', disconnectTitle: (connectionName = 'integratie') => `Verbinding met ${connectionName} verbreken`, - connectTitle: (connectionName) => `Verbind ${connectionName}`, syncError: (connectionName) => `Kan geen verbinding maken met ${connectionName}`, accounts: 'Rekeningschema', taxes: 'Belastingen', @@ -7430,12 +7405,10 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Standaard NetSuite-medewerker', }, disconnectPrompt: (connectionName = 'deze integratie') => `Weet je zeker dat je ${connectionName} wilt ontkoppelen?`, - connectPrompt: (connectionName) => `Weet je zeker dat je ${connectionName} wilt koppelen? Hierdoor worden alle bestaande boekhoudkundige koppelingen verwijderd.`, enterCredentials: 'Voer je inloggegevens in', reconnect: 'Opnieuw verbinden', updateCredentials: 'Inloggegevens bijwerken', claimOffer: { - badgeText: 'Aanbieding beschikbaar!', xero: { headline: 'Krijg Xero 6 maanden gratis!', description: 'Nieuw bij Xero? Expensify-klanten krijgen 6 maanden gratis. Claim jouw aanbieding hieronder.', @@ -8889,10 +8862,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, subscriptions: 'Abonnementen', }, hr: { - title: 'HR', connectionsSubtitle: 'Verbind met je HR-systeem om werknemersgegevens te synchroniseren, terugbetalingen automatisch aan de juiste personen te koppelen en de uitgaven van je team nauwkeurig te houden zonder handmatig werk.', - subtitle: 'Koppel HR-tools en houd goedkeuringen van medewerkers gesynchroniseerd.', alreadyConnectedTitle: 'Kan niet met meerdere HR-platformen verbinden', alreadyConnectedPrompt: 'Je moet je huidige HR-platform loskoppelen voordat je een ander kunt verbinden.', connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`, @@ -8955,7 +8926,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, }, recruiting: { - title: 'Werving', connectionsSubtitle: 'Koppel je recruitmentsysteem om kandidaatgegevens te synchroniseren, vergoedingen automatisch aan de juiste personen te koppelen en de uitgaven van je team nauwkeurig te houden zonder handmatig werk.', alreadyConnectedTitle: 'Kan niet met meerdere ATS-platformen verbinden', @@ -8963,7 +8933,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, syncing: 'Kandidaten synchroniseren', setupIncomplete: (setupLink: string | undefined) => `Verbonden. ${setupLink ? `Installatie voltooien` : 'Voltooi installatie'} om kandidaten te importeren.`, - dontSeeYourATS: `Zie je je ATS hier niet? Vraag het aan Concierge en we kunnen het toevoegen.`, importSettings: 'Importinstellingen', defaultApprover: 'Standaardgoedkeurder', approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coördinator werving'}, @@ -8989,7 +8958,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, enableJobStagesOrTags: 'Selecteer minstens één functiestap of tag om door te gaan', }, - subtitle: 'Koppel wervingstools en houd kandidaategoedkeuringen gesynchroniseerd.', syncResults: { importedCount: () => ({ one: '1 kandidaat', @@ -9008,9 +8976,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, }, merge: { - connections: 'Verbindingen', - connect: 'Verbinden', - findIntegration: 'Integratie zoeken', syncNow: 'Nu synchroniseren', disconnect: 'Ontkoppelen', disconnectTitle: (providerName: string) => `${providerName} ontkoppelen`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f69a837fffba..550240f23fb8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3642,7 +3642,7 @@ ${amount} dla ${merchant} - ${date}`, 1. Kliknij *Workspaces*. 2. Wybierz swoją przestrzeń roboczą. - 3. Kliknij *Accounting*. + 3. Kliknij *Połączenia*. 4. Znajdź ${integrationName}. 5. Kliknij *Connect*. @@ -5033,22 +5033,14 @@ ${amount} dla ${merchant} - ${date}`, description: 'Świetna wiadomość 🎉. Skontaktuj się z nami, jeśli potrzebują pomocy przy konfiguracji.', }, mcp: { - connectors: 'Konektory', - connectorsSubtitle: 'Połącz asystenta AI ze swoim kontem Expensify.', - connect: 'Połącz', - helpPrompt: 'Potrzebujesz pomocy z połączeniem?', - helpLink: 'Przeczytaj nasz przewodnik.', claude: { title: 'Claude', - subtitle: 'od Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'od Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'od OpenAI', }, }, connections: { @@ -6642,10 +6634,6 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy title: 'Organizuj', subtitle: 'Grupuj i analizuj wydatki, zapisuj każdy zapłacony podatek.', }, - integrateSection: { - title: 'Integruj', - subtitle: 'Połącz Expensify z popularnymi produktami finansowymi.', - }, distanceRates: { title: 'Stawki za dystans', subtitle: 'Dodawaj, aktualizuj i egzekwuj stawki.', @@ -6877,23 +6865,18 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy }, mcp: { title: 'MCP', - subtitle: 'Połącz asystenta AI ze swoim kontem Expensify.', }, receiptPartners: { title: 'Partnerzy paragonów', - subtitle: 'Automatycznie importuj paragony.', }, connectionsWarningModal: { featureEnabledTitle: 'Nie tak szybko...', featureEnabledText: 'Aby włączyć lub wyłączyć tę funkcję, musisz zmienić ustawienia importu księgowego.', - disconnectText: 'Aby wyłączyć księgowość, musisz odłączyć połączenie księgowe od swojego obszaru roboczego.', manageSettings: 'Zarządzaj ustawieniami', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Odłącz Uber', - disconnectText: 'Aby wyłączyć tę funkcję, najpierw odłącz integrację Uber for Business.', description: 'Czy na pewno chcesz odłączyć tę integrację?', - confirmText: 'Rozumiem', }, workflowWarningModal: { featureEnabledTitle: 'Nie tak szybko...', @@ -6910,14 +6893,12 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy subtitle: 'Ustaw godzinową stawkę rozliczeniową do śledzenia czasu.', 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.', disabledTitle: 'Nie tak szybko...', disabledMessage: 'Aby włączyć lub wyłączyć tę funkcję, musisz zmienić ustawienia importu księgowego.', }, - recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Aby wyłączyć Rekrutację, najpierw odłącz ${integration} od tego workspace.`}, }, reports: { reportsCustomTitleExamples: 'Przykłady:', @@ -7383,7 +7364,6 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, accounting: { settings: 'ustawienia', title: 'Połączenia', - subtitle: 'Podłącz swoje oprogramowanie księgowe, aby włączyć automatyczną synchronizację.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7395,10 +7375,6 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Porozmawiaj ze swoim opiekunem klienta.', - talkYourAccountManager: 'Porozmawiaj ze swoim opiekunem konta.', - talkToConcierge: 'Czat z Concierge.', - needAnotherAccounting: 'Potrzebujesz innego programu księgowego?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7439,7 +7415,6 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, disconnect: 'Rozłącz', reinstall: 'Zainstaluj ponownie konektor', disconnectTitle: (connectionName = 'integracja') => `Odłącz ${connectionName}`, - connectTitle: (connectionName) => `Połącz ${connectionName}`, syncError: (connectionName) => `Nie można połączyć się z ${connectionName}`, accounts: 'Plan kont', taxes: 'Podatki', @@ -7456,12 +7431,10 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Domyślny pracownik NetSuite', }, disconnectPrompt: (connectionName = 'ta integracja') => `Czy na pewno chcesz odłączyć ${connectionName}?`, - connectPrompt: (connectionName) => `Czy na pewno chcesz połączyć ${connectionName}? Spowoduje to usunięcie wszystkich istniejących połączeń księgowych.`, enterCredentials: 'Wprowadź swoje dane logowania', reconnect: 'Połącz ponownie', updateCredentials: 'Zaktualizuj dane logowania', claimOffer: { - badgeText: 'Oferta dostępna!', xero: { headline: 'Korzystaj z Xero za darmo przez 6 miesięcy!', description: 'Nowy w Xero? Klienci Expensify otrzymują 6 miesięcy za darmo. Odbierz swoją ofertę poniżej.', @@ -8910,10 +8883,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, subscriptions: 'Subskrypcje', }, hr: { - title: 'HR', connectionsSubtitle: 'Połącz się ze swoim systemem HR, aby synchronizować dane pracowników, automatycznie dopasowywać zwroty do właściwych osób i utrzymywać wydatki zespołu w porządku bez ręcznej pracy.', - subtitle: 'Połącz narzędzia HR i utrzymuj zgody pracowników w synchronizacji.', alreadyConnectedTitle: 'Nie można połączyć się z wieloma platformami HR', alreadyConnectedPrompt: 'Musisz odłączyć swoją obecną platformę HR, zanim podłączysz inną.', connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`, @@ -8976,7 +8947,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, }, recruiting: { - title: 'Rekrutacja', connectionsSubtitle: 'Połącz się ze swoim systemem rekrutacyjnym, aby synchronizować dane kandydatów, automatycznie dopasowywać zwroty kosztów do właściwych osób i utrzymywać poprawność wydatków zespołu bez ręcznej pracy.', alreadyConnectedTitle: 'Nie można połączyć się z wieloma platformami ATS', @@ -8984,7 +8954,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, syncing: 'Synchronizowanie kandydatów', setupIncomplete: (setupLink: string | undefined) => `Połączono. ${setupLink ? `Dokończ konfigurację` : 'Zakończ konfigurację'}, aby zaimportować kandydatów.`, - dontSeeYourATS: `Nie widzisz tutaj swojego ATS? Zapytaj Concierge, a my możemy go dodać.`, importSettings: 'Ustawienia importu', defaultApprover: 'Domyślny zatwierdzający', approverFields: {recruiter: 'Rekruter', recruitingCoordinator: 'Koordynator rekrutacji'}, @@ -9005,7 +8974,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, offices: {title: 'Biuro', description: 'Wybierz biura kandydatów, które chcesz zsynchronizować z tym miejscem pracy', toggleTitle: 'Biura', allSelected: 'Wszystkie biura'}, enableJobStagesOrTags: 'Wybierz co najmniej jeden etap zadania lub znacznik, aby kontynuować', }, - subtitle: 'Połącz narzędzia rekrutacyjne i utrzymuj zgody kandydatów w synchronizacji.', syncResults: { importedCount: () => ({ one: '1 kandydat', @@ -9024,9 +8992,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, }, merge: { - connections: 'Połączenia', - connect: 'Połącz', - findIntegration: 'Znajdź integrację', syncNow: 'Synchronizuj teraz', disconnect: 'Odłącz', disconnectTitle: (providerName: string) => `Odłącz ${providerName}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 83866060d5d2..7fed108904a3 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3599,7 +3599,7 @@ ${amount} para ${merchant} - ${date}`, 1. Clique em *Workspaces*. 2. Selecione seu workspace. - 3. Clique em *Accounting*. + 3. Clique em *Conexões*. 4. Encontre ${integrationName}. 5. Clique em *Connect*. @@ -4993,22 +4993,14 @@ ${amount} para ${merchant} - ${date}`, description: 'Ótimas notícias 🎉. Entre em contato conosco se precisarem de ajuda com a configuração.', }, mcp: { - connectors: 'Conectores', - connectorsSubtitle: 'Conecte um assistente de IA à sua conta do Expensify.', - connect: 'Conectar', - helpPrompt: 'Precisa de ajuda para conectar?', - helpLink: 'Leia nosso guia.', claude: { title: 'Claude', - subtitle: 'da Anthropic', }, cursor: { title: 'Cursor', - subtitle: 'da Anysphere', }, chatgpt: { title: 'ChatGPT', - subtitle: 'da OpenAI', }, }, connections: { @@ -6616,10 +6608,6 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS title: 'Organizar', subtitle: 'Agrupe e analise os gastos, registre todos os impostos pagos.', }, - integrateSection: { - title: 'Integrar', - subtitle: 'Conecte o Expensify a produtos financeiros populares.', - }, distanceRates: { title: 'Taxas de distância', subtitle: 'Adicione, atualize e aplique tarifas.', @@ -6853,23 +6841,18 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS }, mcp: { title: 'MCP', - subtitle: 'Conecte um assistente de IA à sua conta do Expensify.', }, receiptPartners: { title: 'Parceiros de recibos', - subtitle: 'Importe recibos automaticamente.', }, connectionsWarningModal: { featureEnabledTitle: 'Não tão rápido...', featureEnabledText: 'Para ativar ou desativar este recurso, você precisará alterar suas configurações de importação contábil.', - disconnectText: 'Para desativar a contabilidade, você precisará desconectar sua conexão contábil do seu workspace.', manageSettings: 'Gerenciar configurações', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Desconectar Uber', - disconnectText: 'Para desativar este recurso, desconecte primeiro a integração do Uber for Business.', description: 'Tem certeza de que deseja desconectar esta integração?', - confirmText: 'Entendi', }, workflowWarningModal: { featureEnabledTitle: 'Não tão rápido...', @@ -6886,14 +6869,12 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS subtitle: 'Defina uma taxa horária faturável para o controle de tempo.', 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.', disabledTitle: 'Calma lá...', disabledMessage: 'Para ativar ou desativar este recurso, você precisará alterar suas configurações de importação contábil.', }, - recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para desativar Recrutamento, primeiro desconecte ${integration} deste workspace.`}, }, reports: { reportsCustomTitleExamples: 'Exemplos:', @@ -7359,7 +7340,6 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, accounting: { settings: 'configurações', title: 'Conexões', - subtitle: 'Conecte seu software de contabilidade para uma sincronização automática.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7371,10 +7351,6 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: 'Converse com seu executivo de contas.', - talkYourAccountManager: 'Converse com seu gerente de conta.', - talkToConcierge: 'Converse com o Concierge.', - needAnotherAccounting: 'Precisa de outro software de contabilidade?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7415,7 +7391,6 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, disconnect: 'Desconectar', reinstall: 'Reinstalar conector', disconnectTitle: (connectionName = 'integração') => `Desconectar ${connectionName}`, - connectTitle: (connectionName) => `Conectar ${connectionName}`, syncError: (connectionName) => `Não é possível conectar ao ${connectionName}`, accounts: 'Plano de contas', taxes: 'Impostos', @@ -7432,12 +7407,10 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Padrão de funcionário do NetSuite', }, disconnectPrompt: (connectionName = 'esta integração') => `Tem certeza de que deseja desconectar ${connectionName}?`, - connectPrompt: (connectionName) => `Tem certeza de que deseja conectar ${connectionName}? Isso removerá quaisquer conexões contábeis existentes.`, enterCredentials: 'Insira suas credenciais', reconnect: 'Reconectar', updateCredentials: 'Atualizar credenciais', claimOffer: { - badgeText: 'Oferta disponível!', xero: { headline: 'Use Xero grátis por 6 meses!', description: 'Novo no Xero? Clientes Expensify ganham 6 meses grátis. Resgate sua oferta abaixo.', @@ -8892,10 +8865,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, subscriptions: 'Assinaturas', }, hr: { - title: 'RH', connectionsSubtitle: 'Conecte-se ao seu sistema de RH para sincronizar os dados de funcionários, associar automaticamente os reembolsos às pessoas corretas e manter as despesas da sua equipe precisas, sem trabalho manual.', - subtitle: 'Conecte ferramentas de RH e mantenha as aprovações de funcionários em sincronia.', alreadyConnectedTitle: 'Não é possível conectar a várias plataformas de RH', alreadyConnectedPrompt: 'Você precisa desconectar sua plataforma de RH atual antes de conectar outra.', connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`, @@ -8958,7 +8929,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, }, recruiting: { - title: 'Recrutamento', connectionsSubtitle: 'Conecte-se ao seu sistema de recrutamento para sincronizar os dados de candidatos, associar automaticamente os reembolsos às pessoas certas e manter as despesas da sua equipe precisas sem trabalho manual.', alreadyConnectedTitle: 'Não é possível conectar a várias plataformas de ATS', @@ -8966,7 +8936,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, syncing: 'Sincronizando candidatos', setupIncomplete: (setupLink: string | undefined) => `Conectado. ${setupLink ? `Concluir configuração` : 'Concluir configuração'} para importar candidatos.`, - dontSeeYourATS: `Não está vendo seu ATS aqui? Pergunte ao Concierge e podemos adicioná-lo.`, importSettings: 'Importar configurações', defaultApprover: 'Aprovador padrão', approverFields: {recruiter: 'Recrutador', recruitingCoordinator: 'Coordenador de recrutamento'}, @@ -8992,7 +8961,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, enableJobStagesOrTags: 'Selecione pelo menos um estágio de trabalho ou etiqueta para continuar', }, - subtitle: 'Conecte ferramentas de recrutamento e mantenha as aprovações de candidatos em sincronia.', syncResults: { importedCount: () => ({ one: '1 candidato', @@ -9011,9 +8979,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, }, merge: { - connections: 'Conexões', - connect: 'Conectar', - findIntegration: 'Encontrar integração', syncNow: 'Sincronizar agora', disconnect: 'Desconectar', disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index b6df99cf3298..ae2d808fccea 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3493,7 +3493,7 @@ ${amount},商户:${merchant} - 日期:${date}`, 1. 点击 *Workspaces*。 2. 选择您的工作区。 - 3. 点击 *Accounting*。 + 3. 点击 *连接*。 4. 找到 ${integrationName}。 5. 点击 *Connect*。 @@ -4851,22 +4851,14 @@ ${amount},商户:${merchant} - 日期:${date}`, description: '好消息 🎉。如果他们在设置方面需要帮助,请联系我们。', }, mcp: { - connectors: '连接器', - connectorsSubtitle: '将 AI 助手连接到您的 Expensify 账户。', - connect: '连接', - helpPrompt: '需要帮助连接吗?', - helpLink: '阅读我们的指南。', claude: { title: 'Claude', - subtitle: '由 Anthropic 提供', }, cursor: { title: 'Cursor', - subtitle: '由 Anysphere 提供', }, chatgpt: { title: 'ChatGPT', - subtitle: '由 OpenAI 提供', }, }, connections: { @@ -6403,10 +6395,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM title: '整理', subtitle: '分组并分析支出,记录每一笔已缴税款。', }, - integrateSection: { - title: '集成', - subtitle: '将 Expensify 连接到常用的金融产品。', - }, distanceRates: { title: '距离费率', subtitle: '添加、更新并执行费率。', @@ -6612,23 +6600,18 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM }, mcp: { title: 'MCP', - subtitle: '将 AI 助手连接到您的 Expensify 账户。', }, receiptPartners: { title: '收据合作伙伴', - subtitle: '自动导入收据。', }, connectionsWarningModal: { featureEnabledTitle: '别急……', featureEnabledText: '若要启用或停用此功能,您需要更改会计导入设置。', - disconnectText: '要停用会计功能,您需要从工作区中断开您的会计连接。', manageSettings: '管理设置', }, receiptPartnersWarningModal: { featureEnabledTitle: '断开与 Uber 的连接', - disconnectText: '若要禁用此功能,请先断开 Uber for Business 集成。', description: '确定要断开与此集成的连接吗?', - confirmText: '明白了', }, workflowWarningModal: { featureEnabledTitle: '别急……', @@ -6644,14 +6627,12 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM subtitle: '为时间跟踪设置可计费的小时费率。', defaultHourlyRate: '默认时薪', }, - hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `若要禁用人力资源功能,请先将此工作区与 ${integration} 断开连接。`}, vendors: { title: '供应商', subtitle: '将卡片报销与从您的会计软件导入的供应商进行匹配。', disabledTitle: '别急...', disabledMessage: '要启用或禁用此功能,您需要更改会计导入设置。', }, - recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `若要停用招聘功能,请先将此工作区与 ${integration} 断开连接。`}, }, reports: { reportsCustomTitleExamples: '示例:', @@ -7103,7 +7084,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM accounting: { settings: '设置', title: '连接', - subtitle: '连接您的会计软件以自动同步。', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7115,10 +7095,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', - talkYourOnboardingSpecialist: '与您的客户经理聊天。', - talkYourAccountManager: '与您的客户经理聊天。', - talkToConcierge: '与 Concierge 聊天。', - needAnotherAccounting: '需要其他会计软件吗?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7158,7 +7134,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM disconnect: '断开连接', reinstall: '重新安装连接器', disconnectTitle: (connectionName = '集成') => `断开连接 ${connectionName}`, - connectTitle: (connectionName) => `连接 ${connectionName}`, syncError: (connectionName) => `无法连接到 ${connectionName}`, accounts: '会计科目表', taxes: '税费', @@ -7175,12 +7150,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite 员工默认值', }, disconnectPrompt: (connectionName = '此集成') => `你确定要断开与 ${connectionName} 的连接吗?`, - connectPrompt: (connectionName) => `确定要连接 ${connectionName} 吗?这将删除所有现有的会计连接。`, enterCredentials: '请输入您的凭证', reconnect: '重新连接', updateCredentials: '更新凭证', claimOffer: { - badgeText: '优惠可用!', xero: { headline: '免费使用 Xero 长达 6 个月!', description: '第一次使用 Xero?Expensify 客户可享受 6 个月免费试用。请在下方领取优惠。', @@ -8574,9 +8547,7 @@ ${reportName}`, subscriptions: '订阅', }, hr: { - title: '人力资源', connectionsSubtitle: '连接您的 HR 系统以同步员工数据,自动将报销匹配到正确的员工,并在无需手动操作的情况下保持团队报销的准确性。', - subtitle: '连接人力资源工具,保持员工审批同步。', alreadyConnectedTitle: '无法连接到多个人力资源平台', alreadyConnectedPrompt: '在连接其他人力资源平台之前,您必须先断开当前的人力资源平台。', connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`, @@ -8637,14 +8608,12 @@ ${reportName}`, }, }, recruiting: { - title: '招聘', connectionsSubtitle: '连接招聘系统以同步候选人数据,自动将报销匹配到正确的人员,并在无需手动操作的情况下保持团队报销数据的准确。', alreadyConnectedTitle: '无法连接到多个 ATS 平台', alreadyConnectedPrompt: '在连接其他 ATS 之前,必须先断开当前的 ATS。', syncing: '正在同步候选人', setupIncomplete: (setupLink: string | undefined) => `已连接。${setupLink ? `完成设置` : '完成设置'} 个可导入候选项。`, - dontSeeYourATS: `没有看到你的 ATS?询问 Concierge,我们可以帮你添加。`, importSettings: '导入设置', defaultApprover: '默认审批人', approverFields: {recruiter: '招聘人员', recruitingCoordinator: '招聘协调员'}, @@ -8660,7 +8629,6 @@ ${reportName}`, offices: {title: '办公室', description: '选择你想与此工作区同步的候选人办公地点', toggleTitle: '办公室', allSelected: '所有办公室'}, enableJobStagesOrTags: '请选择至少一个工作阶段或标签以继续', }, - subtitle: '连接招聘工具并保持候选人审批同步。', syncResults: { importedCount: () => ({ one: '1 名候选人', @@ -8675,9 +8643,6 @@ ${reportName}`, approvalModeDescriptions: {basic: '选择单个审批人', advanced: `候选人的招聘人员或协调人将成为其报销审批人`, custom: '在 Expensify 中手动设置审批人'}, }, merge: { - connections: '连接', - connect: '连接', - findIntegration: '查找集成', syncNow: '立即同步', disconnect: '断开连接', disconnectTitle: (providerName: string) => `断开 ${providerName}`, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c15230f8ae1d..e9370ef3ec07 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -441,7 +441,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/Contacts/SetDefaultContactMethodConfirmValidateCodePage').default, [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: () => require('../../../../pages/settings/Preferences/PriorityModePage').default, - [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, [SCREENS.WORKSPACE.CONNECTIONS_ACCOUNTING]: () => require('../../../../pages/workspace/connections/ConnectionsAccountingPage').default, [SCREENS.WORKSPACE.CONNECTIONS_HR]: () => require('../../../../pages/workspace/connections/ConnectionsHRPage').default, [SCREENS.WORKSPACE.CONNECTIONS_RECRUITING]: () => require('../../../../pages/workspace/connections/ConnectionsRecruitingPage').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index c6acf30a7184..3abb5b8b1b27 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -25,9 +25,9 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default, [SCREENS.WORKSPACE.AGENTS]: () => require('../../../../pages/workspace/WorkspaceAgentsPage').default, [SCREENS.WORKSPACE.ROOMS]: () => require('../../../../pages/workspace/rooms/WorkspaceRoomsPage').default, - [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default, - [SCREENS.WORKSPACE.HR]: () => require('../../../../pages/workspace/hr/WorkspaceHRPage').default, - [SCREENS.WORKSPACE.RECRUITING]: () => require('../../../../pages/workspace/recruiting/WorkspaceRecruitingPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, + [SCREENS.WORKSPACE.HR]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, + [SCREENS.WORKSPACE.RECRUITING]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, [SCREENS.WORKSPACE.CONNECTIONS]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default, [SCREENS.WORKSPACE.VENDORS]: () => require('../../../../pages/workspace/vendors/WorkspaceVendorsPage').default, @@ -38,8 +38,8 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default, [SCREENS.WORKSPACE.PER_DIEM]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemPage').default, - [SCREENS.WORKSPACE.MCP]: () => require('../../../../pages/workspace/mcp/WorkspaceMCPPage').default, - [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: () => require('../../../../pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage').default, + [SCREENS.WORKSPACE.MCP]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, + [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../pages/workspace/travel/PolicyTravelPage').default, [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPageRevamp').default, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3774fd074252..275658de61e3 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1078,6 +1078,7 @@ type WorkspaceSplitNavigatorParamList = { integrationToDisconnect?: ConnectionName; shouldDisconnectIntegrationBeforeConnecting?: boolean; isIntuitEnterpriseSuite?: string; + tab?: ValueOf; }; [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE]: { policyID: string; diff --git a/src/libs/PolicyUtils/features.ts b/src/libs/PolicyUtils/features.ts index ef57eeeaa58c..aff902544993 100644 --- a/src/libs/PolicyUtils/features.ts +++ b/src/libs/PolicyUtils/features.ts @@ -103,4 +103,4 @@ function getDefaultTimeTrackingRate(policy: Partial>): number return policy?.units?.time?.rate !== undefined ? convertToBackendAmount(policy.units.time.rate) : undefined; } -export {isAttendeeTrackingEnabled, arePolicyRulesEnabled, isInvoiceFieldsEnabled, isPolicyFeatureEnabled, isTimeTrackingEnabled, isMCPEnabled, getDefaultTimeTrackingRate}; +export {isAttendeeTrackingEnabled, arePolicyRulesEnabled, isInvoiceFieldsEnabled, isPolicyFeatureEnabled, isTimeTrackingEnabled, getDefaultTimeTrackingRate}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 834be88b6d9e..8e6569efeda6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13339,7 +13339,7 @@ function prepareOnboardingOnyxData({ workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID)}`, workspaceConfirmationLink: `${environmentURL}/${createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CONFIRMATION.path, ROUTES.WORKSPACES_LIST.route)}`, testDriveURL: `${environmentURL}/${testDriveURL}`, - workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID)}`, + workspaceAccountingLink: `${environmentURL}/${ROUTES.WORKSPACE_CONNECTIONS.getRoute(onboardingPolicyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)}`, corporateCardLink: `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(onboardingPolicyID)}`, companyDomain: companyDomain ?? '', workEmail: workEmail ?? '', diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 19f30907477e..c0fa09d74884 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -8244,8 +8244,6 @@ export { enablePolicyConnections, enablePolicyHR, enablePolicyRecruiting, - enablePolicyMCP, - enablePolicyReceiptPartners, enablePolicyFeatureForConnection, enablePolicyReportFields, enablePolicyInvoiceFields, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a25b2c687b20..fb1e5e4c1ee0 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -12,7 +12,6 @@ import useCardFeedErrors from '@hooks/useCardFeedErrors'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; import useHasApprovalWorkflowWithNonMemberApprover from '@hooks/useHasApprovalWorkflowWithNonMemberApprover'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useIsWorkspacesTabFocused from '@hooks/useIsWorkspacesTabFocused'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -82,7 +81,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const {translate} = useLocalize(); const {convertToDisplayString} = useCurrencyListActions(); const {isBetaEnabled} = usePermissions(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const isFocused = useIsFocused(); const isWorkspacesTabFocused = useIsWorkspacesTabFocused(); @@ -203,9 +201,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac shouldShowRBR, hasApprovalWorkflowWithNonMemberApprover, isVendorMatchingBetaEnabled: isBetaEnabled(CONST.BETAS.VENDOR_MATCHING), - isRecruitingBetaEnabled: isBetaEnabled(CONST.BETAS.MERGE_ATS), isCompanyAgentsBetaEnabled: isBetaEnabled(CONST.BETAS.COMPANY_AGENTS), - isUnifiedConnectionsBetaEnabled, convertToDisplayString, }).map((item) => ({ ...item, diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 28079f9de2bb..827ff86a500f 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -8,8 +8,6 @@ import useCardFeeds from '@hooks/useCardFeeds'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDefaultFundID from '@hooks/useDefaultFundID'; -import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -25,10 +23,6 @@ import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {enablePolicyTravel} from '@libs/actions/Policy/Travel'; import {filterInactiveCards, getAllCardsForWorkspace, getCardSettings, getCompanyFeeds, isSmartLimitEnabled as isSmartLimitEnabledUtil} from '@libs/CardUtils'; -import {getLatestErrorField} from '@libs/ErrorUtils'; -import {getConnectedHRProvider, isAnyHRConnected, isGustoConnected, isZenefitsConnected} from '@libs/merge/HRUtils'; -import {isMergeConnected} from '@libs/merge/MergeUtils'; -import {getConnectedATSProvider, isAnyRecruitingConnected} from '@libs/merge/RecruitingUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; @@ -40,12 +34,9 @@ import { getDistanceRateCustomUnit, getPerDiemCustomUnit, hasAccountingConnections, - hasAccountingFeatureConnection, hasVendorFeature, isControlPolicy, - isMCPEnabled, isPerDiemEnabled, - isPolicyFeatureEnabled, isPolicyTaxEnabled, isTimeTrackingEnabled, tryNavigateToSubmitWorkspaceUpgrade, @@ -53,7 +44,6 @@ import { import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -61,15 +51,9 @@ import {enablePolicyCategories} from '@userActions/Policy/Category'; import {enablePolicyDistanceRates} from '@userActions/Policy/DistanceRate'; import {enablePerDiem} from '@userActions/Policy/PerDiem'; import { - clearPolicyErrorField, enableCompanyCards, enableExpensifyCard, - enablePolicyConnections, - enablePolicyHR, enablePolicyInvoicing, - enablePolicyRecruiting, - enablePolicyMCP, - enablePolicyReceiptPartners, enablePolicyRules, enablePolicyTaxes, enablePolicyTimeTracking, @@ -98,17 +82,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate} = useLocalize(); - const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions(); + const {isBetaEnabledOrUnknown} = usePermissions(); // Undefined until the betas load. The action calls below need that distinction, the UI gating just treats it as off const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {showConfirmModal} = useConfirmModal(); - const isRecruitingBetaEnabled = isBetaEnabled(CONST.BETAS.MERGE_ATS); - // Connections replaces this section's pages, and turns each feature on as its first integration connects - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const illustrations = useMemoizedLazyIllustrations([ 'FolderOpen', - 'Accounting', 'CompanyCard', 'Workflows', 'InvoiceBlue', @@ -121,21 +101,15 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro 'Luggage', 'Car', 'Gears', - 'ReceiptPartners', 'Clock', - 'Members', - 'AiAutomation', - 'NewUser', ]); const policyID = policy?.id; const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasAccountingConnection = hasAccountingConnections(policy); - const isAccountingEnabled = !!policy?.areConnectionsEnabled || hasAccountingFeatureConnection(policy); const perDiemCustomUnit = getPerDiemCustomUnit(policy); const distanceRateCustomUnit = getDistanceRateCustomUnit(policy); - const isUberConnected = useIsPolicyConnectedToUberReceiptPartner({policyID}); const [cardFeeds] = useCardFeeds(policyID); const policyData = usePolicyData(policyID); const {categories: policyCategories} = policyData; @@ -219,7 +193,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); + Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)); }; const warnVendorsManagedByAccounting = async () => { @@ -231,66 +205,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }); }; - const warnDisconnectAccountingFirst = async () => { - if (!hasAccountingConnection || !policyID) { - return; - } - const {action} = await showConfirmModal({ - title: translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledTitle'), - prompt: translate('workspace.moreFeatures.connectionsWarningModal.disconnectText'), - confirmText: translate('workspace.moreFeatures.connectionsWarningModal.manageSettings'), - cancelText: translate('common.cancel'), - }); - if (action !== ModalActions.CONFIRM) { - return; - } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); - }; - - const warnReceiptPartnersStillConnected = async () => { - if (!isUberConnected) { - return; - } - await showConfirmModal({ - title: translate('workspace.moreFeatures.receiptPartnersWarningModal.featureEnabledTitle'), - prompt: translate('workspace.moreFeatures.receiptPartnersWarningModal.disconnectText'), - confirmText: translate('workspace.moreFeatures.receiptPartnersWarningModal.confirmText'), - shouldShowCancelButton: false, - }); - }; - - const warnDisconnectHRFirst = async () => { - if (!isAnyHRConnected(policy)) { - return; - } - let integration = ''; - if (isZenefitsConnected(policy)) { - integration = translate('workspace.hr.zenefits.title'); - } else if (isGustoConnected(policy)) { - integration = translate('workspace.hr.gusto.title'); - } else if (isMergeConnected(policy, CONST.POLICY.CONNECTIONS.NAME.MERGE_HR)) { - integration = getConnectedHRProvider(policy)?.displayName ?? ''; - } - await showConfirmModal({ - title: translate('workspace.distanceRates.oopsNotSoFast'), - prompt: translate('workspace.moreFeatures.hrWarningModal.disconnectText', {integration}), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - }; - - const warnDisconnectRecruitingFirst = async () => { - if (!isAnyRecruitingConnected(policy)) { - return; - } - await showConfirmModal({ - title: translate('workspace.distanceRates.oopsNotSoFast'), - prompt: translate('workspace.moreFeatures.recruitingWarningModal.disconnectText', {integration: getConnectedATSProvider(policy)?.displayName ?? ''}), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - }; - const promptDisableExpensifyCardViaConcierge = async () => { const {action} = await showConfirmModal({ title: translate('workspace.moreFeatures.expensifyCard.disableCardTitle'), @@ -384,156 +298,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro {translate('workspace.moreFeatures.subtitle')} - {!isUnifiedConnectionsBetaEnabled && ( - - { - if (!policyID) { - return; - } - if (tryNavigateToSubmitWorkspaceUpgrade(policy, isEnabled, CONST.UPGRADE_FEATURE_INTRO_MAPPING.accounting.alias)) { - return; - } - enablePolicyConnections(policyID, isEnabled); - }} - errors={getLatestErrorField(policy ?? {}, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED)} - onCloseError={() => { - if (!policyID) { - return; - } - clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); - }} - onPress={() => { - if (!policyID) { - return; - } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); - }} - /> - { - if (!policyID) { - return; - } - if (isEnabled && !isControlPolicy(policy)) { - Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.hr.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)), - ); - return; - } - enablePolicyHR(policyID, isEnabled); - }} - onPress={() => { - if (!policyID) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_HR.getRoute(policyID)); - }} - /> - { - if (!policyID) { - return; - } - enablePolicyReceiptPartners(policyID, isEnabled); - }} - errors={getLatestErrorField(policy ?? {}, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED)} - onCloseError={() => { - if (!policyID) { - return; - } - clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); - }} - onPress={() => { - if (!policyID) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policyID)); - }} - /> - {isRecruitingBetaEnabled && ( - { - if (!policyID) { - return; - } - if (isEnabled && !isControlPolicy(policy)) { - Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute( - policyID, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.recruiting.alias, - ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID), - ), - ); - return; - } - enablePolicyRecruiting(policyID, isEnabled); - }} - onPress={() => { - if (!policyID) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_RECRUITING.getRoute(policyID)); - }} - /> - )} - { - if (!policyID) { - return; - } - enablePolicyMCP(policyID, isEnabled); - }} - onPress={() => { - if (!policyID) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_MCP.getRoute(policyID)); - }} - /> - - )} - (); const {translate} = useLocalize(); const {showConfirmModal, closeModalByID} = useConfirmModal(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const policyID = policy?.id; // `removePolicyConnection` only runs once the user confirms, which can be a while after the flow started, so the @@ -122,14 +120,7 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider return; } - if ( - tryNavigateToSubmitWorkspaceUpgrade( - policy, - true, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.accounting.alias, - isUnifiedConnectionsBetaEnabled ? ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID) : undefined, - ) - ) { + if (tryNavigateToSubmitWorkspaceUpgrade(policy, true, CONST.UPGRADE_FEATURE_INTRO_MAPPING.accounting.alias, ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))) { return; } @@ -152,14 +143,11 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider cardFeeds, cardList: cardLists, isIntuitEnterpriseSuiteOverride: newActiveIntegration.isIntuitEnterpriseSuite, - isUnifiedConnectionsBetaEnabled, }, ); // Connections has no feature toggle, so turn it on here. The Control upgrade only enables the integration's own feature. - if (isUnifiedConnectionsBetaEnabled) { - enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); - } + enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); const workspaceUpgradeNavigationDetails = accountingIntegrationData?.workspaceUpgradeNavigationDetails; if (workspaceUpgradeNavigationDetails && !isControlPolicy(policy)) { @@ -180,32 +168,17 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider // Mirrors `shouldShowConfirmationModal` below, which keeps `renderActiveIntegration()` from mounting the // setup flow until the user has decided what to do with the connection that has to be disconnected first. - const connectionName = newActiveIntegration.isIntuitEnterpriseSuite - ? translate('workspace.accounting.intuitEnterpriseSuite') - : (CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[newActiveIntegration.name] ?? newActiveIntegration.name); - const modalCopy = isUnifiedConnectionsBetaEnabled - ? { - title: translate('workspace.connections.replaceConnectionTitle'), - prompt: translate('workspace.connections.replaceConnectionPrompt', getAccountingIntegrationDisplayName(policy, integrationToDisconnect, translate)), - confirmText: translate('common.replace'), - buttonVariant: CONST.BUTTON_VARIANT.DANGER, - } - : { - title: translate('workspace.accounting.connectTitle', connectionName), - prompt: translate('workspace.accounting.connectPrompt', connectionName), - confirmText: translate('workspace.accounting.setup'), - buttonVariant: CONST.BUTTON_VARIANT.SUCCESS, - }; - isDisconnectConfirmationPendingRef.current = true; showConfirmModal({ // `startIntegrationFlow` can run more than once for the same flow (the `useFocusEffect` in - // `PolicyAccountingPage` and `WorkspaceConnectionsPage` re-fires whenever `startIntegrationFlow` is - // re-created). A stable id keeps the repeat call updating this prompt in place instead of stacking a - // second copy behind it. + // `WorkspaceConnectionsPage` re-fires whenever `startIntegrationFlow` is re-created). A stable id keeps the + // repeat call updating this prompt in place instead of stacking a second copy behind it. id: ACCOUNTING_CONNECTION_CONFIRMATION_MODAL_ID, - ...modalCopy, + title: translate('workspace.connections.replaceConnectionTitle'), + prompt: translate('workspace.connections.replaceConnectionPrompt', getAccountingIntegrationDisplayName(policy, integrationToDisconnect, translate)), + confirmText: translate('common.replace'), + buttonVariant: CONST.BUTTON_VARIANT.DANGER, cancelText: translate('common.cancel'), }).then((result) => { // A repeat call for the same id is handed back the promise the first call got, so every call's handler @@ -248,7 +221,6 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider accountingIcons, cardFeeds, cardLists, - isUnifiedConnectionsBetaEnabled, ], ); @@ -291,7 +263,6 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider cardFeeds, cardList: cardLists, isIntuitEnterpriseSuiteOverride: activeIntegration.isIntuitEnterpriseSuite, - isUnifiedConnectionsBetaEnabled, }, )?.setupConnectionFlow; }; diff --git a/src/pages/workspace/accounting/ClaimOfferPage.tsx b/src/pages/workspace/accounting/ClaimOfferPage.tsx index 624d931c4e0b..3c8e2a8296e4 100644 --- a/src/pages/workspace/accounting/ClaimOfferPage.tsx +++ b/src/pages/workspace/accounting/ClaimOfferPage.tsx @@ -9,7 +9,6 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -31,7 +30,6 @@ import {enablePolicyFeatureForConnection} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; @@ -47,7 +45,6 @@ type IntegrationConfig = { claimOfferLink?: string; connectButtonText: string; connectionName: string; - featureName: PolicyFeatureName; onConnect: () => void; }; @@ -61,7 +58,6 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { const expensifyIcons = useMemoizedLazyExpensifyIcons(['TreasureChestGreenWithSparkle']); const integrations = policy?.receiptPartners; const {isUberConnected} = useGetReceiptPartnersIntegrationData(policyID); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); const accountingIntegrations = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES; @@ -86,7 +82,6 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { claimOfferLink: CONST.XERO_PARTNER_LINK, connectButtonText: translate('workspace.accounting.claimOffer.xero.connectButton'), connectionName: CONST.POLICY.CONNECTIONS.NAME.XERO, - featureName: CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED, onConnect: () => { startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.XERO}); }, @@ -97,12 +92,9 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { descriptionHtml: translate('workspace.accounting.claimOffer.uber.description'), connectButtonText: translate('workspace.accounting.claimOffer.uber.connectButton'), connectionName: CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER, - featureName: CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED, onConnect: () => { // Connections offers Uber before Receipt partners is on, so the feature is turned on as Uber connects - if (isUnifiedConnectionsBetaEnabled) { - enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); - } + enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); openExternalLink(`${CONST.UBER_CONNECT_URL}?${integrations?.uber?.connectFormData}`); }, }, @@ -152,9 +144,8 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx deleted file mode 100644 index 76519f4b39af..000000000000 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import ActivityIndicator from '@components/ActivityIndicator'; -import Button from '@components/Button'; -import ButtonDisabledWhenOffline from '@components/Button/composed/ButtonDisabledWhenOffline'; -import CollapsibleSection from '@components/CollapsibleSection'; -import FormHelpMessage from '@components/FormHelpMessage'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Icon from '@components/Icon'; -import MenuItem from '@components/MenuItem'; -import MenuItemList from '@components/MenuItemList'; -import type {MenuItemWithLink} from '@components/MenuItemList'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Section from '@components/Section'; -import Text from '@components/Text'; -import TextLink from '@components/TextLink'; -import ThreeDotsMenu from '@components/ThreeDotsMenu'; - -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; - -import getPlatform from '@libs/getPlatform'; -import {hasAccountingConnections, isControlPolicy} from '@libs/PolicyUtils'; - -import Navigation from '@navigation/Navigation'; - -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; -import withPolicyConnections from '@pages/workspace/withPolicyConnections'; - -import {openOldDotLink} from '@userActions/Link'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type {ConnectionName} from '@src/types/onyx/Policy'; - -import {useFocusEffect, useRoute} from '@react-navigation/native'; -import React, {useCallback, useMemo, useRef} from 'react'; -import {View} from 'react-native'; - -import type {MenuItemData, PolicyAccountingPageProps} from './types'; - -import {AccountingContextProvider, useAccountingActions, useAccountingState} from './AccountingContext'; -import useConnectedAccountingIntegration from './useConnectedAccountingIntegration'; - -type RouteParams = { - newConnectionName?: ConnectionName; - integrationToDisconnect?: ConnectionName; - shouldDisconnectIntegrationBeforeConnecting?: boolean; - isIntuitEnterpriseSuite?: string; -}; - -function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { - useWorkspaceDocumentTitle(policy?.name, 'workspace.common.accounting'); - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {popoverAnchorRefs} = useAccountingState(); - const {startIntegrationFlow} = useAccountingActions(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const {isLargeScreenWidth} = useResponsiveLayout(); - const route = useRoute(); - const params = route.params as RouteParams | undefined; - const newConnectionName = params?.newConnectionName; - const integrationToDisconnect = params?.integrationToDisconnect; - const shouldDisconnectIntegrationBeforeConnecting = params?.shouldDisconnectIntegrationBeforeConnecting; - const shouldConnectToIntuitEnterpriseSuite = params?.isIntuitEnterpriseSuite === 'true'; - const policyID = policy?.id; - const icons = useMemoizedLazyExpensifyIcons(['QuestionMark']); - const { - accountingIntegrations, - connectedIntegration, - isConnectedToIntuitEnterpriseSuite, - isSyncInProgress, - hasAccountingConnection, - hasSyncError, - hasUnsupportedNDIntegration, - synchronizationError, - canWriteAccounting, - showReadOnlyModal, - overflowMenu, - connectionDetails, - qboTokenExpiryHint, - oldDotPolicyConnectionsURL, - getAvailableIntegrationData, - } = useConnectedAccountingIntegration(policy, {menuItemWrapperStyle: [styles.sectionMenuItemTopDescription]}); - const accountingIntegrationOptions = accountingIntegrations.flatMap((name) => [ - {name, isIntuitEnterpriseSuite: name === CONST.POLICY.CONNECTIONS.NAME.QBO ? false : undefined}, - ...(name === CONST.POLICY.CONNECTIONS.NAME.QBO ? [{name, isIntuitEnterpriseSuite: true}] : []), - ]); - const shouldShowSynchronizationError = !!synchronizationError; - - // `startIntegrationFlow` changes identity whenever `policy` does, which re-runs this effect. The - // Navigation.setParams below clears newConnectionName through a navigation state update that lands in a later - // render, so that re-run can still see the param set. Key the guard on the value to start the flow only once. - const startedIntegrationFlowForRef = useRef(undefined); - - useFocusEffect( - useCallback(() => { - if (!newConnectionName || !isControlPolicy(policy) || !canWriteAccounting) { - // Re-arm the guard once the param is gone, so a later round-trip that asks for the same integration - // again is not mistaken for the re-run this guard exists to swallow. - if (!newConnectionName) { - startedIntegrationFlowForRef.current = undefined; - } - return; - } - - if (startedIntegrationFlowForRef.current === newConnectionName) { - return; - } - startedIntegrationFlowForRef.current = newConnectionName; - - startIntegrationFlow({ - name: newConnectionName, - isIntuitEnterpriseSuite: shouldConnectToIntuitEnterpriseSuite, - integrationToDisconnect, - shouldDisconnectIntegrationBeforeConnecting, - }); - Navigation.setParams({ - newConnectionName: undefined, - isIntuitEnterpriseSuite: undefined, - integrationToDisconnect: undefined, - shouldDisconnectIntegrationBeforeConnecting: undefined, - }); - }, [newConnectionName, shouldConnectToIntuitEnterpriseSuite, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting, policy, startIntegrationFlow, canWriteAccounting]), - ); - - const getConnectionsMenuItems = (): MenuItemData[] => { - if (!hasAccountingConnection && !isSyncInProgress && policyID) { - return accountingIntegrationOptions - .map(({name: integration, isIntuitEnterpriseSuite}) => { - const integrationData = getAvailableIntegrationData(integration, isIntuitEnterpriseSuite); - if (!integrationData) { - return undefined; - } - - const isXero = integration === CONST.POLICY.CONNECTIONS.NAME.XERO; - const iconProps = integrationData?.icon - ? { - icon: integrationData.icon, - iconType: CONST.ICON_TYPE_AVATAR, - } - : {}; - - return { - ...iconProps, - interactive: false, - // On native iOS, `accessible={true}` collapses the row and all its descendants into a single accessibility element, - // so VoiceOver focuses the whole row instead of the nested Connect button. Disabling it only on native iOS lets - // VoiceOver focus/activate the button on its own. Other platforms (Android/TalkBack, web, iOS mWeb→WEB) keep grouping. - shouldBeAccessible: getPlatform() !== CONST.PLATFORM.IOS, - wrapperStyle: [styles.sectionMenuItemTopDescription], - shouldShowRightComponent: true, - title: integrationData?.title, - badgeText: isXero ? translate('workspace.accounting.claimOffer.badgeText') : undefined, - onBadgePress: - isXero && canWriteAccounting - ? () => { - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_CLAIM_OFFER.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO)); - } - : undefined, - badgeStyle: styles.mr3, - isBadgeSuccess: isXero, - shouldShowBadgeBelow: shouldUseNarrowLayout, - rightComponent: ( - { - if (!canWriteAccounting) { - showReadOnlyModal(); - return; - } - startIntegrationFlow({name: integration, isIntuitEnterpriseSuite}); - }} - style={styles.justifyContentCenter} - innerStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} - hoverStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} - size={CONST.BUTTON_SIZE.SMALL} - ref={(ref) => { - if (!popoverAnchorRefs?.current) { - return; - } - const integrationKey = isIntuitEnterpriseSuite ? CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES.INTUIT_ENTERPRISE_SUITE : integration; - popoverAnchorRefs.current[integrationKey].current = ref; - }} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.ACCOUNTING.SETUP_BUTTON} - > - {translate('workspace.accounting.setup')} - - ), - }; - }) - .filter(Boolean) as MenuItemData[]; - } - - if (!connectionDetails) { - return []; - } - const iconProps = connectionDetails.icon ? {icon: connectionDetails.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; - - let rightComponent; - if (isSyncInProgress) { - rightComponent = ; - } else if (canWriteAccounting) { - rightComponent = ( - - ); - } - - return [ - { - ...iconProps, - interactive: false, - wrapperStyle: [styles.sectionMenuItemTopDescription, shouldShowSynchronizationError && styles.pb0], - shouldShowRightComponent: true, - title: connectionDetails.title, - errorText: synchronizationError, - errorTextStyle: [styles.mt5], - shouldShowRedDotIndicator: true, - description: connectionDetails.connectionMessage, - hintText: qboTokenExpiryHint, - rightComponent, - }, - ...connectionDetails.settingsMenuItems, - ]; - }; - const connectionsMenuItems = getConnectionsMenuItems(); - - const getOtherIntegrationsItems = () => { - if ((!hasAccountingConnection && !isSyncInProgress) || !policyID) { - return; - } - const otherIntegrations = accountingIntegrationOptions.filter( - ({name, isIntuitEnterpriseSuite}) => name !== connectedIntegration || !!isIntuitEnterpriseSuite !== isConnectedToIntuitEnterpriseSuite, - ); - return otherIntegrations - .map(({name: integration, isIntuitEnterpriseSuite}) => { - const integrationData = getAvailableIntegrationData(integration, isIntuitEnterpriseSuite); - if (!integrationData) { - return undefined; - } - - const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; - - return { - ...iconProps, - title: integrationData?.title, - rightComponent: ( - { - if (!canWriteAccounting) { - showReadOnlyModal(); - return; - } - startIntegrationFlow({ - name: integration, - isIntuitEnterpriseSuite, - integrationToDisconnect: connectedIntegration, - shouldDisconnectIntegrationBeforeConnecting: true, - }); - }} - style={styles.justifyContentCenter} - innerStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} - hoverStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} - size={CONST.BUTTON_SIZE.SMALL} - ref={(r) => { - if (!popoverAnchorRefs?.current) { - return; - } - const integrationKey = isIntuitEnterpriseSuite ? CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES.INTUIT_ENTERPRISE_SUITE : integration; - popoverAnchorRefs.current[integrationKey].current = r; - }} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.ACCOUNTING.SETUP_BUTTON} - > - {translate('workspace.accounting.setup')} - - ), - interactive: false, - // On native iOS, `accessible={true}` collapses the row and all its descendants into a single accessibility element, - // so VoiceOver focuses the whole row instead of the nested Connect button. Disabling it only on native iOS lets - // VoiceOver focus/activate the button on its own. Other platforms (Android/TalkBack, web, iOS mWeb→WEB) keep grouping. - shouldBeAccessible: getPlatform() !== CONST.PLATFORM.IOS, - shouldShowRightComponent: true, - wrapperStyle: styles.sectionMenuItemTopDescription, - }; - }) - .filter(Boolean) as MenuItemWithLink[]; - }; - const otherIntegrationsItems = getOtherIntegrationsItems(); - - const [chatTextLink, chatReportID] = useMemo(() => { - // If they have an onboarding specialist assigned display the following and link to the #admins room with the account executive. - if (policy?.chatReportIDAdmins) { - return [translate('workspace.accounting.talkYourOnboardingSpecialist'), policy?.chatReportIDAdmins?.toString()]; - } - - // If not, if they have an account manager assigned display the following and link to the DM with their account manager. - if (account?.accountManagerAccountID) { - return [translate('workspace.accounting.talkYourAccountManager'), account?.accountManagerReportID]; - } - // Else, display the following and link to their Concierge DM. - return [translate('workspace.accounting.talkToConcierge'), conciergeReportID]; - }, [account?.accountManagerAccountID, account?.accountManagerReportID, conciergeReportID, policy?.chatReportIDAdmins, translate]); - - return ( - - - - - -
- {!hasUnsupportedNDIntegration && - connectionsMenuItems.map((menuItem) => ( - - - - ))} - {hasUnsupportedNDIntegration && hasSyncError && !!policyID && ( - - )} - {hasUnsupportedNDIntegration && !hasSyncError && !!policyID && ( - - - { - // Go to Expensify Classic. - openOldDotLink(CONST.OLDDOT_URLS.POLICY_CONNECTIONS_URL(policyID)); - }} - > - {translate('workspace.accounting.goToODToSettings')} - - - - )} - {!!otherIntegrationsItems && ( - - - - )} - {!!account?.guideDetails?.email && !hasAccountingConnections(policy) && canWriteAccounting && ( - - - - {translate('workspace.accounting.needAnotherAccounting')} - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(chatReportID)))}>{chatTextLink} - - - )} -
-
-
-
-
- ); -} - -function PolicyAccountingPageWrapper(props: PolicyAccountingPageProps) { - return ( - - - - ); -} - -export default withUnifiedConnectionsBeta(withPolicyConnections(PolicyAccountingPageWrapper)); diff --git a/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx b/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx index da34005bf9be..65c1a9acaa65 100644 --- a/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx @@ -6,7 +6,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -23,7 +22,6 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -41,7 +39,6 @@ function BusinessCentralAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const bankAccounts = policy?.connections?.businessCentral?.data?.bankAccounts; const autoSync = businessCentralConfig?.autoSync?.enabled ?? false; @@ -64,7 +61,7 @@ function BusinessCentralAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="BusinessCentralAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx b/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx index aea10b2bdf2d..5283e418c676 100644 --- a/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx @@ -4,14 +4,12 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; @@ -35,7 +33,6 @@ function BusinessCentralExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const businessCentralData = policy?.connections?.businessCentral?.data; const exportConfig = businessCentralConfig?.export; @@ -119,7 +116,7 @@ function BusinessCentralExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="BusinessCentralExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx b/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx index 423b0ae41ebb..ece5b46ce2bf 100644 --- a/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx @@ -6,7 +6,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,12 +21,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type {ValueOf} from 'type-fest'; @@ -78,7 +77,6 @@ function BusinessCentralImportPage({policy}: WithPolicyConnectionsProps) { const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const businessCentralData = policy?.connections?.businessCentral?.data; @@ -108,7 +106,7 @@ function BusinessCentralImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="BusinessCentralImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx b/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx index cd6f91e9afef..fe7cc3a22e38 100644 --- a/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx +++ b/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx @@ -5,7 +5,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -24,7 +23,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -40,7 +38,6 @@ function CampfireAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const campfireConfig = policy?.connections?.campfire?.config; const campfireData = policy?.connections?.campfire?.data; const autoSync = campfireConfig?.autoSync?.enabled ?? false; @@ -69,7 +66,7 @@ function CampfireAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="CampfireAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx b/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx index 1a6ad429d275..bddd2ab6f5ca 100644 --- a/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx +++ b/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx @@ -9,7 +9,6 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -19,7 +18,6 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -34,7 +32,6 @@ function CampfireExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -60,7 +57,7 @@ function CampfireExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="CampfireExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx b/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx index e41ffb7b4c23..4da26996618c 100644 --- a/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx +++ b/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx @@ -1,7 +1,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,12 +9,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -24,7 +23,6 @@ function CampfireImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const campfireConfig = policy?.connections?.campfire?.config; const campfireData = policy?.connections?.campfire?.data; const enableNewCategories = campfireConfig?.enableNewCategories ?? false; @@ -34,7 +32,7 @@ function CampfireImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="CampfireImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx b/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx index a2d6db80522f..77738aa68da3 100644 --- a/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx +++ b/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx @@ -3,7 +3,6 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,7 +12,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {CERTINIA_DIMENSION_PARAMS, getDimensionLabel, getDisplayTypeLabel, getParentTagMappingLabel} from '@pages/workspace/accounting/certinia/utils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -28,14 +26,13 @@ function CertiniaImportPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.CERTINIA]?.config; const coding = config?.coding; const hasPSA = !!config?.hasPSA; return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="CertiniaImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx b/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx index 3fe2c35e460f..7f8dc76a95cc 100644 --- a/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx +++ b/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx @@ -7,7 +7,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -26,7 +25,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -44,7 +42,6 @@ function DualEntryAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const dualentryConfig = policy?.connections?.dualEntry?.config; const dualentryData = policy?.connections?.dualEntry?.data; const autoSync = dualentryConfig?.autoSync?.enabled ?? false; @@ -75,7 +72,7 @@ function DualEntryAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="DualEntryAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx b/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx index 79f3970f9014..20264d597be4 100644 --- a/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx +++ b/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx @@ -8,7 +8,6 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,7 +17,6 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -33,7 +31,6 @@ function DualEntryExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -59,7 +56,7 @@ function DualEntryExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="DualEntryExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx b/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx index f0c78422b749..01118e10e038 100644 --- a/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx +++ b/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx @@ -1,7 +1,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,12 +9,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -24,7 +23,6 @@ function DualEntryImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const dualentryConfig = policy?.connections?.dualEntry?.config; const dualentryData = policy?.connections?.dualEntry?.data; const enableNewCategories = dualentryConfig?.enableNewCategories ?? false; @@ -33,7 +31,7 @@ function DualEntryImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="DualEntryImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx index d739c638074c..16ff6f4931c4 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -6,7 +6,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +15,6 @@ import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendi import createDynamicRoute from '@navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@navigation/Navigation'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -46,7 +44,6 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const styles = useThemeStyles(); const {importEmployees, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {}; @@ -111,7 +108,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { contentContainerStyle={styles.pb2} titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} - onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} > Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_AUTO_SYNC.path)))}> diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx index 4c99a0f0a93e..7d783787fa60 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx @@ -3,7 +3,6 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,7 +11,6 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -53,7 +51,6 @@ function SageIntacctImportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const sageIntacctConfig = policy?.connections?.intacct?.config; const sageIntacctData = policy?.connections?.intacct?.data; @@ -75,7 +72,7 @@ function SageIntacctImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="SageIntacctImportPage" headerTitle="workspace.accounting.import" headerSubtitle={getCurrentSageIntacctEntityName(policy, translate('workspace.common.topLevel'))} diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx index fa8312b3f339..a22ef567e7f5 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx @@ -1,12 +1,10 @@ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; - import {getNetSuiteSetupLink} from '@libs/actions/connections/NetSuiteCommands'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AccountingSetupWebViewPage from '@pages/workspace/accounting/AccountingSetupWebViewPage'; -import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -16,7 +14,6 @@ type NetSuiteSetupPageProps = PlatformStackScreenProps ); } diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx index b2a7fc4ceffa..c76145a8236f 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx @@ -1,7 +1,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemList from '@components/MenuItemList'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useReusablePoliciesConnectedTo from '@hooks/useReusablePoliciesConnectedTo'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,9 +10,8 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; - import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -25,8 +23,7 @@ function NetSuiteExistingConnectionsPage({route}: ExistingConnectionsPageProps) const {translate, datetimeToRelative} = useLocalize(); const styles = useThemeStyles(); const policyID: string = route.params.policyID; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); - const accountingConnectionsRoute = getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID); + const accountingConnectionsRoute = ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID); const {reusablePoliciesConnectedTo: reusablePoliciesConnectedToNetSuite} = useReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.NETSUITE, policyID); const menuItems = reusablePoliciesConnectedToNetSuite.map((policy) => { diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx index 5c668f21de14..7205b4bdf2ee 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx @@ -5,7 +5,6 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -31,7 +30,6 @@ import { shouldHideReimbursedReportsSection, shouldHideReportsExportTo, } from '@pages/workspace/accounting/netsuite/utils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -52,7 +50,6 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id ?? CONST.DEFAULT_NUMBER_ID.toString(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.netsuite?.options?.config; const autoSyncConfig = policy?.connections?.netsuite?.config; @@ -287,7 +284,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="NetSuiteAdvancedPage" headerTitle="workspace.accounting.advanced" headerSubtitle={config?.subsidiary ?? ''} diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx index 2767f7120590..4f03a7397098 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx @@ -2,7 +2,6 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,7 +13,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {canUseProvincialTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/NetSuiteUtils'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -33,9 +31,8 @@ function NetSuiteProvincialTaxPostingAccountSelectPage({policy}: WithPolicyConne const illustrations = useMemoizedLazyIllustrations(['Telescope']); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const netSuiteExportBackPath = policyID - ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)) + ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)) : ROUTES.HOME; const config = policy?.connections?.netsuite?.options.config; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx index 999c690799d6..a2d5056ec13a 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx @@ -2,7 +2,6 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -15,7 +14,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {canUseTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/NetSuiteUtils'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -35,9 +33,8 @@ function NetSuiteTaxPostingAccountSelectPage({policy}: WithPolicyConnectionsProp const {isBetaEnabled} = usePermissions(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const netSuiteExportBackPath = policyID - ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)) + ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)) : ROUTES.HOME; const config = policy?.connections?.netsuite?.options.config; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx index 53465fc131c6..4aa32d943c1a 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx @@ -3,7 +3,6 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,7 +13,6 @@ import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils import Navigation from '@navigation/Navigation'; import TravelBillingContinuousReconciliationSection from '@pages/workspace/accounting/common/TravelBillingContinuousReconciliationSection'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -43,8 +41,7 @@ function NetSuiteTravelBillingConfigurationPage({policy}: WithPolicyConnectionsP const styles = useThemeStyles(); const policyID = policy?.id ?? String(CONST.DEFAULT_NUMBER_ID); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); - const netSuiteExportBackPath = createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)); + const netSuiteExportBackPath = createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)); const config = policy?.connections?.netsuite?.options?.config; const {payableList} = policy?.connections?.netsuite?.options?.data ?? {}; diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx index 15272dcdecf5..d92a9c2cdf38 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx @@ -4,7 +4,6 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +15,6 @@ import {canUseTaxNetSuite, getCustomersOrJobsLabelNetSuite, getNetSuiteImportCus import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getImportCustomFieldsSettings} from '@pages/workspace/accounting/netsuite/utils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -34,14 +32,13 @@ function NetSuiteImportPage({policy}: WithPolicyConnectionsProps) { const {isBetaEnabled} = usePermissions(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.netsuite?.options?.config; const {subsidiaryList} = policy?.connections?.netsuite?.options?.data ?? {}; const selectedSubsidiary = useMemo(() => (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID), [subsidiaryList, config?.subsidiaryID]); return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="NetSuiteImportPage" headerTitle="workspace.accounting.import" headerSubtitle={config?.subsidiary ?? ''} diff --git a/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx b/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx index 9acc7157d51b..ce766398a408 100644 --- a/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx +++ b/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx @@ -1,4 +1,3 @@ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useOnyx from '@hooks/useOnyx'; import {isConnectionInProgress, syncConnection} from '@libs/actions/connections'; @@ -6,10 +5,9 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; - import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {useEffect} from 'react'; @@ -20,7 +18,6 @@ function QuickBooksDesktopSetupFlowSyncPage({route}: QuickBooksDesktopSetupFlowS const policyID: string = route.params.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '-1'}`); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID ?? '-1'}`); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); useEffect(() => { if (!policyID) { @@ -32,7 +29,7 @@ function QuickBooksDesktopSetupFlowSyncPage({route}: QuickBooksDesktopSetupFlowS syncConnection(policy, CONST.POLICY.CONNECTIONS.NAME.QBD, true); } - Navigation.navigate(getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); + Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)); // disabling this rule, as we want this to run only on the first render // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx index 2a2139bf588b..b9e980aa4630 100644 --- a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx +++ b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx @@ -2,7 +2,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,7 +11,6 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {clearQBDErrorField} from '@userActions/Policy/Policy'; @@ -37,7 +35,6 @@ function QuickbooksDesktopAutoSyncPageBase({policy, navigateBackTo}: QuickbooksD const styles = useThemeStyles(); const {translate} = useLocalize(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.quickbooksDesktop?.config; const {autoSync, pendingFields} = config ?? {}; const accountingMethod = config?.export?.accountingMethod ?? COMMON_CONST.INTEGRATIONS.ACCOUNTING_METHOD.CASH; @@ -47,11 +44,9 @@ function QuickbooksDesktopAutoSyncPageBase({policy, navigateBackTo}: QuickbooksD const goBack = useCallback(() => { Navigation.goBack( navigateBackTo ?? - (policyID - ? createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)) - : undefined), + (policyID ? createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)) : undefined), ); - }, [isUnifiedConnectionsBetaEnabled, policyID, navigateBackTo]); + }, [policyID, navigateBackTo]); return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} > {sections.map((section) => ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="QuickbooksAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx index 8e0e488a99a9..2d369ff0da24 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx @@ -3,7 +3,6 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,7 +13,6 @@ import Navigation from '@navigation/Navigation'; import {shouldSwitchLocationsToReportFields} from '@pages/workspace/accounting/qbo/utils'; import {getQuickbooksOnlineIntegrationName, isIntuitEnterpriseSuiteConnection} from '@pages/workspace/accounting/utils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -36,7 +34,6 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { const integrationName = getQuickbooksOnlineIntegrationName(policy, translate); const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const qboConfig = policy?.connections?.quickbooksOnline?.config; const {syncClasses, syncCustomers, syncLocations, syncItems, syncTax, syncCustomDimensions, pendingFields, errorFields} = qboConfig ?? {}; const isIntuitEnterpriseSuite = isIntuitEnterpriseSuiteConnection(policy); @@ -109,7 +106,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="QuickbooksImportPage" headerTitle="workspace.accounting.import" title="workspace.qbo.importDescription" diff --git a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx index 5f1fd43c5d23..fb909e356fa0 100644 --- a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx @@ -8,7 +8,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useEnvironment from '@hooks/useEnvironment'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReconciliationFundID from '@hooks/useReconciliationFundID'; @@ -26,7 +25,6 @@ import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -55,7 +53,6 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) { const {translate} = useLocalize(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {environmentURL} = useEnvironment(); // Continuous Reconciliation is configured per card feed, and this workspace can sit on more than one: its own @@ -150,7 +147,7 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) { > Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} /> Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="RilletAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx b/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx index 4f2182c02874..595ba280d241 100644 --- a/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx +++ b/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx @@ -8,7 +8,6 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,7 +17,6 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -34,7 +32,6 @@ function RilletExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -59,7 +56,7 @@ function RilletExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="RilletExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx index a246e41caa3d..27c33cc60fb3 100644 --- a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx +++ b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx @@ -1,7 +1,6 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,12 +9,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -24,7 +23,6 @@ function RilletImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const rilletConfig = policy?.connections?.rillet?.config; const rilletData = policy?.connections?.rillet?.data; const enableNewCategories = rilletConfig?.enableNewCategories ?? false; @@ -33,7 +31,7 @@ function RilletImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="RilletImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx index c0377f33adb3..1a92b6134a84 100644 --- a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx +++ b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx @@ -8,7 +8,6 @@ import useConfirmModal from '@hooks/useConfirmModal'; import useEnvironment from '@hooks/useEnvironment'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import useHasReusablePoliciesConnectedTo from '@hooks/useHasReusablePoliciesConnectedTo'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -86,7 +85,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW const {environment} = useEnvironment(); const {isOffline} = useNetwork(); const {isBetaEnabled} = usePermissions(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {showConfirmModal} = useConfirmModal(); const {activeIntegration} = useAccountingState(); const {startIntegrationFlow} = useAccountingActions(); @@ -458,7 +456,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW cardFeeds, cardList: cardLists, isIntuitEnterpriseSuiteOverride: isConnectedToIntuitEnterpriseSuite, - isUnifiedConnectionsBetaEnabled, }); let connectionMessage; if (isSyncInProgress && connectionSyncStage) { diff --git a/src/pages/workspace/accounting/utils.tsx b/src/pages/workspace/accounting/utils.tsx index cfc8f8727bc8..6e64bb6cfcc2 100644 --- a/src/pages/workspace/accounting/utils.tsx +++ b/src/pages/workspace/accounting/utils.tsx @@ -21,8 +21,6 @@ import {getCurrentConnectionName} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; -import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; - import type {ThemeStyles} from '@styles/index'; import {getTrackingCategories} from '@userActions/connections/Xero'; @@ -92,9 +90,6 @@ type GetAccountingIntegrationDataOptions = { /** Picks QBO or Intuit Enterprise Suite explicitly instead of following the existing connection */ isIntuitEnterpriseSuiteOverride?: boolean; - - /** Routes the connect flows through the Connections page instead of the Accounting page */ - isUnifiedConnectionsBetaEnabled?: boolean; }; function getAccountingIntegrationData( @@ -112,7 +107,6 @@ function getAccountingIntegrationData( cardFeeds, cardList, isIntuitEnterpriseSuiteOverride, - isUnifiedConnectionsBetaEnabled = false, }: GetAccountingIntegrationDataOptions = {}, ): AccountingIntegration | undefined { const qboConfig = policy?.connections?.quickbooksOnline?.config; @@ -123,16 +117,16 @@ function getAccountingIntegrationData( const netsuiteSelectedSubsidiary = (policy?.connections?.netsuite?.options?.data?.subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === netsuiteConfig?.subsidiaryID); const getBackToAfterWorkspaceUpgradeRouteForIntacct = () => { if (integrationToDisconnect) { - return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.sageIntacct) { return ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.getRoute(policyID); } - return createDynamicRoute(DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path, getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); + return createDynamicRoute(DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path, ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)); }; const getBackToAfterWorkspaceUpgradeRouteForQBD = () => { if (integrationToDisconnect) { - return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.qbd) { return ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXISTING_CONNECTIONS.getRoute(policyID); @@ -141,7 +135,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForCertinia = () => { if (integrationToDisconnect) { - return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.certinia) { return ROUTES.POLICY_ACCOUNTING_CERTINIA_EXISTING_CONNECTIONS.getRoute(policyID); @@ -150,7 +144,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForRillet = () => { if (integrationToDisconnect) { - return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.rillet) { return ROUTES.POLICY_ACCOUNTING_RILLET_EXISTING_CONNECTIONS.getRoute(policyID); @@ -159,7 +153,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForDualEntry = () => { if (integrationToDisconnect) { - return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.dualEntry) { return ROUTES.POLICY_ACCOUNTING_DUALENTRY_EXISTING_CONNECTIONS.getRoute(policyID); @@ -168,7 +162,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForCampfire = () => { if (integrationToDisconnect) { - return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.campfire) { return ROUTES.POLICY_ACCOUNTING_CAMPFIRE_EXISTING_CONNECTIONS.getRoute(policyID); @@ -233,8 +227,7 @@ function getAccountingIntegrationData( ? { workspaceUpgradeNavigationDetails: { integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.intuitEnterpriseSuite.alias, - backToAfterWorkspaceUpgradeRoute: getAccountingConnectionsRoute( - isUnifiedConnectionsBetaEnabled, + backToAfterWorkspaceUpgradeRoute: ROUTES.WORKSPACE_CONNECTIONS.getRoute( policyID, connectionName, integrationToDisconnect, @@ -351,7 +344,7 @@ function getAccountingIntegrationData( workspaceUpgradeNavigationDetails: { integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.netsuite.alias, backToAfterWorkspaceUpgradeRoute: integrationToDisconnect - ? getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) + ? ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) : ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.getRoute(policyID, getInitialSubPageForNetsuiteTokenInput(policy)), }, pendingFields: {...netsuiteConfig?.pendingFields, ...policy?.connections?.netsuite?.config?.pendingFields, ...policy?.connections?.netsuite?.options?.config?.pendingFields}, @@ -727,7 +720,7 @@ function getAccountingIntegrationData( workspaceUpgradeNavigationDetails: { integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.businessCentral.alias, backToAfterWorkspaceUpgradeRoute: integrationToDisconnect - ? getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) + ? ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) : ROUTES.POLICY_ACCOUNTING_BUSINESS_CENTRAL_PREREQUISITES.getRoute(policyID), }, pendingFields: policy?.connections?.businessCentral?.config?.pendingFields, diff --git a/src/pages/workspace/accounting/xero/XeroImportPage.tsx b/src/pages/workspace/accounting/xero/XeroImportPage.tsx index 2480cf38eb6b..e16701cde3d0 100644 --- a/src/pages/workspace/accounting/xero/XeroImportPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroImportPage.tsx @@ -4,7 +4,6 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,7 +11,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {getCurrentXeroOrganizationName} from '@libs/PolicyUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -28,7 +26,6 @@ function XeroImportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {importCustomers, importTaxRates, importTrackingCategories, pendingFields, errorFields} = policy?.connections?.xero?.config ?? {}; const currentXeroOrganizationName = useMemo(() => getCurrentXeroOrganizationName(policy ?? undefined), [policy]); @@ -70,7 +67,7 @@ function XeroImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="XeroImportPage" headerTitle="workspace.accounting.import" headerSubtitle={currentXeroOrganizationName} diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index 7476bd10da6a..b4ae50e34ede 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -7,7 +7,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +15,6 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getCurrentXeroOrganizationName, settingsPendingAction} from '@libs/PolicyUtils'; -import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -37,7 +35,6 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id; - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const xeroConfig = policy?.connections?.xero?.config; const {pendingFields, errorFields, sync} = xeroConfig ?? {}; const {bankAccounts, expenseAccounts} = policy?.connections?.xero?.data ?? {}; @@ -66,7 +63,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} displayName="XeroAdvancedPage" headerTitle="workspace.accounting.advanced" headerSubtitle={currentXeroOrganizationName} diff --git a/src/pages/workspace/connections/ConnectionsAccountingPage.tsx b/src/pages/workspace/connections/ConnectionsAccountingPage.tsx index 73f45a49a233..fc015708f65d 100644 --- a/src/pages/workspace/connections/ConnectionsAccountingPage.tsx +++ b/src/pages/workspace/connections/ConnectionsAccountingPage.tsx @@ -9,7 +9,6 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -37,7 +36,6 @@ function ConnectionsAccountingPage({policy}: PolicyAccountingPageProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const policyID = policy?.id; const { connectedIntegration, @@ -63,7 +61,7 @@ function ConnectionsAccountingPage({policy}: PolicyAccountingPageProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.ACCOUNTING} - shouldBeBlocked={!isUnifiedConnectionsBetaEnabled || (!connectedIntegration && !hasUnsupportedNDIntegration)} + shouldBeBlocked={!connectedIntegration && !hasUnsupportedNDIntegration} fullPageNotFoundViewProps={{onBackButtonPress: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))}} > Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))}} > {isFetchingConnections ? ( diff --git a/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx b/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx index 56e25c7d6f53..1bca427c3390 100644 --- a/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx +++ b/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx @@ -8,7 +8,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useScreenBoundDynamicRoute from '@hooks/useScreenBoundDynamicRoute'; @@ -40,7 +39,6 @@ function ConnectionsReceiptPartnersPage({route}: ConnectionsReceiptPartnersPageP const buildDynamicRoute = useScreenBoundDynamicRoute(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const { policy, integrations, @@ -70,7 +68,7 @@ function ConnectionsReceiptPartnersPage({route}: ConnectionsReceiptPartnersPageP policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.MORE_FEATURES} - shouldBeBlocked={!isUnifiedConnectionsBetaEnabled || (!isLoading && !isUberConnected && !shouldShowEnterCredentialsError)} + shouldBeBlocked={!isLoading && !isUberConnected && !shouldShowEnterCredentialsError} fullPageNotFoundViewProps={{onBackButtonPress: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))}} > {isLoading ? ( diff --git a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx index e5457ecfc520..fad4c2573682 100644 --- a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx +++ b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx @@ -18,7 +18,6 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -69,6 +68,7 @@ type RouteParams = { integrationToDisconnect?: ConnectionName; shouldDisconnectIntegrationBeforeConnecting?: boolean; isIntuitEnterpriseSuite?: string; + tab?: ConnectionsTab; }; type MergeSetupFlow = { @@ -106,7 +106,7 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { const {openConciergeAnywhere} = useOpenConciergeAnywhere(); const {startIntegrationFlow} = useAccountingActions(); const {canWrite: canWriteAccounting} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.ACCOUNTING); - const [lastSelectedTab, setLastSelectedTab] = useState(CONST.TAB.CONNECTIONS.ALL); + const [lastSelectedTab, setLastSelectedTab] = useState(); const [mergeSetupFlow, setMergeSetupFlow] = useState(); const route = useRoute(); const params = route.params as RouteParams | undefined; @@ -135,7 +135,9 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { // Recruiting providers are behind a beta, so the tab only shows once there is something to list in it const hasRecruitingListings = listings.some((listing) => listing.category === CONST.TAB.CONNECTIONS.RECRUITING); const visibleTabs = Object.values(CONST.TAB.CONNECTIONS).filter((tab) => tab !== CONST.TAB.CONNECTIONS.RECRUITING || hasRecruitingListings); - const activeTab = visibleTabs.includes(lastSelectedTab) ? lastSelectedTab : CONST.TAB.CONNECTIONS.ALL; + // A link can open a tab, which applies until the user picks one + const requestedTab = lastSelectedTab ?? params?.tab ?? CONST.TAB.CONNECTIONS.ALL; + const activeTab = visibleTabs.includes(requestedTab) ? requestedTab : CONST.TAB.CONNECTIONS.ALL; const [searchValue, setSearchValue, searchResults, appliedSearchValue] = useSearchResults( availableListings, @@ -347,14 +349,11 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { } function WorkspaceConnectionsPageWrapper(props: WithPolicyConnectionsProps) { - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); - return ( diff --git a/src/pages/workspace/connections/useAccountingConnectionListings.ts b/src/pages/workspace/connections/useAccountingConnectionListings.ts index a213688c9911..ec185f5263aa 100644 --- a/src/pages/workspace/connections/useAccountingConnectionListings.ts +++ b/src/pages/workspace/connections/useAccountingConnectionListings.ts @@ -2,7 +2,6 @@ * Builds the accounting listings for the Connections page and starts the accounting connect flows from them. * Must be rendered inside an AccountingContextProvider. */ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -37,7 +36,6 @@ function useAccountingConnectionListings(policy: OnyxEntry): ConnectionL const styles = useThemeStyles(); const {translate, datetimeToRelative, getLocalDateFromDatetime} = useLocalize(); const {isBetaEnabled} = usePermissions(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {startIntegrationFlow} = useAccountingActions(); const {activeIntegration, popoverAnchorRefs} = useAccountingState(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); @@ -124,7 +122,6 @@ function useAccountingConnectionListings(policy: OnyxEntry): ConnectionL const integrationData = getAccountingIntegrationData(name, policyID, translate, NO_REUSABLE_CONNECTIONS, { expensifyIcons: accountingIcons, isIntuitEnterpriseSuiteOverride: isIntuitEnterpriseSuite, - isUnifiedConnectionsBetaEnabled, }); if (!integrationData) { return []; diff --git a/src/pages/workspace/connections/utils.ts b/src/pages/workspace/connections/utils.ts index eba5318849b9..978b74a86152 100644 --- a/src/pages/workspace/connections/utils.ts +++ b/src/pages/workspace/connections/utils.ts @@ -4,7 +4,6 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type {ConnectionListing, ConnectionsTab} from './types'; @@ -44,27 +43,4 @@ function getListingsForTab(listings: ConnectionListing[], tab: ConnectionsTab): return listings.filter((listing) => listing.category === tab); } -/** The page listing the workspace's accounting integrations, which is Connections while the unified Connections beta is on */ -function getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled: boolean, ...params: Parameters) { - if (isUnifiedConnectionsBetaEnabled) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(...params); - } - return ROUTES.POLICY_ACCOUNTING.getRoute(...params); -} - -/** The page an accounting integration's settings open from, which is the Connections accounting panel while the unified Connections beta is on */ -function getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled: boolean, policyID: string | undefined) { - if (isUnifiedConnectionsBetaEnabled) { - return ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); - } - return ROUTES.POLICY_ACCOUNTING.getRoute(policyID); -} - -function getHRSettingsRoute(isUnifiedConnectionsBetaEnabled: boolean, policyID: string) { - if (isUnifiedConnectionsBetaEnabled) { - return ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID); - } - return ROUTES.WORKSPACE_HR.getRoute(policyID); -} - -export {MCP_CONNECTOR, getSyncStatusMessage, getListingsForTab, getAccountingConnectionsRoute, getAccountingSettingsRoute, getHRSettingsRoute}; +export {MCP_CONNECTOR, getSyncStatusMessage, getListingsForTab}; diff --git a/src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx b/src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx deleted file mode 100644 index c5f9fe60decd..000000000000 --- a/src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; - -import type {ComponentProps, ComponentType} from 'react'; - -import React from 'react'; - -import WorkspaceConnectionsPage from './WorkspaceConnectionsPage'; - -type WorkspaceConnectionsPageProps = ComponentProps; - -/** - * Renders the Connections page in place of a page it replaces while the unified Connections beta is on. Old links and - * deep links into those pages' settings still resolve to their routes, so they need to land on Connections for beta users. - */ -export default function withUnifiedConnectionsBeta>(WrappedComponent: ComponentType): ComponentType { - function WithUnifiedConnectionsBeta(props: TProps) { - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); - - if (isUnifiedConnectionsBetaEnabled) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- these are the screen props of a page Connections replaced, and it only reads the policy ID and the connection to start from the route - return ; - } - - return ; - } - - return WithUnifiedConnectionsBeta; -} diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index 2fa26cf6f11f..49e6d5028b91 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -3,20 +3,17 @@ */ import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList'; -import {isAnyHRConnected, isMergeHRCompleteSetupNeeded, shouldShowHRConnectionError} from '@libs/merge/HRUtils'; -import {isAnyRecruitingConnected} from '@libs/merge/RecruitingUtils'; +import {isMergeHRCompleteSetupNeeded, shouldShowHRConnectionError} from '@libs/merge/HRUtils'; import {getObjectKeys} from '@libs/ObjectUtils'; import { arePolicyRulesEnabled, canMemberRead, canPolicyAccessFeature, - hasAccountingFeatureConnection, hasPolicyCategoriesError, hasPolicyRulesError, hasVendorFeature, isGroupPolicy, isMatchingVendorListLoaded, - isMCPEnabled, isPerDiemEnabled, isPolicyAdmin, isQBORefreshTokenExpiringSoonSelector, @@ -52,9 +49,7 @@ type WorkspaceMenuIconMap = Record< | 'Users' | 'Hashtag' | 'Document' - | 'Sync' | 'Connect' - | 'Receipt' | 'Briefcase' | 'Folder' | 'Tag' @@ -69,8 +64,7 @@ type WorkspaceMenuIconMap = Record< | 'InvoiceGeneric' | 'Gear' | 'Bolt' - | 'Bot' - | 'UserPlus', + | 'Bot', IconAsset >; @@ -120,12 +114,8 @@ type GetWorkspaceMenuItemsParams = { hasApprovalWorkflowWithNonMemberApprover?: boolean; /** Whether the vendor matching beta is enabled. */ isVendorMatchingBetaEnabled?: boolean; - /** Whether the Merge ATS beta gating the Recruiting feature is enabled. */ - isRecruitingBetaEnabled?: boolean; /** Whether the company agents beta is enabled. */ isCompanyAgentsBetaEnabled?: boolean; - /** Whether the unified Connections beta replaces the Accounting, HR, Recruiting, Receipt partners, and MCP items. */ - isUnifiedConnectionsBetaEnabled?: boolean; /** Formats the invoice account balance for its menu badge. */ convertToDisplayString: CurrencyListActionsContextType['convertToDisplayString']; }; @@ -143,9 +133,7 @@ function getWorkspaceMenuItems({ shouldShowRBR = false, hasApprovalWorkflowWithNonMemberApprover = false, isVendorMatchingBetaEnabled = false, - isRecruitingBetaEnabled = false, isCompanyAgentsBetaEnabled = false, - isUnifiedConnectionsBetaEnabled = false, convertToDisplayString, }: GetWorkspaceMenuItemsParams): WorkspaceMenuItem[] { const canReadPolicyFeature = (policyFeature: PolicyFeature) => canMemberRead(policy, currentUserLogin ?? '', policyFeature); @@ -168,14 +156,6 @@ function getWorkspaceMenuItems({ const accountingConnectionNames = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES; const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress, accountingConnectionNames); const hasHRError = shouldShowHRConnectionError(policy, isConnectionInProgress, isPolicyAdmin(policy)); - const getHRBrickRoadIndicator = () => { - if (hasHRError) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - } - if (isMergeHRCompleteSetupNeeded(policy)) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; - } - }; const getConnectionsBrickRoadIndicator = () => { if (hasSyncError || shouldShowQBOReimbursableExportDestinationAccountError(policy) || hasHRError || shouldShowEnterCredentialsError) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; @@ -199,19 +179,11 @@ function getWorkspaceMenuItems({ [CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]: policy?.areTagsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]: policy?.tax?.trackingEnabled, [CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]: policy?.areCompanyCardsEnabled, - [CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: !!policy?.areConnectionsEnabled || hasAccountingFeatureConnection(policy), - [CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED]: (policy?.isHREnabled === true || isAnyHRConnected(policy)) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED), - [CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED]: - isRecruitingBetaEnabled && - (policy?.isRecruitingEnabled === true || isAnyRecruitingConnected(policy)) && - canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED), [CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: arePolicyRulesEnabled(policy, policyCategories), [CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED]: policy?.areInvoicesEnabled, [CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED]: isPerDiemEnabled(policy) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED), - [CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED]: policy?.receiptPartners?.enabled ?? false, - [CONST.POLICY.MORE_FEATURES.IS_MCP_ENABLED]: isMCPEnabled(policy), [CONST.POLICY.MORE_FEATURES.IS_TRAVEL_ENABLED]: policy?.isTravelEnabled, [CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED]: isTimeTrackingEnabled(policy), }; @@ -274,7 +246,7 @@ function getWorkspaceMenuItems({ }); } - if (isUnifiedConnectionsBetaEnabled && canReadMoreFeatures) { + if (canReadMoreFeatures) { items.push({ translationKey: 'workspace.common.connections', searchAliasKeys: ['workspace.common.accounting', 'workspace.common.hr', 'workspace.common.recruiting', 'workspace.common.receiptPartners', 'workspace.common.mcp'], @@ -287,70 +259,6 @@ function getWorkspaceMenuItems({ }); } - if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.ACCOUNTING)) { - let accountingBrickRoadIndicator; - if (hasSyncError || shouldShowQBOReimbursableExportDestinationAccountError(policy)) { - accountingBrickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - } else if (isQBORefreshTokenExpiringSoonSelector(policy)) { - accountingBrickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; - } - items.push({ - translationKey: 'workspace.common.accounting', - icon: icons.Sync, - getRoute: () => ROUTES.POLICY_ACCOUNTING.getRoute(policyID), - brickRoadIndicator: accountingBrickRoadIndicator, - screenName: SCREENS.WORKSPACE.ACCOUNTING.ROOT, - sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.ACCOUNTING, - highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED, - }); - } - - if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED] && canReadMoreFeatures) { - items.push({ - translationKey: 'workspace.common.hr', - icon: icons.Users, - getRoute: () => ROUTES.WORKSPACE_HR.getRoute(policyID), - brickRoadIndicator: getHRBrickRoadIndicator(), - screenName: SCREENS.WORKSPACE.HR, - sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.HR, - highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - }); - } - - if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED] && canReadMoreFeatures) { - items.push({ - translationKey: 'workspace.common.recruiting', - icon: icons.UserPlus, - getRoute: () => ROUTES.WORKSPACE_RECRUITING.getRoute(policyID), - screenName: SCREENS.WORKSPACE.RECRUITING, - sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.RECRUITING, - highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED, - }); - } - - if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED] && canReadMoreFeatures) { - items.push({ - translationKey: 'workspace.common.receiptPartners', - icon: icons.Receipt, - getRoute: () => ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policyID), - brickRoadIndicator: shouldShowEnterCredentialsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - screenName: SCREENS.WORKSPACE.RECEIPT_PARTNERS, - sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.RECEIPT_PARTNERS, - highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED, - }); - } - - if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.IS_MCP_ENABLED] && canReadMoreFeatures) { - items.push({ - translationKey: 'workspace.common.mcp', - icon: icons.Bot, - getRoute: () => ROUTES.WORKSPACE_MCP.getRoute(policyID), - screenName: SCREENS.WORKSPACE.MCP, - sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.MCP, - highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.IS_MCP_ENABLED, - }); - } - if (policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.CATEGORIES)) { items.push({ translationKey: 'workspace.common.categories', diff --git a/src/pages/workspace/hr/WorkspaceHRPage.tsx b/src/pages/workspace/hr/WorkspaceHRPage.tsx deleted file mode 100644 index cd8cb4bc30bc..000000000000 --- a/src/pages/workspace/hr/WorkspaceHRPage.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin'; -import usePolicy from '@hooks/usePolicy'; - -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; - -import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; -import MergeConnectionsPageBase from '@pages/workspace/merge/MergeConnectionsPageBase'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; - -import React from 'react'; - -import {getHRCards} from './utils'; - -type WorkspaceHRPageProps = PlatformStackScreenProps; - -function WorkspaceHRPage({ - route: { - params: {policyID}, - }, -}: WorkspaceHRPageProps) { - const {translate, getLocalDateFromDatetime, formatPhoneNumber} = useLocalize(); - const policy = usePolicy(policyID); - const policyEmployeePersonalDetails = usePersonalDetailsByLogins([...Object.keys(policy?.employeeList ?? {})]); - const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); - const icons = useMemoizedLazyExpensifyIcons(['GustoSquare', 'TriNetSquare']); - - const cards = getHRCards({ - policy, - policyEmployeePersonalDetails, - connectionSyncProgress, - getLocalDateFromDatetime, - translate, - formatPhoneNumber, - policyID, - gustoIcon: icons.GustoSquare, - trinetIcon: icons.TriNetSquare, - }); - - return ( - - ); -} - -export default withUnifiedConnectionsBeta(WorkspaceHRPage); diff --git a/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx b/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx index a35667fb3e89..2971195ae225 100644 --- a/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx +++ b/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx @@ -1,4 +1,3 @@ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import {updateGustoFinalApprover} from '@libs/actions/connections/Gusto'; @@ -6,11 +5,11 @@ import {isGustoConnected} from '@libs/merge/HRUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {getHRSettingsRoute} from '@pages/workspace/connections/utils'; import MergeFinalApproverPageBase from '@pages/workspace/merge/MergeFinalApproverPageBase'; import type {MergeFinalApproverProviderConfig} from '@pages/workspace/merge/MergeFinalApproverPageBase'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -23,13 +22,12 @@ function GustoFinalApproverPage({ }, }: GustoFinalApproverPageProps) { const {translate} = useLocalize(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config: MergeFinalApproverProviderConfig = { testID: 'GustoFinalApproverPage', isConnected: isGustoConnected, featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - backRoute: getHRSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID), + backRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), getCurrentFinalApprover: (policy) => policy?.connections?.gusto?.config?.finalApprover ?? null, getProviderName: () => translate('workspace.hr.gusto.title'), getHeaderTitle: () => translate('workspace.merge.finalApprover'), diff --git a/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx b/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx index 4104798c0406..5f6e4cb1b6e9 100644 --- a/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx +++ b/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx @@ -1,4 +1,3 @@ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import {updateMergeFinalApprover} from '@libs/actions/connections/merge'; @@ -7,11 +6,11 @@ import {isMergeConnected} from '@libs/merge/MergeUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {getHRSettingsRoute} from '@pages/workspace/connections/utils'; import MergeFinalApproverPageBase from '@pages/workspace/merge/MergeFinalApproverPageBase'; import type {MergeFinalApproverProviderConfig} from '@pages/workspace/merge/MergeFinalApproverPageBase'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -24,13 +23,12 @@ function MergeHRFinalApproverPage({ }, }: MergeHRFinalApproverPageProps) { const {translate} = useLocalize(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config: MergeFinalApproverProviderConfig = { testID: 'MergeHRFinalApproverPage', isConnected: (policy) => isMergeConnected(policy, CONST.POLICY.CONNECTIONS.NAME.MERGE_HR), featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - backRoute: getHRSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID), + backRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), getCurrentFinalApprover: (policy) => policy?.connections?.merge_hris?.config?.finalApprover ?? null, getProviderName: (policy) => getConnectedHRProvider(policy)?.displayName ?? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.merge_hris, getHeaderTitle: (providerName) => translate('workspace.hr.providerFinalApprover', providerName), diff --git a/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx b/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx index 7241784dacd1..31e2e611df59 100644 --- a/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx +++ b/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx @@ -1,4 +1,3 @@ -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import {updateZenefitsFinalApprover} from '@libs/actions/connections/Zenefits'; @@ -6,11 +5,11 @@ import {isZenefitsConnected} from '@libs/merge/HRUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {getHRSettingsRoute} from '@pages/workspace/connections/utils'; import MergeFinalApproverPageBase from '@pages/workspace/merge/MergeFinalApproverPageBase'; import type {MergeFinalApproverProviderConfig} from '@pages/workspace/merge/MergeFinalApproverPageBase'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -23,13 +22,12 @@ function ZenefitsFinalApproverPage({ }, }: ZenefitsFinalApproverPageProps) { const {translate} = useLocalize(); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config: MergeFinalApproverProviderConfig = { testID: 'ZenefitsFinalApproverPage', isConnected: isZenefitsConnected, featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - backRoute: getHRSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID), + backRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), getCurrentFinalApprover: (policy) => policy?.connections?.zenefits?.config?.finalApprover ?? null, getProviderName: () => translate('workspace.hr.zenefits.title'), getHeaderTitle: () => translate('workspace.merge.finalApprover'), diff --git a/src/pages/workspace/mcp/WorkspaceMCPPage.tsx b/src/pages/workspace/mcp/WorkspaceMCPPage.tsx deleted file mode 100644 index 00b74532d621..000000000000 --- a/src/pages/workspace/mcp/WorkspaceMCPPage.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import Button from '@components/Button'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Icon from '@components/Icon'; -import MenuItem from '@components/MenuItem'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Section from '@components/Section'; -import Text from '@components/Text'; -import TextLink from '@components/TextLink'; - -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import usePolicy from '@hooks/usePolicy'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; - -import Navigation from '@navigation/Navigation'; -import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; -import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; - -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; - -import variables from '@styles/variables'; - -import {openExternalLink} from '@userActions/Link'; - -import CONST from '@src/CONST'; -import type SCREENS from '@src/SCREENS'; - -import React from 'react'; -import {View} from 'react-native'; - -type WorkspaceMCPPageProps = PlatformStackScreenProps; - -function WorkspaceMCPPage({route}: WorkspaceMCPPageProps) { - const policyID = route.params.policyID; - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const theme = useTheme(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const icons = useMemoizedLazyExpensifyIcons(['ChatGPTSquare', 'ClaudeSquare', 'CursorSquare', 'QuestionMark']); - const policy = usePolicy(policyID); - - useWorkspaceDocumentTitle(policy?.name, 'workspace.common.mcp'); - - const connectors = [ - {icon: icons.ChatGPTSquare, title: translate('workspace.mcp.chatgpt.title'), description: translate('workspace.mcp.chatgpt.subtitle'), url: CONST.CHATGPT_CONNECT_URL}, - {icon: icons.ClaudeSquare, title: translate('workspace.mcp.claude.title'), description: translate('workspace.mcp.claude.subtitle'), url: CONST.CLAUDE_CONNECT_URL}, - {icon: icons.CursorSquare, title: translate('workspace.mcp.cursor.title'), description: translate('workspace.mcp.cursor.subtitle'), url: CONST.CURSOR_MCP_HELP_URL}, - ]; - - return ( - - - Navigation.goBack()} - /> - - -
- {connectors.map((connector) => ( - openExternalLink(connector.url)} - style={styles.justifyContentCenter} - size={CONST.BUTTON_SIZE.SMALL} - > - {translate('workspace.mcp.connect')} - - } - /> - ))} - - - - {translate('workspace.mcp.helpPrompt')} openExternalLink(CONST.MCP_HELP_URL)}>{translate('workspace.mcp.helpLink')} - - -
-
-
-
-
- ); -} - -export default withUnifiedConnectionsBeta(WorkspaceMCPPage); diff --git a/src/pages/workspace/merge/MergeConnectionsPageBase.tsx b/src/pages/workspace/merge/MergeConnectionsPageBase.tsx deleted file mode 100644 index 5bd34ee2eac4..000000000000 --- a/src/pages/workspace/merge/MergeConnectionsPageBase.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import CollapsibleSection from '@components/CollapsibleSection'; -import ConnectToMergeFlow from '@components/ConnectToMergeFlow'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import CompactSearchBar from '@components/SearchBar/CompactSearchBar'; -import Section from '@components/Section'; - -import useConfirmModal from '@hooks/useConfirmModal'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePolicy from '@hooks/usePolicy'; -import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSearchResults from '@hooks/useSearchResults'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; - -import {openPolicyHRPage, openPolicyRecruitingPage} from '@libs/actions/PolicyConnections'; -import {isMergeConnectionName} from '@libs/merge/MergeUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import tokenizedSearch from '@libs/tokenizedSearch'; - -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; - -import variables from '@styles/variables'; - -import CONST from '@src/CONST'; - -import React, {useEffect, useState} from 'react'; -import {View} from 'react-native'; - -import type {MergeProviderCardCategory, MergeProviderCardDescriptor} from './types'; - -import MergeInitialSyncingModalListener from './MergeInitialSyncingModalListener'; -import MergeProviderCard from './MergeProviderCard'; -import MergeSyncResultsListener from './MergeSyncResultsListener'; - -/** The handful of things that differ between the HR page and the Recruiting page. Everything else is shared. */ -const PAGE_CONFIG = { - [CONST.POLICY.CONNECTIONS.CATEGORY.HR]: { - featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - openPage: openPolicyHRPage, - testID: 'WorkspaceHRPage', - }, - [CONST.POLICY.CONNECTIONS.CATEGORY.RECRUITING]: { - featureName: CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED, - openPage: openPolicyRecruitingPage, - testID: 'WorkspaceRecruitingPage', - }, -} as const; - -type MergeConnectionsPageBaseContentProps = { - /** The workspace whose connections are listed. */ - policyID: string; - - /** Which page this is. Picks the category-specific copy, feature flag, and data-fetching command. */ - category: MergeProviderCardCategory; - - /** Provider cards to list, already built by the category's `utils`. */ - cards: MergeProviderCardDescriptor[]; - - /** Category-specific content rendered under the provider list while nothing is connected, e.g. what to do when the provider isn't listed. */ - footer?: React.ReactNode; -}; - -type MergeConnectionsPageBaseProps = MergeConnectionsPageBaseContentProps & { - /** Whether to block access to the page, e.g. when the category is still behind a beta. */ - shouldBeBlocked?: boolean; -}; - -function MergeConnectionsPageBaseContent({policyID, category, cards, footer}: MergeConnectionsPageBaseContentProps) { - const {translate, localeCompare} = useLocalize(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const policy = usePolicy(policyID); - const [activeSetupFlow, setActiveSetupFlow] = useState<{setupLink: string; key: number} | undefined>(); - const {showConfirmModal} = useConfirmModal(); - - const {testID} = PAGE_CONFIG[category]; - - const connectedCards: MergeProviderCardDescriptor[] = []; - const disconnectedCards: MergeProviderCardDescriptor[] = []; - for (const card of cards) { - (card.isConnected ? connectedCards : disconnectedCards).push(card); - } - // At most one provider of a category can be connected to a workspace at a time. - const connectedConnectionName = connectedCards.at(0)?.connectionName; - const byName = (a: MergeProviderCardDescriptor, b: MergeProviderCardDescriptor) => localeCompare(a.displayName, b.displayName); - connectedCards.sort(byName); - disconnectedCards.sort(byName); - - const filterCard = (card: MergeProviderCardDescriptor, searchInput: string) => { - return tokenizedSearch([card], searchInput, (c) => [c.displayName]).length > 0; - }; - const [inputValue, setInputValue, filteredDisconnectedCards] = useSearchResults(disconnectedCards, filterCard); - - const {canWrite: canWriteMoreFeatures, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES); - - const handleConnect = (card: MergeProviderCardDescriptor) => { - if (!card.setupLink) { - return; - } - - if (!canWriteMoreFeatures) { - showReadOnlyModal(); - return; - } - - if (!card.isConnected && connectedCards.length > 0) { - showConfirmModal({ - title: translate(`workspace.${category}.alreadyConnectedTitle`), - prompt: translate(`workspace.${category}.alreadyConnectedPrompt`), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - innerContainerStyle: shouldUseNarrowLayout ? undefined : StyleUtils.getWidthStyle(variables.wideConfirmModalWidth), - }); - return; - } - - // eslint-disable-next-line react-hooks/purity -- random key forces remount on every press, even for the same provider - setActiveSetupFlow({setupLink: card.setupLink, key: Math.random()}); - }; - - const maybeSearchBar = disconnectedCards.length >= CONST.STANDARD_LIST_ITEM_LIMIT && ( - - ); - const disconnectedProviderCards = filteredDisconnectedCards.map((card) => ( - handleConnect(card)} - canWriteMoreFeatures={canWriteMoreFeatures} - showReadOnlyModal={showReadOnlyModal} - /> - )); - - return ( - - {!!connectedConnectionName && ( - - )} - {!!connectedConnectionName && isMergeConnectionName(connectedConnectionName) && ( - - )} - {!!activeSetupFlow && ( - setActiveSetupFlow(undefined)} - /> - )} - Navigation.goBack()} - /> - - -
- {connectedCards.map((card) => ( - handleConnect(card)} - canWriteMoreFeatures={canWriteMoreFeatures} - showReadOnlyModal={showReadOnlyModal} - /> - ))} - {connectedCards.length === 0 && ( - <> - {maybeSearchBar} - {disconnectedProviderCards} - {footer} - - )} - - {connectedCards.length > 0 && disconnectedCards.length > 0 && !connectedCards.some((c) => c.isInitialSyncInProgress) && ( - - {maybeSearchBar} - {disconnectedProviderCards} - - )} -
-
-
-
- ); -} - -function MergeConnectionsPageBase({policyID, category, cards, footer, shouldBeBlocked}: MergeConnectionsPageBaseProps) { - const {featureName, openPage} = PAGE_CONFIG[category]; - - useWorkspaceDocumentTitle(undefined, `workspace.common.${category}`); - - useNetwork({onReconnect: () => openPage(policyID)}); - - useEffect(() => { - openPage(policyID); - }, [openPage, policyID]); - - return ( - - - - ); -} - -export default MergeConnectionsPageBase; diff --git a/src/pages/workspace/merge/MergeProviderCard.tsx b/src/pages/workspace/merge/MergeProviderCard.tsx deleted file mode 100644 index 4e6b09867a6e..000000000000 --- a/src/pages/workspace/merge/MergeProviderCard.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import ActivityIndicator from '@components/ActivityIndicator'; -import Button from '@components/Button'; -import ButtonDisabledWhenOffline from '@components/Button/composed/ButtonDisabledWhenOffline'; -import MenuItem from '@components/MenuItem'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import RenderHTML from '@components/RenderHTML'; -import ThreeDotsMenu from '@components/ThreeDotsMenu'; - -import useEnvironment from '@hooks/useEnvironment'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import {clearMergeConnectionErrorField} from '@libs/actions/connections/merge'; -import Navigation from '@libs/Navigation/Navigation'; - -import CONST from '@src/CONST'; -import type Policy from '@src/types/onyx/Policy'; - -import React from 'react'; -import {View} from 'react-native'; - -import type {MergeProviderCardDescriptor} from './types'; - -import useMergeProviderCardDetails from './useMergeProviderCardDetails'; - -type MergeProviderCardProps = { - /** Descriptor object containing the Merge provider's display info, connection state, and sync status. */ - card: MergeProviderCardDescriptor; - - /** The workspace policy that owns this Merge integration. */ - policy: Policy | undefined; - - /** Callback invoked when the user taps the "Connect" or "Reconnect" button. */ - handleConnect: () => void; - - /** Whether the current user can edit this Merge connection. */ - canWriteMoreFeatures: boolean; - - /** Shows the read-only action modal. */ - showReadOnlyModal: () => void; -}; - -function MergeProviderCard({card, policy, handleConnect, canWriteMoreFeatures, showReadOnlyModal}: MergeProviderCardProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {environmentURL} = useEnvironment(); - const {fallbackIcon, cardIcon, connectionDescription, lastSyncErrorMessage, overflowMenu, visibleConfigRows} = useMergeProviderCardDetails({ - card, - policy, - handleConnect, - canWriteMoreFeatures, - showReadOnlyModal, - }); - - let rightInset: React.ReactNode; - if (!card.isConnected) { - rightInset = ( - - {translate('workspace.merge.connect')} - - ); - } else if (card.isSyncInProgress) { - rightInset = ; - } else { - rightInset = ( - - ); - } - - const rightComponent = {rightInset}; - - return ( - <> - - ) : undefined - } - errorText={lastSyncErrorMessage} - errorTextStyle={styles.mt5} - shouldShowRedDotIndicator - shouldShowRightComponent={!!rightInset} - brickRoadIndicator={card.completeSetupRoute ? CONST.BRICK_ROAD_INDICATOR_STATUS.INFO : undefined} - rightComponent={rightComponent} - fallbackIcon={fallbackIcon} - /> - {visibleConfigRows.length > 0 && ( - - {visibleConfigRows.map((row) => { - const RowMenuItem = row.shouldRenderAsMenuItem ? MenuItem : MenuItemWithTopDescription; - - return ( - clearMergeConnectionErrorField(policy?.id, card.connectionName, row.field)} - > - Navigation.navigate(row.route)} - interactive={canWriteMoreFeatures} - /> - - ); - })} - - )} - - ); -} - -export default MergeProviderCard; diff --git a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx deleted file mode 100644 index cf10b7f71f45..000000000000 --- a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItem from '@components/MenuItem'; -import MenuItemField from '@components/MenuItem/presets/MenuItemField'; -import MenuItemSectionRoot from '@components/MenuItem/presets/MenuItemSectionRoot'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Section from '@components/Section'; - -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useScreenBoundDynamicRoute from '@hooks/useScreenBoundDynamicRoute'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; - -import Navigation from '@navigation/Navigation'; -import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; -import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; - -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; -import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; - -import CONST from '@src/CONST'; -import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; - -import React, {useEffect} from 'react'; -import {View} from 'react-native'; - -import type {ReceiptPartnerName, ReceiptPartnerRowComponent} from './connectionRows/types'; - -import UberConnectionRow from './connectionRows/UberConnectionRow'; -import useReceiptPartnersSettings from './useReceiptPartnersSettings'; - -const CONNECTION_ROWS: Record = { - [CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER]: UberConnectionRow, -}; - -const receiptPartnerNames = Object.values(CONST.POLICY.RECEIPT_PARTNERS.NAME); - -type WorkspaceReceiptPartnersPageProps = PlatformStackScreenProps; - -function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps) { - const policyID = route.params.policyID; - const icons = useMemoizedLazyExpensifyIcons(['Mail']); - const {translate} = useLocalize(); - const buildDynamicRoute = useScreenBoundDynamicRoute(); - const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {policy, integrations, isAutoInvite, isAutoRemove, isUberConnected, canWriteMoreFeatures, withReadOnlyFallback, toggleUberAutoInvite, toggleUberAutoRemove} = - useReceiptPartnersSettings(policyID); - useWorkspaceDocumentTitle(policy?.name, 'workspace.common.receiptPartners'); - const isLoading = policy?.isLoading; - const centralBillingAccountEmail = !!integrations?.uber?.centralBillingAccountEmail; - // Track focus and connection change to route to the invite flow once after successful connection - const prevIsUberConnected = usePrevious(isUberConnected); - - // When Uber connection status flips from false -> true, navigate to the invite flow once - useEffect(() => { - if (!isUberConnected || prevIsUberConnected || !canWriteMoreFeatures) { - return; - } - Navigation.navigate(buildDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE.getRoute(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER))); - }, [prevIsUberConnected, isUberConnected, policyID, canWriteMoreFeatures, buildDynamicRoute]); - - return ( - - {isLoading ? ( - - ) : ( - - - - -
- {receiptPartnerNames.map((name) => { - const ConnectionRow = CONNECTION_ROWS[name]; - return ( - - ); - })} - {isUberConnected && ( - <> - - - - - - - - - - - {centralBillingAccountEmail && ( - - - - Navigation.navigate( - ROUTES.WORKSPACE_RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT.getRoute( - policyID, - CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER, - ), - ) - : undefined - } - > - - {canWriteMoreFeatures && } - - - - - )} - {canWriteMoreFeatures && ( - - - Navigation.navigate( - buildDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.getRoute(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER)), - ) - } - > - - - - - - {translate('workspace.receiptPartners.uber.manageInvites')} - - - - - - - - )} - - )} -
-
-
-
- )} -
- ); -} - -export default withUnifiedConnectionsBeta(WorkspaceReceiptPartnersPage); diff --git a/src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx b/src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx deleted file mode 100644 index ca2bbc2654de..000000000000 --- a/src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Uber for Business row on the receipt partners page. Shows the partner, a claim-offer badge and either a setup - * button or, once connected, a three-dots menu to re-enter credentials or disconnect. - */ -import UserAvatar from '@components/Avatar/UserAvatar'; -import Badge from '@components/Badge'; -import Button from '@components/Button'; -import FormHelpMessage from '@components/FormHelpMessage'; -import MenuItem from '@components/MenuItem'; -import MenuItemSectionRoot from '@components/MenuItem/presets/MenuItemSectionRoot'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import ThreeDotsMenu from '@components/ThreeDotsMenu'; - -import useConfirmModal from '@hooks/useConfirmModal'; -import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePolicy from '@hooks/usePolicy'; -import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import Navigation from '@navigation/Navigation'; - -import getSynchronizationErrorMessage from '@pages/workspace/receiptPartners/utils'; - -import {openExternalLink} from '@userActions/Link'; -import {openPolicyReceiptPartnersPage, removePolicyReceiptPartnersConnection} from '@userActions/Policy/Policy'; - -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {AnchorPosition} from '@src/styles'; - -import type {ComponentRef} from 'react'; - -import React, {useRef} from 'react'; -import {View} from 'react-native'; - -import type {ReceiptPartnerRowProps} from './types'; - -const UBER = CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER; - -function UberConnectionRow({policyID}: ReceiptPartnerRowProps) { - const icons = useMemoizedLazyExpensifyIcons(['Key', 'NewWindow', 'Trashcan']); - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {isOffline} = useNetwork(); - const {showConfirmModal} = useConfirmModal(); - const overflowMenuAnchorRef = useRef>(null); - const policy = usePolicy(policyID); - const uber = policy?.receiptPartners?.uber; - const {getReceiptPartnersIntegrationData, shouldShowEnterCredentialsError, isUberConnected} = useGetReceiptPartnersIntegrationData(policyID); - const {canWrite, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES); - const integrationData = getReceiptPartnersIntegrationData(UBER); - - if (!integrationData) { - return null; - } - - const hasError = !!integrationData.errorFields || shouldShowEnterCredentialsError; - - const openUberConnect = () => openExternalLink(`${CONST.UBER_CONNECT_URL}?${uber?.connectFormData}`); - - const disconnect = () => { - showConfirmModal({ - title: translate('workspace.moreFeatures.receiptPartnersWarningModal.featureEnabledTitle'), - prompt: translate('workspace.moreFeatures.receiptPartnersWarningModal.description'), - confirmText: translate('workspace.accounting.disconnect'), - cancelText: translate('common.cancel'), - buttonVariant: CONST.BUTTON_VARIANT.DANGER, - }).then(({action}) => { - if (action !== ModalActions.CONFIRM) { - return; - } - removePolicyReceiptPartnersConnection(policyID, UBER, uber); - openPolicyReceiptPartnersPage(policyID); - }); - }; - - const getOverflowMenuAnchorPosition = () => { - if (shouldUseNarrowLayout) { - return Promise.resolve({horizontal: 0, vertical: 0}); - } - return new Promise((resolve) => { - overflowMenuAnchorRef.current?.measureInWindow((x, y, width, height) => { - resolve({horizontal: x + width, vertical: y + height}); - }); - }); - }; - - let overflowMenuItems: PopoverMenuItem[] = []; - if (canWrite && shouldShowEnterCredentialsError) { - overflowMenuItems = [ - { - icon: icons.Key, - text: translate('workspace.accounting.enterCredentials'), - onSelected: openUberConnect, - shouldCallAfterModalHide: true, - disabled: isOffline, - iconRight: icons.NewWindow, - }, - ]; - } else if (canWrite && isUberConnected) { - overflowMenuItems = [ - { - icon: icons.Trashcan, - text: translate('workspace.accounting.disconnect'), - onSelected: disconnect, - shouldCallAfterModalHide: true, - }, - ]; - } - - const badge = ( - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_CLAIM_OFFER.getRoute(policyID, UBER)) : undefined} - pressable={canWrite} - badgeStyles={shouldUseNarrowLayout ? [styles.alignSelfStart, styles.ml13, styles.mt2] : [styles.ml0, hasError && styles.mr1]} - /> - ); - - return ( - - - - - - - {integrationData.title} - {integrationData.description} - - - {!shouldUseNarrowLayout && badge} - {hasError && } - {overflowMenuItems.length > 0 ? ( - - - - ) : ( - - )} - - - {shouldUseNarrowLayout && badge} - {shouldShowEnterCredentialsError && ( - - )} - - ); -} - -export default UberConnectionRow; diff --git a/src/pages/workspace/receiptPartners/connectionRows/types.ts b/src/pages/workspace/receiptPartners/connectionRows/types.ts deleted file mode 100644 index 8d63b4da0344..000000000000 --- a/src/pages/workspace/receiptPartners/connectionRows/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** Contract between the receipt partners page and the per-partner connection rows it renders */ -import type CONST from '@src/CONST'; - -import type {ComponentType} from 'react'; -import type {ValueOf} from 'type-fest'; - -type ReceiptPartnerName = ValueOf; - -/** Props every partner's connection row receives from the receipt partners page */ -type ReceiptPartnerRowProps = { - policyID: string; -}; - -type ReceiptPartnerRowComponent = ComponentType; - -export type {ReceiptPartnerName, ReceiptPartnerRowProps, ReceiptPartnerRowComponent}; diff --git a/src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx b/src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx deleted file mode 100644 index 33a4f1aeb2ba..000000000000 --- a/src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import RenderHTML from '@components/RenderHTML'; - -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; -import usePermissions from '@hooks/usePermissions'; -import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin'; -import usePolicy from '@hooks/usePolicy'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; - -import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; -import MergeConnectionsPageBase from '@pages/workspace/merge/MergeConnectionsPageBase'; - -import CONST from '@src/CONST'; -import type SCREENS from '@src/SCREENS'; - -import React from 'react'; -import {View} from 'react-native'; - -import {getRecruitingCards} from './utils'; - -type WorkspaceRecruitingPageProps = PlatformStackScreenProps; - -function WorkspaceRecruitingPage({ - route: { - params: {policyID}, - }, -}: WorkspaceRecruitingPageProps) { - const {translate, formatPhoneNumber} = useLocalize(); - const styles = useThemeStyles(); - const icons = useMemoizedLazyExpensifyIcons(['Download']); - const policy = usePolicy(policyID); - const policyEmployeePersonalDetails = usePersonalDetailsByLogins([...Object.keys(policy?.employeeList ?? {})]); - const {openConciergeAnywhere} = useOpenConciergeAnywhere(); - const {isBetaEnabled} = usePermissions(); - - const cards = getRecruitingCards({policy, policyEmployeePersonalDetails, policyID, icons, translate, formatPhoneNumber}); - - return ( - - openConciergeAnywhere()} - /> - - } - /> - ); -} - -export default withUnifiedConnectionsBeta(WorkspaceRecruitingPage); diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 0d663f74ae2c..0e4c607bda5e 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -3,7 +3,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -129,7 +128,6 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { const {translate} = useLocalize(); const {isBetaEnabledOrUnknown} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); - const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {accountID, email = ''} = useCurrentUserPersonalDetails(); const getReviewWorkspaceSettingsTaskCompletion = useReviewWorkspaceSettingsTaskCompletion(); const [priorFirstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL); @@ -193,8 +191,8 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { case CONST.UPGRADE_FEATURE_INTRO_MAPPING.travelSubmit.id: return Navigation.goBack(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); case CONST.UPGRADE_FEATURE_INTRO_MAPPING.hr.id: - // Connections also upgrades for HR, and passes itself as the page to return to - return Navigation.goBack((isUnifiedConnectionsBetaEnabled ? route.params.backTo : undefined) ?? ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); + // Connections upgrades for HR and passes itself as the page to return to + return Navigation.goBack(route.params.backTo ?? ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); default: return route.params.backTo ? Navigation.goBack(route.params.backTo) : Navigation.goBack(); } @@ -335,10 +333,10 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { enablePerDiem(policyID, true, perDiemCustomUnit?.customUnitID, false); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.hr.id: - enablePolicyHR(policyID, true, !isUnifiedConnectionsBetaEnabled); + enablePolicyHR(policyID, true, false); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.recruiting.id: - enablePolicyRecruiting(policyID, true, !isUnifiedConnectionsBetaEnabled); + enablePolicyRecruiting(policyID, true, false); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id: case CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvalSubmit.id: diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index d88efc22f740..9be279c95a6a 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -5808,7 +5808,6 @@ describe('actions/Policy', () => { // Given a workspace with Connections off, which means it has no connections to fetch const policy: PolicyType = {...createRandomPolicy(20, CONST.POLICY.TYPE.CORPORATE), areConnectionsEnabled: false, connections: undefined}; await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); - mockFetch?.pause?.(); // When connecting an integration from the Connections page turns Connections on Policy.enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); diff --git a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx index fdc91f331572..a79eb8ac168f 100644 --- a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx +++ b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx @@ -6,7 +6,6 @@ import {ModalProvider} from '@components/Modal/Global/ModalContext'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; -import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; @@ -26,7 +25,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type Beta from '@src/types/onyx/Beta'; import {PortalProvider} from '@gorhom/portal'; import {NavigationContainer} from '@react-navigation/native'; @@ -46,8 +44,6 @@ jest.mock('@components/Modal/ReanimatedModal', () => { return MockReanimatedModal; }); -jest.mock('@hooks/useIsPolicyConnectedToUberReceiptPartner', () => ({__esModule: true, default: jest.fn(() => false)})); - jest.mock('@libs/CardUtils', () => { const actual: typeof CardUtils = jest.requireActual('@libs/CardUtils'); return { @@ -122,7 +118,6 @@ const isSmartLimitEnabledMock = jest.mocked(CardUtils.isSmartLimitEnabled); const getCompanyFeedsMock = jest.mocked(CardUtils.getCompanyFeeds); const hasAccountingConnectionsMock = jest.mocked(PolicyUtils.hasAccountingConnections); const hasAccountingFeatureConnectionMock = jest.mocked(PolicyUtils.hasAccountingFeatureConnection); -const useIsUberConnectedMock = jest.mocked(useIsPolicyConnectedToUberReceiptPartner); const navigateSpy = jest.spyOn(Navigation, 'navigate').mockImplementation(() => undefined); const navigateToConciergeChatSpy = jest.spyOn(ReportActions, 'navigateToConciergeChat').mockImplementation(() => Promise.resolve()); @@ -160,7 +155,6 @@ describe('WorkspaceMoreFeaturesPage', () => { getCompanyFeedsMock.mockReturnValue({}); hasAccountingConnectionsMock.mockReturnValue(false); hasAccountingFeatureConnectionMock.mockReturnValue(false); - useIsUberConnectedMock.mockReturnValue(false); }); afterEach(async () => { @@ -348,45 +342,6 @@ describe('WorkspaceMoreFeaturesPage', () => { }); }); - describe('Accounting toggle (locked when an integration is connected)', () => { - it('locks the Accounting switch when the policy has an active connection', async () => { - await TestHelper.signInWithTestUser(); - hasAccountingConnectionsMock.mockReturnValue(true); - await act(async () => { - // The test user is on the 'all' beta, which includes the unified Connections beta that hides this toggle - await Onyx.merge(ONYXKEYS.BETAS, []); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); - }); - - renderPage({policyID: POLICY_ID}); - await waitForBatchedUpdatesWithAct(); - - await expect(findLockedSwitch('workspace.moreFeatures.connections.subtitle')).resolves.toBeOnTheScreen(); - }); - - it('routes confirm to the accounting page', async () => { - await TestHelper.signInWithTestUser(); - hasAccountingConnectionsMock.mockReturnValue(true); - await act(async () => { - // The test user is on the 'all' beta, which includes the unified Connections beta that hides this toggle - await Onyx.merge(ONYXKEYS.BETAS, []); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); - }); - - renderPage({policyID: POLICY_ID}); - await waitForBatchedUpdatesWithAct(); - fireEvent.press(await findLockedSwitch('workspace.moreFeatures.connections.subtitle')); - - await waitFor(() => { - expect(screen.getByText(TestHelper.translateLocal('workspace.moreFeatures.connectionsWarningModal.disconnectText'))).toBeOnTheScreen(); - }); - fireEvent.press(await screen.findByLabelText(TestHelper.translateLocal('workspace.moreFeatures.connectionsWarningModal.manageSettings'))); - await waitForBatchedUpdatesWithAct(); - - expect(navigateSpy).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); - }); - }); - describe('Concierge-routed disable flows', () => { it('opens Concierge chat when the user confirms the Expensify Card disable warning', async () => { await TestHelper.signInWithTestUser(); @@ -433,66 +388,22 @@ describe('WorkspaceMoreFeaturesPage', () => { }); }); - describe('Receipt partners (informational warning, no confirm action)', () => { - it('opens the disconnect-Uber info modal without a Cancel button when locked', async () => { - await TestHelper.signInWithTestUser(); - useIsUberConnectedMock.mockReturnValue(true); - await act(async () => { - // The test user is on the 'all' beta, which includes the unified Connections beta that hides this toggle - await Onyx.merge(ONYXKEYS.BETAS, []); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); - }); - - renderPage({policyID: POLICY_ID}); - await waitForBatchedUpdatesWithAct(); - fireEvent.press(await findLockedSwitch('workspace.moreFeatures.receiptPartners.subtitle')); - - await waitFor(() => { - expect(screen.getByText(TestHelper.translateLocal('workspace.moreFeatures.receiptPartnersWarningModal.disconnectText'))).toBeOnTheScreen(); - }); - expect(screen.queryByText(TestHelper.translateLocal('common.cancel'))).toBeNull(); - - fireEvent.press(await screen.findByLabelText(TestHelper.translateLocal('workspace.moreFeatures.receiptPartnersWarningModal.confirmText'))); - await waitForBatchedUpdatesWithAct(); - - expect(navigateSpy).not.toHaveBeenCalled(); - expect(navigateToConciergeChatSpy).not.toHaveBeenCalled(); - }); - }); - describe('Integrate section', () => { - const renderWithBetas = async (betas: Beta[]) => { + it('does not show the integration toggles', async () => { + // Given a workspace await TestHelper.signInWithTestUser(); await act(async () => { - await Onyx.merge(ONYXKEYS.BETAS, betas); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); }); + + // When More Features renders renderPage({policyID: POLICY_ID}); await waitForBatchedUpdatesWithAct(); - }; - - const accountingSwitchQuery = () => - screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.connections.subtitle')), 'i')}); - it('shows the Accounting toggle without the unified Connections beta', async () => { - // Given a workspace - // When More Features renders without the beta - await renderWithBetas([]); - - // Then the integration features can still be turned on here, since their pages are only reachable once they are on - expect(accountingSwitchQuery()).toBeOnTheScreen(); - }); - - it('hides the integration toggles with the unified Connections beta', async () => { - // Given a workspace - // When More Features renders with the beta - await renderWithBetas([CONST.BETAS.UNIFIED_CONNECTIONS]); - - // Then the toggles are gone, because Connections turns each feature on as its first integration connects - expect(accountingSwitchQuery()).toBeNull(); + // Then the Accounting toggle is gone, because Connections turns each feature on as its first integration connects + expect(screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.connections.subtitle')), 'i')})).toBeNull(); }); }); - describe('Vendors row (visibility gated on a supported integration)', () => { const renderWithVendorMatching = async (connections: Record, isBetaEnabled = true) => { await TestHelper.signInWithTestUser(); diff --git a/tests/unit/AccountingConnectionConfirmationTest.tsx b/tests/unit/AccountingConnectionConfirmationTest.tsx index eaa05033e0a0..66de76a14d5c 100644 --- a/tests/unit/AccountingConnectionConfirmationTest.tsx +++ b/tests/unit/AccountingConnectionConfirmationTest.tsx @@ -87,8 +87,7 @@ jest.mock('@hooks/useHasReusablePoliciesConnectedTo', () => ({ default: () => false, })); -let mockEnabledBetas: string[] = []; -jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: (beta: string) => mockEnabledBetas.includes(beta)})); +jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: () => false})); const mockEnablePolicyFeatureForConnection = jest.fn(); jest.mock('@userActions/Policy/Policy', () => ({ @@ -165,16 +164,12 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { // Then the user is asked first, and the setup flow stays unmounted so it cannot start behind the prompt expect(mockShowConfirmModal).toHaveBeenCalledTimes(1); - expect(getShowConfirmModalOption('title')).toBe(`workspace.accounting.connectTitle:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.quickbooksOnline}`); - expect(getShowConfirmModalOption('prompt')).toBe(`workspace.accounting.connectPrompt:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.quickbooksOnline}`); - expect(getShowConfirmModalOption('confirmText')).toBe('workspace.accounting.setup'); expect(getShowConfirmModalOption('cancelText')).toBe('common.cancel'); - expect(getShowConfirmModalOption('buttonVariant')).toBe(CONST.BUTTON_VARIANT.SUCCESS); expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); }); it('should reuse one prompt when the flow is started again while the prompt is still unanswered', async () => { - // Given a prompt already raised for a flow that needs a disconnect. `PolicyAccountingPage` starts the flow + // Given a prompt already raised for a flow that needs a disconnect. `WorkspaceConnectionsPage` starts the flow // from a `useFocusEffect` that re-fires whenever `startIntegrationFlow` is re-created, so the same flow can be // started twice before the user has answered. const ref = renderProvider(); @@ -200,25 +195,6 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { expect(screen.getByTestId(SETUP_FLOW_TEST_ID)).toBeOnTheScreen(); }); - it('should use the Intuit Enterprise Suite display name when the integration is one', async () => { - // Given a workspace with an integration connected - const ref = renderProvider(); - - // When the flow being started is the Intuit Enterprise Suite variant, which shares QBO's connection name - await act(async () => { - ref.current?.startIntegrationFlow({ - name: CONST.POLICY.CONNECTIONS.NAME.QBO, - isIntuitEnterpriseSuite: true, - shouldDisconnectIntegrationBeforeConnecting: true, - integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.XERO, - }); - await waitForBatchedUpdates(); - }); - - // Then the prompt names the suite rather than QuickBooks Online, so the user recognises what they bought - expect(getShowConfirmModalOption('title')).toBe('workspace.accounting.connectTitle:workspace.accounting.intuitEnterpriseSuite'); - }); - it('should disconnect the old connection and release the setup flow on confirm', async () => { // Given a prompt raised for a flow that needs the existing connection dropped const ref = renderProvider(); @@ -322,63 +298,53 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); }); - describe('with the unified Connections beta', () => { - beforeEach(() => { - mockEnabledBetas = [CONST.BETAS.UNIFIED_CONNECTIONS]; - }); - - afterEach(() => { - mockEnabledBetas = []; - }); + it('should ask to replace the connected integration, naming the one that would be removed', async () => { + // Given a workspace with an accounting integration connected + const ref = renderProvider(); - it('should ask to replace the connected integration, naming the one that would be removed', async () => { - // Given a workspace with an accounting integration connected - const ref = renderProvider(); + // When a flow that has to disconnect it first is started + await startFlowNeedingDisconnect(ref); - // When a flow that has to disconnect it first is started - await startFlowNeedingDisconnect(ref); + // Then the prompt is about replacing that connection, because Connections lists every integration side by side + expect(getShowConfirmModalOption('title')).toBe('workspace.connections.replaceConnectionTitle'); + expect(getShowConfirmModalOption('prompt')).toBe(`workspace.connections.replaceConnectionPrompt:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.xero}`); + expect(getShowConfirmModalOption('confirmText')).toBe('common.replace'); + expect(getShowConfirmModalOption('buttonVariant')).toBe(CONST.BUTTON_VARIANT.DANGER); + expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); + }); - // Then the prompt is about replacing that connection, because Connections lists every integration side by side - expect(getShowConfirmModalOption('title')).toBe('workspace.connections.replaceConnectionTitle'); - expect(getShowConfirmModalOption('prompt')).toBe(`workspace.connections.replaceConnectionPrompt:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.xero}`); - expect(getShowConfirmModalOption('confirmText')).toBe('common.replace'); - expect(getShowConfirmModalOption('buttonVariant')).toBe(CONST.BUTTON_VARIANT.DANGER); - expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); + it('should name the connected Intuit Enterprise Suite when that is the connection being replaced', async () => { + // Given a workspace whose QuickBooks Online connection is the Intuit Enterprise Suite variant + const ref = renderProvider({ + ...policy, + connections: createMock({quickbooksOnline: {config: {credentials: {scope: CONST.POLICY.CONNECTIONS.INTUIT_ENTERPRISE_SUITE_SCOPE}}}}), }); - it('should name the connected Intuit Enterprise Suite when that is the connection being replaced', async () => { - // Given a workspace whose QuickBooks Online connection is the Intuit Enterprise Suite variant - const ref = renderProvider({ - ...policy, - connections: createMock({quickbooksOnline: {config: {credentials: {scope: CONST.POLICY.CONNECTIONS.INTUIT_ENTERPRISE_SUITE_SCOPE}}}}), - }); - - // When a different integration is started, which means replacing that connection - await act(async () => { - ref.current?.startIntegrationFlow({ - name: CONST.POLICY.CONNECTIONS.NAME.XERO, - shouldDisconnectIntegrationBeforeConnecting: true, - integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.QBO, - }); - await waitForBatchedUpdates(); + // When a different integration is started, which means replacing that connection + await act(async () => { + ref.current?.startIntegrationFlow({ + name: CONST.POLICY.CONNECTIONS.NAME.XERO, + shouldDisconnectIntegrationBeforeConnecting: true, + integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.QBO, }); - - // Then the prompt names the suite rather than QuickBooks Online, so the user recognizes what they would lose - expect(getShowConfirmModalOption('prompt')).toBe('workspace.connections.replaceConnectionPrompt:workspace.accounting.intuitEnterpriseSuite'); + await waitForBatchedUpdates(); }); - it('should turn the Accounting feature on when a flow starts, since Connections has no toggle for it', async () => { - // Given a workspace with the Accounting feature off, which Connections has no toggle for - const ref = renderProvider({...policy, areConnectionsEnabled: false}); + // Then the prompt names the suite rather than QuickBooks Online, so the user recognizes what they would lose + expect(getShowConfirmModalOption('prompt')).toBe('workspace.connections.replaceConnectionPrompt:workspace.accounting.intuitEnterpriseSuite'); + }); - // When a connect flow is started - await act(async () => { - ref.current?.startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.QBO}); - await waitForBatchedUpdates(); - }); + it('should turn the Accounting feature on when a flow starts, since Connections has no toggle for it', async () => { + // Given a workspace with the Accounting feature off, which Connections has no toggle for + const ref = renderProvider({...policy, areConnectionsEnabled: false}); - // Then the feature is turned on without navigating away, so the integration can be used once it connects - expect(mockEnablePolicyFeatureForConnection).toHaveBeenCalledWith(POLICY_ID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + // When a connect flow is started + await act(async () => { + ref.current?.startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.QBO}); + await waitForBatchedUpdates(); }); + + // Then the feature is turned on without navigating away, so the integration can be used once it connects + expect(mockEnablePolicyFeatureForConnection).toHaveBeenCalledWith(POLICY_ID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); }); }); diff --git a/tests/unit/PolicyAccountingPageQBOTokenExpiryTest.tsx b/tests/unit/ConnectionsAccountingPageQBOTokenExpiryTest.tsx similarity index 96% rename from tests/unit/PolicyAccountingPageQBOTokenExpiryTest.tsx rename to tests/unit/ConnectionsAccountingPageQBOTokenExpiryTest.tsx index 77b52432932d..ed4a3018badc 100644 --- a/tests/unit/PolicyAccountingPageQBOTokenExpiryTest.tsx +++ b/tests/unit/ConnectionsAccountingPageQBOTokenExpiryTest.tsx @@ -8,7 +8,6 @@ import type TextComponent from '@components/Text'; import DateUtils from '@libs/DateUtils'; -import PolicyAccountingPage from '@pages/workspace/accounting/PolicyAccountingPage'; import ConnectionsAccountingPage from '@pages/workspace/connections/ConnectionsAccountingPage'; import CONST from '@src/CONST'; @@ -112,8 +111,6 @@ jest.mock('@hooks/usePolicyFeatureWriteAccess', () => ({ // mocked to an identity wrapper above, so the component under test takes `policy` directly. // The Accounting page and the Connections accounting panel share the connected integration's logic, so both are covered. const PAGES_UNDER_TEST = [ - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test-only: the HOC that would inject `policy` is mocked out, so it is passed as a prop here - {pageName: 'PolicyAccountingPage', PageUnderTest: PolicyAccountingPage as unknown as React.ComponentType<{policy: Policy}>}, // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test-only: the HOC that would inject `policy` is mocked out, so it is passed as a prop here {pageName: 'ConnectionsAccountingPage', PageUnderTest: ConnectionsAccountingPage as unknown as React.ComponentType<{policy: Policy}>}, ]; diff --git a/tests/unit/ConnectionsPanelGatesTest.tsx b/tests/unit/ConnectionsPanelGatesTest.tsx index 197cdc840249..885c969754f9 100644 --- a/tests/unit/ConnectionsPanelGatesTest.tsx +++ b/tests/unit/ConnectionsPanelGatesTest.tsx @@ -18,7 +18,6 @@ const NOT_FOUND = 'Not found'; const LOADING = 'Loading'; const POLICY_ID = 'policy-1'; -let mockIsUnifiedConnectionsBetaEnabled = true; let mockConnectedIntegration: string | undefined; let mockIsUberConnected = false; let mockHasConnectionsDataBeenFetched: boolean | undefined; @@ -36,7 +35,6 @@ jest.mock('@components/FullscreenLoadingIndicator', () => { const {default: MockText} = jest.requireActual<{default: typeof TextComponent}>('@components/Text'); return {__esModule: true, default: () => Loading}; }); -jest.mock('@hooks/useIsUnifiedConnectionsBetaEnabled', () => () => mockIsUnifiedConnectionsBetaEnabled); jest.mock('@hooks/useWorkspaceDocumentTitle', () => () => {}); jest.mock('@hooks/useScreenBoundDynamicRoute', () => () => jest.fn()); jest.mock('@hooks/useLocalize', () => () => ({translate: (key: string) => key})); @@ -85,35 +83,22 @@ function buildHRCard(isConnected: boolean): MergeProviderCardDescriptor { describe('Connections panel gates', () => { beforeEach(() => { - mockIsUnifiedConnectionsBetaEnabled = true; mockConnectedIntegration = undefined; mockIsUberConnected = false; mockHasConnectionsDataBeenFetched = true; }); describe('ConnectionsAccountingPage', () => { - it('should block the panel for a user without the beta, even with an integration connected', () => { - // Given a connected workspace viewed by a user without the unified Connections beta - mockIsUnifiedConnectionsBetaEnabled = false; - mockConnectedIntegration = CONST.POLICY.CONNECTIONS.NAME.QBO; - - // When the panel opens from a link - render(); - - // Then it shows Not Found, since this user still has the Accounting page - expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); - }); - it('should block the panel when no accounting integration is connected', () => { - // Given a beta user on a workspace with nothing connected + // Given a workspace with nothing connected render(); // Then the panel shows Not Found, since it only holds a connected integration's settings expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); }); - it('should open the panel for a beta user with an integration connected', () => { - // Given a beta user on a workspace connected to QuickBooks Online + it('should open the panel with an integration connected', () => { + // Given a workspace connected to QuickBooks Online mockConnectedIntegration = CONST.POLICY.CONNECTIONS.NAME.QBO; // When the panel opens @@ -126,7 +111,7 @@ describe('Connections panel gates', () => { describe('ConnectionsMergePageBase', () => { it('should block the panel when no provider of the category is connected', () => { - // Given a beta user on a workspace with no HR provider connected + // Given a workspace with no HR provider connected render( { }); it('should show a loading screen until the connections are fetched', () => { - // Given a beta user deep linking to a workspace whose connections haven't loaded yet + // Given a user deep linking to a workspace whose connections haven't loaded yet mockHasConnectionsDataBeenFetched = undefined; // When the panel opens before any provider shows as connected @@ -158,7 +143,7 @@ describe('Connections panel gates', () => { }); it('should open the panel once a provider is connected', () => { - // Given a beta user on a workspace with Gusto connected + // Given a workspace with Gusto connected render( { describe('ConnectionsReceiptPartnersPage', () => { it('should block the panel when Uber is not connected', () => { - // Given a beta user on a workspace without Uber + // Given a workspace without Uber render(); // Then the panel shows Not Found expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); }); - - it('should block the panel for a user without the beta', () => { - // Given Uber is connected but the user doesn't have the beta - mockIsUnifiedConnectionsBetaEnabled = false; - mockIsUberConnected = true; - render(); - - // Then the panel shows Not Found, since this user still has the Receipt partners page - expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); - }); }); }); diff --git a/tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx b/tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx deleted file mode 100644 index f1b9acae0807..000000000000 --- a/tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import {fireEvent, render, screen, within} from '@testing-library/react-native'; - -import ComposeProviders from '@components/ComposeProviders'; -import {LocaleContextProvider} from '@components/LocaleContextProvider'; -import type {MenuItemProps} from '@components/MenuItem'; -import OnyxListItemProvider from '@components/OnyxListItemProvider'; - -import Navigation from '@libs/Navigation/Navigation'; - -import PolicyAccountingPage from '@pages/workspace/accounting/PolicyAccountingPage'; - -import CONST from '@src/CONST'; -import IntlStore from '@src/languages/IntlStore'; -import type {ConnectionName} from '@src/types/onyx/Policy'; -import type Policy from '@src/types/onyx/Policy'; - -import type * as ReactNavigation from '@react-navigation/native'; -import type {View} from 'react-native'; - -import React from 'react'; - -import createRandomPolicy from '../utils/collections/policies'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; - -const POLICY_ID = 'policy-1'; - -type RouteParams = { - newConnectionName?: ConnectionName; - integrationToDisconnect?: ConnectionName; - shouldDisconnectIntegrationBeforeConnecting?: boolean; - isIntuitEnterpriseSuite?: string; -}; - -let mockRouteParams: RouteParams = {}; - -const mockStartIntegrationFlow = jest.fn(); -const mockShowReadOnlyModal = jest.fn(); -const mockPopoverAnchorRefs = { - current: Object.fromEntries( - [...CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES, ...Object.values(CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES)].map((name) => [name, {current: null}]), - ), -}; -let mockCanWriteAccounting = true; -let mockEnabledBetas: string[] = []; - -jest.mock('@hooks/usePermissions', () => ({ - __esModule: true, - default: () => ({isBetaEnabled: (beta: string) => mockEnabledBetas.includes(beta)}), -})); - -// The real `useFocusEffect` re-runs its callback whenever the callback's identity changes while the screen is focused. -// `useEffect(cb, [cb])` reproduces exactly that, which is the behaviour the guard under test has to survive. -jest.mock('@react-navigation/native', () => { - const actualNavigation = jest.requireActual('@react-navigation/native'); - const {useEffect: mockUseEffect} = jest.requireActual('react'); - return { - ...actualNavigation, - useFocusEffect: (callback: () => void) => { - mockUseEffect(callback, [callback]); - }, - useRoute: () => ({params: mockRouteParams}), - useNavigation: () => ({}), - useIsFocused: () => true, - }; -}); - -// `Navigation.setParams` clears `newConnectionName` through react-navigation's state update, which lands in a later -// render than the synchronous call. The mock therefore only records the requested params; each test applies them when -// it wants that state update to land, which is what lets the two orderings below be told apart. -let mockPendingParams: RouteParams | undefined; -jest.mock('@libs/Navigation/Navigation', () => ({ - __esModule: true, - default: { - setParams: jest.fn((params: RouteParams) => { - mockPendingParams = params; - }), - navigate: jest.fn(), - goBack: jest.fn(), - isNavigationReady: () => Promise.resolve(), - }, -})); - -/** Lands the pending `Navigation.setParams` update, as a real navigation state change eventually would. */ -function landPendingParamsUpdate() { - mockRouteParams = {...mockRouteParams, ...mockPendingParams}; - mockPendingParams = undefined; -} - -jest.mock('@pages/workspace/accounting/AccountingContext', () => ({ - __esModule: true, - AccountingContextProvider: ({children}: {children: React.ReactNode}) => children, - useAccountingActions: () => ({startIntegrationFlow: mockStartIntegrationFlow}), - useAccountingState: () => ({activeIntegration: undefined, popoverAnchorRefs: mockPopoverAnchorRefs}), -})); - -jest.mock('@pages/workspace/withPolicyConnections', () => ({ - __esModule: true, - default: (Component: React.ComponentType<{policy: Policy}>) => Component, -})); - -jest.mock('@pages/workspace/AccessOrNotFoundWrapper', () => ({ - __esModule: true, - default: ({children}: {children: React.ReactNode}) => children, -})); - -jest.mock('@components/ScreenWrapper', () => ({ - __esModule: true, - default: ({children}: {children: React.ReactNode}) => children, -})); - -jest.mock('@components/ScrollView', () => ({ - __esModule: true, - default: ({children}: {children: React.ReactNode}) => children, -})); - -jest.mock('@components/HeaderWithBackButton', () => ({__esModule: true, default: () => null})); -jest.mock('@components/MenuItemList', () => ({__esModule: true, default: () => null})); -jest.mock('@components/MenuItem', () => { - const {View: MockView} = jest.requireActual<{View: typeof View}>('react-native'); - return { - __esModule: true, - default: ({title, rightComponent}: MenuItemProps) => {rightComponent}, - }; -}); -jest.mock('@components/Section', () => ({__esModule: true, default: ({children}: {children: React.ReactNode}) => children})); -jest.mock('@components/CollapsibleSection', () => ({__esModule: true, default: () => null})); -jest.mock('@components/ThreeDotsMenu', () => ({__esModule: true, default: () => null})); -jest.mock('@components/ActivityIndicator', () => ({__esModule: true, default: () => null})); - -jest.mock('@libs/PolicyUtils', () => { - const actual = jest.requireActual>('@libs/PolicyUtils'); - return { - ...actual, - isControlPolicy: () => true, - }; -}); - -jest.mock('@hooks/usePolicyFeatureWriteAccess', () => ({ - __esModule: true, - default: () => ({canWrite: mockCanWriteAccounting, showReadOnlyModal: mockShowReadOnlyModal}), -})); - -// The real `withPolicyConnections` HOC reads `policy` from Onyx and strips it from the component's public props. It is -// mocked to an identity wrapper above, so the component under test takes `policy` directly. -// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test-only: the HOC that would inject `policy` is mocked out, so it is passed as a prop here -const PolicyAccountingPageUnderTest = PolicyAccountingPage as unknown as React.ComponentType<{policy: Policy}>; - -function buildPolicy(overrides: Partial = {}): Policy { - return {...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE, 'Test workspace'), id: POLICY_ID, ...overrides}; -} - -function AccountingTestWrapper({children}: {children: React.ReactNode}) { - return {children}; -} - -describe('PolicyAccountingPage auto-started connect flow', () => { - beforeAll(() => IntlStore.load(CONST.LOCALES.EN)); - - beforeEach(() => { - jest.clearAllMocks(); - mockCanWriteAccounting = true; - mockEnabledBetas = []; - mockPendingParams = undefined; - mockRouteParams = { - newConnectionName: CONST.POLICY.CONNECTIONS.NAME.QBO, - integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.XERO, - shouldDisconnectIntegrationBeforeConnecting: true, - }; - }); - - it('should start the flow once when the effect re-runs before the cleared param has landed', async () => { - // Given the page opened by a route that asks for a connect flow, which starts it and asks for the param to be - // cleared so it cannot be acted on twice - const {rerender} = render(, {wrapper: AccountingTestWrapper}); - await waitForBatchedUpdates(); - - expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); - expect(Navigation.setParams).toHaveBeenCalled(); - - // When anything re-creates `startIntegrationFlow` and re-runs the effect before that clear has landed, which a - // policy update does. This is the window the guard exists for: `newConnectionName` is still set here. - rerender(); - await waitForBatchedUpdates(); - - // Then the flow is not started again, because a second start is what used to stack a second confirmation - // prompt the user had to dismiss twice - expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); - }); - - it('should start the flow again for a later round-trip that asks for the same integration', async () => { - // Given a connect flow that was started from the route param and then let the clear land, so nothing is - // pending any more - const {rerender} = render(, {wrapper: AccountingTestWrapper}); - await waitForBatchedUpdates(); - - expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); - - landPendingParamsUpdate(); - rerender(); - await waitForBatchedUpdates(); - - expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); - - // When the user comes back later and asks for the very same integration again - mockRouteParams = { - newConnectionName: CONST.POLICY.CONNECTIONS.NAME.QBO, - integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.XERO, - shouldDisconnectIntegrationBeforeConnecting: true, - }; - rerender(); - await waitForBatchedUpdates(); - - // Then it is honoured rather than swallowed as a repeat of the first run, or the connect flow would silently - // do nothing the second time round - expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(2); - }); - - it.each([{enabledBetas: []}, {enabledBetas: ['campfire']}])('should offer Campfire with beta enrollment $enabledBetas', async ({enabledBetas}) => { - // Given an eligible workspace with no accounting connection - mockEnabledBetas = enabledBetas; - mockRouteParams = {}; - - // When the admin connects Campfire from the accounting page - render(, {wrapper: AccountingTestWrapper}); - await waitForBatchedUpdates(); - fireEvent.press(within(screen.getByTestId('Campfire')).getByRole('button')); - - // Then Campfire starts without enabling other integrations - expect(mockStartIntegrationFlow).toHaveBeenCalledWith({name: CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE, isIntuitEnterpriseSuite: undefined}); - expect(screen.queryByTestId(CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.businessCentral)).toBeNull(); - }); - - it('should keep Campfire connection changes read-only without accounting write access', async () => { - // Given a workspace whose accounting settings are read-only - mockCanWriteAccounting = false; - mockRouteParams = {}; - - // When the user presses the Campfire connect button - render(, {wrapper: AccountingTestWrapper}); - await waitForBatchedUpdates(); - fireEvent.press(within(screen.getByTestId('Campfire')).getByRole('button')); - - // Then the read-only explanation appears without starting a connection - expect(mockShowReadOnlyModal).toHaveBeenCalledTimes(1); - expect(mockStartIntegrationFlow).not.toHaveBeenCalled(); - }); - - it('should preserve Business Central beta access alongside Campfire', async () => { - // Given an eligible workspace enrolled in the Business Central beta - mockEnabledBetas = [CONST.BETAS.BUSINESS_CENTRAL]; - mockRouteParams = {}; - - // When viewing the available accounting integrations - render(, {wrapper: AccountingTestWrapper}); - await waitForBatchedUpdates(); - - // Then the enrolled integration and Campfire are both available - expect(screen.getByTestId(CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.businessCentral)).toBeOnTheScreen(); - expect(screen.getByTestId('Campfire')).toBeOnTheScreen(); - }); -}); diff --git a/tests/unit/SearchRouterNavigationTest.ts b/tests/unit/SearchRouterNavigationTest.ts index 8f2ffc545e48..f2a2427053c6 100644 --- a/tests/unit/SearchRouterNavigationTest.ts +++ b/tests/unit/SearchRouterNavigationTest.ts @@ -203,9 +203,7 @@ const workspaceIcons = { Users: mockIcon, Hashtag: mockIcon, Document: mockIcon, - Sync: mockIcon, Connect: mockIcon, - Receipt: mockIcon, Briefcase: mockIcon, Folder: mockIcon, Tag: mockIcon, @@ -222,7 +220,6 @@ const workspaceIcons = { Gear: mockIcon, Bolt: mockIcon, Bot: mockIcon, - UserPlus: mockIcon, }; function createWorkspacePolicy(id: string, name: string, overrides: Partial = {}): Policy { @@ -705,7 +702,7 @@ describe('Workspace Search Router navigation source', () => { jest.clearAllMocks(); }); - const buildItems = (policies: Policy[], isOffline = false, isUnifiedConnectionsBetaEnabled = false) => + const buildItems = (policies: Policy[], isOffline = false) => buildWorkspaceNavigationItems({ policies: Object.fromEntries(policies.map((policy) => [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy])), policyCategories: undefined, @@ -713,9 +710,7 @@ describe('Workspace Search Router navigation source', () => { icons: workspaceIcons, isOffline, isVendorMatchingBetaEnabled: false, - isRecruitingBetaEnabled: false, isCompanyAgentsBetaEnabled: false, - isUnifiedConnectionsBetaEnabled, shouldUseNarrowLayout: false, convertToDisplayString: () => '$0.00', getItemText: (item) => { @@ -756,14 +751,14 @@ describe('Workspace Search Router navigation source', () => { const items = buildItems([createWorkspacePolicy('1', 'Beta Workspace', {isHREnabled: true}), createWorkspacePolicy('2', 'Alpha Workspace', {isHREnabled: true})]); expect(buildNavigationSuggestions('hr', [items], localeCompare).map((item) => item.keyForList)).toEqual([ - `workspace_2_${SCREENS.WORKSPACE.HR}`, - `workspace_1_${SCREENS.WORKSPACE.HR}`, + `workspace_2_${SCREENS.WORKSPACE.CONNECTIONS}`, + `workspace_1_${SCREENS.WORKSPACE.CONNECTIONS}`, ]); }); - it('matches the Connections row by the name of a page it replaced when the unified Connections beta is on', () => { - // Given a workspace with the HR feature on, viewed with the beta on so HR has no row of its own - const items = buildItems([createWorkspacePolicy('1', 'Alpha Workspace', {isHREnabled: true})], false, true); + it('matches the Connections row by the name of a page it replaced', () => { + // Given a workspace with the HR feature on, which has no row of its own + const items = buildItems([createWorkspacePolicy('1', 'Alpha Workspace', {isHREnabled: true})]); // When searching for the old HR page by its short name const suggestions = buildNavigationSuggestions('hr', [items], localeCompare); @@ -846,11 +841,9 @@ describe('Workspace Search Router navigation source', () => { expect.objectContaining({ policy: activePolicy, isVendorMatchingBetaEnabled: true, - isRecruitingBetaEnabled: true, }), ); expect(mockIsBetaEnabled).toHaveBeenCalledWith(CONST.BETAS.VENDOR_MATCHING); - expect(mockIsBetaEnabled).toHaveBeenCalledWith(CONST.BETAS.MERGE_ATS); }); }); diff --git a/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx b/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx index c4dec08dde9f..67625d6f53d3 100644 --- a/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx +++ b/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx @@ -22,9 +22,11 @@ type RouteParams = { integrationToDisconnect?: ConnectionName; shouldDisconnectIntegrationBeforeConnecting?: boolean; isIntuitEnterpriseSuite?: string; + tab?: string; }; let mockRouteParams: RouteParams = {}; +let mockActiveTabKey: string | undefined; const mockStartIntegrationFlow = jest.fn(); @@ -94,7 +96,13 @@ jest.mock('@components/ScrollView', () => ({ })); jest.mock('@components/SidePanel/SidePanelButton', () => ({__esModule: true, default: () => null})); -jest.mock('@components/TabSelector/TabSelectorBase', () => ({__esModule: true, default: () => null})); +jest.mock('@components/TabSelector/TabSelectorBase', () => ({ + __esModule: true, + default: ({activeTabKey}: {activeTabKey?: string}) => { + mockActiveTabKey = activeTabKey; + return null; + }, +})); jest.mock('@components/RenderHTML', () => ({__esModule: true, default: () => null})); jest.mock('@pages/workspace/connections/ConnectionsGrid', () => ({__esModule: true, default: () => null})); jest.mock('@pages/workspace/connections/useAccountingConnectionListings', () => ({__esModule: true, default: () => []})); @@ -184,3 +192,29 @@ describe('WorkspaceConnectionsPage auto-started connect flow', () => { expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(2); }); }); + +describe('WorkspaceConnectionsPage tab from the route', () => { + it('should open on the tab the link asks for', async () => { + // Given a link to Connections that asks for the Accounting tab, like the onboarding accounting task + mockRouteParams = {tab: CONST.TAB.CONNECTIONS.ACCOUNTING}; + + // When the page opens + render(); + await waitForBatchedUpdates(); + + // Then the Accounting tab is selected, so the user lands on the integrations the link was about + expect(mockActiveTabKey).toBe(CONST.TAB.CONNECTIONS.ACCOUNTING); + }); + + it('should fall back to All for a tab it does not know', async () => { + // Given a link with a tab value that is not one of the page's tabs + mockRouteParams = {tab: 'unknown'}; + + // When the page opens + render(); + await waitForBatchedUpdates(); + + // Then the page opens on All instead of an empty selection + expect(mockActiveTabKey).toBe(CONST.TAB.CONNECTIONS.ALL); + }); +}); diff --git a/tests/unit/getWorkspaceMenuItemsTest.ts b/tests/unit/getWorkspaceMenuItemsTest.ts index 209529ae1562..def36b238a38 100644 --- a/tests/unit/getWorkspaceMenuItemsTest.ts +++ b/tests/unit/getWorkspaceMenuItemsTest.ts @@ -16,9 +16,7 @@ const icons = { Users: mockIcon, Hashtag: mockIcon, Document: mockIcon, - Sync: mockIcon, Connect: mockIcon, - Receipt: mockIcon, Briefcase: mockIcon, Folder: mockIcon, Tag: mockIcon, @@ -35,7 +33,6 @@ const icons = { Gear: mockIcon, Bolt: mockIcon, Bot: mockIcon, - UserPlus: mockIcon, }; function buildPolicy(role: Policy['role']): Policy { @@ -153,7 +150,7 @@ describe('getWorkspaceMenuItems', () => { expect(items.find((item) => item.translationKey === 'workspace.common.profile')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); - it('shows an accounting sync error only when a sync is not in progress', () => { + it('shows an accounting sync error on Connections only when a sync is not in progress', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), areConnectionsEnabled: true, @@ -181,23 +178,8 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(buildItems(false).find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); - expect(buildItems(true).find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBeUndefined(); - }); - - it('shows an error indicator when receipt partner credentials require attention', () => { - const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), receiptPartners: {enabled: true}}); - - const items = getWorkspaceMenuItems({ - policy, - policyID: policy.id, - currentUserLogin, - icons, - shouldShowEnterCredentialsError: true, - convertToDisplayString: () => '', - }); - - expect(items.find((item) => item.translationKey === 'workspace.common.receiptPartners')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + expect(buildItems(false).find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + expect(buildItems(true).find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBeUndefined(); }); it('shows an error indicator when company cards require attention', () => { @@ -317,7 +299,7 @@ describe('getWorkspaceMenuItems', () => { expect(items.find((item) => item.translationKey === 'workspace.common.rules')?.brickRoadIndicator).toBeUndefined(); }); - it('shows an information indicator when Merge HR setup is incomplete', () => { + it('shows an information indicator on Connections when Merge HR setup is incomplete', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), isHREnabled: true, @@ -338,10 +320,10 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.hr')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); + expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); }); - it('shows an information indicator on Accounting when the QBO refresh token is about to expire', () => { + it('shows an information indicator on Connections when the QBO refresh token is about to expire', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), areConnectionsEnabled: true, @@ -361,10 +343,10 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); + expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); }); - it('keeps the error indicator on Accounting when a sync error exists even if the QBO refresh token is about to expire', () => { + it('keeps the error indicator on Connections when a sync error exists even if the QBO refresh token is about to expire', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), areConnectionsEnabled: true, @@ -384,10 +366,10 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); - it('shows an error indicator when the Merge HR connection has an authentication error', () => { + it('shows an error indicator on Connections when the Merge HR connection has an authentication error', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), isHREnabled: true, @@ -407,85 +389,7 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.hr')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); - }); - - it('hides the Recruiting row when the Merge ATS beta is disabled', () => { - const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), isRecruitingEnabled: true}); - - const buildItems = (isRecruitingBetaEnabled: boolean) => - getWorkspaceMenuItems({ - policy, - policyID: policy.id, - currentUserLogin, - icons, - isRecruitingBetaEnabled, - convertToDisplayString: () => '', - }); - - expect(buildItems(false).find((item) => item.translationKey === 'workspace.common.recruiting')).toBeUndefined(); - - const recruitingItem = buildItems(true).find((item) => item.translationKey === 'workspace.common.recruiting'); - expect(recruitingItem?.getRoute()).toBe(ROUTES.WORKSPACE_RECRUITING.getRoute(policy.id)); - expect(recruitingItem?.screenName).toBe(SCREENS.WORKSPACE.RECRUITING); - }); - - it('shows the Recruiting row when an ATS connection exists even without the policy flag', () => { - const policy = createMock({ - ...buildPolicy(CONST.POLICY.ROLE.ADMIN), - isRecruitingEnabled: undefined, - connections: { - [CONST.POLICY.CONNECTIONS.NAME.MERGE_ATS]: { - config: {integration: 'greenhouse'}, - lastSync: {syncStatus: CONST.MERGE.SYNC_STATUS.DONE}, - }, - }, - }); - - const items = getWorkspaceMenuItems({ - policy, - policyID: policy.id, - currentUserLogin, - icons, - isRecruitingBetaEnabled: true, - convertToDisplayString: () => '', - }); - - expect(items.find((item) => item.translationKey === 'workspace.common.recruiting')).toBeDefined(); - }); - - it('hides the Recruiting row on a workspace whose plan cannot access the feature', () => { - const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), type: CONST.POLICY.TYPE.TEAM, isRecruitingEnabled: true}); - - const items = getWorkspaceMenuItems({ - policy, - policyID: policy.id, - currentUserLogin, - icons, - isRecruitingBetaEnabled: true, - convertToDisplayString: () => '', - }); - - expect(items.find((item) => item.translationKey === 'workspace.common.recruiting')).toBeUndefined(); - }); - - it('does not highlight a newly enabled Recruiting feature while the Merge ATS beta is disabled', () => { - const policy = createMock({ - ...buildPolicy(CONST.POLICY.ROLE.ADMIN), - isRecruitingEnabled: true, - pendingFields: {isRecruitingEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, - }); - - const items = getWorkspaceMenuItems({ - policy, - policyID: policy.id, - currentUserLogin, - icons, - previousPendingFields: {}, - convertToDisplayString: () => '', - }); - - expect(items.filter((item) => item.highlighted)).toHaveLength(0); + expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); it('preserves the complete enabled Workspace menu order and presentation data', () => { @@ -523,7 +427,6 @@ describe('getWorkspaceMenuItems', () => { currentUserLogin, icons, isVendorMatchingBetaEnabled: true, - isRecruitingBetaEnabled: true, convertToDisplayString, }); @@ -532,11 +435,7 @@ describe('getWorkspaceMenuItems', () => { 'workspace.common.members', 'workspace.common.rooms', 'common.reports', - 'workspace.common.accounting', - 'workspace.common.hr', - 'workspace.common.recruiting', - 'workspace.common.receiptPartners', - 'workspace.common.mcp', + 'workspace.common.connections', 'workspace.common.categories', 'workspace.common.tags', 'workspace.common.taxes', @@ -557,11 +456,7 @@ describe('getWorkspaceMenuItems', () => { ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id), ROUTES.WORKSPACE_ROOMS.getRoute(policy.id), ROUTES.WORKSPACE_REPORTS.getRoute(policy.id), - ROUTES.POLICY_ACCOUNTING.getRoute(policy.id), - ROUTES.WORKSPACE_HR.getRoute(policy.id), - ROUTES.WORKSPACE_RECRUITING.getRoute(policy.id), - ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policy.id), - ROUTES.WORKSPACE_MCP.getRoute(policy.id), + ROUTES.WORKSPACE_CONNECTIONS.getRoute(policy.id), ROUTES.WORKSPACE_CATEGORIES.getRoute(policy.id), ROUTES.WORKSPACE_TAGS.getRoute(policy.id), ROUTES.WORKSPACE_TAXES.getRoute(policy.id), @@ -582,11 +477,7 @@ describe('getWorkspaceMenuItems', () => { SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.ROOMS, SCREENS.WORKSPACE.REPORTS, - SCREENS.WORKSPACE.ACCOUNTING.ROOT, - SCREENS.WORKSPACE.HR, - SCREENS.WORKSPACE.RECRUITING, - SCREENS.WORKSPACE.RECEIPT_PARTNERS, - SCREENS.WORKSPACE.MCP, + SCREENS.WORKSPACE.CONNECTIONS, SCREENS.WORKSPACE.CATEGORIES, SCREENS.WORKSPACE.TAGS, SCREENS.WORKSPACE.TAXES, @@ -608,7 +499,7 @@ describe('getWorkspaceMenuItems', () => { }); }); -describe('getWorkspaceMenuItems with the unified Connections beta', () => { +describe('getWorkspaceMenuItems Connections row', () => { it('replaces the separate integration rows with a single Connections row', () => { // Given an admin on a workspace with every integration feature turned on const policy = createMock({ @@ -620,14 +511,12 @@ describe('getWorkspaceMenuItems with the unified Connections beta', () => { isMCPEnabled: true, }); - // When the Workspace menu is built with the beta on + // When the Workspace menu is built const items = getWorkspaceMenuItems({ policy, policyID: policy.id, currentUserLogin, icons, - isRecruitingBetaEnabled: true, - isUnifiedConnectionsBetaEnabled: true, convertToDisplayString: () => '', }); @@ -663,13 +552,12 @@ describe('getWorkspaceMenuItems with the unified Connections beta', () => { receiptPartners: {enabled: false}, }); - // When the Workspace menu is built with the beta on + // When the Workspace menu is built const items = getWorkspaceMenuItems({ policy, policyID: policy.id, currentUserLogin, icons, - isUnifiedConnectionsBetaEnabled: true, convertToDisplayString: () => '', }); @@ -681,14 +569,13 @@ describe('getWorkspaceMenuItems with the unified Connections beta', () => { // Given a workspace whose receipt partner credentials need attention const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), receiptPartners: {enabled: true}}); - // When the Workspace menu is built with the beta on + // When the Workspace menu is built const items = getWorkspaceMenuItems({ policy, policyID: policy.id, currentUserLogin, icons, shouldShowEnterCredentialsError: true, - isUnifiedConnectionsBetaEnabled: true, convertToDisplayString: () => '', }); diff --git a/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx b/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx index 4027823e98ee..4dea5275cabe 100644 --- a/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx +++ b/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx @@ -89,13 +89,13 @@ describe('NetSuiteSetupPage', () => { expect(await screen.findByTestId('netsuite-webview')).toBeOnTheScreen(); }); - it('returns to the accounting page on back, skipping the token input wizard underneath', async () => { + it('returns to the Connections page on back, skipping the token input wizard underneath', async () => { renderNetSuiteSetupPage(ACCOUNT_ID); expect(await screen.findByTestId('netsuite-webview')).toBeOnTheScreen(); mockHeaderProps.current?.onBackButtonPress?.(); - expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); + expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID)); }); it('renders nothing when the account ID is missing from the route', () => { diff --git a/tests/unit/useAccountingConnectionListingsTest.ts b/tests/unit/useAccountingConnectionListingsTest.ts index bdc53ce368dd..d1a7c67a75e0 100644 --- a/tests/unit/useAccountingConnectionListingsTest.ts +++ b/tests/unit/useAccountingConnectionListingsTest.ts @@ -14,8 +14,8 @@ let mockCanWrite = true; let mockConnectedIntegration: ConnectionName | undefined; jest.mock('@hooks/useLocalize', () => () => ({translate: (key: string) => key, datetimeToRelative: () => '', getLocalDateFromDatetime: jest.fn()})); -jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: () => false})); -jest.mock('@hooks/useIsUnifiedConnectionsBetaEnabled', () => () => true); +let mockEnabledBetas: string[] = []; +jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: (beta: string) => mockEnabledBetas.includes(beta)})); jest.mock('@hooks/useOnyx', () => () => [undefined]); jest.mock('@hooks/useThemeStyles', () => () => ({})); jest.mock('@hooks/useLazyAsset', () => ({useMemoizedLazyExpensifyIcons: () => ({})})); @@ -50,6 +50,7 @@ describe('useAccountingConnectionListings', () => { jest.clearAllMocks(); mockCanWrite = true; mockConnectedIntegration = undefined; + mockEnabledBetas = []; }); it('should start the connect flow for a workspace with no accounting integration', () => { @@ -103,4 +104,27 @@ describe('useAccountingConnectionListings', () => { // Then QuickBooks Online carries a healthy status, so it shows in the connected section with Configure expect(qbo?.status?.isBroken).toBe(false); }); + it.each([{enabledBetas: []}, {enabledBetas: ['campfire']}])('should offer Campfire with beta enrollment $enabledBetas', ({enabledBetas}) => { + // Given a workspace with no accounting connection + mockEnabledBetas = enabledBetas; + + // When the accounting listings are built + const campfire = findListing(CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE); + + // Then Campfire is offered, because it no longer needs a beta, and Business Central stays hidden without its beta + expect(campfire).toBeDefined(); + expect(findListing(CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL)).toBeUndefined(); + }); + + it('should offer Business Central alongside Campfire to a workspace in the Business Central beta', () => { + // Given a workspace enrolled in the Business Central beta + mockEnabledBetas = [CONST.BETAS.BUSINESS_CENTRAL]; + + // When the accounting listings are built + const businessCentral = findListing(CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL); + + // Then both integrations are available + expect(businessCentral).toBeDefined(); + expect(findListing(CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE)).toBeDefined(); + }); }); diff --git a/tests/unit/withUnifiedConnectionsBetaTest.tsx b/tests/unit/withUnifiedConnectionsBetaTest.tsx deleted file mode 100644 index 55082759b98a..000000000000 --- a/tests/unit/withUnifiedConnectionsBetaTest.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import {render, screen} from '@testing-library/react-native'; - -import Text from '@components/Text'; - -import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; - -import React from 'react'; - -let mockIsUnifiedConnectionsBetaEnabled = false; -jest.mock('@hooks/useIsUnifiedConnectionsBetaEnabled', () => ({ - __esModule: true, - default: () => mockIsUnifiedConnectionsBetaEnabled, -})); - -jest.mock('@pages/workspace/connections/WorkspaceConnectionsPage', () => { - const {default: MockText} = jest.requireActual<{default: typeof Text}>('@components/Text'); - return { - __esModule: true, - default: () => Connections page, - }; -}); - -function OldIntegrationPage({label}: {label: string}) { - return {label}; -} - -const OldIntegrationPageWithBeta = withUnifiedConnectionsBeta(OldIntegrationPage); - -describe('withUnifiedConnectionsBeta', () => { - it('should render the wrapped page while the beta is off', () => { - // Given a user without the unified Connections beta - mockIsUnifiedConnectionsBetaEnabled = false; - - // When an old integration page renders - render(); - - // Then the old page shows as before, since only beta users moved to Connections - expect(screen.getByText('Accounting page')).toBeOnTheScreen(); - expect(screen.queryByText('Connections page')).not.toBeOnTheScreen(); - }); - - it('should render the Connections page in its place while the beta is on', () => { - // Given a user with the unified Connections beta - mockIsUnifiedConnectionsBetaEnabled = true; - - // When an old link opens an integration page - render(); - - // Then Connections shows instead, since the old page is no longer in the workspace menu for this user - expect(screen.getByText('Connections page')).toBeOnTheScreen(); - expect(screen.queryByText('Accounting page')).not.toBeOnTheScreen(); - }); -}); From 0b1c2bd6179fd49f1be4de8b515f5ca010337930 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Oct 2026 05:10:15 +0530 Subject: [PATCH 4/8] Open the HR and Recruiting panels from their links, keep Card reconciliation Back on the stack, and only skip the connections fetch for a workspace still being created Signed-off-by: krishna2323 --- src/components/Navigation/DebugTabView.tsx | 2 +- src/libs/WorkflowUtils.ts | 4 +-- src/libs/actions/Policy/Policy.ts | 7 +++--- .../items/FixPolicyConnection.tsx | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- .../reconciliation/CardReconciliationPage.tsx | 5 +--- tests/actions/PolicyTest.ts | 25 ++++++++++++++++--- tests/unit/WorkflowUtilsTest.ts | 4 +-- 8 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx index f031578e1cce..2c6f86aa6c91 100644 --- a/src/components/Navigation/DebugTabView.tsx +++ b/src/components/Navigation/DebugTabView.tsx @@ -138,7 +138,7 @@ function getSettingsRoute(status: IndicatorStatus | undefined, reimbursementAcco return ROUTES.SETTINGS_WALLET; case CONST.INDICATOR_STATUS.HAS_MERGE_HR_SETUP_NEEDED: case CONST.INDICATOR_STATUS.HAS_HR_CONNECTION_ERROR: - return ROUTES.WORKSPACE_HR.getRoute(indicatorPolicyID); + return ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(indicatorPolicyID); default: return undefined; } diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 5122c8169bf8..e478c13113ab 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -56,13 +56,13 @@ function getApprovalWorkflowSource(policy: OnyxEntry, policyID: string | if (isAnyHRConnected(policy)) { return { providerName: getConnectedHRProvider(policy)?.displayName ?? '', - settingsRoute: ROUTES.WORKSPACE_HR.getRoute(policyID), + settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), }; } if (isAnyRecruitingReadOnlyWorkflowMode(policy)) { return { providerName: getConnectedATSProvider(policy)?.displayName ?? '', - settingsRoute: ROUTES.WORKSPACE_RECRUITING.getRoute(policyID), + settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_RECRUITING.getRoute(policyID), }; } return undefined; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index c0fa09d74884..21fc39406455 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -5160,9 +5160,10 @@ function enablePolicyFeatureForConnection(policy: OnyxEntry, featureName if (!policy?.id || PolicyUtils.isPolicyFeatureEnabled(policy, featureName)) { return; } - // A workspace can't be connected while Connections is off, so there is nothing to fetch. Without this, the open - // Connections page would be replaced by a loader. It's set first so the page never sees the feature on without it. - if (featureName === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED && isEmptyObject(policy.connections)) { + // A workspace that is still being created has no connections to fetch. Without this, the open Connections page + // would be replaced by a loader while the fetch waits behind the create request. It's set first so the page never + // sees the feature on without it. + if (featureName === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED && policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policy.id}`, true); } ENABLE_CONNECTION_FEATURE[featureName](policy.id, true, false); diff --git a/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx b/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx index 6d983be067ef..514ec7540510 100644 --- a/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx +++ b/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx @@ -34,7 +34,7 @@ function FixPolicyConnection({connectionName, policyID, policyName, integrationN : translate('homePage.timeSensitiveSection.fixPolicyConnection.defaultSubtitle'); const isHRConnection = (CONST.POLICY.CONNECTIONS.HR_CONNECTION_NAMES as readonly PolicyConnectionName[]).includes(connectionName); - const fixRoute = isHRConnection ? ROUTES.WORKSPACE_HR.getRoute(policyID) : ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID); + const fixRoute = isHRConnection ? ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID) : ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID); return ( Navigation.navigate(ROUTES.WORKSPACE_HR.getRoute(policyID))}> + Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID))}> {translate('workspace.people.configureHRSync', connectedHRProvider?.displayName ?? '')} )} diff --git a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx index fb909e356fa0..e6cb49ff4fb3 100644 --- a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx +++ b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx @@ -145,10 +145,7 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) { shouldEnableMaxHeight testID="CardReconciliationPage" > - Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} - /> + { apiWriteSpy.mockRestore(); }); - it('should mark the connections as fetched when it turns Connections on for a workspace with no connections', async () => { - // Given a workspace with Connections off, which means it has no connections to fetch - const policy: PolicyType = {...createRandomPolicy(20, CONST.POLICY.TYPE.CORPORATE), areConnectionsEnabled: false, connections: undefined}; + it('should mark the connections as fetched when it turns Connections on for a workspace that is still being created', async () => { + // Given a workspace whose create request hasn't finished, so it can't have connections yet + const policy: PolicyType = { + ...createRandomPolicy(20, CONST.POLICY.TYPE.CORPORATE), + areConnectionsEnabled: false, + connections: undefined, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); // When connecting an integration from the Connections page turns Connections on @@ -5817,6 +5822,20 @@ describe('actions/Policy', () => { expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policy.id}`)).toBe(true); expect((await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`))?.areConnectionsEnabled).toBe(true); }); + + it('should leave the connections to be fetched for an existing workspace', async () => { + // Given an existing workspace with Connections off, which can still hold connections that haven't loaded, + // like an integration only supported on OldDot + const policy: PolicyType = {...createRandomPolicy(21, CONST.POLICY.TYPE.CORPORATE), areConnectionsEnabled: false, connections: undefined, pendingAction: undefined}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + + // When connecting an integration from the Connections page turns Connections on + Policy.enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + await waitForBatchedUpdates(); + + // Then the connections are not marked as fetched, so the page still loads any that exist + expect(await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policy.id}`)).toBeUndefined(); + }); }); describe('enablePolicyTaxes', () => { diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts index 7a4a3fd7cf63..372ef6694b44 100644 --- a/tests/unit/WorkflowUtilsTest.ts +++ b/tests/unit/WorkflowUtilsTest.ts @@ -3585,7 +3585,7 @@ describe('WorkflowUtils', () => { (approvalMode) => { expect(getApprovalWorkflowSource(buildPolicyWithConnectedATS(approvalMode), POLICY_ID)).toEqual({ providerName: 'Greenhouse', - settingsRoute: ROUTES.WORKSPACE_RECRUITING.getRoute(POLICY_ID), + settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_RECRUITING.getRoute(POLICY_ID), }); }, ); @@ -3621,7 +3621,7 @@ describe('WorkflowUtils', () => { expect(getApprovalWorkflowSource(policy, POLICY_ID)).toEqual({ providerName: 'Workday', - settingsRoute: ROUTES.WORKSPACE_HR.getRoute(POLICY_ID), + settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(POLICY_ID), }); }); }); From b9ab7dd8c2cb96488ecb85fb206b310726091d72 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Oct 2026 05:31:35 +0530 Subject: [PATCH 5/8] Point the remaining Accounting links at Connections: the accounting panel where an integration is connected, the Accounting tab otherwise Signed-off-by: krishna2323 --- src/ROUTES.ts | 4 ---- src/components/ImportedFromAccountingSoftware.tsx | 2 +- src/components/Navigation/DebugTabView.tsx | 2 +- src/components/Rule/RuleCategoriesDisabledEmptyState.tsx | 2 +- src/components/Rule/RuleTaxesDisabledEmptyState.tsx | 2 +- src/hooks/useDynamicForwardPath.ts | 4 ++-- src/libs/ReportActionsUtils.ts | 4 +++- .../GettingStartedSection/hooks/useGettingStartedItems.ts | 2 +- .../home/TimeSensitiveSection/items/FixPolicyConnection.tsx | 2 +- .../accounting/intacct/export/SageIntacctDatePage.tsx | 2 +- .../export/SageIntacctNonReimbursableExpensesPage.tsx | 4 +++- .../intacct/export/SageIntacctPreferredExporterPage.tsx | 2 +- .../intacct/export/SageIntacctReimbursableExpensesPage.tsx | 4 +++- .../accounting/netsuite/NetSuiteSetupPage/index.tsx | 2 +- .../accounting/qbo/QuickbooksOnlineSetupPage/index.tsx | 2 +- src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx | 2 +- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 6 +++++- src/pages/workspace/companyCards/utils.tsx | 2 +- src/pages/workspace/rules/RulesRequireFieldsPage.tsx | 4 ++-- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 6 +++++- tests/unit/CompanyCardExportTest.ts | 2 +- tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx | 4 ++-- tests/unit/components/XeroSetupPageWebTest.tsx | 4 ++-- tests/unit/hooks/useGettingStartedItems.test.ts | 2 +- .../GettingStartedSection/GettingStartedSectionTest.tsx | 4 +++- 25 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c18d4cd7be50..32c55f0ac7f3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3157,10 +3157,6 @@ const ROUTES = { return getUrlWithBackToParam(`workspaces/${policyID}/overview` as const, backTo); }, }, - WORKSPACE_ACCOUNTING: { - route: 'workspaces/:policyID/accounting', - getRoute: (policyID: string) => `workspaces/${policyID}/accounting` as const, - }, WORKSPACE_OVERVIEW_CURRENCY: { route: 'workspaces/:policyID/overview/currency', getRoute: ( diff --git a/src/components/ImportedFromAccountingSoftware.tsx b/src/components/ImportedFromAccountingSoftware.tsx index 7bc1d7acd1a4..b212b86e6a8a 100644 --- a/src/components/ImportedFromAccountingSoftware.tsx +++ b/src/components/ImportedFromAccountingSoftware.tsx @@ -74,7 +74,7 @@ function ImportedFromAccountingSoftware({policyID, currentConnectionName, transl /> 1) { message += ` ${translate('report.actions.type.integrationSyncFailedRecurrence', {count: recurrenceCount})}`; diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 145463f91914..05aee8570f70 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -296,7 +296,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'), subText: translate('homePage.gettingStartedSection.connectAccountingSubText'), isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate), - route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), + route: ROUTES.WORKSPACE_CONNECTIONS.getRoute(activePolicyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING), }); } else if (policy.areCategoriesEnabled) { items.push({ diff --git a/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx b/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx index 514ec7540510..532970c50366 100644 --- a/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx +++ b/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx @@ -34,7 +34,7 @@ function FixPolicyConnection({connectionName, policyID, policyName, integrationN : translate('homePage.timeSensitiveSection.fixPolicyConnection.defaultSubtitle'); const isHRConnection = (CONST.POLICY.CONNECTIONS.HR_CONNECTION_NAMES as readonly PolicyConnectionName[]).includes(connectionName); - const fixRoute = isHRConnection ? ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID) : ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID); + const fixRoute = isHRConnection ? ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID) : ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); return ( { - Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))); + Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)))); }, [policyID, backTo]); const headerContent = useMemo( diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx index 6ee066b083bc..94b00de26fe4 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx @@ -123,7 +123,9 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP headerTitle="workspace.accounting.exportCompanyCard" title="workspace.sageIntacct.nonReimbursableExpenses.description" onBackButtonPress={() => - Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))) + Navigation.goBack( + backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))), + ) } accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx index 3b643ad77ae9..d45204a36d3c 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx @@ -47,7 +47,7 @@ function SageIntacctPreferredExporterPage({policy}: WithPolicyProps) { const backTo = route.params?.backTo; const goBack = useCallback(() => { - Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))); + Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)))); }, [policyID, backTo]); const data: CardListItem[] = useMemo(() => { diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx index ffc52cf555f3..c7de54405640 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -126,7 +126,9 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProp headerTitle="workspace.accounting.exportOutOfPocket" title="workspace.sageIntacct.reimbursableExpenses.description" onBackButtonPress={() => - Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))) + Navigation.goBack( + backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))), + ) } accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx index 2b30f9db5b6d..d67c4ba8d595 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx @@ -18,7 +18,7 @@ function NetSuiteSetupPage({route}: NetSuiteSetupPageProps) { // only reached via a deep link, so just return to the workspace accounting page. We wait for the RHP open // transition to finish first, otherwise goBack fires mid-transition and is dropped. const handle = TransitionTracker.runAfterTransitions({ - callback: () => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)), + callback: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), waitForUpcomingTransition: true, }); return () => handle.cancel(); diff --git a/src/pages/workspace/accounting/qbo/QuickbooksOnlineSetupPage/index.tsx b/src/pages/workspace/accounting/qbo/QuickbooksOnlineSetupPage/index.tsx index 70b6f67d251f..aed1e3cc5723 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksOnlineSetupPage/index.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksOnlineSetupPage/index.tsx @@ -18,7 +18,7 @@ function QuickbooksOnlineSetupPage({route}: QuickbooksOnlineSetupPageProps) { // only reached via a deep link, so just return to the workspace accounting page. We wait for the RHP open // transition to finish first, otherwise goBack fires mid-transition and is dropped. const handle = TransitionTracker.runAfterTransitions({ - callback: () => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)), + callback: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), waitForUpcomingTransition: true, }); return () => handle.cancel(); diff --git a/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx b/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx index 9dc2a960fefd..49917e324b83 100644 --- a/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx +++ b/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx @@ -18,7 +18,7 @@ function XeroSetupPage({route}: XeroSetupPageProps) { // only reached via a deep link, so just return to the workspace accounting page. We wait for the RHP open // transition to finish first, otherwise goBack fires mid-transition and is dropped. const handle = TransitionTracker.runAfterTransitions({ - callback: () => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)), + callback: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), waitForUpcomingTransition: true, }); return () => handle.cancel(); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 4f7bb9e9f4ca..4469dbfba276 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -680,7 +680,11 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return ( ); diff --git a/src/pages/workspace/companyCards/utils.tsx b/src/pages/workspace/companyCards/utils.tsx index aaaa961df9bd..2ec7cda47899 100644 --- a/src/pages/workspace/companyCards/utils.tsx +++ b/src/pages/workspace/companyCards/utils.tsx @@ -203,7 +203,7 @@ function getPolicyCardExportSettings( policy?: Policy, backTo?: string | undefined, ): CardExportSettings | undefined { - const basePath = ROUTES.POLICY_ACCOUNTING.getRoute(policyID); + const basePath = ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); const currentConnectionName = getCurrentAccountingIntegrationName(policy, translate); const defaultCard = translate('workspace.moreFeatures.companyCards.defaultCard'); const defaultVendor = translate('workspace.accounting.defaultVendor'); diff --git a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx index 9685f36e6111..29d98ba54b2f 100644 --- a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx +++ b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx @@ -222,7 +222,7 @@ function RulesRequireFieldsPage({ if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)); return; } @@ -272,7 +272,7 @@ function RulesRequireFieldsPage({ if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)); return; } diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index f808b2c8c4de..ee3520dc6ab9 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -786,7 +786,11 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const subtitleText = useMemo(() => { const emptyTagsSubtitle = hasAccountingConnections - ? translate('workspace.tags.emptyTags.subtitleWithAccounting', `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`, canWriteTags) + ? translate( + 'workspace.tags.emptyTags.subtitleWithAccounting', + `${environmentURL}/${ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)}`, + canWriteTags, + ) : translate('workspace.tags.emptyTags.subtitleHTML'); return ( diff --git a/tests/unit/CompanyCardExportTest.ts b/tests/unit/CompanyCardExportTest.ts index bef186d9eae0..63f3ef72aedd 100644 --- a/tests/unit/CompanyCardExportTest.ts +++ b/tests/unit/CompanyCardExportTest.ts @@ -99,7 +99,7 @@ function createCard(nvpExportAccount?: string): Card { const translate: LocaleContextProps['translate'] = translateLocal; const themeStyles = createMock({}); -const basePath = ROUTES.POLICY_ACCOUNTING.getRoute(MOCK_POLICY_ID); +const basePath = ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(MOCK_POLICY_ID); describe('getExportMenuItem - QBD credit card account resolution', () => { it('resolves account by ID when NVP contains a QBD ListID (Classic-saved)', () => { diff --git a/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx b/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx index 0ccaa1268ee9..ebc84a8787db 100644 --- a/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx +++ b/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx @@ -57,10 +57,10 @@ describe('QuickbooksOnlineSetupPage (web)', () => { jest.clearAllMocks(); }); - it('returns to the workspace accounting page', () => { + it('returns to the workspace Connections page', () => { renderQuickbooksOnlineSetupPage(); - expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); + expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID)); }); it('renders nothing', () => { diff --git a/tests/unit/components/XeroSetupPageWebTest.tsx b/tests/unit/components/XeroSetupPageWebTest.tsx index 06c449ace7c3..05e236149839 100644 --- a/tests/unit/components/XeroSetupPageWebTest.tsx +++ b/tests/unit/components/XeroSetupPageWebTest.tsx @@ -57,10 +57,10 @@ describe('XeroSetupPage (web)', () => { jest.clearAllMocks(); }); - it('returns to the workspace accounting page', () => { + it('returns to the workspace Connections page', () => { renderXeroSetupPage(); - expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); + expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID)); }); it('renders nothing', () => { diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index 654739866231..eb821235a3ea 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -390,7 +390,7 @@ describe('useGettingStartedItems', () => { await waitForBatchedUpdates(); const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); - expect(connectItem?.route).toBe(ROUTES.WORKSPACE_ACCOUNTING.getRoute(POLICY_ID)); + expect(connectItem?.route).toBe(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)); }); it('should be not completed when workspace has no accounting connection', async () => { diff --git a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx index 17820abd1498..2ba935ea07e0 100644 --- a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx +++ b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx @@ -546,7 +546,9 @@ describe('GettingStartedSection', () => { const row = screen.getByText(/homePage\.gettingStartedSection\.connectAccounting:/); fireEvent.press(row); - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_ACCOUNTING.getRoute(TEST_POLICY_ID)); + expect(Navigation.navigate).toHaveBeenCalledWith( + ROUTES.WORKSPACE_CONNECTIONS.getRoute(TEST_POLICY_ID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING), + ); }); it('navigates to workspace categories when "Customize categories" row is pressed', async () => { From 1ddb84bb8ac76e98fe19c0fec1c215d131356ee2 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 20 Oct 2026 23:33:45 +0530 Subject: [PATCH 6/8] Fetch the accounting data when Connections opens on a non-Control workspace, so connecting an integration doesn't show a loader, and drop the leftovers of the deleted pages Signed-off-by: krishna2323 --- .../useConnectedAccountingIntegration.tsx | 23 ++--------- .../connections/WorkspaceConnectionsPage.tsx | 8 +++- .../merge/useMergeProviderCardDetails.tsx | 2 +- .../useReceiptPartnersSettings.ts | 2 +- ...kspaceConnectionsPageAutoStartFlowTest.tsx | 39 ++++++++++++++++++- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx index 1a92b6134a84..f5ee8d80693f 100644 --- a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx +++ b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx @@ -48,7 +48,6 @@ import type {Policy} from '@src/types/onyx'; import type {ConnectionName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import React, {useEffect} from 'react'; @@ -60,18 +59,15 @@ import {getCertiniaSelectedCompanyID, isCertiniaFFAConnection} from './certinia/ import {getAccountingIntegrationData, getAccountingIntegrationDisplayName, getSynchronizationErrorMessage, isIntuitEnterpriseSuiteConnection} from './utils'; type UseConnectedAccountingIntegrationOptions = { - /** Style applied to the settings rows, which the Accounting page lays out as cards */ - menuItemWrapperStyle?: StyleProp; - /** Runs once the user confirms disconnecting and takes over calling `disconnect`, so the panel can close before the connection is removed */ onDisconnectConfirmed?: (disconnect: () => void) => void; }; /** - * Everything the Accounting page and the Connections accounting panel derive for the workspace's connected accounting - * integration: its status, the overflow menu, and the settings rows. + * Everything the Connections accounting panel derives for the workspace's connected accounting integration: its status, + * the overflow menu, and the settings rows. */ -function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemWrapperStyle, onDisconnectConfirmed}: UseConnectedAccountingIntegrationOptions = {}) { +function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconnectConfirmed}: UseConnectedAccountingIntegrationOptions = {}) { const policyID = policy?.id; const hasReusablePoliciesConnectedToSageIntacct = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, policyID); const hasReusablePoliciesConnectedToQBD = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.QBD, policyID); @@ -273,7 +269,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.xero.organization'), iconRight: icons.ArrowRight, title: getCurrentXeroOrganizationName(policy), - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && tenants.length > 1, shouldShowDescriptionOnTop: true, @@ -296,7 +291,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.netsuite.subsidiary'), iconRight: icons.ArrowRight, title: policy?.connections?.netsuite?.options?.config?.subsidiary ?? '', - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && netSuiteSubsidiaryList?.length > 1, shouldShowDescriptionOnTop: true, @@ -317,7 +311,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.intacct.entity'), iconRight: icons.ArrowRight, title: getCurrentSageIntacctEntityName(policy, translate('workspace.common.topLevel')), - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting, shouldShowDescriptionOnTop: true, @@ -333,7 +326,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate(isConnectedToIntuitEnterpriseSuite ? 'workspace.qbo.entity' : 'workspace.qbo.connectedTo'), iconRight: isConnectedToIntuitEnterpriseSuite ? icons.ArrowRight : undefined, title: policy?.connections?.quickbooksOnline?.config?.companyName, - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: isConnectedToIntuitEnterpriseSuite && canWriteAccounting, shouldShowDescriptionOnTop: true, @@ -352,7 +344,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.certinia.company'), iconRight: icons.ArrowRight, title: selectedCertiniaCompany?.name ?? certiniaCompanyID ?? translate('common.none'), - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting, shouldShowDescriptionOnTop: true, @@ -368,7 +359,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.rillet.entity'), iconRight: icons.ArrowRight, title: rilletSubsidiaryList?.find((subsidiary) => subsidiary.id === policy?.connections?.rillet?.config?.subsidiaryID)?.tradeName ?? '', - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && rilletSubsidiaryList && rilletSubsidiaryList.length > 1, shouldShowDescriptionOnTop: true, @@ -387,7 +377,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.dualEntry.company'), iconRight: icons.ArrowRight, title: dualEntryCompanyList?.find((company) => company.id === policy?.connections?.dualEntry?.config?.subsidiaryID)?.name ?? '', - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && dualEntryCompanyList && dualEntryCompanyList.length > 1, shouldShowDescriptionOnTop: true, @@ -406,7 +395,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.campfire.entity'), iconRight: icons.ArrowRight, title: campfireSubsidiaryList?.find((subsidiary) => subsidiary.id === policy?.connections?.campfire?.config?.subsidiaryID)?.name ?? '', - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && campfireSubsidiaryList && campfireSubsidiaryList.length > 1, shouldShowDescriptionOnTop: true, @@ -425,7 +413,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW description: translate('workspace.businessCentral.subsidiary'), iconRight: icons.ArrowRight, title: businessCentralCompanyList.find((company) => company.id === policy?.connections?.businessCentral?.config?.companyID)?.displayName ?? '', - wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && businessCentralCompanyList.length > 1, shouldShowDescriptionOnTop: true, @@ -473,7 +460,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.import'), - wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onImportPagePress, brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedImportSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR @@ -485,7 +471,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.export'), - wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onExportPagePress, brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedExportSettings, integrationData?.errorFields) || @@ -503,7 +488,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.cardReconciliation'), - wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onCardReconciliationPagePress, }, ] @@ -513,7 +497,6 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemW iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.advanced'), - wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onAdvancedPagePress, brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR diff --git a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx index fad4c2573682..8cf595105a63 100644 --- a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx +++ b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx @@ -30,7 +30,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; -import {openPolicyHRPage, openPolicyRecruitingPage} from '@libs/actions/PolicyConnections'; +import {openPolicyAccountingPage, openPolicyHRPage, openPolicyRecruitingPage} from '@libs/actions/PolicyConnections'; import Navigation from '@libs/Navigation/Navigation'; import {isControlPolicy} from '@libs/PolicyUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; @@ -77,13 +77,17 @@ type MergeSetupFlow = { key: number; }; -/** HR and recruiting providers are Control-only, so their data is only requested for Control workspaces */ +/** + * HR and recruiting providers are Control-only, so their data is only requested for Control workspaces. Other workspaces + * fetch the accounting data here instead, so connecting an integration doesn't replace the page with a loader. + */ function fetchConnectionsData(policyID: string | undefined, canUseMergeConnections: boolean, isRecruitingBetaEnabled: boolean) { if (!policyID) { return; } openPolicyReceiptPartnersPage(policyID); if (!canUseMergeConnections) { + openPolicyAccountingPage(policyID); return; } openPolicyHRPage(policyID); diff --git a/src/pages/workspace/merge/useMergeProviderCardDetails.tsx b/src/pages/workspace/merge/useMergeProviderCardDetails.tsx index 831157a088a6..f9c0915c407c 100644 --- a/src/pages/workspace/merge/useMergeProviderCardDetails.tsx +++ b/src/pages/workspace/merge/useMergeProviderCardDetails.tsx @@ -32,7 +32,7 @@ type UseMergeProviderCardDetailsParams = { onDisconnect?: (disconnect: () => void) => void; }; -/** The status text, sync error and overflow menu of a Merge provider, shared by the HR and Recruiting cards and their Connections panel */ +/** The status text, sync error and overflow menu of a Merge provider on the Connections HR and Recruiting panels */ function useMergeProviderCardDetails({card, policy, handleConnect, canWriteMoreFeatures, showReadOnlyModal, onDisconnect}: UseMergeProviderCardDetailsParams) { const {translate, datetimeToRelative} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts b/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts index 8198698fb49f..1a5ab1c1ab16 100644 --- a/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts +++ b/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts @@ -23,7 +23,7 @@ type UseReceiptPartnersSettingsOptions = { onDisconnectConfirmed?: (disconnect: () => void) => void; }; -/** State and actions for the workspace's receipt partner connections, shared by the Receipt partners page and its Connections panel */ +/** State and actions for the workspace's receipt partner connections, used by the Connections receipt partners panel */ function useReceiptPartnersSettings(policyID: string, {onDisconnectConfirmed}: UseReceiptPartnersSettingsOptions = {}) { const icons = useMemoizedLazyExpensifyIcons(['Key', 'NewWindow', 'Trashcan']); const {translate} = useLocalize(); diff --git a/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx b/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx index 67625d6f53d3..9279cd8d79ce 100644 --- a/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx +++ b/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx @@ -1,5 +1,6 @@ import {render} from '@testing-library/react-native'; +import {openPolicyAccountingPage, openPolicyHRPage} from '@libs/actions/PolicyConnections'; import Navigation from '@libs/Navigation/Navigation'; import WorkspaceConnectionsPage from '@pages/workspace/connections/WorkspaceConnectionsPage'; @@ -109,14 +110,15 @@ jest.mock('@pages/workspace/connections/useAccountingConnectionListings', () => jest.mock('@pages/workspace/connections/useMergeConnectionListings', () => ({__esModule: true, default: () => []})); jest.mock('@pages/workspace/connections/useReceiptPartnerConnectionListings', () => ({__esModule: true, default: () => []})); jest.mock('@pages/workspace/connections/useMCPConnectionListings', () => ({__esModule: true, default: () => []})); -jest.mock('@libs/actions/PolicyConnections', () => ({openPolicyHRPage: jest.fn(), openPolicyRecruitingPage: jest.fn()})); +jest.mock('@libs/actions/PolicyConnections', () => ({openPolicyAccountingPage: jest.fn(), openPolicyHRPage: jest.fn(), openPolicyRecruitingPage: jest.fn()})); jest.mock('@userActions/Policy/Policy', () => ({openPolicyReceiptPartnersPage: jest.fn()})); +let mockIsControlPolicy = true; jest.mock('@libs/PolicyUtils', () => { const actual = jest.requireActual>('@libs/PolicyUtils'); return { ...actual, - isControlPolicy: () => true, + isControlPolicy: () => mockIsControlPolicy, }; }); @@ -218,3 +220,36 @@ describe('WorkspaceConnectionsPage tab from the route', () => { expect(mockActiveTabKey).toBe(CONST.TAB.CONNECTIONS.ALL); }); }); + +describe('WorkspaceConnectionsPage data fetched on open', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRouteParams = {}; + mockIsControlPolicy = true; + }); + + it('should fetch the accounting data for a workspace that is not on Control', async () => { + // Given a Collect workspace, which has no HR request to set the connections fetched flag + mockIsControlPolicy = false; + + // When the page opens + render(); + await waitForBatchedUpdates(); + + // Then the accounting data is fetched up front, so connecting an integration later doesn't swap the page for a loader + expect(openPolicyAccountingPage).toHaveBeenCalledWith(POLICY_ID); + expect(openPolicyHRPage).not.toHaveBeenCalled(); + }); + + it('should fetch the HR data instead for a Control workspace', async () => { + // Given a Control workspace, whose HR request already sets the connections fetched flag + + // When the page opens + render(); + await waitForBatchedUpdates(); + + // Then only the HR data is fetched, so the page doesn't send a second request for the same flag + expect(openPolicyHRPage).toHaveBeenCalledWith(POLICY_ID); + expect(openPolicyAccountingPage).not.toHaveBeenCalled(); + }); +}); From c20a95ea696105e96e1d0570174e34eb78a6d94f Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 21 Oct 2026 00:04:23 +0530 Subject: [PATCH 7/8] Move the Connections launch to its own PR, so a staging issue only reverts the launch and these fixes still ship Signed-off-by: krishna2323 --- src/CONST/index.ts | 1 + src/ROUTES.ts | 9 +- .../ConnectToSageIntacctFlow/index.tsx | 6 +- .../ImportedFromAccountingSoftware.tsx | 2 +- src/components/Navigation/DebugTabView.tsx | 4 +- .../Rule/RuleCategoriesDisabledEmptyState.tsx | 2 +- .../Rule/RuleTaxesDisabledEmptyState.tsx | 2 +- .../SearchRouter/useNavigationSuggestions.tsx | 14 + src/components/SearchBar/CompactSearchBar.tsx | 45 ++ src/hooks/useDynamicForwardPath.ts | 4 +- .../useIsUnifiedConnectionsBetaEnabled.ts | 11 + src/languages/de.ts | 39 +- src/languages/el.ts | 42 +- src/languages/en.ts | 41 +- src/languages/es.ts | 37 +- src/languages/fr.ts | 39 +- src/languages/it.ts | 37 +- src/languages/ja.ts | 39 +- src/languages/nl.ts | 37 +- src/languages/pl.ts | 37 +- src/languages/pt-BR.ts | 37 +- src/languages/zh-hans.ts | 37 +- .../ModalStackNavigators/index.tsx | 2 +- .../Navigators/WorkspaceSplitNavigator.tsx | 10 +- src/libs/Navigation/types.ts | 1 - src/libs/PolicyUtils/features.ts | 2 +- src/libs/ReportActionsUtils.ts | 4 +- src/libs/ReportUtils.ts | 2 +- src/libs/WorkflowUtils.ts | 4 +- src/libs/actions/Policy/Policy.ts | 2 + .../hooks/useGettingStartedItems.ts | 2 +- .../items/FixPolicyConnection.tsx | 2 +- src/pages/workspace/WorkspaceInitialPage.tsx | 4 + src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- .../WorkspaceMoreFeaturesPage/index.tsx | 240 +++++++++- .../accounting/AccountingContext/index.tsx | 45 +- .../workspace/accounting/ClaimOfferPage.tsx | 15 +- .../accounting/PolicyAccountingPage.tsx | 432 ++++++++++++++++++ .../advanced/BusinessCentralAdvancedPage.tsx | 5 +- .../export/BusinessCentralExportPage.tsx | 5 +- .../import/BusinessCentralImportPage.tsx | 6 +- .../advanced/CampfireAdvancedPage.tsx | 5 +- .../campfire/export/CampfireExportPage.tsx | 5 +- .../campfire/import/CampfireImportPage.tsx | 6 +- .../certinia/import/CertiniaImportPage.tsx | 5 +- .../advanced/DualEntryAdvancedPage.tsx | 5 +- .../dualentry/export/DualEntryExportPage.tsx | 5 +- .../dualentry/import/DualEntryImportPage.tsx | 6 +- .../advanced/SageIntacctAdvancedPage.tsx | 5 +- .../intacct/export/SageIntacctDatePage.tsx | 2 +- ...SageIntacctNonReimbursableExpensesPage.tsx | 4 +- .../SageIntacctPreferredExporterPage.tsx | 2 +- .../SageIntacctReimbursableExpensesPage.tsx | 4 +- .../intacct/import/SageIntacctImportPage.tsx | 5 +- .../NetSuiteSetupPage/index.native.tsx | 7 +- .../netsuite/NetSuiteSetupPage/index.tsx | 2 +- .../NetSuiteExistingConnectionsPage.tsx | 7 +- .../advanced/NetSuiteAdvancedPage.tsx | 5 +- ...eProvincialTaxPostingAccountSelectPage.tsx | 5 +- .../NetSuiteTaxPostingAccountSelectPage.tsx | 5 +- ...NetSuiteTravelBillingConfigurationPage.tsx | 5 +- .../netsuite/import/NetSuiteImportPage.tsx | 5 +- .../QuickBooksDesktopSetupFlowSyncPage.tsx | 7 +- .../QuickbooksDesktopAutoSyncPageBase.tsx | 9 +- .../import/QuickbooksDesktopImportPage.tsx | 5 +- .../qbo/QuickbooksOnlineSetupPage/index.tsx | 2 +- .../qbo/advanced/QuickbooksAdvancedPage.tsx | 5 +- .../qbo/import/QuickbooksImportPage.tsx | 5 +- .../rillet/advanced/RilletAdvancedPage.tsx | 5 +- .../rillet/export/RilletExportPage.tsx | 5 +- .../rillet/import/RilletImportPage.tsx | 6 +- .../useConnectedAccountingIntegration.tsx | 26 +- src/pages/workspace/accounting/utils.tsx | 27 +- .../accounting/xero/XeroImportPage.tsx | 5 +- .../accounting/xero/XeroSetupPage/index.tsx | 2 +- .../xero/advanced/XeroAdvancedPage.tsx | 5 +- .../categories/WorkspaceCategoriesPage.tsx | 6 +- src/pages/workspace/companyCards/utils.tsx | 2 +- .../connections/ConnectionsAccountingPage.tsx | 4 +- .../connections/ConnectionsMergePageBase.tsx | 4 +- .../ConnectionsReceiptPartnersPage.tsx | 4 +- .../connections/WorkspaceConnectionsPage.tsx | 11 +- .../useAccountingConnectionListings.ts | 3 + src/pages/workspace/connections/utils.ts | 26 +- .../withUnifiedConnectionsBeta.tsx | 28 ++ src/pages/workspace/getWorkspaceMenuItems.ts | 98 +++- src/pages/workspace/hr/WorkspaceHRPage.tsx | 55 +++ .../hr/gusto/GustoFinalApproverPage.tsx | 6 +- .../hr/merge/MergeHRFinalApproverPage.tsx | 6 +- .../hr/zenefits/ZenefitsFinalApproverPage.tsx | 6 +- src/pages/workspace/mcp/WorkspaceMCPPage.tsx | 129 ++++++ .../merge/MergeConnectionsPageBase.tsx | 261 +++++++++++ .../workspace/merge/MergeProviderCard.tsx | 139 ++++++ .../merge/useMergeProviderCardDetails.tsx | 2 +- .../WorkspaceReceiptPartnersPage.tsx | 205 +++++++++ .../connectionRows/UberConnectionRow.tsx | 182 ++++++++ .../receiptPartners/connectionRows/types.ts | 16 + .../useReceiptPartnersSettings.ts | 2 +- .../recruiting/WorkspaceRecruitingPage.tsx | 60 +++ .../rules/RulesRequireFieldsPage.tsx | 4 +- .../workspace/tags/WorkspaceTagsPage.tsx | 6 +- .../upgrade/WorkspaceUpgradePage.tsx | 10 +- tests/ui/WorkspaceMoreFeaturesPageTest.tsx | 101 +++- .../AccountingConnectionConfirmationTest.tsx | 112 +++-- tests/unit/CompanyCardExportTest.ts | 2 +- tests/unit/ConnectionsPanelGatesTest.tsx | 39 +- .../PolicyAccountingPageAutoStartFlowTest.tsx | 261 +++++++++++ ...olicyAccountingPageQBOTokenExpiryTest.tsx} | 3 + tests/unit/SearchRouterNavigationTest.ts | 19 +- tests/unit/WorkflowUtilsTest.ts | 4 +- ...kspaceConnectionsPageAutoStartFlowTest.tsx | 37 +- .../QuickbooksOnlineSetupPageWebTest.tsx | 4 +- .../unit/components/XeroSetupPageWebTest.tsx | 4 +- tests/unit/getWorkspaceMenuItemsTest.ts | 149 +++++- .../unit/hooks/useGettingStartedItems.test.ts | 2 +- .../GettingStartedSectionTest.tsx | 4 +- .../netsuite/NetSuiteSetupPageTest.tsx | 4 +- .../useAccountingConnectionListingsTest.ts | 28 +- tests/unit/withUnifiedConnectionsBetaTest.tsx | 53 +++ 119 files changed, 3319 insertions(+), 303 deletions(-) create mode 100644 src/components/SearchBar/CompactSearchBar.tsx create mode 100644 src/hooks/useIsUnifiedConnectionsBetaEnabled.ts create mode 100644 src/pages/workspace/accounting/PolicyAccountingPage.tsx create mode 100644 src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx create mode 100644 src/pages/workspace/hr/WorkspaceHRPage.tsx create mode 100644 src/pages/workspace/mcp/WorkspaceMCPPage.tsx create mode 100644 src/pages/workspace/merge/MergeConnectionsPageBase.tsx create mode 100644 src/pages/workspace/merge/MergeProviderCard.tsx create mode 100644 src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx create mode 100644 src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx create mode 100644 src/pages/workspace/receiptPartners/connectionRows/types.ts create mode 100644 src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx create mode 100644 tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx rename tests/unit/{ConnectionsAccountingPageQBOTokenExpiryTest.tsx => PolicyAccountingPageQBOTokenExpiryTest.tsx} (96%) create mode 100644 tests/unit/withUnifiedConnectionsBetaTest.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e2397ec93233..a295f031849a 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1206,6 +1206,7 @@ const CONST = { INSIGHTS_PAGE: 'insightsPage', INSIGHTS_COMPARE: 'insightsCompare', PAYMENT_HISTORY: 'paymentHistory', + UNIFIED_CONNECTIONS: 'unifiedConnections', SPEND_FOOTER_SELECTORS: 'spendFooterSelectors', SUPPORT_TICKET: 'supportTicket', ANCHORED_FIELD_DROPDOWNS: 'anchoredFieldDropdowns', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 32c55f0ac7f3..51eba403a336 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3157,6 +3157,10 @@ const ROUTES = { return getUrlWithBackToParam(`workspaces/${policyID}/overview` as const, backTo); }, }, + WORKSPACE_ACCOUNTING: { + route: 'workspaces/:policyID/accounting', + getRoute: (policyID: string) => `workspaces/${policyID}/accounting` as const, + }, WORKSPACE_OVERVIEW_CURRENCY: { route: 'workspaces/:policyID/overview/currency', getRoute: ( @@ -3605,16 +3609,11 @@ const ROUTES = { integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean, isIntuitEnterpriseSuite?: boolean, - tab?: ValueOf, ) => { if (!policyID) { Log.warn('Invalid policyID is used to build the WORKSPACE_CONNECTIONS route'); } - if (tab) { - return `workspaces/${policyID}/connections?tab=${tab}` as const; - } - let queryParams = ''; if (newConnectionName) { queryParams += `?newConnectionName=${newConnectionName}`; diff --git a/src/components/ConnectToSageIntacctFlow/index.tsx b/src/components/ConnectToSageIntacctFlow/index.tsx index a471bb264049..73ff7e5d307e 100644 --- a/src/components/ConnectToSageIntacctFlow/index.tsx +++ b/src/components/ConnectToSageIntacctFlow/index.tsx @@ -1,9 +1,12 @@ import useHasReusablePoliciesConnectedTo from '@hooks/useHasReusablePoliciesConnectedTo'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useOnyx from '@hooks/useOnyx'; import {isAuthenticationError} from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; + import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -18,6 +21,7 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) { const hasReusablePoliciesConnectedToSageIntacct = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, policyID); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const shouldGoToEnterCredentials = isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); useEffect(() => { if (shouldGoToEnterCredentials) { @@ -25,7 +29,7 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) { return; } if (!hasReusablePoliciesConnectedToSageIntacct) { - Navigation.navigate(`${ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)}/${DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path}`); + Navigation.navigate(`${getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)}/${DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path}`); return; } Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.getRoute(policyID)); diff --git a/src/components/ImportedFromAccountingSoftware.tsx b/src/components/ImportedFromAccountingSoftware.tsx index b212b86e6a8a..7bc1d7acd1a4 100644 --- a/src/components/ImportedFromAccountingSoftware.tsx +++ b/src/components/ImportedFromAccountingSoftware.tsx @@ -74,7 +74,7 @@ function ImportedFromAccountingSoftware({policyID, currentConnectionName, transl /> translate(item.translationKey), diff --git a/src/components/SearchBar/CompactSearchBar.tsx b/src/components/SearchBar/CompactSearchBar.tsx new file mode 100644 index 000000000000..c623e6196f2c --- /dev/null +++ b/src/components/SearchBar/CompactSearchBar.tsx @@ -0,0 +1,45 @@ +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import React from 'react'; + +import type {SharedSearchBarProps} from './BaseSearchBar'; + +import BaseSearchBar from './BaseSearchBar'; + +type CompactSearchBarProps = SharedSearchBarProps; + +/** + * The compact list-style search input (placeholder instead of a floating label), matching the one used above selection lists. + */ +function CompactSearchBar({ref, label, style, inputValue, onChangeText, onSubmitEditing, shouldShowEmptyState, emptyStateContainerStyle}: CompactSearchBarProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + // The compact search input is sized by the physical device width (`isSmallScreenWidth`), not `shouldUseNarrowLayout`, so + // it stays compact when rendered inside an RHP/narrow pane on web/desktop and only grows to the tall size on mobile. + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + + return ( + + ); +} + +export default CompactSearchBar; diff --git a/src/hooks/useDynamicForwardPath.ts b/src/hooks/useDynamicForwardPath.ts index 3a5735cc58f6..1fae7aa8289b 100644 --- a/src/hooks/useDynamicForwardPath.ts +++ b/src/hooks/useDynamicForwardPath.ts @@ -37,8 +37,8 @@ const FORWARD_TO_MAPPINGS: Record `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.', disabledTitle: 'Nicht so schnell...', disabledMessage: 'Um diese Funktion zu aktivieren oder zu deaktivieren, müssen Sie Ihre Buchhaltungsimporteinstellungen ändern.', }, + recruitingWarningModal: { + disconnectText: ({integration}: {integration: string}) => `Um Recruiting zu deaktivieren, trennen Sie bitte zuerst die Verbindung von ${integration} mit diesem Workspace.`, + }, }, reports: { reportsCustomTitleExamples: 'Beispiele:', @@ -7396,6 +7417,7 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, accounting: { settings: 'Einstellungen', title: 'Verknüpfungen', + subtitle: 'Verbinden Sie Ihre Buchhaltungssoftware für eine automatische Synchronisierung.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7407,6 +7429,10 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Chatten Sie mit Ihrer Kundenbetreuung.', + talkYourAccountManager: 'Chatte mit deiner/deinem Account Manager/in.', + talkToConcierge: 'Chatte mit Concierge.', + needAnotherAccounting: 'Benötigen Sie eine weitere Buchhaltungssoftware?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7447,6 +7473,7 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, disconnect: 'Trennen', reinstall: 'Connector neu installieren', disconnectTitle: (connectionName = 'Integration') => `${connectionName} trennen`, + connectTitle: (connectionName) => `${connectionName} verbinden`, syncError: (connectionName) => `Verbindung mit ${connectionName} nicht möglich`, accounts: 'Kontenplan', taxes: 'Steuern', @@ -7463,10 +7490,12 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Standardmäßige NetSuite-Mitarbeiterperson', }, disconnectPrompt: (connectionName = 'diese Integration') => `Möchtest du ${connectionName} wirklich trennen?`, + connectPrompt: (connectionName) => `Sind Sie sicher, dass Sie ${connectionName} verbinden möchten? Dadurch werden alle bestehenden Buchhaltungsverbindungen entfernt.`, enterCredentials: 'Gib deine Anmeldedaten ein', reconnect: 'Erneut verbinden', updateCredentials: 'Anmeldedaten aktualisieren', claimOffer: { + badgeText: 'Angebot verfügbar!', xero: { headline: 'Hol dir Xero 6 Monate lang kostenlos!', description: 'Neu bei Xero? Expensify-Kund*innen erhalten 6 Monate kostenlos. Fordern Sie unten Ihr Angebot an.', @@ -8939,8 +8968,10 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc subscriptions: 'Abonnements', }, hr: { + title: 'Personalwesen', connectionsSubtitle: 'Verbinden Sie sich mit Ihrem HR-System, um Mitarbeiterdaten zu synchronisieren, Erstattungen automatisch den richtigen Personen zuzuordnen und die Ausgaben Ihres Teams ohne manuellen Aufwand korrekt zu halten.', + subtitle: 'HR-Tools verbinden und Mitarbeitergenehmigungen synchron halten.', alreadyConnectedTitle: 'Es kann keine Verbindung zu mehreren HR-Plattformen hergestellt werden', alreadyConnectedPrompt: 'Sie müssen Ihre aktuelle HR-Plattform trennen, bevor Sie eine andere verbinden.', connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`, @@ -9003,6 +9034,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, }, recruiting: { + title: 'Personalbeschaffung', connectionsSubtitle: 'Verbinden Sie Ihr Recruiting-System, um Kandidatendaten zu synchronisieren, Erstattungen automatisch den richtigen Personen zuzuordnen und die Ausgaben Ihres Teams ohne manuelle Arbeit korrekt zu halten.', alreadyConnectedTitle: 'Es kann keine Verbindung zu mehreren ATS-Plattformen hergestellt werden', @@ -9010,6 +9042,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc syncing: 'Kandidaten werden synchronisiert', setupIncomplete: (setupLink: string | undefined) => `Verbunden. ${setupLink ? `Einrichtung abschließen` : 'Einrichtung abschließen'} zum Importieren von Kandidaten.`, + dontSeeYourATS: `Sie sehen Ihr ATS hier nicht? Fragen Sie Concierge, und wir können es hinzufügen.`, importSettings: 'Import-Einstellungen', defaultApprover: 'Standardgenehmiger', approverFields: {recruiter: 'Personalvermittler', recruitingCoordinator: 'Recruiting-Koordinator'}, @@ -9035,6 +9068,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, enableJobStagesOrTags: 'Wählen Sie mindestens eine Jobphase oder ein Tag aus, um fortzufahren', }, + subtitle: 'Verknüpfen Sie Recruiting-Tools und halten Sie Kandidatengenehmigungen synchron.', syncResults: { importedCount: () => ({ one: '1 Kandidat', @@ -9054,6 +9088,9 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc }, }, merge: { + connections: 'Verbindungen', + connect: 'Verbinden', + findIntegration: 'Integration finden', syncNow: 'Jetzt synchronisieren', disconnect: 'Trennen', disconnectTitle: (providerName: string) => `${providerName} trennen`, diff --git a/src/languages/el.ts b/src/languages/el.ts index b7b5634228ad..098778ccaa3c 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -3672,7 +3672,7 @@ ${amount} για ${merchant} - ${date}`, 1. Κάντε κλικ στο *χώρο εργασίας*. 2. Επιλέξτε τον χώρο εργασίας σας. - 3. Κάντε κλικ στις *Συνδέσεις*. + 3. Κάντε κλικ στο *λογιστική*. 4. Βρείτε το ${integrationName}. 5. Κάντε κλικ στο *σύνδεση*. @@ -5106,14 +5106,22 @@ ${amount} για ${merchant} - ${date}`, description: 'Καλά νέα 🎉. Επικοινωνήστε μαζί μας αν χρειαστούν βοήθεια με τη ρύθμιση.', }, mcp: { + connectors: 'Συνδέσεις', + connectorsSubtitle: 'Συνδέστε έναν βοηθό AI με τον λογαριασμό σας στο Expensify.', + connect: 'Σύνδεση', + helpPrompt: 'Χρειάζεστε βοήθεια με τη σύνδεση;', + helpLink: 'Διαβάστε τον οδηγό μας.', claude: { title: 'Claude', + subtitle: 'από την Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'από την Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'από την OpenAI', }, }, connections: { @@ -6776,6 +6784,10 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ title: 'Οργανώστε', subtitle: 'Ομαδοποιήστε και αναλύστε τις δαπάνες, καταγράψτε κάθε φόρο που πληρώθηκε.', }, + integrateSection: { + title: 'Ενσωμάτωση', + subtitle: 'Συνδέστε το Expensify με δημοφιλή χρηματοοικονομικά προϊόντα.', + }, distanceRates: { title: 'Τιμές χιλιομετρικής αποζημίωσης', subtitle: 'Προσθέστε, ενημερώστε και εφαρμόστε τιμές.', @@ -7015,18 +7027,26 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, mcp: { title: 'MCP', + subtitle: 'Συνδέστε έναν βοηθό AI με τον λογαριασμό σας στο Expensify.', }, receiptPartners: { title: 'Συνεργάτες αποδείξεων', + subtitle: 'Αυτόματη εισαγωγή αποδείξεων.', }, connectionsWarningModal: { featureEnabledTitle: 'Όχι και τόσο γρήγορα...', featureEnabledText: 'Για να ενεργοποιήσετε ή να απενεργοποιήσετε αυτή τη λειτουργία, θα χρειαστεί να αλλάξετε τις ρυθμίσεις εισαγωγής λογιστικής.', + disconnectText: 'Για να απενεργοποιήσετε τη λογιστική, θα χρειαστεί να αποσυνδέσετε τη λογιστική σύνδεση από τον χώρο εργασίας σας.', manageSettings: 'Διαχείριση ρυθμίσεων', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Αποσύνδεση του Uber', + disconnectText: 'Για να απενεργοποιήσετε αυτή τη λειτουργία, αποσυνδέστε πρώτα την ενσωμάτωση Uber for Business.', description: 'Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε αυτήν την ενοποίηση;', + confirmText: 'Εντάξει', + }, + hrWarningModal: { + disconnectText: ({integration}: {integration: string}) => `Για να απενεργοποιήσετε το HR, αποσυνδέστε πρώτα το ${integration} από αυτόν τον χώρο εργασίας.`, }, workflowWarningModal: { featureEnabledTitle: 'Όχι και τόσο γρήγορα...', @@ -7049,6 +7069,10 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ disabledTitle: 'Όχι τόσο γρήγορα...', disabledMessage: 'Για να ενεργοποιήσετε ή να απενεργοποιήσετε αυτήν τη λειτουργία, θα πρέπει να αλλάξετε τις ρυθμίσεις εισαγωγής λογιστικής.', }, + recruitingWarningModal: { + disconnectText: ({integration}: {integration: string}) => + `Για να απενεργοποιήσετε την προσέλκυση προσωπικού, αποσυνδέστε πρώτα το ${integration} από αυτόν τον χώρο εργασίας.`, + }, }, reports: { reportsCustomTitleExamples: 'Παραδείγματα:', @@ -7526,6 +7550,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ accounting: { settings: 'ρυθμίσεις', title: 'Συνδέσεις', + subtitle: 'Συνδέστε το λογιστικό σας λογισμικό για αυτόματο συγχρονισμό.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7537,6 +7562,10 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Συνομιλήστε με τον υπεύθυνο λογαριασμού σας.', + talkYourAccountManager: 'Συνομιλήστε με τον υπεύθυνο λογαριασμού σας.', + talkToConcierge: 'Συνομιλήστε με το Concierge.', + needAnotherAccounting: 'Χρειάζεστε άλλο λογιστικό λογισμικό;', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7577,6 +7606,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ disconnect: 'Αποσύνδεση', reinstall: 'Επανεγκατάσταση συνδέτη', disconnectTitle: (connectionName = 'ενσωμάτωση') => `Αποσύνδεση ${connectionName}`, + connectTitle: (connectionName) => `Συνδέστε ${connectionName}`, syncError: (connectionName) => `Δεν είναι δυνατή η σύνδεση με το ${connectionName}`, accounts: 'Λογιστικό σχέδιο', taxes: 'Φόροι', @@ -7593,10 +7623,12 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Προεπιλογή εργαζομένου NetSuite', }, disconnectPrompt: (connectionName = 'αυτή η ενοποίηση') => `Είστε βέβαιοι ότι θέλετε να αποσυνδέσετε το ${connectionName};`, + connectPrompt: (connectionName) => `Είστε βέβαιοι ότι θέλετε να συνδέσετε το ${connectionName}; Αυτό θα αφαιρέσει τυχόν υπάρχουσες λογιστικές συνδέσεις.`, reconnect: 'Επανασύνδεση', enterCredentials: 'Εισαγάγετε τα διαπιστευτήριά σας', updateCredentials: 'Ενημέρωση διαπιστευτηρίων', claimOffer: { + badgeText: 'Προσφορά διαθέσιμη!', xero: { headline: 'Αποκτήστε το Xero δωρεάν για 6 μήνες!', description: @@ -7847,8 +7879,10 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ qboConnectionExpired: ({date}: {date: string}) => `Η σύνδεσή σας με το QuickBooks Online έληξε στις ${date}.`, }, hr: { + title: 'HR', connectionsSubtitle: 'Συνδεθείτε με το σύστημα HR σας για να συγχρονίζετε τα στοιχεία των εργαζομένων, να αντιστοιχίζετε αυτόματα τις αποζημιώσεις στα σωστά άτομα και να διατηρείτε τα έξοδα της ομάδας σας ακριβή χωρίς χειροκίνητη εργασία.', + subtitle: 'Συνδέστε εργαλεία HR και διατηρήστε τις εγκρίσεις εργαζομένων συγχρονισμένες.', alreadyConnectedTitle: 'Δεν είναι δυνατή η σύνδεση σε πολλές πλατφόρμες HR', alreadyConnectedPrompt: 'Πρέπει να αποσυνδέσετε την τρέχουσα πλατφόρμα HR προτού συνδέσετε μια άλλη.', connectionDescription: (providerName: string) => `Συνδέστε το ${providerName} για να διατηρείτε τις εγκρίσεις εργαζομένων συγχρονισμένες με τον χώρο εργασίας σας.`, @@ -7911,6 +7945,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, }, recruiting: { + title: 'Προσλήψεις', connectionsSubtitle: 'Συνδεθείτε με το σύστημα προσλήψεών σας για να συγχρονίζετε τα δεδομένα υποψηφίων, να αντιστοιχίζετε αυτόματα τις αποζημιώσεις στα σωστά άτομα και να διατηρείτε τις δαπάνες της ομάδας σας ακριβείς χωρίς χειροκίνητη εργασία.', alreadyConnectedTitle: 'Αδυναμία σύνδεσης σε πολλές πλατφόρμες ATS', @@ -7918,6 +7953,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ syncing: 'Γίνεται συγχρονισμός υποψηφίων', setupIncomplete: (setupLink: string | undefined) => `Συνδέθηκε. ${setupLink ? `Ολοκληρώστε τη ρύθμιση` : 'Ολοκληρώστε τη ρύθμιση'} για εισαγωγή υποψηφίων.`, + dontSeeYourATS: `Δεν βλέπετε εδώ το ATS σας; Ρωτήστε το Concierge και μπορούμε να το προσθέσουμε.`, importSettings: 'Ρυθμίσεις εισαγωγής', defaultApprover: 'Προεπιλεγμένος εγκρίνων', approverFields: {recruiter: 'Στρατολογητής', recruitingCoordinator: 'Συντονιστής προσλήψεων'}, @@ -7943,6 +7979,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, enableJobStagesOrTags: 'Επιλέξτε τουλάχιστον ένα στάδιο εργασίας ή ετικέτα για να συνεχίσετε', }, + subtitle: 'Συνδέστε τα εργαλεία προσλήψεων και διατηρήστε τις εγκρίσεις υποψηφίων συγχρονισμένες.', syncResults: { importedCount: () => ({ one: '1 υποψήφιος', @@ -7962,6 +7999,9 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ }, }, merge: { + connections: 'Συνδέσεις', + connect: 'Σύνδεση', + findIntegration: 'Βρείτε ενοποίηση', syncNow: 'Συγχρονισμός τώρα', disconnect: 'Αποσύνδεση', disconnectTitle: (providerName: string) => `Αποσύνδεση ${providerName}`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 78ae176660c4..1b1bd7a97e23 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3769,7 +3769,7 @@ const translations = { 1. Click *Workspaces*. 2. Select your workspace. - 3. Click *Connections*. + 3. Click *Accounting*. 4. Find ${integrationName}. 5. Click *Connect*. @@ -5218,14 +5218,22 @@ const translations = { description: 'Great news 🎉. Reach out to us if they need any help with the setup.', }, mcp: { + connectors: 'Connectors', + connectorsSubtitle: 'Connect an AI assistant to your Expensify account.', + connect: 'Connect', + helpPrompt: 'Need help connecting?', + helpLink: 'Read our guide.', claude: { title: 'Claude', + subtitle: 'by Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'by Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'by OpenAI', }, }, connections: { @@ -7045,6 +7053,10 @@ const translations = { title: 'Organize', subtitle: 'Group and analyze spend, record every tax paid.', }, + integrateSection: { + title: 'Integrate', + subtitle: 'Connect Expensify to popular financial products.', + }, distanceRates: { title: 'Distance rates', subtitle: 'Add, update, and enforce rates.', @@ -7283,18 +7295,29 @@ const translations = { }, mcp: { title: 'MCP', + subtitle: 'Connect an AI assistant to your Expensify account.', }, receiptPartners: { title: 'Receipt partners', + subtitle: 'Automatically import receipts.', }, connectionsWarningModal: { featureEnabledTitle: 'Not so fast...', featureEnabledText: "To enable or disable this feature, you'll need to change your accounting import settings.", + disconnectText: "To disable accounting, you'll need to disconnect your accounting connection from your workspace.", manageSettings: 'Manage settings', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Disconnect Uber', + disconnectText: 'To disable this feature, please disconnect the Uber for Business integration first.', description: 'Are you sure you want to disconnect this integration?', + confirmText: 'Got it', + }, + hrWarningModal: { + disconnectText: ({integration}: {integration: string}) => `To disable HR, please disconnect ${integration} from this workspace first.`, + }, + recruitingWarningModal: { + disconnectText: ({integration}: {integration: string}) => `To disable Recruiting, please disconnect ${integration} from this workspace first.`, }, workflowWarningModal: { featureEnabledTitle: 'Not so fast...', @@ -7789,6 +7812,7 @@ const translations = { accounting: { settings: 'settings', title: 'Connections', + subtitle: 'Connect your accounting software for an automatic sync.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7803,6 +7827,10 @@ const translations = { sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Chat with your account executive.', + talkYourAccountManager: 'Chat with your account manager.', + talkToConcierge: 'Chat with Concierge.', + needAnotherAccounting: 'Need another accounting software? ', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7843,6 +7871,7 @@ const translations = { disconnect: 'Disconnect', reinstall: 'Reinstall connector', disconnectTitle: (connectionName = 'integration') => `Disconnect ${connectionName}`, + connectTitle: (connectionName: string) => `Connect ${connectionName}`, syncError: (connectionName: string) => `Can't connect to ${connectionName}`, accounts: 'Chart of accounts', @@ -7860,12 +7889,14 @@ const translations = { [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite employee default', }, disconnectPrompt: (connectionName = 'this integration') => `Are you sure you want to disconnect ${connectionName}?`, + connectPrompt: (connectionName: string) => `Are you sure you want to connect ${connectionName}? This will remove any existing accounting connections.`, reconnect: 'Reconnect', enterCredentials: 'Enter your credentials', updateCredentials: 'Update credentials', qboConnectionExpiring: ({date}: {date: string}) => `Your QuickBooks Online connection expires on ${date}.`, qboConnectionExpired: ({date}: {date: string}) => `Your QuickBooks Online connection expired on ${date}.`, claimOffer: { + badgeText: 'Offer available!', xero: { headline: 'Get Xero free for 6 months!', description: 'New to Xero? Expensify customers get 6 months free. Claim your offer below.', @@ -8113,8 +8144,10 @@ const translations = { }, }, hr: { + title: 'HR', connectionsSubtitle: "Connect to your HR system to sync employee data, auto-match reimbursements to the right people, and keep your team's expenses accurate without the manual work.", + subtitle: 'Connect HR tools and keep employee approvals in sync.', alreadyConnectedTitle: 'Cannot connect to multiple HR platforms', alreadyConnectedPrompt: 'You must disconnect your current HR platform before connecting another.', connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`, @@ -8176,6 +8209,8 @@ const translations = { }, }, recruiting: { + title: 'Recruiting', + subtitle: 'Connect Recruiting tools and keep candidate approvals in sync.', connectionsSubtitle: "Connect to your recruiting system to sync candidate data, auto-match reimbursements to the right people, and keep your team's expenses accurate without the manual work.", alreadyConnectedTitle: 'Cannot connect to multiple ATS platforms', @@ -8189,6 +8224,7 @@ const translations = { }, setupIncomplete: (setupLink: string | undefined) => `Connected. ${setupLink ? `Complete setup` : 'Complete setup'} to import candidates.`, + dontSeeYourATS: `Don't see your ATS here? Ask Concierge and we can add it.`, importSettings: 'Import settings', defaultApprover: 'Default approver', approverField: `First approver`, @@ -8229,6 +8265,9 @@ const translations = { }, }, merge: { + connections: 'Connections', + connect: 'Connect', + findIntegration: 'Find integration', syncNow: 'Sync now', disconnect: 'Disconnect', disconnectTitle: (providerName: string) => `Disconnect ${providerName}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index eb7dace5ab72..61d8e76d105a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3521,7 +3521,7 @@ ${amount} para ${merchant} - ${date}`, 1. Haz clic en *Espacios de trabajo*. 2. Selecciona tu espacio de trabajo. - 3. Haz clic en *Conexiones*. + 3. Haz clic en *Contabilidad*. 4. Busca ${integrationName}. 5. Haz clic en *Conectar*. @@ -4956,14 +4956,22 @@ ${amount} para ${merchant} - ${date}`, description: 'Excelentes noticias 🎉. Contáctanos si necesitan ayuda con la configuración.', }, mcp: { + connectors: 'Conectores', + connectorsSubtitle: 'Conecta un asistente de IA a tu cuenta de Expensify.', + connect: 'Conectar', + helpPrompt: '¿Necesitas ayuda para conectar?', + helpLink: 'Lee nuestra guía.', claude: { title: 'Claude', + subtitle: 'de Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'de Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'de OpenAI', }, }, connections: { @@ -6521,6 +6529,10 @@ ${amount} para ${merchant} - ${date}`, title: 'Organizar', subtitle: 'Agrupa y analiza el gasto, registra cada impuesto pagado.', }, + integrateSection: { + title: 'Integrar', + subtitle: 'Conecta Expensify a otros productos financieros populares.', + }, distanceRates: { title: 'Tasas de distancia', subtitle: 'Añade, actualiza y haz cumplir las tasas.', @@ -6762,19 +6774,25 @@ ${amount} para ${merchant} - ${date}`, }, mcp: { title: 'MCP', + subtitle: 'Conecta un asistente de IA a tu cuenta de Expensify.', }, receiptPartners: { title: 'Socios de recibos', + subtitle: 'Importación automática de recibos.', }, connectionsWarningModal: { featureEnabledTitle: 'No tan rápido...', featureEnabledText: 'Para activar o desactivar esta función, cambia la configuración de importación contable.', + disconnectText: 'Para desactivar la contabilidad, desconecta tu conexión contable del espacio de trabajo.', manageSettings: 'Gestionar la configuración', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Desconectar Uber', + disconnectText: 'Para desactivar esta función, desconecta primero la integración de Uber for Business.', description: '¿Está seguro de que desea desconectar esta integración?', + confirmText: 'Entendido', }, + hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para deshabilitar RR. HH., primero desconecta ${integration} de este espacio de trabajo.`}, workflowWarningModal: { featureEnabledTitle: 'No tan rápido...', featureEnabledText: @@ -6790,6 +6808,7 @@ ${amount} para ${merchant} - ${date}`, subtitle: 'Establecer una tarifa por hora facturable para el seguimiento de tiempo.', defaultHourlyRate: 'Tarifa por hora predeterminada', }, + recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para desactivar Recruiting, primero desconecta ${integration} de este espacio de trabajo.`}, }, reports: { reportsCustomTitleExamples: 'Ejemplos:', @@ -7196,6 +7215,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, accounting: { settings: 'configuración', title: 'Conexiones', + subtitle: 'Conecta tu software de contabilidad para una sincronización automática.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7207,6 +7227,10 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Chatea con tu ejecutivo de cuenta.', + talkYourAccountManager: 'Chatea con tu gestor de cuenta.', + talkToConcierge: 'Chatear con Concierge.', + needAnotherAccounting: '¿Necesitas otro software de contabilidad? ', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7247,6 +7271,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, disconnect: 'Desconectar', reinstall: 'Reinstalar el conector', disconnectTitle: (connectionName = 'integración') => `Desconectar ${connectionName}`, + connectTitle: (connectionName) => `Conectar ${connectionName}`, syncError: (connectionName) => `No se puede conectar a ${connectionName}`, accounts: 'Plan de cuentas', taxes: 'Impuestos', @@ -7263,10 +7288,12 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Predeterminado del empleado NetSuite', }, disconnectPrompt: (connectionName = 'integración') => `¿Estás seguro de que quieres desconectar ${connectionName}?`, + connectPrompt: (connectionName) => `¿Estás seguro de que quieres conectar a ${connectionName}? Esto eliminará cualquier conexión contable existente.`, enterCredentials: 'Ingresa tus credenciales', reconnect: 'Reconectar', updateCredentials: 'Actualizar credenciales', claimOffer: { + badgeText: '¡Oferta disponible!', xero: { headline: '¡Obtén Xero gratis por 6 meses!', description: @@ -7581,8 +7608,10 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, }, hr: { + title: 'HR', connectionsSubtitle: 'Conéctate a tu sistema de RR. HH. para sincronizar los datos de empleados, emparejar automáticamente los reembolsos con las personas correctas y mantener los gastos de tu equipo precisos sin trabajo manual.', + subtitle: 'Conecta herramientas de HR y mantén sincronizadas las aprobaciones de empleados.', alreadyConnectedTitle: 'No se puede conectar a varias plataformas de RR. HH.', alreadyConnectedPrompt: 'Debes desconectar tu plataforma de RR. HH. actual antes de conectar otra.', connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`, @@ -7645,6 +7674,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, }, recruiting: { + title: 'Reclutamiento', connectionsSubtitle: 'Conéctate a tu sistema de selección para sincronizar los datos de los candidatos, vincular automáticamente los reembolsos con las personas correctas y mantener los gastos de tu equipo precisos sin trabajo manual.', alreadyConnectedTitle: 'No se puede conectar a varias plataformas ATS', @@ -7652,6 +7682,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, syncing: 'Sincronizando candidatos', setupIncomplete: (setupLink: string | undefined) => `Conectado. ${setupLink ? `Completar configuración` : 'Completar configuración'} para importar candidatos.`, + dontSeeYourATS: `¿No ves tu ATS aquí? Pregunta a Concierge y podremos añadirlo.`, importSettings: 'Importar ajustes', defaultApprover: 'Aprobador predeterminado', approverFields: {recruiter: 'Reclutador', recruitingCoordinator: 'Coordinador de selección'}, @@ -7677,6 +7708,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, enableJobStagesOrTags: 'Selecciona al menos una etapa de trabajo o etiqueta para continuar', }, + subtitle: 'Conecta herramientas de selección y mantén sincronizadas las aprobaciones de candidatos.', syncResults: { importedCount: () => ({ one: '1 candidato', @@ -7695,6 +7727,9 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, }, }, merge: { + connections: 'Conexiones', + connect: 'Conectar', + findIntegration: 'Buscar integración', syncNow: 'Sincronizar ahora', disconnect: 'Desconectar', disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e5f0b61132a2..db786756547a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3635,7 +3635,7 @@ ${amount} pour ${merchant} - ${date}`, 1. Cliquez sur *Espaces de travail*. 2. Sélectionnez votre espace de travail. - 3. Cliquez sur *Connexions*. + 3. Cliquez sur *Comptabilité*. 4. Recherchez ${integrationName}. 5. Cliquez sur *Connecter*. @@ -5042,14 +5042,22 @@ ${amount} pour ${merchant} - ${date}`, description: "Excellente nouvelle 🎉. Contactez-nous si votre client a besoin d'aide pour la configuration.", }, mcp: { + connectors: 'Connecteurs', + connectorsSubtitle: 'Connectez un assistant IA à votre compte Expensify.', + connect: 'Connecter', + helpPrompt: "Besoin d'aide pour la connexion ?", + helpLink: 'Consultez notre guide.', claude: { title: 'Claude', + subtitle: 'par Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'par Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'par OpenAI', }, }, connections: { @@ -6677,6 +6685,10 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. title: 'Organiser', subtitle: 'Regroupez et analysez les dépenses, enregistrez chaque taxe payée.', }, + integrateSection: { + title: 'Intégrer', + subtitle: 'Connectez Expensify à des produits financiers populaires.', + }, distanceRates: { title: 'Taux de distance', subtitle: 'Ajoutez, mettez à jour et appliquez les taux.', @@ -6915,18 +6927,23 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. }, mcp: { title: 'MCP', + subtitle: 'Connectez un assistant IA à votre compte Expensify.', }, receiptPartners: { title: 'Partenaires de reçus', + subtitle: 'Importer automatiquement les reçus.', }, connectionsWarningModal: { featureEnabledTitle: 'Pas si vite…', featureEnabledText: 'Pour activer ou désactiver cette fonctionnalité, vous devez modifier vos paramètres d’import comptable.', + disconnectText: 'Pour désactiver la comptabilité, vous devez déconnecter votre connexion comptable de votre espace de travail.', manageSettings: 'Gérer les paramètres', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Dissocier Uber', + disconnectText: 'Pour désactiver cette fonctionnalité, veuillez d’abord déconnecter l’intégration Uber for Business.', description: 'Voulez-vous vraiment déconnecter cette intégration ?', + confirmText: 'Compris', }, workflowWarningModal: { featureEnabledTitle: 'Pas si vite…', @@ -6943,12 +6960,16 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. subtitle: 'Définissez un taux horaire facturable pour le suivi du temps.', 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é.', disabledTitle: 'Pas si vite...', disabledMessage: 'Pour activer ou désactiver cette fonctionnalité, vous devrez modifier vos paramètres d’importation comptable.', }, + recruitingWarningModal: { + disconnectText: ({integration}: {integration: string}) => `Pour désactiver le recrutement, veuillez d’abord déconnecter ${integration} de cet espace de travail.`, + }, }, reports: { reportsCustomTitleExamples: 'Exemples :', @@ -7420,6 +7441,7 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, accounting: { settings: 'paramètres', title: 'Connexions', + subtitle: 'Connectez votre logiciel de comptabilité pour une synchronisation automatique.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7431,6 +7453,10 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Discutez avec votre chargé de compte.', + talkYourAccountManager: 'Discuter avec votre gestionnaire de compte.', + talkToConcierge: 'Discuter avec Concierge.', + needAnotherAccounting: 'Besoin d’un autre logiciel comptable ?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7471,6 +7497,7 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, disconnect: 'Déconnecter', reinstall: 'Réinstaller le connecteur', disconnectTitle: (connectionName = 'intégration') => `Déconnecter ${connectionName}`, + connectTitle: (connectionName) => `Connecter ${connectionName}`, syncError: (connectionName) => `Impossible de se connecter à ${connectionName}`, accounts: 'Plan comptable', taxes: 'Taxes', @@ -7487,10 +7514,12 @@ Le forfait Control commence à 9 $ par Membre actif et par mois.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Par défaut employé NetSuite', }, disconnectPrompt: (connectionName = 'cette intégration') => `Voulez-vous vraiment déconnecter ${connectionName} ?`, + connectPrompt: (connectionName) => `Voulez-vous vraiment connecter ${connectionName} ? Cette action supprimera toutes les connexions comptables existantes.`, enterCredentials: 'Saisissez vos identifiants', reconnect: 'Reconnecter', updateCredentials: 'Mettre à jour les identifiants', claimOffer: { + badgeText: 'Offre disponible !', xero: { headline: 'Obtenez Xero gratuitement pendant 6 mois !', description: @@ -8962,8 +8991,10 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e subscriptions: 'Abonnements', }, hr: { + title: 'RH', connectionsSubtitle: 'Connectez votre système RH pour synchroniser les données des employé·es, faire correspondre automatiquement les remboursements aux bonnes personnes et garder les dépenses de votre équipe exactes, sans travail manuel.', + subtitle: 'Connectez vos outils RH et gardez les approbations des employés synchronisées.', alreadyConnectedTitle: 'Impossible de se connecter à plusieurs plateformes RH', alreadyConnectedPrompt: 'Vous devez déconnecter votre plateforme RH actuelle avant d’en connecter une autre.', connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`, @@ -9026,6 +9057,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, }, recruiting: { + title: 'Recrutement', connectionsSubtitle: 'Connectez votre système de recrutement pour synchroniser les données des candidats, faire correspondre automatiquement les remboursements aux bonnes personnes et maintenir des dépenses d’équipe exactes sans travail manuel.', alreadyConnectedTitle: 'Impossible de se connecter à plusieurs plateformes ATS', @@ -9033,6 +9065,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e syncing: 'Synchronisation des candidats', setupIncomplete: (setupLink: string | undefined) => `Connecté. ${setupLink ? `Terminer la configuration` : 'Terminer la configuration'} pour importer des candidats.`, + dontSeeYourATS: `Vous ne voyez pas votre ATS ici ? Demandez à Concierge et nous pourrons l’ajouter.`, importSettings: 'Paramètres d’importation', defaultApprover: 'Approbateur par défaut', approverFields: {recruiter: 'Recruteur', recruitingCoordinator: 'Coordinateur recrutement'}, @@ -9059,6 +9092,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, enableJobStagesOrTags: 'Sélectionnez au moins une étape de poste ou un tag pour continuer', }, + subtitle: 'Connectez les outils de recrutement et synchronisez les validations de candidats.', syncResults: { importedCount: () => ({ one: '1 candidat', @@ -9078,6 +9112,9 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e }, }, merge: { + connections: 'Connexions', + connect: 'Connecter', + findIntegration: 'Rechercher une intégration', syncNow: 'Synchroniser maintenant', disconnect: 'Déconnecter', disconnectTitle: (providerName: string) => `Déconnecter ${providerName}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 30dc3ad3531e..6af0639dac18 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3608,7 +3608,7 @@ ${amount} per ${merchant} - ${date}`, 1. Fai clic su *Spazi di lavoro*. 2. Seleziona il tuo spazio di lavoro. - 3. Fai clic su *Connessioni*. + 3. Fai clic su *Contabilità*. 4. Trova ${integrationName}. 5. Fai clic su *Connetti*. @@ -5000,14 +5000,22 @@ ${amount} per ${merchant} - ${date}`, description: 'Ottime notizie 🎉. Contattaci se hanno bisogno di aiuto con la configurazione.', }, mcp: { + connectors: 'Connettori', + connectorsSubtitle: 'Collega un assistente IA al tuo account Expensify.', + connect: 'Connetti', + helpPrompt: 'Hai bisogno di aiuto con la connessione?', + helpLink: 'Leggi la nostra guida.', claude: { title: 'Claude', + subtitle: 'di Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'di Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'di OpenAI', }, }, connections: { @@ -6618,6 +6626,10 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. title: 'Organizza', subtitle: 'Raggruppa e analizza le spese, registra ogni imposta pagata.', }, + integrateSection: { + title: 'Integra', + subtitle: 'Collega Expensify ai più diffusi prodotti finanziari.', + }, distanceRates: { title: 'Tariffe chilometriche', subtitle: 'Aggiungi, aggiorna e applica le tariffe.', @@ -6855,18 +6867,23 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. }, mcp: { title: 'MCP', + subtitle: 'Collega un assistente IA al tuo account Expensify.', }, receiptPartners: { title: 'Partner ricevute', + subtitle: 'Importa automaticamente le ricevute.', }, connectionsWarningModal: { featureEnabledTitle: 'Non così in fretta...', featureEnabledText: 'Per attivare o disattivare questa funzionalità, devi modificare le impostazioni di importazione contabile.', + disconnectText: 'Per disattivare la contabilità, devi disconnettere la connessione contabile dal tuo spazio di lavoro.', manageSettings: 'Gestisci impostazioni', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Disconnetti Uber', + disconnectText: 'Per disattivare questa funzionalità, disconnetti prima l’integrazione Uber for Business.', description: 'Sei sicuro di voler disconnettere questa integrazione?', + confirmText: 'Ho capito', }, workflowWarningModal: { featureEnabledTitle: 'Non così in fretta...', @@ -6883,12 +6900,14 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. subtitle: 'Imposta una tariffa oraria fatturabile per il rilevamento del tempo.', 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à.', disabledTitle: 'Non così in fretta...', disabledMessage: 'Per abilitare o disabilitare questa funzione, dovrai modificare le impostazioni di importazione della contabilità.', }, + recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Per disattivare Recruiting, scollega prima ${integration} da questo spazio di lavoro.`}, }, reports: { reportsCustomTitleExamples: 'Esempi:', @@ -7355,6 +7374,7 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, accounting: { settings: 'impostazioni', title: 'Connessioni', + subtitle: 'Collega il tuo software di contabilità per una sincronizzazione automatica.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7366,6 +7386,10 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Chatta con il tuo account executive.', + talkYourAccountManager: 'Chatta con il tuo account manager.', + talkToConcierge: 'Chatta con Concierge.', + needAnotherAccounting: 'Ti serve un altro software di contabilità?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7406,6 +7430,7 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, disconnect: 'Disconnetti', reinstall: 'Reinstalla connettore', disconnectTitle: (connectionName = 'integrazione') => `Disconnetti ${connectionName}`, + connectTitle: (connectionName) => `Collega ${connectionName}`, syncError: (connectionName) => `Impossibile connettersi a ${connectionName}`, accounts: 'Piano dei conti', taxes: 'Tasse', @@ -7422,10 +7447,12 @@ Il piano Control parte da 9 $ al mese per ogni membro attivo.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Impostazione predefinita dipendente NetSuite', }, disconnectPrompt: (connectionName = 'questa integrazione') => `Sei sicuro di voler disconnettere ${connectionName}?`, + connectPrompt: (connectionName) => `Sei sicuro di voler collegare ${connectionName}? Questo rimuoverà tutte le connessioni contabili esistenti.`, enterCredentials: 'Inserisci le tue credenziali', reconnect: 'Riconnetti', updateCredentials: 'Aggiorna credenziali', claimOffer: { + badgeText: 'Offerta disponibile!', xero: { headline: 'Ottieni Xero gratis per 6 mesi!', description: 'Nuovo su Xero? I clienti Expensify ricevono 6 mesi gratis. Richiedi la tua offerta qui sotto.', @@ -8884,8 +8911,10 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, subscriptions: 'Abbonamenti', }, hr: { + title: 'Risorse umane', connectionsSubtitle: 'Collega il tuo sistema HR per sincronizzare i dati dei dipendenti, abbinare automaticamente i rimborsi alle persone giuste e mantenere le spese del tuo team accurate senza lavoro manuale.', + subtitle: 'Collega gli strumenti HR e mantieni sincronizzate le approvazioni dei dipendenti.', alreadyConnectedTitle: 'Impossibile connettersi a più piattaforme HR', alreadyConnectedPrompt: 'Devi disconnettere la tua attuale piattaforma HR prima di collegarne un’altra.', connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`, @@ -8948,6 +8977,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, }, recruiting: { + title: 'Assunzioni', connectionsSubtitle: 'Connetti il tuo sistema di recruiting per sincronizzare i dati dei candidati, abbinare automaticamente i rimborsi alle persone giuste e mantenere le spese del team accurate senza lavoro manuale.', alreadyConnectedTitle: 'Impossibile connettersi a più piattaforme ATS', @@ -8955,6 +8985,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, syncing: 'Sincronizzazione candidati', setupIncomplete: (setupLink: string | undefined) => `Connesso. ${setupLink ? `Completa la configurazione` : 'Completa configurazione'} per importare i candidati.`, + dontSeeYourATS: `Non vedi il tuo ATS qui? Chiedi a Concierge e possiamo aggiungerlo.`, importSettings: 'Impostazioni di importazione', defaultApprover: 'Approvatore predefinito', approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coordinatore recruiting'}, @@ -8980,6 +9011,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, enableJobStagesOrTags: 'Seleziona almeno una fase del lavoro o un tag per continuare', }, + subtitle: 'Collega gli strumenti di recruiting e mantieni sincronizzate le approvazioni dei candidati.', syncResults: { importedCount: () => ({ one: '1 candidato', @@ -8999,6 +9031,9 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, }, }, merge: { + connections: 'Connessioni', + connect: 'Connetti', + findIntegration: 'Trova integrazione', syncNow: 'Sincronizza ora', disconnect: 'Disconnetti', disconnectTitle: (providerName: string) => `Disconnetti ${providerName}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 000c9598d0d1..693bc3953bff 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3580,7 +3580,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' 1.「*ワークスペース*」をクリックします。 2. ワークスペースを選択します。 - 3.「*連携*」をクリックします。 + 3.「*会計*」をクリックします。 4. ${integrationName} を探します。 5.「*接続*」をクリックします。 @@ -4965,14 +4965,22 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' description: '素晴らしいニュースです 🎉。セットアップにサポートが必要な場合はお問い合わせください。', }, mcp: { + connectors: 'コネクタ', + connectorsSubtitle: 'AIアシスタントをExpensifyアカウントに接続します。', + connect: '接続', + helpPrompt: '接続にお困りですか?', + helpLink: 'ガイドをご覧ください。', claude: { title: 'Claude', + subtitle: 'Anthropic 提供', }, cursor: { title: 'Cursor', + subtitle: 'Anysphere 提供', }, chatgpt: { title: 'ChatGPT', + subtitle: 'OpenAI 提供', }, }, connections: { @@ -6542,6 +6550,10 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO title: '整理', subtitle: '支出をグループ化して分析し、支払ったすべての税金を記録します。', }, + integrateSection: { + title: '連携する', + subtitle: 'Expensify を人気の金融サービスに接続しましょう。', + }, distanceRates: { title: '距離単価', subtitle: 'レートを追加、更新し、適用します。', @@ -6764,18 +6776,23 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO }, mcp: { title: 'MCP', + subtitle: 'AIアシスタントをExpensifyアカウントに接続します。', }, receiptPartners: { title: '領収書パートナー', + subtitle: '領収書を自動で取り込みます。', }, connectionsWarningModal: { featureEnabledTitle: 'ちょっと待ってください…', featureEnabledText: 'この機能を有効または無効にするには、会計インポート設定を変更する必要があります。', + disconnectText: '会計機能を無効にするには、ワークスペースから会計連携を切断する必要があります。', manageSettings: '設定を管理', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Uber との連携を解除', + disconnectText: 'この機能を無効にするには、まず Uber for Business の連携を解除してください。', description: 'この連携を解除してもよろしいですか?', + confirmText: '了解しました', }, workflowWarningModal: { featureEnabledTitle: 'ちょっと待ってください…', @@ -6792,12 +6809,16 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO subtitle: '時間追跡用の請求可能な時間単価を設定します。', defaultHourlyRate: 'デフォルトの時給率', }, + hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `HR を無効にするには、まずこのワークスペースから ${integration} を切断してください。`}, vendors: { title: 'ベンダー', subtitle: '会計ソフトからインポートした取引先にカード経費を照合します。', disabledTitle: 'ちょっと待ってください...', disabledMessage: 'この機能を有効または無効にするには、会計インポート設定を変更する必要があります。', }, + recruitingWarningModal: { + disconnectText: ({integration}: {integration: string}) => `採用機能を無効にするには、まずこのワークスペースから ${integration} の連携を解除してください。`, + }, }, reports: { reportsCustomTitleExamples: '例:', @@ -7262,6 +7283,7 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から accounting: { settings: '設定', title: '接続', + subtitle: '会計ソフトを接続して、自動同期を行いましょう。', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7273,6 +7295,10 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'アカウントエグゼクティブとチャットします。', + talkYourAccountManager: 'アカウントマネージャーとチャットする', + talkToConcierge: 'Conciergeとチャットする', + needAnotherAccounting: 'ほかの会計ソフトが必要ですか?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7313,6 +7339,7 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から disconnect: '切断', reinstall: 'コネクタを再インストール', disconnectTitle: (connectionName = '連携') => `${connectionName}の接続を解除`, + connectTitle: (connectionName) => `${connectionName} を接続`, syncError: (connectionName) => `${connectionName} に接続できません`, accounts: '勘定科目表', taxes: '税金', @@ -7329,10 +7356,12 @@ Control プランは、アクティブメンバー1人あたり月額 $9 から [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite 従業員のデフォルト', }, disconnectPrompt: (connectionName = 'この連携') => `${connectionName} の接続を本当に解除しますか?`, + connectPrompt: (connectionName) => `${connectionName} を接続してもよろしいですか?これにより、既存の会計連携はすべて削除されます。`, enterCredentials: '認証情報を入力してください', reconnect: '再接続', updateCredentials: '認証情報を更新', claimOffer: { + badgeText: 'オファーをご利用いただけます!', xero: { headline: 'Xero を6か月間無料で利用しましょう!', description: @@ -8771,7 +8800,9 @@ ${reportName}`, subscriptions: 'サブスクリプション', }, hr: { + title: '人事', connectionsSubtitle: '人事システムと連携して従業員データを同期し、精算を自動で正しい担当者に紐づけることで、手作業なしでチームの経費を正確に管理できます。', + subtitle: '人事ツールを連携して、従業員の承認を常に同期させます。', alreadyConnectedTitle: '複数の人事プラットフォームには接続できません', alreadyConnectedPrompt: '別の人事プラットフォームに接続する前に、現在の人事プラットフォームとの接続を解除する必要があります。', connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`, @@ -8833,12 +8864,14 @@ ${reportName}`, }, }, recruiting: { + title: '採用', connectionsSubtitle: '採用システムに接続して候補者データを同期し、払い戻しを自動的に適切な人に紐づけることで、手作業なしでチームの経費精度を保てます。', alreadyConnectedTitle: '複数の ATS プラットフォームには接続できません', alreadyConnectedPrompt: '別の ATS を接続する前に、現在の ATS を必ず切断する必要があります。', syncing: '候補者を同期しています', setupIncomplete: (setupLink: string | undefined) => `接続済み。${setupLink ? `セットアップを完了` : '設定を完了'} をインポート候補として追加します。`, + dontSeeYourATS: `ご利用のATSが見つかりませんか?Concierge にお問い合わせいただければ、追加できます。`, importSettings: 'インポート設定', defaultApprover: 'デフォルト承認者', approverFields: {recruiter: '採用担当者', recruitingCoordinator: '採用コーディネーター'}, @@ -8859,6 +8892,7 @@ ${reportName}`, offices: {title: 'オフィス', description: 'このワークスペースと同期したい候補者のオフィスを選択してください', toggleTitle: 'オフィス', allSelected: 'すべてのオフィス'}, enableJobStagesOrTags: '続行するには、少なくとも 1 つのジョブのステージまたはタグを選択してください', }, + subtitle: '採用ツールを連携して、候補者の承認を常に同期させます。', syncResults: { importedCount: () => ({ one: '1 名の候補者', @@ -8878,6 +8912,9 @@ ${reportName}`, }, }, merge: { + connections: '接続', + connect: '接続', + findIntegration: '連携を検索', syncNow: '今すぐ同期', disconnect: '切断', disconnectTitle: (providerName: string) => `${providerName}を切断`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 570b3e58978c..5ffd40e382e3 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3611,7 +3611,7 @@ ${amount} voor ${merchant} - ${date}`, 1. Klik op *Workspaces*. 2. Selecteer je workspace. - 3. Klik op *Verbindingen*. + 3. Klik op *Accounting*. 4. Zoek ${integrationName}. 5. Klik op *Connect*. @@ -5002,14 +5002,22 @@ ${amount} voor ${merchant} - ${date}`, description: 'Geweldig nieuws 🎉. Neem contact met ons op als ze hulp nodig hebben bij de configuratie.', }, mcp: { + connectors: 'Connectoren', + connectorsSubtitle: 'Verbind een AI-assistent met je Expensify-account.', + connect: 'Verbinden', + helpPrompt: 'Hulp nodig bij het verbinden?', + helpLink: 'Lees onze handleiding.', claude: { title: 'Claude', + subtitle: 'van Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'van Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'van OpenAI', }, }, connections: { @@ -6610,6 +6618,10 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ title: 'Organiseren', subtitle: 'Groepeer en analyseer uitgaven, leg elke betaalde belasting vast.', }, + integrateSection: { + title: 'Integreren', + subtitle: 'Koppel Expensify aan populaire financiële producten.', + }, distanceRates: { title: 'Kilometertarieven', subtitle: 'Tarieven toevoegen, bijwerken en afdwingen.', @@ -6839,18 +6851,23 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ }, mcp: { title: 'MCP', + subtitle: 'Verbind een AI-assistent met je Expensify-account.', }, receiptPartners: { title: 'Bonpartners', + subtitle: 'Automatisch bonnetjes importeren.', }, connectionsWarningModal: { featureEnabledTitle: 'Niet zo snel...', featureEnabledText: 'Om deze functie in of uit te schakelen, moet je je instellingen voor boekhoudkundige import wijzigen.', + disconnectText: 'Om de boekhouding uit te schakelen, moet je de boekhoudkoppeling van je werkruimte loskoppelen.', manageSettings: 'Instellingen beheren', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Verbinding met Uber verbreken', + disconnectText: 'Om deze functie uit te schakelen, verbreek eerst de koppeling met de Uber for Business-integratie.', description: 'Weet je zeker dat je deze integratie wilt ontkoppelen?', + confirmText: 'Begrepen', }, workflowWarningModal: { featureEnabledTitle: 'Niet zo snel...', @@ -6867,12 +6884,14 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ subtitle: 'Stel een factureerbaar uurtarief in voor tijdregistratie.', 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.', disabledTitle: 'Niet zo snel...', disabledMessage: 'Om deze functie in of uit te schakelen, moet je je boekhoudimportinstellingen wijzigen.', }, + recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Om Werving uit te schakelen, koppel ${integration} eerst los van deze workspace.`}, }, reports: { reportsCustomTitleExamples: 'Voorbeelden:', @@ -7338,6 +7357,7 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, accounting: { settings: 'instellingen', title: 'Verbindingen', + subtitle: 'Verbind je boekhoudsoftware voor een automatische synchronisatie.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7349,6 +7369,10 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Chat met je accountmanager.', + talkYourAccountManager: 'Chat met je accountmanager.', + talkToConcierge: 'Chat met Concierge.', + needAnotherAccounting: 'Nog een boekhoudprogramma nodig?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7389,6 +7413,7 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, disconnect: 'Verbinding verbreken', reinstall: 'Connector opnieuw installeren', disconnectTitle: (connectionName = 'integratie') => `Verbinding met ${connectionName} verbreken`, + connectTitle: (connectionName) => `Verbind ${connectionName}`, syncError: (connectionName) => `Kan geen verbinding maken met ${connectionName}`, accounts: 'Rekeningschema', taxes: 'Belastingen', @@ -7405,10 +7430,12 @@ Het Control-abonnement begint bij $9 per actieve deelnemer per maand.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Standaard NetSuite-medewerker', }, disconnectPrompt: (connectionName = 'deze integratie') => `Weet je zeker dat je ${connectionName} wilt ontkoppelen?`, + connectPrompt: (connectionName) => `Weet je zeker dat je ${connectionName} wilt koppelen? Hierdoor worden alle bestaande boekhoudkundige koppelingen verwijderd.`, enterCredentials: 'Voer je inloggegevens in', reconnect: 'Opnieuw verbinden', updateCredentials: 'Inloggegevens bijwerken', claimOffer: { + badgeText: 'Aanbieding beschikbaar!', xero: { headline: 'Krijg Xero 6 maanden gratis!', description: 'Nieuw bij Xero? Expensify-klanten krijgen 6 maanden gratis. Claim jouw aanbieding hieronder.', @@ -8862,8 +8889,10 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, subscriptions: 'Abonnementen', }, hr: { + title: 'HR', connectionsSubtitle: 'Verbind met je HR-systeem om werknemersgegevens te synchroniseren, terugbetalingen automatisch aan de juiste personen te koppelen en de uitgaven van je team nauwkeurig te houden zonder handmatig werk.', + subtitle: 'Koppel HR-tools en houd goedkeuringen van medewerkers gesynchroniseerd.', alreadyConnectedTitle: 'Kan niet met meerdere HR-platformen verbinden', alreadyConnectedPrompt: 'Je moet je huidige HR-platform loskoppelen voordat je een ander kunt verbinden.', connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`, @@ -8926,6 +8955,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, }, recruiting: { + title: 'Werving', connectionsSubtitle: 'Koppel je recruitmentsysteem om kandidaatgegevens te synchroniseren, vergoedingen automatisch aan de juiste personen te koppelen en de uitgaven van je team nauwkeurig te houden zonder handmatig werk.', alreadyConnectedTitle: 'Kan niet met meerdere ATS-platformen verbinden', @@ -8933,6 +8963,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, syncing: 'Kandidaten synchroniseren', setupIncomplete: (setupLink: string | undefined) => `Verbonden. ${setupLink ? `Installatie voltooien` : 'Voltooi installatie'} om kandidaten te importeren.`, + dontSeeYourATS: `Zie je je ATS hier niet? Vraag het aan Concierge en we kunnen het toevoegen.`, importSettings: 'Importinstellingen', defaultApprover: 'Standaardgoedkeurder', approverFields: {recruiter: 'Recruiter', recruitingCoordinator: 'Coördinator werving'}, @@ -8958,6 +8989,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, enableJobStagesOrTags: 'Selecteer minstens één functiestap of tag om door te gaan', }, + subtitle: 'Koppel wervingstools en houd kandidaategoedkeuringen gesynchroniseerd.', syncResults: { importedCount: () => ({ one: '1 kandidaat', @@ -8976,6 +9008,9 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, }, }, merge: { + connections: 'Verbindingen', + connect: 'Verbinden', + findIntegration: 'Integratie zoeken', syncNow: 'Nu synchroniseren', disconnect: 'Ontkoppelen', disconnectTitle: (providerName: string) => `${providerName} ontkoppelen`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 550240f23fb8..f69a837fffba 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3642,7 +3642,7 @@ ${amount} dla ${merchant} - ${date}`, 1. Kliknij *Workspaces*. 2. Wybierz swoją przestrzeń roboczą. - 3. Kliknij *Połączenia*. + 3. Kliknij *Accounting*. 4. Znajdź ${integrationName}. 5. Kliknij *Connect*. @@ -5033,14 +5033,22 @@ ${amount} dla ${merchant} - ${date}`, description: 'Świetna wiadomość 🎉. Skontaktuj się z nami, jeśli potrzebują pomocy przy konfiguracji.', }, mcp: { + connectors: 'Konektory', + connectorsSubtitle: 'Połącz asystenta AI ze swoim kontem Expensify.', + connect: 'Połącz', + helpPrompt: 'Potrzebujesz pomocy z połączeniem?', + helpLink: 'Przeczytaj nasz przewodnik.', claude: { title: 'Claude', + subtitle: 'od Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'od Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'od OpenAI', }, }, connections: { @@ -6634,6 +6642,10 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy title: 'Organizuj', subtitle: 'Grupuj i analizuj wydatki, zapisuj każdy zapłacony podatek.', }, + integrateSection: { + title: 'Integruj', + subtitle: 'Połącz Expensify z popularnymi produktami finansowymi.', + }, distanceRates: { title: 'Stawki za dystans', subtitle: 'Dodawaj, aktualizuj i egzekwuj stawki.', @@ -6865,18 +6877,23 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy }, mcp: { title: 'MCP', + subtitle: 'Połącz asystenta AI ze swoim kontem Expensify.', }, receiptPartners: { title: 'Partnerzy paragonów', + subtitle: 'Automatycznie importuj paragony.', }, connectionsWarningModal: { featureEnabledTitle: 'Nie tak szybko...', featureEnabledText: 'Aby włączyć lub wyłączyć tę funkcję, musisz zmienić ustawienia importu księgowego.', + disconnectText: 'Aby wyłączyć księgowość, musisz odłączyć połączenie księgowe od swojego obszaru roboczego.', manageSettings: 'Zarządzaj ustawieniami', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Odłącz Uber', + disconnectText: 'Aby wyłączyć tę funkcję, najpierw odłącz integrację Uber for Business.', description: 'Czy na pewno chcesz odłączyć tę integrację?', + confirmText: 'Rozumiem', }, workflowWarningModal: { featureEnabledTitle: 'Nie tak szybko...', @@ -6893,12 +6910,14 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy subtitle: 'Ustaw godzinową stawkę rozliczeniową do śledzenia czasu.', 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.', disabledTitle: 'Nie tak szybko...', disabledMessage: 'Aby włączyć lub wyłączyć tę funkcję, musisz zmienić ustawienia importu księgowego.', }, + recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Aby wyłączyć Rekrutację, najpierw odłącz ${integration} od tego workspace.`}, }, reports: { reportsCustomTitleExamples: 'Przykłady:', @@ -7364,6 +7383,7 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, accounting: { settings: 'ustawienia', title: 'Połączenia', + subtitle: 'Podłącz swoje oprogramowanie księgowe, aby włączyć automatyczną synchronizację.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7375,6 +7395,10 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Porozmawiaj ze swoim opiekunem klienta.', + talkYourAccountManager: 'Porozmawiaj ze swoim opiekunem konta.', + talkToConcierge: 'Czat z Concierge.', + needAnotherAccounting: 'Potrzebujesz innego programu księgowego?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7415,6 +7439,7 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, disconnect: 'Rozłącz', reinstall: 'Zainstaluj ponownie konektor', disconnectTitle: (connectionName = 'integracja') => `Odłącz ${connectionName}`, + connectTitle: (connectionName) => `Połącz ${connectionName}`, syncError: (connectionName) => `Nie można połączyć się z ${connectionName}`, accounts: 'Plan kont', taxes: 'Podatki', @@ -7431,10 +7456,12 @@ Plan Control zaczyna się od 9 USD za aktywnego członka miesięcznie.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Domyślny pracownik NetSuite', }, disconnectPrompt: (connectionName = 'ta integracja') => `Czy na pewno chcesz odłączyć ${connectionName}?`, + connectPrompt: (connectionName) => `Czy na pewno chcesz połączyć ${connectionName}? Spowoduje to usunięcie wszystkich istniejących połączeń księgowych.`, enterCredentials: 'Wprowadź swoje dane logowania', reconnect: 'Połącz ponownie', updateCredentials: 'Zaktualizuj dane logowania', claimOffer: { + badgeText: 'Oferta dostępna!', xero: { headline: 'Korzystaj z Xero za darmo przez 6 miesięcy!', description: 'Nowy w Xero? Klienci Expensify otrzymują 6 miesięcy za darmo. Odbierz swoją ofertę poniżej.', @@ -8883,8 +8910,10 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, subscriptions: 'Subskrypcje', }, hr: { + title: 'HR', connectionsSubtitle: 'Połącz się ze swoim systemem HR, aby synchronizować dane pracowników, automatycznie dopasowywać zwroty do właściwych osób i utrzymywać wydatki zespołu w porządku bez ręcznej pracy.', + subtitle: 'Połącz narzędzia HR i utrzymuj zgody pracowników w synchronizacji.', alreadyConnectedTitle: 'Nie można połączyć się z wieloma platformami HR', alreadyConnectedPrompt: 'Musisz odłączyć swoją obecną platformę HR, zanim podłączysz inną.', connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`, @@ -8947,6 +8976,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, }, recruiting: { + title: 'Rekrutacja', connectionsSubtitle: 'Połącz się ze swoim systemem rekrutacyjnym, aby synchronizować dane kandydatów, automatycznie dopasowywać zwroty kosztów do właściwych osób i utrzymywać poprawność wydatków zespołu bez ręcznej pracy.', alreadyConnectedTitle: 'Nie można połączyć się z wieloma platformami ATS', @@ -8954,6 +8984,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, syncing: 'Synchronizowanie kandydatów', setupIncomplete: (setupLink: string | undefined) => `Połączono. ${setupLink ? `Dokończ konfigurację` : 'Zakończ konfigurację'}, aby zaimportować kandydatów.`, + dontSeeYourATS: `Nie widzisz tutaj swojego ATS? Zapytaj Concierge, a my możemy go dodać.`, importSettings: 'Ustawienia importu', defaultApprover: 'Domyślny zatwierdzający', approverFields: {recruiter: 'Rekruter', recruitingCoordinator: 'Koordynator rekrutacji'}, @@ -8974,6 +9005,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, offices: {title: 'Biuro', description: 'Wybierz biura kandydatów, które chcesz zsynchronizować z tym miejscem pracy', toggleTitle: 'Biura', allSelected: 'Wszystkie biura'}, enableJobStagesOrTags: 'Wybierz co najmniej jeden etap zadania lub znacznik, aby kontynuować', }, + subtitle: 'Połącz narzędzia rekrutacyjne i utrzymuj zgody kandydatów w synchronizacji.', syncResults: { importedCount: () => ({ one: '1 kandydat', @@ -8992,6 +9024,9 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, }, }, merge: { + connections: 'Połączenia', + connect: 'Połącz', + findIntegration: 'Znajdź integrację', syncNow: 'Synchronizuj teraz', disconnect: 'Odłącz', disconnectTitle: (providerName: string) => `Odłącz ${providerName}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 7fed108904a3..83866060d5d2 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3599,7 +3599,7 @@ ${amount} para ${merchant} - ${date}`, 1. Clique em *Workspaces*. 2. Selecione seu workspace. - 3. Clique em *Conexões*. + 3. Clique em *Accounting*. 4. Encontre ${integrationName}. 5. Clique em *Connect*. @@ -4993,14 +4993,22 @@ ${amount} para ${merchant} - ${date}`, description: 'Ótimas notícias 🎉. Entre em contato conosco se precisarem de ajuda com a configuração.', }, mcp: { + connectors: 'Conectores', + connectorsSubtitle: 'Conecte um assistente de IA à sua conta do Expensify.', + connect: 'Conectar', + helpPrompt: 'Precisa de ajuda para conectar?', + helpLink: 'Leia nosso guia.', claude: { title: 'Claude', + subtitle: 'da Anthropic', }, cursor: { title: 'Cursor', + subtitle: 'da Anysphere', }, chatgpt: { title: 'ChatGPT', + subtitle: 'da OpenAI', }, }, connections: { @@ -6608,6 +6616,10 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS title: 'Organizar', subtitle: 'Agrupe e analise os gastos, registre todos os impostos pagos.', }, + integrateSection: { + title: 'Integrar', + subtitle: 'Conecte o Expensify a produtos financeiros populares.', + }, distanceRates: { title: 'Taxas de distância', subtitle: 'Adicione, atualize e aplique tarifas.', @@ -6841,18 +6853,23 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS }, mcp: { title: 'MCP', + subtitle: 'Conecte um assistente de IA à sua conta do Expensify.', }, receiptPartners: { title: 'Parceiros de recibos', + subtitle: 'Importe recibos automaticamente.', }, connectionsWarningModal: { featureEnabledTitle: 'Não tão rápido...', featureEnabledText: 'Para ativar ou desativar este recurso, você precisará alterar suas configurações de importação contábil.', + disconnectText: 'Para desativar a contabilidade, você precisará desconectar sua conexão contábil do seu workspace.', manageSettings: 'Gerenciar configurações', }, receiptPartnersWarningModal: { featureEnabledTitle: 'Desconectar Uber', + disconnectText: 'Para desativar este recurso, desconecte primeiro a integração do Uber for Business.', description: 'Tem certeza de que deseja desconectar esta integração?', + confirmText: 'Entendi', }, workflowWarningModal: { featureEnabledTitle: 'Não tão rápido...', @@ -6869,12 +6886,14 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS subtitle: 'Defina uma taxa horária faturável para o controle de tempo.', 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.', disabledTitle: 'Calma lá...', disabledMessage: 'Para ativar ou desativar este recurso, você precisará alterar suas configurações de importação contábil.', }, + recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para desativar Recrutamento, primeiro desconecte ${integration} deste workspace.`}, }, reports: { reportsCustomTitleExamples: 'Exemplos:', @@ -7340,6 +7359,7 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, accounting: { settings: 'configurações', title: 'Conexões', + subtitle: 'Conecte seu software de contabilidade para uma sincronização automática.', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7351,6 +7371,10 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: 'Converse com seu executivo de contas.', + talkYourAccountManager: 'Converse com seu gerente de conta.', + talkToConcierge: 'Converse com o Concierge.', + needAnotherAccounting: 'Precisa de outro software de contabilidade?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7391,6 +7415,7 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, disconnect: 'Desconectar', reinstall: 'Reinstalar conector', disconnectTitle: (connectionName = 'integração') => `Desconectar ${connectionName}`, + connectTitle: (connectionName) => `Conectar ${connectionName}`, syncError: (connectionName) => `Não é possível conectar ao ${connectionName}`, accounts: 'Plano de contas', taxes: 'Impostos', @@ -7407,10 +7432,12 @@ O plano Control começa em US$ 9 por membro ativo por mês.`, [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'Padrão de funcionário do NetSuite', }, disconnectPrompt: (connectionName = 'esta integração') => `Tem certeza de que deseja desconectar ${connectionName}?`, + connectPrompt: (connectionName) => `Tem certeza de que deseja conectar ${connectionName}? Isso removerá quaisquer conexões contábeis existentes.`, enterCredentials: 'Insira suas credenciais', reconnect: 'Reconectar', updateCredentials: 'Atualizar credenciais', claimOffer: { + badgeText: 'Oferta disponível!', xero: { headline: 'Use Xero grátis por 6 meses!', description: 'Novo no Xero? Clientes Expensify ganham 6 meses grátis. Resgate sua oferta abaixo.', @@ -8865,8 +8892,10 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, subscriptions: 'Assinaturas', }, hr: { + title: 'RH', connectionsSubtitle: 'Conecte-se ao seu sistema de RH para sincronizar os dados de funcionários, associar automaticamente os reembolsos às pessoas corretas e manter as despesas da sua equipe precisas, sem trabalho manual.', + subtitle: 'Conecte ferramentas de RH e mantenha as aprovações de funcionários em sincronia.', alreadyConnectedTitle: 'Não é possível conectar a várias plataformas de RH', alreadyConnectedPrompt: 'Você precisa desconectar sua plataforma de RH atual antes de conectar outra.', connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`, @@ -8929,6 +8958,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, }, recruiting: { + title: 'Recrutamento', connectionsSubtitle: 'Conecte-se ao seu sistema de recrutamento para sincronizar os dados de candidatos, associar automaticamente os reembolsos às pessoas certas e manter as despesas da sua equipe precisas sem trabalho manual.', alreadyConnectedTitle: 'Não é possível conectar a várias plataformas de ATS', @@ -8936,6 +8966,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, syncing: 'Sincronizando candidatos', setupIncomplete: (setupLink: string | undefined) => `Conectado. ${setupLink ? `Concluir configuração` : 'Concluir configuração'} para importar candidatos.`, + dontSeeYourATS: `Não está vendo seu ATS aqui? Pergunte ao Concierge e podemos adicioná-lo.`, importSettings: 'Importar configurações', defaultApprover: 'Aprovador padrão', approverFields: {recruiter: 'Recrutador', recruitingCoordinator: 'Coordenador de recrutamento'}, @@ -8961,6 +8992,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, enableJobStagesOrTags: 'Selecione pelo menos um estágio de trabalho ou etiqueta para continuar', }, + subtitle: 'Conecte ferramentas de recrutamento e mantenha as aprovações de candidatos em sincronia.', syncResults: { importedCount: () => ({ one: '1 candidato', @@ -8979,6 +9011,9 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, }, }, merge: { + connections: 'Conexões', + connect: 'Conectar', + findIntegration: 'Encontrar integração', syncNow: 'Sincronizar agora', disconnect: 'Desconectar', disconnectTitle: (providerName: string) => `Desconectar ${providerName}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ae2d808fccea..b6df99cf3298 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3493,7 +3493,7 @@ ${amount},商户:${merchant} - 日期:${date}`, 1. 点击 *Workspaces*。 2. 选择您的工作区。 - 3. 点击 *连接*。 + 3. 点击 *Accounting*。 4. 找到 ${integrationName}。 5. 点击 *Connect*。 @@ -4851,14 +4851,22 @@ ${amount},商户:${merchant} - 日期:${date}`, description: '好消息 🎉。如果他们在设置方面需要帮助,请联系我们。', }, mcp: { + connectors: '连接器', + connectorsSubtitle: '将 AI 助手连接到您的 Expensify 账户。', + connect: '连接', + helpPrompt: '需要帮助连接吗?', + helpLink: '阅读我们的指南。', claude: { title: 'Claude', + subtitle: '由 Anthropic 提供', }, cursor: { title: 'Cursor', + subtitle: '由 Anysphere 提供', }, chatgpt: { title: 'ChatGPT', + subtitle: '由 OpenAI 提供', }, }, connections: { @@ -6395,6 +6403,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM title: '整理', subtitle: '分组并分析支出,记录每一笔已缴税款。', }, + integrateSection: { + title: '集成', + subtitle: '将 Expensify 连接到常用的金融产品。', + }, distanceRates: { title: '距离费率', subtitle: '添加、更新并执行费率。', @@ -6600,18 +6612,23 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM }, mcp: { title: 'MCP', + subtitle: '将 AI 助手连接到您的 Expensify 账户。', }, receiptPartners: { title: '收据合作伙伴', + subtitle: '自动导入收据。', }, connectionsWarningModal: { featureEnabledTitle: '别急……', featureEnabledText: '若要启用或停用此功能,您需要更改会计导入设置。', + disconnectText: '要停用会计功能,您需要从工作区中断开您的会计连接。', manageSettings: '管理设置', }, receiptPartnersWarningModal: { featureEnabledTitle: '断开与 Uber 的连接', + disconnectText: '若要禁用此功能,请先断开 Uber for Business 集成。', description: '确定要断开与此集成的连接吗?', + confirmText: '明白了', }, workflowWarningModal: { featureEnabledTitle: '别急……', @@ -6627,12 +6644,14 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM subtitle: '为时间跟踪设置可计费的小时费率。', defaultHourlyRate: '默认时薪', }, + hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `若要禁用人力资源功能,请先将此工作区与 ${integration} 断开连接。`}, vendors: { title: '供应商', subtitle: '将卡片报销与从您的会计软件导入的供应商进行匹配。', disabledTitle: '别急...', disabledMessage: '要启用或禁用此功能,您需要更改会计导入设置。', }, + recruitingWarningModal: {disconnectText: ({integration}: {integration: string}) => `若要停用招聘功能,请先将此工作区与 ${integration} 断开连接。`}, }, reports: { reportsCustomTitleExamples: '示例:', @@ -7084,6 +7103,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM accounting: { settings: '设置', title: '连接', + subtitle: '连接您的会计软件以自动同步。', qbo: 'QuickBooks Online', intuitEnterpriseSuite: 'Intuit Enterprise Suite', qbd: 'QuickBooks Desktop', @@ -7095,6 +7115,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', + talkYourOnboardingSpecialist: '与您的客户经理聊天。', + talkYourAccountManager: '与您的客户经理聊天。', + talkToConcierge: '与 Concierge 聊天。', + needAnotherAccounting: '需要其他会计软件吗?', connectionName: (connectionName: AllConnectionName) => { switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -7134,6 +7158,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM disconnect: '断开连接', reinstall: '重新安装连接器', disconnectTitle: (connectionName = '集成') => `断开连接 ${connectionName}`, + connectTitle: (connectionName) => `连接 ${connectionName}`, syncError: (connectionName) => `无法连接到 ${connectionName}`, accounts: '会计科目表', taxes: '税费', @@ -7150,10 +7175,12 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM [CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: 'NetSuite 员工默认值', }, disconnectPrompt: (connectionName = '此集成') => `你确定要断开与 ${connectionName} 的连接吗?`, + connectPrompt: (connectionName) => `确定要连接 ${connectionName} 吗?这将删除所有现有的会计连接。`, enterCredentials: '请输入您的凭证', reconnect: '重新连接', updateCredentials: '更新凭证', claimOffer: { + badgeText: '优惠可用!', xero: { headline: '免费使用 Xero 长达 6 个月!', description: '第一次使用 Xero?Expensify 客户可享受 6 个月免费试用。请在下方领取优惠。', @@ -8547,7 +8574,9 @@ ${reportName}`, subscriptions: '订阅', }, hr: { + title: '人力资源', connectionsSubtitle: '连接您的 HR 系统以同步员工数据,自动将报销匹配到正确的员工,并在无需手动操作的情况下保持团队报销的准确性。', + subtitle: '连接人力资源工具,保持员工审批同步。', alreadyConnectedTitle: '无法连接到多个人力资源平台', alreadyConnectedPrompt: '在连接其他人力资源平台之前,您必须先断开当前的人力资源平台。', connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`, @@ -8608,12 +8637,14 @@ ${reportName}`, }, }, recruiting: { + title: '招聘', connectionsSubtitle: '连接招聘系统以同步候选人数据,自动将报销匹配到正确的人员,并在无需手动操作的情况下保持团队报销数据的准确。', alreadyConnectedTitle: '无法连接到多个 ATS 平台', alreadyConnectedPrompt: '在连接其他 ATS 之前,必须先断开当前的 ATS。', syncing: '正在同步候选人', setupIncomplete: (setupLink: string | undefined) => `已连接。${setupLink ? `完成设置` : '完成设置'} 个可导入候选项。`, + dontSeeYourATS: `没有看到你的 ATS?询问 Concierge,我们可以帮你添加。`, importSettings: '导入设置', defaultApprover: '默认审批人', approverFields: {recruiter: '招聘人员', recruitingCoordinator: '招聘协调员'}, @@ -8629,6 +8660,7 @@ ${reportName}`, offices: {title: '办公室', description: '选择你想与此工作区同步的候选人办公地点', toggleTitle: '办公室', allSelected: '所有办公室'}, enableJobStagesOrTags: '请选择至少一个工作阶段或标签以继续', }, + subtitle: '连接招聘工具并保持候选人审批同步。', syncResults: { importedCount: () => ({ one: '1 名候选人', @@ -8643,6 +8675,9 @@ ${reportName}`, approvalModeDescriptions: {basic: '选择单个审批人', advanced: `候选人的招聘人员或协调人将成为其报销审批人`, custom: '在 Expensify 中手动设置审批人'}, }, merge: { + connections: '连接', + connect: '连接', + findIntegration: '查找集成', syncNow: '立即同步', disconnect: '断开连接', disconnectTitle: (providerName: string) => `断开 ${providerName}`, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index e9370ef3ec07..c15230f8ae1d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -441,7 +441,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/Contacts/SetDefaultContactMethodConfirmValidateCodePage').default, [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: () => require('../../../../pages/settings/Preferences/PriorityModePage').default, - [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default, [SCREENS.WORKSPACE.CONNECTIONS_ACCOUNTING]: () => require('../../../../pages/workspace/connections/ConnectionsAccountingPage').default, [SCREENS.WORKSPACE.CONNECTIONS_HR]: () => require('../../../../pages/workspace/connections/ConnectionsHRPage').default, [SCREENS.WORKSPACE.CONNECTIONS_RECRUITING]: () => require('../../../../pages/workspace/connections/ConnectionsRecruitingPage').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 3abb5b8b1b27..c6acf30a7184 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -25,9 +25,9 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default, [SCREENS.WORKSPACE.AGENTS]: () => require('../../../../pages/workspace/WorkspaceAgentsPage').default, [SCREENS.WORKSPACE.ROOMS]: () => require('../../../../pages/workspace/rooms/WorkspaceRoomsPage').default, - [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, - [SCREENS.WORKSPACE.HR]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, - [SCREENS.WORKSPACE.RECRUITING]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default, + [SCREENS.WORKSPACE.HR]: () => require('../../../../pages/workspace/hr/WorkspaceHRPage').default, + [SCREENS.WORKSPACE.RECRUITING]: () => require('../../../../pages/workspace/recruiting/WorkspaceRecruitingPage').default, [SCREENS.WORKSPACE.CONNECTIONS]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default, [SCREENS.WORKSPACE.VENDORS]: () => require('../../../../pages/workspace/vendors/WorkspaceVendorsPage').default, @@ -38,8 +38,8 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default, [SCREENS.WORKSPACE.PER_DIEM]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemPage').default, - [SCREENS.WORKSPACE.MCP]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, - [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: () => require('../../../../pages/workspace/connections/WorkspaceConnectionsPage').default, + [SCREENS.WORKSPACE.MCP]: () => require('../../../../pages/workspace/mcp/WorkspaceMCPPage').default, + [SCREENS.WORKSPACE.RECEIPT_PARTNERS]: () => require('../../../../pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../pages/workspace/travel/PolicyTravelPage').default, [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPageRevamp').default, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 275658de61e3..3774fd074252 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1078,7 +1078,6 @@ type WorkspaceSplitNavigatorParamList = { integrationToDisconnect?: ConnectionName; shouldDisconnectIntegrationBeforeConnecting?: boolean; isIntuitEnterpriseSuite?: string; - tab?: ValueOf; }; [SCREENS.WORKSPACE.DYNAMIC_RECEIPT_PARTNERS_INVITE]: { policyID: string; diff --git a/src/libs/PolicyUtils/features.ts b/src/libs/PolicyUtils/features.ts index aff902544993..ef57eeeaa58c 100644 --- a/src/libs/PolicyUtils/features.ts +++ b/src/libs/PolicyUtils/features.ts @@ -103,4 +103,4 @@ function getDefaultTimeTrackingRate(policy: Partial>): number return policy?.units?.time?.rate !== undefined ? convertToBackendAmount(policy.units.time.rate) : undefined; } -export {isAttendeeTrackingEnabled, arePolicyRulesEnabled, isInvoiceFieldsEnabled, isPolicyFeatureEnabled, isTimeTrackingEnabled, getDefaultTimeTrackingRate}; +export {isAttendeeTrackingEnabled, arePolicyRulesEnabled, isInvoiceFieldsEnabled, isPolicyFeatureEnabled, isTimeTrackingEnabled, isMCPEnabled, getDefaultTimeTrackingRate}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 74a76d287821..78befcadc834 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -5357,9 +5357,7 @@ function getIntegrationSyncFailedMessage(translate: LocalizedTranslate, action: }; const param = encodeURIComponent(`{"policyID": "${policyID}"}`); - const workspaceAccountingLink = shouldShowOldDotLink - ? `${oldDotEnvironmentURL}/policy?param=${param}#connections` - : `${environmentURL}/${ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)}`; + const workspaceAccountingLink = shouldShowOldDotLink ? `${oldDotEnvironmentURL}/policy?param=${param}#connections` : `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`; let message = translate('report.actions.type.integrationSyncFailed', label, errorMessage, workspaceAccountingLink); if (recurrenceCount && recurrenceCount > 1) { message += ` ${translate('report.actions.type.integrationSyncFailedRecurrence', {count: recurrenceCount})}`; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8e6569efeda6..834be88b6d9e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13339,7 +13339,7 @@ function prepareOnboardingOnyxData({ workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID)}`, workspaceConfirmationLink: `${environmentURL}/${createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_CONFIRMATION.path, ROUTES.WORKSPACES_LIST.route)}`, testDriveURL: `${environmentURL}/${testDriveURL}`, - workspaceAccountingLink: `${environmentURL}/${ROUTES.WORKSPACE_CONNECTIONS.getRoute(onboardingPolicyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)}`, + workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID)}`, corporateCardLink: `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(onboardingPolicyID)}`, companyDomain: companyDomain ?? '', workEmail: workEmail ?? '', diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index e478c13113ab..5122c8169bf8 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -56,13 +56,13 @@ function getApprovalWorkflowSource(policy: OnyxEntry, policyID: string | if (isAnyHRConnected(policy)) { return { providerName: getConnectedHRProvider(policy)?.displayName ?? '', - settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), + settingsRoute: ROUTES.WORKSPACE_HR.getRoute(policyID), }; } if (isAnyRecruitingReadOnlyWorkflowMode(policy)) { return { providerName: getConnectedATSProvider(policy)?.displayName ?? '', - settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_RECRUITING.getRoute(policyID), + settingsRoute: ROUTES.WORKSPACE_RECRUITING.getRoute(policyID), }; } return undefined; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 21fc39406455..06b1cc7a8edf 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -8245,6 +8245,8 @@ export { enablePolicyConnections, enablePolicyHR, enablePolicyRecruiting, + enablePolicyMCP, + enablePolicyReceiptPartners, enablePolicyFeatureForConnection, enablePolicyReportFields, enablePolicyInvoiceFields, diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index 05aee8570f70..145463f91914 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -296,7 +296,7 @@ function useGettingStartedItems(): UseGettingStartedItemsResult { label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'), subText: translate('homePage.gettingStartedSection.connectAccountingSubText'), isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate), - route: ROUTES.WORKSPACE_CONNECTIONS.getRoute(activePolicyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING), + route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID), }); } else if (policy.areCategoriesEnabled) { items.push({ diff --git a/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx b/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx index 532970c50366..6d983be067ef 100644 --- a/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx +++ b/src/pages/home/TimeSensitiveSection/items/FixPolicyConnection.tsx @@ -34,7 +34,7 @@ function FixPolicyConnection({connectionName, policyID, policyName, integrationN : translate('homePage.timeSensitiveSection.fixPolicyConnection.defaultSubtitle'); const isHRConnection = (CONST.POLICY.CONNECTIONS.HR_CONNECTION_NAMES as readonly PolicyConnectionName[]).includes(connectionName); - const fixRoute = isHRConnection ? ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID) : ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); + const fixRoute = isHRConnection ? ROUTES.WORKSPACE_HR.getRoute(policyID) : ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID); return ( ({ ...item, diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index f3040855c8ca..1b664bfa8ad5 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -501,7 +501,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers {translate('workspace.people.workspaceMembersCount', memberCount)} {shouldShowHRSyncLink && '. '} {shouldShowHRSyncLink && ( - Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID))}> + Navigation.navigate(ROUTES.WORKSPACE_HR.getRoute(policyID))}> {translate('workspace.people.configureHRSync', connectedHRProvider?.displayName ?? '')} )} diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 827ff86a500f..28079f9de2bb 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -8,6 +8,8 @@ import useCardFeeds from '@hooks/useCardFeeds'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDefaultFundID from '@hooks/useDefaultFundID'; +import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -23,6 +25,10 @@ import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {enablePolicyTravel} from '@libs/actions/Policy/Travel'; import {filterInactiveCards, getAllCardsForWorkspace, getCardSettings, getCompanyFeeds, isSmartLimitEnabled as isSmartLimitEnabledUtil} from '@libs/CardUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; +import {getConnectedHRProvider, isAnyHRConnected, isGustoConnected, isZenefitsConnected} from '@libs/merge/HRUtils'; +import {isMergeConnected} from '@libs/merge/MergeUtils'; +import {getConnectedATSProvider, isAnyRecruitingConnected} from '@libs/merge/RecruitingUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; @@ -34,9 +40,12 @@ import { getDistanceRateCustomUnit, getPerDiemCustomUnit, hasAccountingConnections, + hasAccountingFeatureConnection, hasVendorFeature, isControlPolicy, + isMCPEnabled, isPerDiemEnabled, + isPolicyFeatureEnabled, isPolicyTaxEnabled, isTimeTrackingEnabled, tryNavigateToSubmitWorkspaceUpgrade, @@ -44,6 +53,7 @@ import { import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -51,9 +61,15 @@ import {enablePolicyCategories} from '@userActions/Policy/Category'; import {enablePolicyDistanceRates} from '@userActions/Policy/DistanceRate'; import {enablePerDiem} from '@userActions/Policy/PerDiem'; import { + clearPolicyErrorField, enableCompanyCards, enableExpensifyCard, + enablePolicyConnections, + enablePolicyHR, enablePolicyInvoicing, + enablePolicyRecruiting, + enablePolicyMCP, + enablePolicyReceiptPartners, enablePolicyRules, enablePolicyTaxes, enablePolicyTimeTracking, @@ -82,13 +98,17 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate} = useLocalize(); - const {isBetaEnabledOrUnknown} = usePermissions(); + const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions(); // Undefined until the betas load. The action calls below need that distinction, the UI gating just treats it as off const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {showConfirmModal} = useConfirmModal(); + const isRecruitingBetaEnabled = isBetaEnabled(CONST.BETAS.MERGE_ATS); + // Connections replaces this section's pages, and turns each feature on as its first integration connects + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const illustrations = useMemoizedLazyIllustrations([ 'FolderOpen', + 'Accounting', 'CompanyCard', 'Workflows', 'InvoiceBlue', @@ -101,15 +121,21 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro 'Luggage', 'Car', 'Gears', + 'ReceiptPartners', 'Clock', + 'Members', + 'AiAutomation', + 'NewUser', ]); const policyID = policy?.id; const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasAccountingConnection = hasAccountingConnections(policy); + const isAccountingEnabled = !!policy?.areConnectionsEnabled || hasAccountingFeatureConnection(policy); const perDiemCustomUnit = getPerDiemCustomUnit(policy); const distanceRateCustomUnit = getDistanceRateCustomUnit(policy); + const isUberConnected = useIsPolicyConnectedToUberReceiptPartner({policyID}); const [cardFeeds] = useCardFeeds(policyID); const policyData = usePolicyData(policyID); const {categories: policyCategories} = policyData; @@ -193,7 +219,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)); + Navigation.navigate(getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); }; const warnVendorsManagedByAccounting = async () => { @@ -205,6 +231,66 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }); }; + const warnDisconnectAccountingFirst = async () => { + if (!hasAccountingConnection || !policyID) { + return; + } + const {action} = await showConfirmModal({ + title: translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledTitle'), + prompt: translate('workspace.moreFeatures.connectionsWarningModal.disconnectText'), + confirmText: translate('workspace.moreFeatures.connectionsWarningModal.manageSettings'), + cancelText: translate('common.cancel'), + }); + if (action !== ModalActions.CONFIRM) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + }; + + const warnReceiptPartnersStillConnected = async () => { + if (!isUberConnected) { + return; + } + await showConfirmModal({ + title: translate('workspace.moreFeatures.receiptPartnersWarningModal.featureEnabledTitle'), + prompt: translate('workspace.moreFeatures.receiptPartnersWarningModal.disconnectText'), + confirmText: translate('workspace.moreFeatures.receiptPartnersWarningModal.confirmText'), + shouldShowCancelButton: false, + }); + }; + + const warnDisconnectHRFirst = async () => { + if (!isAnyHRConnected(policy)) { + return; + } + let integration = ''; + if (isZenefitsConnected(policy)) { + integration = translate('workspace.hr.zenefits.title'); + } else if (isGustoConnected(policy)) { + integration = translate('workspace.hr.gusto.title'); + } else if (isMergeConnected(policy, CONST.POLICY.CONNECTIONS.NAME.MERGE_HR)) { + integration = getConnectedHRProvider(policy)?.displayName ?? ''; + } + await showConfirmModal({ + title: translate('workspace.distanceRates.oopsNotSoFast'), + prompt: translate('workspace.moreFeatures.hrWarningModal.disconnectText', {integration}), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + }; + + const warnDisconnectRecruitingFirst = async () => { + if (!isAnyRecruitingConnected(policy)) { + return; + } + await showConfirmModal({ + title: translate('workspace.distanceRates.oopsNotSoFast'), + prompt: translate('workspace.moreFeatures.recruitingWarningModal.disconnectText', {integration: getConnectedATSProvider(policy)?.displayName ?? ''}), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + }; + const promptDisableExpensifyCardViaConcierge = async () => { const {action} = await showConfirmModal({ title: translate('workspace.moreFeatures.expensifyCard.disableCardTitle'), @@ -298,6 +384,156 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro {translate('workspace.moreFeatures.subtitle')} + {!isUnifiedConnectionsBetaEnabled && ( + + { + if (!policyID) { + return; + } + if (tryNavigateToSubmitWorkspaceUpgrade(policy, isEnabled, CONST.UPGRADE_FEATURE_INTRO_MAPPING.accounting.alias)) { + return; + } + enablePolicyConnections(policyID, isEnabled); + }} + errors={getLatestErrorField(policy ?? {}, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED)} + onCloseError={() => { + if (!policyID) { + return; + } + clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + }} + onPress={() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + }} + /> + { + if (!policyID) { + return; + } + if (isEnabled && !isControlPolicy(policy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.hr.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)), + ); + return; + } + enablePolicyHR(policyID, isEnabled); + }} + onPress={() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_HR.getRoute(policyID)); + }} + /> + { + if (!policyID) { + return; + } + enablePolicyReceiptPartners(policyID, isEnabled); + }} + errors={getLatestErrorField(policy ?? {}, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED)} + onCloseError={() => { + if (!policyID) { + return; + } + clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); + }} + onPress={() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policyID)); + }} + /> + {isRecruitingBetaEnabled && ( + { + if (!policyID) { + return; + } + if (isEnabled && !isControlPolicy(policy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute( + policyID, + CONST.UPGRADE_FEATURE_INTRO_MAPPING.recruiting.alias, + ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID), + ), + ); + return; + } + enablePolicyRecruiting(policyID, isEnabled); + }} + onPress={() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_RECRUITING.getRoute(policyID)); + }} + /> + )} + { + if (!policyID) { + return; + } + enablePolicyMCP(policyID, isEnabled); + }} + onPress={() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_MCP.getRoute(policyID)); + }} + /> + + )} + (); const {translate} = useLocalize(); const {showConfirmModal, closeModalByID} = useConfirmModal(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const policyID = policy?.id; // `removePolicyConnection` only runs once the user confirms, which can be a while after the flow started, so the @@ -120,7 +122,14 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider return; } - if (tryNavigateToSubmitWorkspaceUpgrade(policy, true, CONST.UPGRADE_FEATURE_INTRO_MAPPING.accounting.alias, ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))) { + if ( + tryNavigateToSubmitWorkspaceUpgrade( + policy, + true, + CONST.UPGRADE_FEATURE_INTRO_MAPPING.accounting.alias, + isUnifiedConnectionsBetaEnabled ? ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID) : undefined, + ) + ) { return; } @@ -143,11 +152,14 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider cardFeeds, cardList: cardLists, isIntuitEnterpriseSuiteOverride: newActiveIntegration.isIntuitEnterpriseSuite, + isUnifiedConnectionsBetaEnabled, }, ); // Connections has no feature toggle, so turn it on here. The Control upgrade only enables the integration's own feature. - enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + if (isUnifiedConnectionsBetaEnabled) { + enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + } const workspaceUpgradeNavigationDetails = accountingIntegrationData?.workspaceUpgradeNavigationDetails; if (workspaceUpgradeNavigationDetails && !isControlPolicy(policy)) { @@ -168,17 +180,32 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider // Mirrors `shouldShowConfirmationModal` below, which keeps `renderActiveIntegration()` from mounting the // setup flow until the user has decided what to do with the connection that has to be disconnected first. + const connectionName = newActiveIntegration.isIntuitEnterpriseSuite + ? translate('workspace.accounting.intuitEnterpriseSuite') + : (CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[newActiveIntegration.name] ?? newActiveIntegration.name); + const modalCopy = isUnifiedConnectionsBetaEnabled + ? { + title: translate('workspace.connections.replaceConnectionTitle'), + prompt: translate('workspace.connections.replaceConnectionPrompt', getAccountingIntegrationDisplayName(policy, integrationToDisconnect, translate)), + confirmText: translate('common.replace'), + buttonVariant: CONST.BUTTON_VARIANT.DANGER, + } + : { + title: translate('workspace.accounting.connectTitle', connectionName), + prompt: translate('workspace.accounting.connectPrompt', connectionName), + confirmText: translate('workspace.accounting.setup'), + buttonVariant: CONST.BUTTON_VARIANT.SUCCESS, + }; + isDisconnectConfirmationPendingRef.current = true; showConfirmModal({ // `startIntegrationFlow` can run more than once for the same flow (the `useFocusEffect` in - // `WorkspaceConnectionsPage` re-fires whenever `startIntegrationFlow` is re-created). A stable id keeps the - // repeat call updating this prompt in place instead of stacking a second copy behind it. + // `PolicyAccountingPage` and `WorkspaceConnectionsPage` re-fires whenever `startIntegrationFlow` is + // re-created). A stable id keeps the repeat call updating this prompt in place instead of stacking a + // second copy behind it. id: ACCOUNTING_CONNECTION_CONFIRMATION_MODAL_ID, - title: translate('workspace.connections.replaceConnectionTitle'), - prompt: translate('workspace.connections.replaceConnectionPrompt', getAccountingIntegrationDisplayName(policy, integrationToDisconnect, translate)), - confirmText: translate('common.replace'), - buttonVariant: CONST.BUTTON_VARIANT.DANGER, + ...modalCopy, cancelText: translate('common.cancel'), }).then((result) => { // A repeat call for the same id is handed back the promise the first call got, so every call's handler @@ -221,6 +248,7 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider accountingIcons, cardFeeds, cardLists, + isUnifiedConnectionsBetaEnabled, ], ); @@ -263,6 +291,7 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider cardFeeds, cardList: cardLists, isIntuitEnterpriseSuiteOverride: activeIntegration.isIntuitEnterpriseSuite, + isUnifiedConnectionsBetaEnabled, }, )?.setupConnectionFlow; }; diff --git a/src/pages/workspace/accounting/ClaimOfferPage.tsx b/src/pages/workspace/accounting/ClaimOfferPage.tsx index 3c8e2a8296e4..624d931c4e0b 100644 --- a/src/pages/workspace/accounting/ClaimOfferPage.tsx +++ b/src/pages/workspace/accounting/ClaimOfferPage.tsx @@ -9,6 +9,7 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -30,6 +31,7 @@ import {enablePolicyFeatureForConnection} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; @@ -45,6 +47,7 @@ type IntegrationConfig = { claimOfferLink?: string; connectButtonText: string; connectionName: string; + featureName: PolicyFeatureName; onConnect: () => void; }; @@ -58,6 +61,7 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { const expensifyIcons = useMemoizedLazyExpensifyIcons(['TreasureChestGreenWithSparkle']); const integrations = policy?.receiptPartners; const {isUberConnected} = useGetReceiptPartnersIntegrationData(policyID); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); const accountingIntegrations = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES; @@ -82,6 +86,7 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { claimOfferLink: CONST.XERO_PARTNER_LINK, connectButtonText: translate('workspace.accounting.claimOffer.xero.connectButton'), connectionName: CONST.POLICY.CONNECTIONS.NAME.XERO, + featureName: CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED, onConnect: () => { startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.XERO}); }, @@ -92,9 +97,12 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { descriptionHtml: translate('workspace.accounting.claimOffer.uber.description'), connectButtonText: translate('workspace.accounting.claimOffer.uber.connectButton'), connectionName: CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER, + featureName: CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED, onConnect: () => { // Connections offers Uber before Receipt partners is on, so the feature is turned on as Uber connects - enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); + if (isUnifiedConnectionsBetaEnabled) { + enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED); + } openExternalLink(`${CONST.UBER_CONNECT_URL}?${integrations?.uber?.connectFormData}`); }, }, @@ -144,8 +152,9 @@ function ClaimOfferPage({route, policy}: ClaimOfferPageProps) { diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx new file mode 100644 index 000000000000..76519f4b39af --- /dev/null +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -0,0 +1,432 @@ +import ActivityIndicator from '@components/ActivityIndicator'; +import Button from '@components/Button'; +import ButtonDisabledWhenOffline from '@components/Button/composed/ButtonDisabledWhenOffline'; +import CollapsibleSection from '@components/CollapsibleSection'; +import FormHelpMessage from '@components/FormHelpMessage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import MenuItem from '@components/MenuItem'; +import MenuItemList from '@components/MenuItemList'; +import type {MenuItemWithLink} from '@components/MenuItemList'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; + +import getPlatform from '@libs/getPlatform'; +import {hasAccountingConnections, isControlPolicy} from '@libs/PolicyUtils'; + +import Navigation from '@navigation/Navigation'; + +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; + +import {openOldDotLink} from '@userActions/Link'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {ConnectionName} from '@src/types/onyx/Policy'; + +import {useFocusEffect, useRoute} from '@react-navigation/native'; +import React, {useCallback, useMemo, useRef} from 'react'; +import {View} from 'react-native'; + +import type {MenuItemData, PolicyAccountingPageProps} from './types'; + +import {AccountingContextProvider, useAccountingActions, useAccountingState} from './AccountingContext'; +import useConnectedAccountingIntegration from './useConnectedAccountingIntegration'; + +type RouteParams = { + newConnectionName?: ConnectionName; + integrationToDisconnect?: ConnectionName; + shouldDisconnectIntegrationBeforeConnecting?: boolean; + isIntuitEnterpriseSuite?: string; +}; + +function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.accounting'); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {popoverAnchorRefs} = useAccountingState(); + const {startIntegrationFlow} = useAccountingActions(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const {isLargeScreenWidth} = useResponsiveLayout(); + const route = useRoute(); + const params = route.params as RouteParams | undefined; + const newConnectionName = params?.newConnectionName; + const integrationToDisconnect = params?.integrationToDisconnect; + const shouldDisconnectIntegrationBeforeConnecting = params?.shouldDisconnectIntegrationBeforeConnecting; + const shouldConnectToIntuitEnterpriseSuite = params?.isIntuitEnterpriseSuite === 'true'; + const policyID = policy?.id; + const icons = useMemoizedLazyExpensifyIcons(['QuestionMark']); + const { + accountingIntegrations, + connectedIntegration, + isConnectedToIntuitEnterpriseSuite, + isSyncInProgress, + hasAccountingConnection, + hasSyncError, + hasUnsupportedNDIntegration, + synchronizationError, + canWriteAccounting, + showReadOnlyModal, + overflowMenu, + connectionDetails, + qboTokenExpiryHint, + oldDotPolicyConnectionsURL, + getAvailableIntegrationData, + } = useConnectedAccountingIntegration(policy, {menuItemWrapperStyle: [styles.sectionMenuItemTopDescription]}); + const accountingIntegrationOptions = accountingIntegrations.flatMap((name) => [ + {name, isIntuitEnterpriseSuite: name === CONST.POLICY.CONNECTIONS.NAME.QBO ? false : undefined}, + ...(name === CONST.POLICY.CONNECTIONS.NAME.QBO ? [{name, isIntuitEnterpriseSuite: true}] : []), + ]); + const shouldShowSynchronizationError = !!synchronizationError; + + // `startIntegrationFlow` changes identity whenever `policy` does, which re-runs this effect. The + // Navigation.setParams below clears newConnectionName through a navigation state update that lands in a later + // render, so that re-run can still see the param set. Key the guard on the value to start the flow only once. + const startedIntegrationFlowForRef = useRef(undefined); + + useFocusEffect( + useCallback(() => { + if (!newConnectionName || !isControlPolicy(policy) || !canWriteAccounting) { + // Re-arm the guard once the param is gone, so a later round-trip that asks for the same integration + // again is not mistaken for the re-run this guard exists to swallow. + if (!newConnectionName) { + startedIntegrationFlowForRef.current = undefined; + } + return; + } + + if (startedIntegrationFlowForRef.current === newConnectionName) { + return; + } + startedIntegrationFlowForRef.current = newConnectionName; + + startIntegrationFlow({ + name: newConnectionName, + isIntuitEnterpriseSuite: shouldConnectToIntuitEnterpriseSuite, + integrationToDisconnect, + shouldDisconnectIntegrationBeforeConnecting, + }); + Navigation.setParams({ + newConnectionName: undefined, + isIntuitEnterpriseSuite: undefined, + integrationToDisconnect: undefined, + shouldDisconnectIntegrationBeforeConnecting: undefined, + }); + }, [newConnectionName, shouldConnectToIntuitEnterpriseSuite, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting, policy, startIntegrationFlow, canWriteAccounting]), + ); + + const getConnectionsMenuItems = (): MenuItemData[] => { + if (!hasAccountingConnection && !isSyncInProgress && policyID) { + return accountingIntegrationOptions + .map(({name: integration, isIntuitEnterpriseSuite}) => { + const integrationData = getAvailableIntegrationData(integration, isIntuitEnterpriseSuite); + if (!integrationData) { + return undefined; + } + + const isXero = integration === CONST.POLICY.CONNECTIONS.NAME.XERO; + const iconProps = integrationData?.icon + ? { + icon: integrationData.icon, + iconType: CONST.ICON_TYPE_AVATAR, + } + : {}; + + return { + ...iconProps, + interactive: false, + // On native iOS, `accessible={true}` collapses the row and all its descendants into a single accessibility element, + // so VoiceOver focuses the whole row instead of the nested Connect button. Disabling it only on native iOS lets + // VoiceOver focus/activate the button on its own. Other platforms (Android/TalkBack, web, iOS mWeb→WEB) keep grouping. + shouldBeAccessible: getPlatform() !== CONST.PLATFORM.IOS, + wrapperStyle: [styles.sectionMenuItemTopDescription], + shouldShowRightComponent: true, + title: integrationData?.title, + badgeText: isXero ? translate('workspace.accounting.claimOffer.badgeText') : undefined, + onBadgePress: + isXero && canWriteAccounting + ? () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_CLAIM_OFFER.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO)); + } + : undefined, + badgeStyle: styles.mr3, + isBadgeSuccess: isXero, + shouldShowBadgeBelow: shouldUseNarrowLayout, + rightComponent: ( + { + if (!canWriteAccounting) { + showReadOnlyModal(); + return; + } + startIntegrationFlow({name: integration, isIntuitEnterpriseSuite}); + }} + style={styles.justifyContentCenter} + innerStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} + hoverStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} + size={CONST.BUTTON_SIZE.SMALL} + ref={(ref) => { + if (!popoverAnchorRefs?.current) { + return; + } + const integrationKey = isIntuitEnterpriseSuite ? CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES.INTUIT_ENTERPRISE_SUITE : integration; + popoverAnchorRefs.current[integrationKey].current = ref; + }} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.ACCOUNTING.SETUP_BUTTON} + > + {translate('workspace.accounting.setup')} + + ), + }; + }) + .filter(Boolean) as MenuItemData[]; + } + + if (!connectionDetails) { + return []; + } + const iconProps = connectionDetails.icon ? {icon: connectionDetails.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; + + let rightComponent; + if (isSyncInProgress) { + rightComponent = ; + } else if (canWriteAccounting) { + rightComponent = ( + + ); + } + + return [ + { + ...iconProps, + interactive: false, + wrapperStyle: [styles.sectionMenuItemTopDescription, shouldShowSynchronizationError && styles.pb0], + shouldShowRightComponent: true, + title: connectionDetails.title, + errorText: synchronizationError, + errorTextStyle: [styles.mt5], + shouldShowRedDotIndicator: true, + description: connectionDetails.connectionMessage, + hintText: qboTokenExpiryHint, + rightComponent, + }, + ...connectionDetails.settingsMenuItems, + ]; + }; + const connectionsMenuItems = getConnectionsMenuItems(); + + const getOtherIntegrationsItems = () => { + if ((!hasAccountingConnection && !isSyncInProgress) || !policyID) { + return; + } + const otherIntegrations = accountingIntegrationOptions.filter( + ({name, isIntuitEnterpriseSuite}) => name !== connectedIntegration || !!isIntuitEnterpriseSuite !== isConnectedToIntuitEnterpriseSuite, + ); + return otherIntegrations + .map(({name: integration, isIntuitEnterpriseSuite}) => { + const integrationData = getAvailableIntegrationData(integration, isIntuitEnterpriseSuite); + if (!integrationData) { + return undefined; + } + + const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; + + return { + ...iconProps, + title: integrationData?.title, + rightComponent: ( + { + if (!canWriteAccounting) { + showReadOnlyModal(); + return; + } + startIntegrationFlow({ + name: integration, + isIntuitEnterpriseSuite, + integrationToDisconnect: connectedIntegration, + shouldDisconnectIntegrationBeforeConnecting: true, + }); + }} + style={styles.justifyContentCenter} + innerStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} + hoverStyles={!canWriteAccounting ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined} + size={CONST.BUTTON_SIZE.SMALL} + ref={(r) => { + if (!popoverAnchorRefs?.current) { + return; + } + const integrationKey = isIntuitEnterpriseSuite ? CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES.INTUIT_ENTERPRISE_SUITE : integration; + popoverAnchorRefs.current[integrationKey].current = r; + }} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.ACCOUNTING.SETUP_BUTTON} + > + {translate('workspace.accounting.setup')} + + ), + interactive: false, + // On native iOS, `accessible={true}` collapses the row and all its descendants into a single accessibility element, + // so VoiceOver focuses the whole row instead of the nested Connect button. Disabling it only on native iOS lets + // VoiceOver focus/activate the button on its own. Other platforms (Android/TalkBack, web, iOS mWeb→WEB) keep grouping. + shouldBeAccessible: getPlatform() !== CONST.PLATFORM.IOS, + shouldShowRightComponent: true, + wrapperStyle: styles.sectionMenuItemTopDescription, + }; + }) + .filter(Boolean) as MenuItemWithLink[]; + }; + const otherIntegrationsItems = getOtherIntegrationsItems(); + + const [chatTextLink, chatReportID] = useMemo(() => { + // If they have an onboarding specialist assigned display the following and link to the #admins room with the account executive. + if (policy?.chatReportIDAdmins) { + return [translate('workspace.accounting.talkYourOnboardingSpecialist'), policy?.chatReportIDAdmins?.toString()]; + } + + // If not, if they have an account manager assigned display the following and link to the DM with their account manager. + if (account?.accountManagerAccountID) { + return [translate('workspace.accounting.talkYourAccountManager'), account?.accountManagerReportID]; + } + // Else, display the following and link to their Concierge DM. + return [translate('workspace.accounting.talkToConcierge'), conciergeReportID]; + }, [account?.accountManagerAccountID, account?.accountManagerReportID, conciergeReportID, policy?.chatReportIDAdmins, translate]); + + return ( + + + + + +
+ {!hasUnsupportedNDIntegration && + connectionsMenuItems.map((menuItem) => ( + + + + ))} + {hasUnsupportedNDIntegration && hasSyncError && !!policyID && ( + + )} + {hasUnsupportedNDIntegration && !hasSyncError && !!policyID && ( + + + { + // Go to Expensify Classic. + openOldDotLink(CONST.OLDDOT_URLS.POLICY_CONNECTIONS_URL(policyID)); + }} + > + {translate('workspace.accounting.goToODToSettings')} + + + + )} + {!!otherIntegrationsItems && ( + + + + )} + {!!account?.guideDetails?.email && !hasAccountingConnections(policy) && canWriteAccounting && ( + + + + {translate('workspace.accounting.needAnotherAccounting')} + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(chatReportID)))}>{chatTextLink} + + + )} +
+
+
+
+
+ ); +} + +function PolicyAccountingPageWrapper(props: PolicyAccountingPageProps) { + return ( + + + + ); +} + +export default withUnifiedConnectionsBeta(withPolicyConnections(PolicyAccountingPageWrapper)); diff --git a/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx b/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx index 65c1a9acaa65..da34005bf9be 100644 --- a/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/advanced/BusinessCentralAdvancedPage.tsx @@ -6,6 +6,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,6 +23,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -39,6 +41,7 @@ function BusinessCentralAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const bankAccounts = policy?.connections?.businessCentral?.data?.bankAccounts; const autoSync = businessCentralConfig?.autoSync?.enabled ?? false; @@ -61,7 +64,7 @@ function BusinessCentralAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="BusinessCentralAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx b/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx index 5283e418c676..aea10b2bdf2d 100644 --- a/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/export/BusinessCentralExportPage.tsx @@ -4,12 +4,14 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; @@ -33,6 +35,7 @@ function BusinessCentralExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const businessCentralData = policy?.connections?.businessCentral?.data; const exportConfig = businessCentralConfig?.export; @@ -116,7 +119,7 @@ function BusinessCentralExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="BusinessCentralExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx b/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx index ece5b46ce2bf..423b0ae41ebb 100644 --- a/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx +++ b/src/pages/workspace/accounting/businessCentral/import/BusinessCentralImportPage.tsx @@ -6,6 +6,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -21,12 +22,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type {ValueOf} from 'type-fest'; @@ -77,6 +78,7 @@ function BusinessCentralImportPage({policy}: WithPolicyConnectionsProps) { const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const businessCentralConfig = policy?.connections?.businessCentral?.config; const businessCentralData = policy?.connections?.businessCentral?.data; @@ -106,7 +108,7 @@ function BusinessCentralImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="BusinessCentralImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx b/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx index fe7cc3a22e38..cd6f91e9afef 100644 --- a/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx +++ b/src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx @@ -5,6 +5,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -23,6 +24,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -38,6 +40,7 @@ function CampfireAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const campfireConfig = policy?.connections?.campfire?.config; const campfireData = policy?.connections?.campfire?.data; const autoSync = campfireConfig?.autoSync?.enabled ?? false; @@ -66,7 +69,7 @@ function CampfireAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CampfireAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx b/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx index bddd2ab6f5ca..1a6ad429d275 100644 --- a/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx +++ b/src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx @@ -9,6 +9,7 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,6 +19,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -32,6 +34,7 @@ function CampfireExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -57,7 +60,7 @@ function CampfireExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CampfireExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx b/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx index 4da26996618c..e41ffb7b4c23 100644 --- a/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx +++ b/src/pages/workspace/accounting/campfire/import/CampfireImportPage.tsx @@ -1,6 +1,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,12 +10,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -23,6 +24,7 @@ function CampfireImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const campfireConfig = policy?.connections?.campfire?.config; const campfireData = policy?.connections?.campfire?.data; const enableNewCategories = campfireConfig?.enableNewCategories ?? false; @@ -32,7 +34,7 @@ function CampfireImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CampfireImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx b/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx index 77738aa68da3..a2d6db80522f 100644 --- a/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx +++ b/src/pages/workspace/accounting/certinia/import/CertiniaImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,6 +13,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {CERTINIA_DIMENSION_PARAMS, getDimensionLabel, getDisplayTypeLabel, getParentTagMappingLabel} from '@pages/workspace/accounting/certinia/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -26,13 +28,14 @@ function CertiniaImportPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.CERTINIA]?.config; const coding = config?.coding; const hasPSA = !!config?.hasPSA; return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="CertiniaImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx b/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx index 7f8dc76a95cc..3fe2c35e460f 100644 --- a/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx +++ b/src/pages/workspace/accounting/dualentry/advanced/DualEntryAdvancedPage.tsx @@ -7,6 +7,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -25,6 +26,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -42,6 +44,7 @@ function DualEntryAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const dualentryConfig = policy?.connections?.dualEntry?.config; const dualentryData = policy?.connections?.dualEntry?.data; const autoSync = dualentryConfig?.autoSync?.enabled ?? false; @@ -72,7 +75,7 @@ function DualEntryAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="DualEntryAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx b/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx index 20264d597be4..79f3970f9014 100644 --- a/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx +++ b/src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx @@ -8,6 +8,7 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,6 +18,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -31,6 +33,7 @@ function DualEntryExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -56,7 +59,7 @@ function DualEntryExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="DualEntryExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx b/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx index 01118e10e038..f0c78422b749 100644 --- a/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx +++ b/src/pages/workspace/accounting/dualentry/import/DualEntryImportPage.tsx @@ -1,6 +1,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,12 +10,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -23,6 +24,7 @@ function DualEntryImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const dualentryConfig = policy?.connections?.dualEntry?.config; const dualentryData = policy?.connections?.dualEntry?.data; const enableNewCategories = dualentryConfig?.enableNewCategories ?? false; @@ -31,7 +33,7 @@ function DualEntryImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="DualEntryImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx index 16ff6f4931c4..d739c638074c 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -6,6 +6,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendi import createDynamicRoute from '@navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@navigation/Navigation'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -44,6 +46,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const styles = useThemeStyles(); const {importEmployees, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {}; @@ -108,7 +111,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { contentContainerStyle={styles.pb2} titleStyle={styles.ph5} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} > Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_AUTO_SYNC.path)))}> diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx index c3c2e3517b5e..3031867bfbfa 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx @@ -51,7 +51,7 @@ function SageIntacctDatePage({policy}: WithPolicyProps) { const backTo = route.params?.backTo; const goBack = useCallback(() => { - Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)))); + Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))); }, [policyID, backTo]); const headerContent = useMemo( diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx index 94b00de26fe4..6ee066b083bc 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx @@ -123,9 +123,7 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP headerTitle="workspace.accounting.exportCompanyCard" title="workspace.sageIntacct.nonReimbursableExpenses.description" onBackButtonPress={() => - Navigation.goBack( - backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))), - ) + Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))) } accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx index d45204a36d3c..3b643ad77ae9 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx @@ -47,7 +47,7 @@ function SageIntacctPreferredExporterPage({policy}: WithPolicyProps) { const backTo = route.params?.backTo; const goBack = useCallback(() => { - Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)))); + Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))); }, [policyID, backTo]); const data: CardListItem[] = useMemo(() => { diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx index c7de54405640..ffc52cf555f3 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -126,9 +126,7 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProp headerTitle="workspace.accounting.exportOutOfPocket" title="workspace.sageIntacct.reimbursableExpenses.description" onBackButtonPress={() => - Navigation.goBack( - backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))), - ) + Navigation.goBack(backTo ?? (policyID && createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.path, ROUTES.POLICY_ACCOUNTING.getRoute(policyID)))) } accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx index 7d783787fa60..4c99a0f0a93e 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +12,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -51,6 +53,7 @@ function SageIntacctImportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const sageIntacctConfig = policy?.connections?.intacct?.config; const sageIntacctData = policy?.connections?.intacct?.data; @@ -72,7 +75,7 @@ function SageIntacctImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="SageIntacctImportPage" headerTitle="workspace.accounting.import" headerSubtitle={getCurrentSageIntacctEntityName(policy, translate('workspace.common.topLevel'))} diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx index a22ef567e7f5..fa8312b3f339 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.native.tsx @@ -1,10 +1,12 @@ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; + import {getNetSuiteSetupLink} from '@libs/actions/connections/NetSuiteCommands'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AccountingSetupWebViewPage from '@pages/workspace/accounting/AccountingSetupWebViewPage'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -14,6 +16,7 @@ type NetSuiteSetupPageProps = PlatformStackScreenProps ); } diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx index d67c4ba8d595..2b30f9db5b6d 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteSetupPage/index.tsx @@ -18,7 +18,7 @@ function NetSuiteSetupPage({route}: NetSuiteSetupPageProps) { // only reached via a deep link, so just return to the workspace accounting page. We wait for the RHP open // transition to finish first, otherwise goBack fires mid-transition and is dropped. const handle = TransitionTracker.runAfterTransitions({ - callback: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), + callback: () => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)), waitForUpcomingTransition: true, }); return () => handle.cancel(); diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx index c76145a8236f..b2a7fc4ceffa 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/NetSuiteExistingConnectionsPage.tsx @@ -1,6 +1,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemList from '@components/MenuItemList'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useReusablePoliciesConnectedTo from '@hooks/useReusablePoliciesConnectedTo'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,8 +11,9 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; + import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -23,7 +25,8 @@ function NetSuiteExistingConnectionsPage({route}: ExistingConnectionsPageProps) const {translate, datetimeToRelative} = useLocalize(); const styles = useThemeStyles(); const policyID: string = route.params.policyID; - const accountingConnectionsRoute = ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); + const accountingConnectionsRoute = getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID); const {reusablePoliciesConnectedTo: reusablePoliciesConnectedToNetSuite} = useReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.NETSUITE, policyID); const menuItems = reusablePoliciesConnectedToNetSuite.map((policy) => { diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx index 7205b4bdf2ee..5c668f21de14 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx @@ -5,6 +5,7 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -30,6 +31,7 @@ import { shouldHideReimbursedReportsSection, shouldHideReportsExportTo, } from '@pages/workspace/accounting/netsuite/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -50,6 +52,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id ?? CONST.DEFAULT_NUMBER_ID.toString(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.netsuite?.options?.config; const autoSyncConfig = policy?.connections?.netsuite?.config; @@ -284,7 +287,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="NetSuiteAdvancedPage" headerTitle="workspace.accounting.advanced" headerSubtitle={config?.subsidiary ?? ''} diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx index 4f03a7397098..2767f7120590 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx @@ -2,6 +2,7 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,6 +14,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {canUseProvincialTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/NetSuiteUtils'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -31,8 +33,9 @@ function NetSuiteProvincialTaxPostingAccountSelectPage({policy}: WithPolicyConne const illustrations = useMemoizedLazyIllustrations(['Telescope']); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const netSuiteExportBackPath = policyID - ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)) + ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)) : ROUTES.HOME; const config = policy?.connections?.netsuite?.options.config; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx index a2d5056ec13a..999c690799d6 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx @@ -2,6 +2,7 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -14,6 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {canUseTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/NetSuiteUtils'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -33,8 +35,9 @@ function NetSuiteTaxPostingAccountSelectPage({policy}: WithPolicyConnectionsProp const {isBetaEnabled} = usePermissions(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const netSuiteExportBackPath = policyID - ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)) + ? createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)) : ROUTES.HOME; const config = policy?.connections?.netsuite?.options.config; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx index 4aa32d943c1a..53465fc131c6 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingConfigurationPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,6 +14,7 @@ import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils import Navigation from '@navigation/Navigation'; import TravelBillingContinuousReconciliationSection from '@pages/workspace/accounting/common/TravelBillingContinuousReconciliationSection'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -41,7 +43,8 @@ function NetSuiteTravelBillingConfigurationPage({policy}: WithPolicyConnectionsP const styles = useThemeStyles(); const policyID = policy?.id ?? String(CONST.DEFAULT_NUMBER_ID); - const netSuiteExportBackPath = createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); + const netSuiteExportBackPath = createDynamicRoute(DYNAMIC_ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)); const config = policy?.connections?.netsuite?.options?.config; const {payableList} = policy?.connections?.netsuite?.options?.data ?? {}; diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx index d92a9c2cdf38..15272dcdecf5 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx @@ -4,6 +4,7 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import {canUseTaxNetSuite, getCustomersOrJobsLabelNetSuite, getNetSuiteImportCus import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getImportCustomFieldsSettings} from '@pages/workspace/accounting/netsuite/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -32,13 +34,14 @@ function NetSuiteImportPage({policy}: WithPolicyConnectionsProps) { const {isBetaEnabled} = usePermissions(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.netsuite?.options?.config; const {subsidiaryList} = policy?.connections?.netsuite?.options?.data ?? {}; const selectedSubsidiary = useMemo(() => (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID), [subsidiaryList, config?.subsidiaryID]); return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="NetSuiteImportPage" headerTitle="workspace.accounting.import" headerSubtitle={config?.subsidiary ?? ''} diff --git a/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx b/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx index ce766398a408..9acc7157d51b 100644 --- a/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx +++ b/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx @@ -1,3 +1,4 @@ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useOnyx from '@hooks/useOnyx'; import {isConnectionInProgress, syncConnection} from '@libs/actions/connections'; @@ -5,9 +6,10 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; + import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {useEffect} from 'react'; @@ -18,6 +20,7 @@ function QuickBooksDesktopSetupFlowSyncPage({route}: QuickBooksDesktopSetupFlowS const policyID: string = route.params.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '-1'}`); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID ?? '-1'}`); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); useEffect(() => { if (!policyID) { @@ -29,7 +32,7 @@ function QuickBooksDesktopSetupFlowSyncPage({route}: QuickBooksDesktopSetupFlowS syncConnection(policy, CONST.POLICY.CONNECTIONS.NAME.QBD, true); } - Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)); + Navigation.navigate(getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); // disabling this rule, as we want this to run only on the first render // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx index b9e980aa4630..2a2139bf588b 100644 --- a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx +++ b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAutoSyncPageBase.tsx @@ -2,6 +2,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +12,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {clearQBDErrorField} from '@userActions/Policy/Policy'; @@ -35,6 +37,7 @@ function QuickbooksDesktopAutoSyncPageBase({policy, navigateBackTo}: QuickbooksD const styles = useThemeStyles(); const {translate} = useLocalize(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config = policy?.connections?.quickbooksDesktop?.config; const {autoSync, pendingFields} = config ?? {}; const accountingMethod = config?.export?.accountingMethod ?? COMMON_CONST.INTEGRATIONS.ACCOUNTING_METHOD.CASH; @@ -44,9 +47,11 @@ function QuickbooksDesktopAutoSyncPageBase({policy, navigateBackTo}: QuickbooksD const goBack = useCallback(() => { Navigation.goBack( navigateBackTo ?? - (policyID ? createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.path, ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)) : undefined), + (policyID + ? createDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.path, getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID)) + : undefined), ); - }, [policyID, navigateBackTo]); + }, [isUnifiedConnectionsBetaEnabled, policyID, navigateBackTo]); return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} > {sections.map((section) => ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), + callback: () => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)), waitForUpcomingTransition: true, }); return () => handle.cancel(); diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx index c84cf6b6f2fc..8782f26fca63 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx @@ -7,6 +7,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; @@ -18,6 +19,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getQuickbooksOnlineIntegrationName} from '@pages/workspace/accounting/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -44,6 +46,7 @@ function QuickbooksAdvancedPage({policy}: WithPolicyConnectionsProps) { const integrationName = getQuickbooksOnlineIntegrationName(policy, translate); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const qboConfig = policy?.connections?.quickbooksOnline?.config; const accountingMethod = policy?.connections?.quickbooksOnline?.config?.accountingMethod; const {bankAccounts, creditCards, expenseAccounts, otherCurrentAssetAccounts, vendors} = policy?.connections?.quickbooksOnline?.data ?? {}; @@ -184,7 +187,7 @@ function QuickbooksAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="QuickbooksAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx index 2d369ff0da24..8e0e488a99a9 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx @@ -3,6 +3,7 @@ import MenuItem from '@components/MenuItem'; import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,6 +14,7 @@ import Navigation from '@navigation/Navigation'; import {shouldSwitchLocationsToReportFields} from '@pages/workspace/accounting/qbo/utils'; import {getQuickbooksOnlineIntegrationName, isIntuitEnterpriseSuiteConnection} from '@pages/workspace/accounting/utils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -34,6 +36,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { const integrationName = getQuickbooksOnlineIntegrationName(policy, translate); const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const qboConfig = policy?.connections?.quickbooksOnline?.config; const {syncClasses, syncCustomers, syncLocations, syncItems, syncTax, syncCustomDimensions, pendingFields, errorFields} = qboConfig ?? {}; const isIntuitEnterpriseSuite = isIntuitEnterpriseSuiteConnection(policy); @@ -106,7 +109,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="QuickbooksImportPage" headerTitle="workspace.accounting.import" title="workspace.qbo.importDescription" diff --git a/src/pages/workspace/accounting/rillet/advanced/RilletAdvancedPage.tsx b/src/pages/workspace/accounting/rillet/advanced/RilletAdvancedPage.tsx index 31c190f4b2b3..8687fbd7d2f8 100644 --- a/src/pages/workspace/accounting/rillet/advanced/RilletAdvancedPage.tsx +++ b/src/pages/workspace/accounting/rillet/advanced/RilletAdvancedPage.tsx @@ -7,6 +7,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -25,6 +26,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import {getIsTravelBillingEnabled, getTravelBillingCardSettingsKey} from '@libs/TravelBillingUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -42,6 +44,7 @@ function RilletAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const rilletConfig = policy?.connections?.rillet?.config; const rilletData = policy?.connections?.rillet?.data; const autoSync = rilletConfig?.autoSync?.enabled ?? true; @@ -71,7 +74,7 @@ function RilletAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="RilletAdvancedPage" headerTitle="workspace.accounting.advanced" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx b/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx index 595ba280d241..4f2182c02874 100644 --- a/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx +++ b/src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx @@ -8,6 +8,7 @@ import Text from '@components/Text'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsLists from '@hooks/useCardsLists'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,6 +18,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -32,6 +34,7 @@ function RilletExportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const [cardFeeds] = useCardFeeds(policyID); const [cardLists] = useCardsLists(); const policyOwner = policy?.owner; @@ -56,7 +59,7 @@ function RilletExportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="RilletExportPage" headerTitle="workspace.accounting.export" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx index 27c33cc60fb3..a246e41caa3d 100644 --- a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx +++ b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx @@ -1,6 +1,7 @@ import ConnectionLayout from '@components/ConnectionLayout'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,12 +10,12 @@ import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import React from 'react'; import {View} from 'react-native'; @@ -23,6 +24,7 @@ function RilletImportPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const rilletConfig = policy?.connections?.rillet?.config; const rilletData = policy?.connections?.rillet?.data; const enableNewCategories = rilletConfig?.enableNewCategories ?? false; @@ -31,7 +33,7 @@ function RilletImportPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="RilletImportPage" headerTitle="workspace.accounting.import" accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]} diff --git a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx index f5ee8d80693f..c0377f33adb3 100644 --- a/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx +++ b/src/pages/workspace/accounting/useConnectedAccountingIntegration.tsx @@ -8,6 +8,7 @@ import useConfirmModal from '@hooks/useConfirmModal'; import useEnvironment from '@hooks/useEnvironment'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import useHasReusablePoliciesConnectedTo from '@hooks/useHasReusablePoliciesConnectedTo'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -48,6 +49,7 @@ import type {Policy} from '@src/types/onyx'; import type {ConnectionName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import React, {useEffect} from 'react'; @@ -59,15 +61,18 @@ import {getCertiniaSelectedCompanyID, isCertiniaFFAConnection} from './certinia/ import {getAccountingIntegrationData, getAccountingIntegrationDisplayName, getSynchronizationErrorMessage, isIntuitEnterpriseSuiteConnection} from './utils'; type UseConnectedAccountingIntegrationOptions = { + /** Style applied to the settings rows, which the Accounting page lays out as cards */ + menuItemWrapperStyle?: StyleProp; + /** Runs once the user confirms disconnecting and takes over calling `disconnect`, so the panel can close before the connection is removed */ onDisconnectConfirmed?: (disconnect: () => void) => void; }; /** - * Everything the Connections accounting panel derives for the workspace's connected accounting integration: its status, - * the overflow menu, and the settings rows. + * Everything the Accounting page and the Connections accounting panel derive for the workspace's connected accounting + * integration: its status, the overflow menu, and the settings rows. */ -function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconnectConfirmed}: UseConnectedAccountingIntegrationOptions = {}) { +function useConnectedAccountingIntegration(policy: OnyxEntry, {menuItemWrapperStyle, onDisconnectConfirmed}: UseConnectedAccountingIntegrationOptions = {}) { const policyID = policy?.id; const hasReusablePoliciesConnectedToSageIntacct = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, policyID); const hasReusablePoliciesConnectedToQBD = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.QBD, policyID); @@ -81,6 +86,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn const {environment} = useEnvironment(); const {isOffline} = useNetwork(); const {isBetaEnabled} = usePermissions(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {showConfirmModal} = useConfirmModal(); const {activeIntegration} = useAccountingState(); const {startIntegrationFlow} = useAccountingActions(); @@ -269,6 +275,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.xero.organization'), iconRight: icons.ArrowRight, title: getCurrentXeroOrganizationName(policy), + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && tenants.length > 1, shouldShowDescriptionOnTop: true, @@ -291,6 +298,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.netsuite.subsidiary'), iconRight: icons.ArrowRight, title: policy?.connections?.netsuite?.options?.config?.subsidiary ?? '', + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && netSuiteSubsidiaryList?.length > 1, shouldShowDescriptionOnTop: true, @@ -311,6 +319,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.intacct.entity'), iconRight: icons.ArrowRight, title: getCurrentSageIntacctEntityName(policy, translate('workspace.common.topLevel')), + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting, shouldShowDescriptionOnTop: true, @@ -326,6 +335,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate(isConnectedToIntuitEnterpriseSuite ? 'workspace.qbo.entity' : 'workspace.qbo.connectedTo'), iconRight: isConnectedToIntuitEnterpriseSuite ? icons.ArrowRight : undefined, title: policy?.connections?.quickbooksOnline?.config?.companyName, + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: isConnectedToIntuitEnterpriseSuite && canWriteAccounting, shouldShowDescriptionOnTop: true, @@ -344,6 +354,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.certinia.company'), iconRight: icons.ArrowRight, title: selectedCertiniaCompany?.name ?? certiniaCompanyID ?? translate('common.none'), + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting, shouldShowDescriptionOnTop: true, @@ -359,6 +370,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.rillet.entity'), iconRight: icons.ArrowRight, title: rilletSubsidiaryList?.find((subsidiary) => subsidiary.id === policy?.connections?.rillet?.config?.subsidiaryID)?.tradeName ?? '', + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && rilletSubsidiaryList && rilletSubsidiaryList.length > 1, shouldShowDescriptionOnTop: true, @@ -377,6 +389,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.dualEntry.company'), iconRight: icons.ArrowRight, title: dualEntryCompanyList?.find((company) => company.id === policy?.connections?.dualEntry?.config?.subsidiaryID)?.name ?? '', + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && dualEntryCompanyList && dualEntryCompanyList.length > 1, shouldShowDescriptionOnTop: true, @@ -395,6 +408,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.campfire.entity'), iconRight: icons.ArrowRight, title: campfireSubsidiaryList?.find((subsidiary) => subsidiary.id === policy?.connections?.campfire?.config?.subsidiaryID)?.name ?? '', + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && campfireSubsidiaryList && campfireSubsidiaryList.length > 1, shouldShowDescriptionOnTop: true, @@ -413,6 +427,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn description: translate('workspace.businessCentral.subsidiary'), iconRight: icons.ArrowRight, title: businessCentralCompanyList.find((company) => company.id === policy?.connections?.businessCentral?.config?.companyID)?.displayName ?? '', + wrapperStyle: menuItemWrapperStyle, titleStyle: styles.fontWeightNormal, shouldShowRightIcon: canWriteAccounting && businessCentralCompanyList.length > 1, shouldShowDescriptionOnTop: true, @@ -443,6 +458,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn cardFeeds, cardList: cardLists, isIntuitEnterpriseSuiteOverride: isConnectedToIntuitEnterpriseSuite, + isUnifiedConnectionsBetaEnabled, }); let connectionMessage; if (isSyncInProgress && connectionSyncStage) { @@ -460,6 +476,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.import'), + wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onImportPagePress, brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedImportSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR @@ -471,6 +488,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.export'), + wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onExportPagePress, brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedExportSettings, integrationData?.errorFields) || @@ -488,6 +506,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.cardReconciliation'), + wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onCardReconciliationPagePress, }, ] @@ -497,6 +516,7 @@ function useConnectedAccountingIntegration(policy: OnyxEntry, {onDisconn iconRight: icons.ArrowRight, shouldShowRightIcon: true, title: translate('workspace.accounting.advanced'), + wrapperStyle: menuItemWrapperStyle, onPress: integrationData?.onAdvancedPagePress, brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR diff --git a/src/pages/workspace/accounting/utils.tsx b/src/pages/workspace/accounting/utils.tsx index 6e64bb6cfcc2..cfc8f8727bc8 100644 --- a/src/pages/workspace/accounting/utils.tsx +++ b/src/pages/workspace/accounting/utils.tsx @@ -21,6 +21,8 @@ import {getCurrentConnectionName} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; +import {getAccountingConnectionsRoute} from '@pages/workspace/connections/utils'; + import type {ThemeStyles} from '@styles/index'; import {getTrackingCategories} from '@userActions/connections/Xero'; @@ -90,6 +92,9 @@ type GetAccountingIntegrationDataOptions = { /** Picks QBO or Intuit Enterprise Suite explicitly instead of following the existing connection */ isIntuitEnterpriseSuiteOverride?: boolean; + + /** Routes the connect flows through the Connections page instead of the Accounting page */ + isUnifiedConnectionsBetaEnabled?: boolean; }; function getAccountingIntegrationData( @@ -107,6 +112,7 @@ function getAccountingIntegrationData( cardFeeds, cardList, isIntuitEnterpriseSuiteOverride, + isUnifiedConnectionsBetaEnabled = false, }: GetAccountingIntegrationDataOptions = {}, ): AccountingIntegration | undefined { const qboConfig = policy?.connections?.quickbooksOnline?.config; @@ -117,16 +123,16 @@ function getAccountingIntegrationData( const netsuiteSelectedSubsidiary = (policy?.connections?.netsuite?.options?.data?.subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === netsuiteConfig?.subsidiaryID); const getBackToAfterWorkspaceUpgradeRouteForIntacct = () => { if (integrationToDisconnect) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.sageIntacct) { return ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.getRoute(policyID); } - return createDynamicRoute(DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path, ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)); + return createDynamicRoute(DYNAMIC_ROUTES.SAGE_INTACCT_PREREQUISITES.path, getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID)); }; const getBackToAfterWorkspaceUpgradeRouteForQBD = () => { if (integrationToDisconnect) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.qbd) { return ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXISTING_CONNECTIONS.getRoute(policyID); @@ -135,7 +141,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForCertinia = () => { if (integrationToDisconnect) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.certinia) { return ROUTES.POLICY_ACCOUNTING_CERTINIA_EXISTING_CONNECTIONS.getRoute(policyID); @@ -144,7 +150,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForRillet = () => { if (integrationToDisconnect) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.rillet) { return ROUTES.POLICY_ACCOUNTING_RILLET_EXISTING_CONNECTIONS.getRoute(policyID); @@ -153,7 +159,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForDualEntry = () => { if (integrationToDisconnect) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.dualEntry) { return ROUTES.POLICY_ACCOUNTING_DUALENTRY_EXISTING_CONNECTIONS.getRoute(policyID); @@ -162,7 +168,7 @@ function getAccountingIntegrationData( }; const getBackToAfterWorkspaceUpgradeRouteForCampfire = () => { if (integrationToDisconnect) { - return ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + return getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); } if (existingConnections.campfire) { return ROUTES.POLICY_ACCOUNTING_CAMPFIRE_EXISTING_CONNECTIONS.getRoute(policyID); @@ -227,7 +233,8 @@ function getAccountingIntegrationData( ? { workspaceUpgradeNavigationDetails: { integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.intuitEnterpriseSuite.alias, - backToAfterWorkspaceUpgradeRoute: ROUTES.WORKSPACE_CONNECTIONS.getRoute( + backToAfterWorkspaceUpgradeRoute: getAccountingConnectionsRoute( + isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, @@ -344,7 +351,7 @@ function getAccountingIntegrationData( workspaceUpgradeNavigationDetails: { integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.netsuite.alias, backToAfterWorkspaceUpgradeRoute: integrationToDisconnect - ? ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) + ? getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) : ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.getRoute(policyID, getInitialSubPageForNetsuiteTokenInput(policy)), }, pendingFields: {...netsuiteConfig?.pendingFields, ...policy?.connections?.netsuite?.config?.pendingFields, ...policy?.connections?.netsuite?.options?.config?.pendingFields}, @@ -720,7 +727,7 @@ function getAccountingIntegrationData( workspaceUpgradeNavigationDetails: { integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.businessCentral.alias, backToAfterWorkspaceUpgradeRoute: integrationToDisconnect - ? ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) + ? getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled, policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting) : ROUTES.POLICY_ACCOUNTING_BUSINESS_CENTRAL_PREREQUISITES.getRoute(policyID), }, pendingFields: policy?.connections?.businessCentral?.config?.pendingFields, diff --git a/src/pages/workspace/accounting/xero/XeroImportPage.tsx b/src/pages/workspace/accounting/xero/XeroImportPage.tsx index e16701cde3d0..2480cf38eb6b 100644 --- a/src/pages/workspace/accounting/xero/XeroImportPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroImportPage.tsx @@ -4,6 +4,7 @@ import MenuItemField from '@components/MenuItem/presets/MenuItemField'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +12,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {getCurrentXeroOrganizationName} from '@libs/PolicyUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -26,6 +28,7 @@ function XeroImportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {importCustomers, importTaxRates, importTrackingCategories, pendingFields, errorFields} = policy?.connections?.xero?.config ?? {}; const currentXeroOrganizationName = useMemo(() => getCurrentXeroOrganizationName(policy ?? undefined), [policy]); @@ -67,7 +70,7 @@ function XeroImportPage({policy}: WithPolicyProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="XeroImportPage" headerTitle="workspace.accounting.import" headerSubtitle={currentXeroOrganizationName} diff --git a/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx b/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx index 49917e324b83..9dc2a960fefd 100644 --- a/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx +++ b/src/pages/workspace/accounting/xero/XeroSetupPage/index.tsx @@ -18,7 +18,7 @@ function XeroSetupPage({route}: XeroSetupPageProps) { // only reached via a deep link, so just return to the workspace accounting page. We wait for the RHP open // transition to finish first, otherwise goBack fires mid-transition and is dropped. const handle = TransitionTracker.runAfterTransitions({ - callback: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID)), + callback: () => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)), waitForUpcomingTransition: true, }); return () => handle.cancel(); diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index b4ae50e34ede..7476bd10da6a 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -7,6 +7,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useCanConfigureCurrencyConversionFees from '@hooks/useCanConfigureCurrencyConversionFees'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getCurrentXeroOrganizationName, settingsPendingAction} from '@libs/PolicyUtils'; +import {getAccountingSettingsRoute} from '@pages/workspace/connections/utils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -35,6 +37,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { const canConfigureCurrencyConversionFees = useCanConfigureCurrencyConversionFees(policy); const policyID = policy?.id; + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const xeroConfig = policy?.connections?.xero?.config; const {pendingFields, errorFields, sync} = xeroConfig ?? {}; const {bankAccounts, expenseAccounts} = policy?.connections?.xero?.data ?? {}; @@ -63,7 +66,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { return ( Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID))} displayName="XeroAdvancedPage" headerTitle="workspace.accounting.advanced" headerSubtitle={currentXeroOrganizationName} diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 4469dbfba276..4f7bb9e9f4ca 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -680,11 +680,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { return ( ); diff --git a/src/pages/workspace/companyCards/utils.tsx b/src/pages/workspace/companyCards/utils.tsx index 2ec7cda47899..aaaa961df9bd 100644 --- a/src/pages/workspace/companyCards/utils.tsx +++ b/src/pages/workspace/companyCards/utils.tsx @@ -203,7 +203,7 @@ function getPolicyCardExportSettings( policy?: Policy, backTo?: string | undefined, ): CardExportSettings | undefined { - const basePath = ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); + const basePath = ROUTES.POLICY_ACCOUNTING.getRoute(policyID); const currentConnectionName = getCurrentAccountingIntegrationName(policy, translate); const defaultCard = translate('workspace.moreFeatures.companyCards.defaultCard'); const defaultVendor = translate('workspace.accounting.defaultVendor'); diff --git a/src/pages/workspace/connections/ConnectionsAccountingPage.tsx b/src/pages/workspace/connections/ConnectionsAccountingPage.tsx index fc015708f65d..73f45a49a233 100644 --- a/src/pages/workspace/connections/ConnectionsAccountingPage.tsx +++ b/src/pages/workspace/connections/ConnectionsAccountingPage.tsx @@ -9,6 +9,7 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -36,6 +37,7 @@ function ConnectionsAccountingPage({policy}: PolicyAccountingPageProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const policyID = policy?.id; const { connectedIntegration, @@ -61,7 +63,7 @@ function ConnectionsAccountingPage({policy}: PolicyAccountingPageProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.ACCOUNTING} - shouldBeBlocked={!connectedIntegration && !hasUnsupportedNDIntegration} + shouldBeBlocked={!isUnifiedConnectionsBetaEnabled || (!connectedIntegration && !hasUnsupportedNDIntegration)} fullPageNotFoundViewProps={{onBackButtonPress: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))}} > Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))}} > {isFetchingConnections ? ( diff --git a/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx b/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx index 1bca427c3390..56e25c7d6f53 100644 --- a/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx +++ b/src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx @@ -8,6 +8,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useScreenBoundDynamicRoute from '@hooks/useScreenBoundDynamicRoute'; @@ -39,6 +40,7 @@ function ConnectionsReceiptPartnersPage({route}: ConnectionsReceiptPartnersPageP const buildDynamicRoute = useScreenBoundDynamicRoute(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const { policy, integrations, @@ -68,7 +70,7 @@ function ConnectionsReceiptPartnersPage({route}: ConnectionsReceiptPartnersPageP policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.MORE_FEATURES} - shouldBeBlocked={!isLoading && !isUberConnected && !shouldShowEnterCredentialsError} + shouldBeBlocked={!isUnifiedConnectionsBetaEnabled || (!isLoading && !isUberConnected && !shouldShowEnterCredentialsError)} fullPageNotFoundViewProps={{onBackButtonPress: () => Navigation.goBack(ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID))}} > {isLoading ? ( diff --git a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx index 8cf595105a63..4e081a3fedcb 100644 --- a/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx +++ b/src/pages/workspace/connections/WorkspaceConnectionsPage.tsx @@ -18,6 +18,7 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -68,7 +69,6 @@ type RouteParams = { integrationToDisconnect?: ConnectionName; shouldDisconnectIntegrationBeforeConnecting?: boolean; isIntuitEnterpriseSuite?: string; - tab?: ConnectionsTab; }; type MergeSetupFlow = { @@ -110,7 +110,7 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { const {openConciergeAnywhere} = useOpenConciergeAnywhere(); const {startIntegrationFlow} = useAccountingActions(); const {canWrite: canWriteAccounting} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.ACCOUNTING); - const [lastSelectedTab, setLastSelectedTab] = useState(); + const [lastSelectedTab, setLastSelectedTab] = useState(CONST.TAB.CONNECTIONS.ALL); const [mergeSetupFlow, setMergeSetupFlow] = useState(); const route = useRoute(); const params = route.params as RouteParams | undefined; @@ -139,9 +139,7 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { // Recruiting providers are behind a beta, so the tab only shows once there is something to list in it const hasRecruitingListings = listings.some((listing) => listing.category === CONST.TAB.CONNECTIONS.RECRUITING); const visibleTabs = Object.values(CONST.TAB.CONNECTIONS).filter((tab) => tab !== CONST.TAB.CONNECTIONS.RECRUITING || hasRecruitingListings); - // A link can open a tab, which applies until the user picks one - const requestedTab = lastSelectedTab ?? params?.tab ?? CONST.TAB.CONNECTIONS.ALL; - const activeTab = visibleTabs.includes(requestedTab) ? requestedTab : CONST.TAB.CONNECTIONS.ALL; + const activeTab = visibleTabs.includes(lastSelectedTab) ? lastSelectedTab : CONST.TAB.CONNECTIONS.ALL; const [searchValue, setSearchValue, searchResults, appliedSearchValue] = useSearchResults( availableListings, @@ -353,11 +351,14 @@ function WorkspaceConnectionsPage({policy}: WithPolicyConnectionsProps) { } function WorkspaceConnectionsPageWrapper(props: WithPolicyConnectionsProps) { + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); + return ( diff --git a/src/pages/workspace/connections/useAccountingConnectionListings.ts b/src/pages/workspace/connections/useAccountingConnectionListings.ts index ec185f5263aa..a213688c9911 100644 --- a/src/pages/workspace/connections/useAccountingConnectionListings.ts +++ b/src/pages/workspace/connections/useAccountingConnectionListings.ts @@ -2,6 +2,7 @@ * Builds the accounting listings for the Connections page and starts the accounting connect flows from them. * Must be rendered inside an AccountingContextProvider. */ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -36,6 +37,7 @@ function useAccountingConnectionListings(policy: OnyxEntry): ConnectionL const styles = useThemeStyles(); const {translate, datetimeToRelative, getLocalDateFromDatetime} = useLocalize(); const {isBetaEnabled} = usePermissions(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {startIntegrationFlow} = useAccountingActions(); const {activeIntegration, popoverAnchorRefs} = useAccountingState(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); @@ -122,6 +124,7 @@ function useAccountingConnectionListings(policy: OnyxEntry): ConnectionL const integrationData = getAccountingIntegrationData(name, policyID, translate, NO_REUSABLE_CONNECTIONS, { expensifyIcons: accountingIcons, isIntuitEnterpriseSuiteOverride: isIntuitEnterpriseSuite, + isUnifiedConnectionsBetaEnabled, }); if (!integrationData) { return []; diff --git a/src/pages/workspace/connections/utils.ts b/src/pages/workspace/connections/utils.ts index 978b74a86152..eba5318849b9 100644 --- a/src/pages/workspace/connections/utils.ts +++ b/src/pages/workspace/connections/utils.ts @@ -4,6 +4,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type {ConnectionListing, ConnectionsTab} from './types'; @@ -43,4 +44,27 @@ function getListingsForTab(listings: ConnectionListing[], tab: ConnectionsTab): return listings.filter((listing) => listing.category === tab); } -export {MCP_CONNECTOR, getSyncStatusMessage, getListingsForTab}; +/** The page listing the workspace's accounting integrations, which is Connections while the unified Connections beta is on */ +function getAccountingConnectionsRoute(isUnifiedConnectionsBetaEnabled: boolean, ...params: Parameters) { + if (isUnifiedConnectionsBetaEnabled) { + return ROUTES.WORKSPACE_CONNECTIONS.getRoute(...params); + } + return ROUTES.POLICY_ACCOUNTING.getRoute(...params); +} + +/** The page an accounting integration's settings open from, which is the Connections accounting panel while the unified Connections beta is on */ +function getAccountingSettingsRoute(isUnifiedConnectionsBetaEnabled: boolean, policyID: string | undefined) { + if (isUnifiedConnectionsBetaEnabled) { + return ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID); + } + return ROUTES.POLICY_ACCOUNTING.getRoute(policyID); +} + +function getHRSettingsRoute(isUnifiedConnectionsBetaEnabled: boolean, policyID: string) { + if (isUnifiedConnectionsBetaEnabled) { + return ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID); + } + return ROUTES.WORKSPACE_HR.getRoute(policyID); +} + +export {MCP_CONNECTOR, getSyncStatusMessage, getListingsForTab, getAccountingConnectionsRoute, getAccountingSettingsRoute, getHRSettingsRoute}; diff --git a/src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx b/src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx new file mode 100644 index 000000000000..c5f9fe60decd --- /dev/null +++ b/src/pages/workspace/connections/withUnifiedConnectionsBeta.tsx @@ -0,0 +1,28 @@ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; + +import type {ComponentProps, ComponentType} from 'react'; + +import React from 'react'; + +import WorkspaceConnectionsPage from './WorkspaceConnectionsPage'; + +type WorkspaceConnectionsPageProps = ComponentProps; + +/** + * Renders the Connections page in place of a page it replaces while the unified Connections beta is on. Old links and + * deep links into those pages' settings still resolve to their routes, so they need to land on Connections for beta users. + */ +export default function withUnifiedConnectionsBeta>(WrappedComponent: ComponentType): ComponentType { + function WithUnifiedConnectionsBeta(props: TProps) { + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); + + if (isUnifiedConnectionsBetaEnabled) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- these are the screen props of a page Connections replaced, and it only reads the policy ID and the connection to start from the route + return ; + } + + return ; + } + + return WithUnifiedConnectionsBeta; +} diff --git a/src/pages/workspace/getWorkspaceMenuItems.ts b/src/pages/workspace/getWorkspaceMenuItems.ts index 49e6d5028b91..2fa26cf6f11f 100644 --- a/src/pages/workspace/getWorkspaceMenuItems.ts +++ b/src/pages/workspace/getWorkspaceMenuItems.ts @@ -3,17 +3,20 @@ */ import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList'; -import {isMergeHRCompleteSetupNeeded, shouldShowHRConnectionError} from '@libs/merge/HRUtils'; +import {isAnyHRConnected, isMergeHRCompleteSetupNeeded, shouldShowHRConnectionError} from '@libs/merge/HRUtils'; +import {isAnyRecruitingConnected} from '@libs/merge/RecruitingUtils'; import {getObjectKeys} from '@libs/ObjectUtils'; import { arePolicyRulesEnabled, canMemberRead, canPolicyAccessFeature, + hasAccountingFeatureConnection, hasPolicyCategoriesError, hasPolicyRulesError, hasVendorFeature, isGroupPolicy, isMatchingVendorListLoaded, + isMCPEnabled, isPerDiemEnabled, isPolicyAdmin, isQBORefreshTokenExpiringSoonSelector, @@ -49,7 +52,9 @@ type WorkspaceMenuIconMap = Record< | 'Users' | 'Hashtag' | 'Document' + | 'Sync' | 'Connect' + | 'Receipt' | 'Briefcase' | 'Folder' | 'Tag' @@ -64,7 +69,8 @@ type WorkspaceMenuIconMap = Record< | 'InvoiceGeneric' | 'Gear' | 'Bolt' - | 'Bot', + | 'Bot' + | 'UserPlus', IconAsset >; @@ -114,8 +120,12 @@ type GetWorkspaceMenuItemsParams = { hasApprovalWorkflowWithNonMemberApprover?: boolean; /** Whether the vendor matching beta is enabled. */ isVendorMatchingBetaEnabled?: boolean; + /** Whether the Merge ATS beta gating the Recruiting feature is enabled. */ + isRecruitingBetaEnabled?: boolean; /** Whether the company agents beta is enabled. */ isCompanyAgentsBetaEnabled?: boolean; + /** Whether the unified Connections beta replaces the Accounting, HR, Recruiting, Receipt partners, and MCP items. */ + isUnifiedConnectionsBetaEnabled?: boolean; /** Formats the invoice account balance for its menu badge. */ convertToDisplayString: CurrencyListActionsContextType['convertToDisplayString']; }; @@ -133,7 +143,9 @@ function getWorkspaceMenuItems({ shouldShowRBR = false, hasApprovalWorkflowWithNonMemberApprover = false, isVendorMatchingBetaEnabled = false, + isRecruitingBetaEnabled = false, isCompanyAgentsBetaEnabled = false, + isUnifiedConnectionsBetaEnabled = false, convertToDisplayString, }: GetWorkspaceMenuItemsParams): WorkspaceMenuItem[] { const canReadPolicyFeature = (policyFeature: PolicyFeature) => canMemberRead(policy, currentUserLogin ?? '', policyFeature); @@ -156,6 +168,14 @@ function getWorkspaceMenuItems({ const accountingConnectionNames = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES; const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress, accountingConnectionNames); const hasHRError = shouldShowHRConnectionError(policy, isConnectionInProgress, isPolicyAdmin(policy)); + const getHRBrickRoadIndicator = () => { + if (hasHRError) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } + if (isMergeHRCompleteSetupNeeded(policy)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + } + }; const getConnectionsBrickRoadIndicator = () => { if (hasSyncError || shouldShowQBOReimbursableExportDestinationAccountError(policy) || hasHRError || shouldShowEnterCredentialsError) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; @@ -179,11 +199,19 @@ function getWorkspaceMenuItems({ [CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]: policy?.areTagsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]: policy?.tax?.trackingEnabled, [CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]: policy?.areCompanyCardsEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: !!policy?.areConnectionsEnabled || hasAccountingFeatureConnection(policy), + [CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED]: (policy?.isHREnabled === true || isAnyHRConnected(policy)) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED), + [CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED]: + isRecruitingBetaEnabled && + (policy?.isRecruitingEnabled === true || isAnyRecruitingConnected(policy)) && + canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED), [CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: arePolicyRulesEnabled(policy, policyCategories), [CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED]: policy?.areInvoicesEnabled, [CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED]: isPerDiemEnabled(policy) && canPolicyAccessFeature(policy, CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_RATES_ENABLED), + [CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED]: policy?.receiptPartners?.enabled ?? false, + [CONST.POLICY.MORE_FEATURES.IS_MCP_ENABLED]: isMCPEnabled(policy), [CONST.POLICY.MORE_FEATURES.IS_TRAVEL_ENABLED]: policy?.isTravelEnabled, [CONST.POLICY.MORE_FEATURES.IS_TIME_TRACKING_ENABLED]: isTimeTrackingEnabled(policy), }; @@ -246,7 +274,7 @@ function getWorkspaceMenuItems({ }); } - if (canReadMoreFeatures) { + if (isUnifiedConnectionsBetaEnabled && canReadMoreFeatures) { items.push({ translationKey: 'workspace.common.connections', searchAliasKeys: ['workspace.common.accounting', 'workspace.common.hr', 'workspace.common.recruiting', 'workspace.common.receiptPartners', 'workspace.common.mcp'], @@ -259,6 +287,70 @@ function getWorkspaceMenuItems({ }); } + if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.ACCOUNTING)) { + let accountingBrickRoadIndicator; + if (hasSyncError || shouldShowQBOReimbursableExportDestinationAccountError(policy)) { + accountingBrickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } else if (isQBORefreshTokenExpiringSoonSelector(policy)) { + accountingBrickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + } + items.push({ + translationKey: 'workspace.common.accounting', + icon: icons.Sync, + getRoute: () => ROUTES.POLICY_ACCOUNTING.getRoute(policyID), + brickRoadIndicator: accountingBrickRoadIndicator, + screenName: SCREENS.WORKSPACE.ACCOUNTING.ROOT, + sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.ACCOUNTING, + highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED, + }); + } + + if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED] && canReadMoreFeatures) { + items.push({ + translationKey: 'workspace.common.hr', + icon: icons.Users, + getRoute: () => ROUTES.WORKSPACE_HR.getRoute(policyID), + brickRoadIndicator: getHRBrickRoadIndicator(), + screenName: SCREENS.WORKSPACE.HR, + sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.HR, + highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, + }); + } + + if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED] && canReadMoreFeatures) { + items.push({ + translationKey: 'workspace.common.recruiting', + icon: icons.UserPlus, + getRoute: () => ROUTES.WORKSPACE_RECRUITING.getRoute(policyID), + screenName: SCREENS.WORKSPACE.RECRUITING, + sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.RECRUITING, + highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED, + }); + } + + if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED] && canReadMoreFeatures) { + items.push({ + translationKey: 'workspace.common.receiptPartners', + icon: icons.Receipt, + getRoute: () => ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policyID), + brickRoadIndicator: shouldShowEnterCredentialsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + screenName: SCREENS.WORKSPACE.RECEIPT_PARTNERS, + sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.RECEIPT_PARTNERS, + highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.ARE_RECEIPT_PARTNERS_ENABLED, + }); + } + + if (!isUnifiedConnectionsBetaEnabled && policyFeatureStates[CONST.POLICY.MORE_FEATURES.IS_MCP_ENABLED] && canReadMoreFeatures) { + items.push({ + translationKey: 'workspace.common.mcp', + icon: icons.Bot, + getRoute: () => ROUTES.WORKSPACE_MCP.getRoute(policyID), + screenName: SCREENS.WORKSPACE.MCP, + sentryLabel: CONST.SENTRY_LABEL.WORKSPACE.INITIAL.MCP, + highlighted: highlightedPolicyFeature === CONST.POLICY.MORE_FEATURES.IS_MCP_ENABLED, + }); + } + if (policyFeatureStates[CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED] && canReadPolicyFeature(CONST.POLICY.POLICY_FEATURE.CATEGORIES)) { items.push({ translationKey: 'workspace.common.categories', diff --git a/src/pages/workspace/hr/WorkspaceHRPage.tsx b/src/pages/workspace/hr/WorkspaceHRPage.tsx new file mode 100644 index 000000000000..cd8cb4bc30bc --- /dev/null +++ b/src/pages/workspace/hr/WorkspaceHRPage.tsx @@ -0,0 +1,55 @@ +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin'; +import usePolicy from '@hooks/usePolicy'; + +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; + +import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; +import MergeConnectionsPageBase from '@pages/workspace/merge/MergeConnectionsPageBase'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; + +import React from 'react'; + +import {getHRCards} from './utils'; + +type WorkspaceHRPageProps = PlatformStackScreenProps; + +function WorkspaceHRPage({ + route: { + params: {policyID}, + }, +}: WorkspaceHRPageProps) { + const {translate, getLocalDateFromDatetime, formatPhoneNumber} = useLocalize(); + const policy = usePolicy(policyID); + const policyEmployeePersonalDetails = usePersonalDetailsByLogins([...Object.keys(policy?.employeeList ?? {})]); + const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); + const icons = useMemoizedLazyExpensifyIcons(['GustoSquare', 'TriNetSquare']); + + const cards = getHRCards({ + policy, + policyEmployeePersonalDetails, + connectionSyncProgress, + getLocalDateFromDatetime, + translate, + formatPhoneNumber, + policyID, + gustoIcon: icons.GustoSquare, + trinetIcon: icons.TriNetSquare, + }); + + return ( + + ); +} + +export default withUnifiedConnectionsBeta(WorkspaceHRPage); diff --git a/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx b/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx index 2971195ae225..a35667fb3e89 100644 --- a/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx +++ b/src/pages/workspace/hr/gusto/GustoFinalApproverPage.tsx @@ -1,3 +1,4 @@ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import {updateGustoFinalApprover} from '@libs/actions/connections/Gusto'; @@ -5,11 +6,11 @@ import {isGustoConnected} from '@libs/merge/HRUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getHRSettingsRoute} from '@pages/workspace/connections/utils'; import MergeFinalApproverPageBase from '@pages/workspace/merge/MergeFinalApproverPageBase'; import type {MergeFinalApproverProviderConfig} from '@pages/workspace/merge/MergeFinalApproverPageBase'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -22,12 +23,13 @@ function GustoFinalApproverPage({ }, }: GustoFinalApproverPageProps) { const {translate} = useLocalize(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config: MergeFinalApproverProviderConfig = { testID: 'GustoFinalApproverPage', isConnected: isGustoConnected, featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - backRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), + backRoute: getHRSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID), getCurrentFinalApprover: (policy) => policy?.connections?.gusto?.config?.finalApprover ?? null, getProviderName: () => translate('workspace.hr.gusto.title'), getHeaderTitle: () => translate('workspace.merge.finalApprover'), diff --git a/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx b/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx index 5f6e4cb1b6e9..4104798c0406 100644 --- a/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx +++ b/src/pages/workspace/hr/merge/MergeHRFinalApproverPage.tsx @@ -1,3 +1,4 @@ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import {updateMergeFinalApprover} from '@libs/actions/connections/merge'; @@ -6,11 +7,11 @@ import {isMergeConnected} from '@libs/merge/MergeUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getHRSettingsRoute} from '@pages/workspace/connections/utils'; import MergeFinalApproverPageBase from '@pages/workspace/merge/MergeFinalApproverPageBase'; import type {MergeFinalApproverProviderConfig} from '@pages/workspace/merge/MergeFinalApproverPageBase'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -23,12 +24,13 @@ function MergeHRFinalApproverPage({ }, }: MergeHRFinalApproverPageProps) { const {translate} = useLocalize(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config: MergeFinalApproverProviderConfig = { testID: 'MergeHRFinalApproverPage', isConnected: (policy) => isMergeConnected(policy, CONST.POLICY.CONNECTIONS.NAME.MERGE_HR), featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - backRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), + backRoute: getHRSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID), getCurrentFinalApprover: (policy) => policy?.connections?.merge_hris?.config?.finalApprover ?? null, getProviderName: (policy) => getConnectedHRProvider(policy)?.displayName ?? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.merge_hris, getHeaderTitle: (providerName) => translate('workspace.hr.providerFinalApprover', providerName), diff --git a/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx b/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx index 31e2e611df59..7241784dacd1 100644 --- a/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx +++ b/src/pages/workspace/hr/zenefits/ZenefitsFinalApproverPage.tsx @@ -1,3 +1,4 @@ +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import {updateZenefitsFinalApprover} from '@libs/actions/connections/Zenefits'; @@ -5,11 +6,11 @@ import {isZenefitsConnected} from '@libs/merge/HRUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getHRSettingsRoute} from '@pages/workspace/connections/utils'; import MergeFinalApproverPageBase from '@pages/workspace/merge/MergeFinalApproverPageBase'; import type {MergeFinalApproverProviderConfig} from '@pages/workspace/merge/MergeFinalApproverPageBase'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React from 'react'; @@ -22,12 +23,13 @@ function ZenefitsFinalApproverPage({ }, }: ZenefitsFinalApproverPageProps) { const {translate} = useLocalize(); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const config: MergeFinalApproverProviderConfig = { testID: 'ZenefitsFinalApproverPage', isConnected: isZenefitsConnected, featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, - backRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(policyID), + backRoute: getHRSettingsRoute(isUnifiedConnectionsBetaEnabled, policyID), getCurrentFinalApprover: (policy) => policy?.connections?.zenefits?.config?.finalApprover ?? null, getProviderName: () => translate('workspace.hr.zenefits.title'), getHeaderTitle: () => translate('workspace.merge.finalApprover'), diff --git a/src/pages/workspace/mcp/WorkspaceMCPPage.tsx b/src/pages/workspace/mcp/WorkspaceMCPPage.tsx new file mode 100644 index 000000000000..00b74532d621 --- /dev/null +++ b/src/pages/workspace/mcp/WorkspaceMCPPage.tsx @@ -0,0 +1,129 @@ +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import MenuItem from '@components/MenuItem'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; + +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; + +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; + +import variables from '@styles/variables'; + +import {openExternalLink} from '@userActions/Link'; + +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; + +import React from 'react'; +import {View} from 'react-native'; + +type WorkspaceMCPPageProps = PlatformStackScreenProps; + +function WorkspaceMCPPage({route}: WorkspaceMCPPageProps) { + const policyID = route.params.policyID; + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const icons = useMemoizedLazyExpensifyIcons(['ChatGPTSquare', 'ClaudeSquare', 'CursorSquare', 'QuestionMark']); + const policy = usePolicy(policyID); + + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.mcp'); + + const connectors = [ + {icon: icons.ChatGPTSquare, title: translate('workspace.mcp.chatgpt.title'), description: translate('workspace.mcp.chatgpt.subtitle'), url: CONST.CHATGPT_CONNECT_URL}, + {icon: icons.ClaudeSquare, title: translate('workspace.mcp.claude.title'), description: translate('workspace.mcp.claude.subtitle'), url: CONST.CLAUDE_CONNECT_URL}, + {icon: icons.CursorSquare, title: translate('workspace.mcp.cursor.title'), description: translate('workspace.mcp.cursor.subtitle'), url: CONST.CURSOR_MCP_HELP_URL}, + ]; + + return ( + + + Navigation.goBack()} + /> + + +
+ {connectors.map((connector) => ( + openExternalLink(connector.url)} + style={styles.justifyContentCenter} + size={CONST.BUTTON_SIZE.SMALL} + > + {translate('workspace.mcp.connect')} + + } + /> + ))} + + + + {translate('workspace.mcp.helpPrompt')} openExternalLink(CONST.MCP_HELP_URL)}>{translate('workspace.mcp.helpLink')} + + +
+
+
+
+
+ ); +} + +export default withUnifiedConnectionsBeta(WorkspaceMCPPage); diff --git a/src/pages/workspace/merge/MergeConnectionsPageBase.tsx b/src/pages/workspace/merge/MergeConnectionsPageBase.tsx new file mode 100644 index 000000000000..5bd34ee2eac4 --- /dev/null +++ b/src/pages/workspace/merge/MergeConnectionsPageBase.tsx @@ -0,0 +1,261 @@ +import CollapsibleSection from '@components/CollapsibleSection'; +import ConnectToMergeFlow from '@components/ConnectToMergeFlow'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import CompactSearchBar from '@components/SearchBar/CompactSearchBar'; +import Section from '@components/Section'; + +import useConfirmModal from '@hooks/useConfirmModal'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; +import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchResults from '@hooks/useSearchResults'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; + +import {openPolicyHRPage, openPolicyRecruitingPage} from '@libs/actions/PolicyConnections'; +import {isMergeConnectionName} from '@libs/merge/MergeUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import tokenizedSearch from '@libs/tokenizedSearch'; + +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; + +import variables from '@styles/variables'; + +import CONST from '@src/CONST'; + +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; + +import type {MergeProviderCardCategory, MergeProviderCardDescriptor} from './types'; + +import MergeInitialSyncingModalListener from './MergeInitialSyncingModalListener'; +import MergeProviderCard from './MergeProviderCard'; +import MergeSyncResultsListener from './MergeSyncResultsListener'; + +/** The handful of things that differ between the HR page and the Recruiting page. Everything else is shared. */ +const PAGE_CONFIG = { + [CONST.POLICY.CONNECTIONS.CATEGORY.HR]: { + featureName: CONST.POLICY.MORE_FEATURES.IS_HR_ENABLED, + openPage: openPolicyHRPage, + testID: 'WorkspaceHRPage', + }, + [CONST.POLICY.CONNECTIONS.CATEGORY.RECRUITING]: { + featureName: CONST.POLICY.MORE_FEATURES.IS_RECRUITING_ENABLED, + openPage: openPolicyRecruitingPage, + testID: 'WorkspaceRecruitingPage', + }, +} as const; + +type MergeConnectionsPageBaseContentProps = { + /** The workspace whose connections are listed. */ + policyID: string; + + /** Which page this is. Picks the category-specific copy, feature flag, and data-fetching command. */ + category: MergeProviderCardCategory; + + /** Provider cards to list, already built by the category's `utils`. */ + cards: MergeProviderCardDescriptor[]; + + /** Category-specific content rendered under the provider list while nothing is connected, e.g. what to do when the provider isn't listed. */ + footer?: React.ReactNode; +}; + +type MergeConnectionsPageBaseProps = MergeConnectionsPageBaseContentProps & { + /** Whether to block access to the page, e.g. when the category is still behind a beta. */ + shouldBeBlocked?: boolean; +}; + +function MergeConnectionsPageBaseContent({policyID, category, cards, footer}: MergeConnectionsPageBaseContentProps) { + const {translate, localeCompare} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const policy = usePolicy(policyID); + const [activeSetupFlow, setActiveSetupFlow] = useState<{setupLink: string; key: number} | undefined>(); + const {showConfirmModal} = useConfirmModal(); + + const {testID} = PAGE_CONFIG[category]; + + const connectedCards: MergeProviderCardDescriptor[] = []; + const disconnectedCards: MergeProviderCardDescriptor[] = []; + for (const card of cards) { + (card.isConnected ? connectedCards : disconnectedCards).push(card); + } + // At most one provider of a category can be connected to a workspace at a time. + const connectedConnectionName = connectedCards.at(0)?.connectionName; + const byName = (a: MergeProviderCardDescriptor, b: MergeProviderCardDescriptor) => localeCompare(a.displayName, b.displayName); + connectedCards.sort(byName); + disconnectedCards.sort(byName); + + const filterCard = (card: MergeProviderCardDescriptor, searchInput: string) => { + return tokenizedSearch([card], searchInput, (c) => [c.displayName]).length > 0; + }; + const [inputValue, setInputValue, filteredDisconnectedCards] = useSearchResults(disconnectedCards, filterCard); + + const {canWrite: canWriteMoreFeatures, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES); + + const handleConnect = (card: MergeProviderCardDescriptor) => { + if (!card.setupLink) { + return; + } + + if (!canWriteMoreFeatures) { + showReadOnlyModal(); + return; + } + + if (!card.isConnected && connectedCards.length > 0) { + showConfirmModal({ + title: translate(`workspace.${category}.alreadyConnectedTitle`), + prompt: translate(`workspace.${category}.alreadyConnectedPrompt`), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + innerContainerStyle: shouldUseNarrowLayout ? undefined : StyleUtils.getWidthStyle(variables.wideConfirmModalWidth), + }); + return; + } + + // eslint-disable-next-line react-hooks/purity -- random key forces remount on every press, even for the same provider + setActiveSetupFlow({setupLink: card.setupLink, key: Math.random()}); + }; + + const maybeSearchBar = disconnectedCards.length >= CONST.STANDARD_LIST_ITEM_LIMIT && ( + + ); + const disconnectedProviderCards = filteredDisconnectedCards.map((card) => ( + handleConnect(card)} + canWriteMoreFeatures={canWriteMoreFeatures} + showReadOnlyModal={showReadOnlyModal} + /> + )); + + return ( + + {!!connectedConnectionName && ( + + )} + {!!connectedConnectionName && isMergeConnectionName(connectedConnectionName) && ( + + )} + {!!activeSetupFlow && ( + setActiveSetupFlow(undefined)} + /> + )} + Navigation.goBack()} + /> + + +
+ {connectedCards.map((card) => ( + handleConnect(card)} + canWriteMoreFeatures={canWriteMoreFeatures} + showReadOnlyModal={showReadOnlyModal} + /> + ))} + {connectedCards.length === 0 && ( + <> + {maybeSearchBar} + {disconnectedProviderCards} + {footer} + + )} + + {connectedCards.length > 0 && disconnectedCards.length > 0 && !connectedCards.some((c) => c.isInitialSyncInProgress) && ( + + {maybeSearchBar} + {disconnectedProviderCards} + + )} +
+
+
+
+ ); +} + +function MergeConnectionsPageBase({policyID, category, cards, footer, shouldBeBlocked}: MergeConnectionsPageBaseProps) { + const {featureName, openPage} = PAGE_CONFIG[category]; + + useWorkspaceDocumentTitle(undefined, `workspace.common.${category}`); + + useNetwork({onReconnect: () => openPage(policyID)}); + + useEffect(() => { + openPage(policyID); + }, [openPage, policyID]); + + return ( + + + + ); +} + +export default MergeConnectionsPageBase; diff --git a/src/pages/workspace/merge/MergeProviderCard.tsx b/src/pages/workspace/merge/MergeProviderCard.tsx new file mode 100644 index 000000000000..4e6b09867a6e --- /dev/null +++ b/src/pages/workspace/merge/MergeProviderCard.tsx @@ -0,0 +1,139 @@ +import ActivityIndicator from '@components/ActivityIndicator'; +import Button from '@components/Button'; +import ButtonDisabledWhenOffline from '@components/Button/composed/ButtonDisabledWhenOffline'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import RenderHTML from '@components/RenderHTML'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; + +import useEnvironment from '@hooks/useEnvironment'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {clearMergeConnectionErrorField} from '@libs/actions/connections/merge'; +import Navigation from '@libs/Navigation/Navigation'; + +import CONST from '@src/CONST'; +import type Policy from '@src/types/onyx/Policy'; + +import React from 'react'; +import {View} from 'react-native'; + +import type {MergeProviderCardDescriptor} from './types'; + +import useMergeProviderCardDetails from './useMergeProviderCardDetails'; + +type MergeProviderCardProps = { + /** Descriptor object containing the Merge provider's display info, connection state, and sync status. */ + card: MergeProviderCardDescriptor; + + /** The workspace policy that owns this Merge integration. */ + policy: Policy | undefined; + + /** Callback invoked when the user taps the "Connect" or "Reconnect" button. */ + handleConnect: () => void; + + /** Whether the current user can edit this Merge connection. */ + canWriteMoreFeatures: boolean; + + /** Shows the read-only action modal. */ + showReadOnlyModal: () => void; +}; + +function MergeProviderCard({card, policy, handleConnect, canWriteMoreFeatures, showReadOnlyModal}: MergeProviderCardProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {environmentURL} = useEnvironment(); + const {fallbackIcon, cardIcon, connectionDescription, lastSyncErrorMessage, overflowMenu, visibleConfigRows} = useMergeProviderCardDetails({ + card, + policy, + handleConnect, + canWriteMoreFeatures, + showReadOnlyModal, + }); + + let rightInset: React.ReactNode; + if (!card.isConnected) { + rightInset = ( + + {translate('workspace.merge.connect')} + + ); + } else if (card.isSyncInProgress) { + rightInset = ; + } else { + rightInset = ( + + ); + } + + const rightComponent = {rightInset}; + + return ( + <> + + ) : undefined + } + errorText={lastSyncErrorMessage} + errorTextStyle={styles.mt5} + shouldShowRedDotIndicator + shouldShowRightComponent={!!rightInset} + brickRoadIndicator={card.completeSetupRoute ? CONST.BRICK_ROAD_INDICATOR_STATUS.INFO : undefined} + rightComponent={rightComponent} + fallbackIcon={fallbackIcon} + /> + {visibleConfigRows.length > 0 && ( + + {visibleConfigRows.map((row) => { + const RowMenuItem = row.shouldRenderAsMenuItem ? MenuItem : MenuItemWithTopDescription; + + return ( + clearMergeConnectionErrorField(policy?.id, card.connectionName, row.field)} + > + Navigation.navigate(row.route)} + interactive={canWriteMoreFeatures} + /> + + ); + })} + + )} + + ); +} + +export default MergeProviderCard; diff --git a/src/pages/workspace/merge/useMergeProviderCardDetails.tsx b/src/pages/workspace/merge/useMergeProviderCardDetails.tsx index f9c0915c407c..831157a088a6 100644 --- a/src/pages/workspace/merge/useMergeProviderCardDetails.tsx +++ b/src/pages/workspace/merge/useMergeProviderCardDetails.tsx @@ -32,7 +32,7 @@ type UseMergeProviderCardDetailsParams = { onDisconnect?: (disconnect: () => void) => void; }; -/** The status text, sync error and overflow menu of a Merge provider on the Connections HR and Recruiting panels */ +/** The status text, sync error and overflow menu of a Merge provider, shared by the HR and Recruiting cards and their Connections panel */ function useMergeProviderCardDetails({card, policy, handleConnect, canWriteMoreFeatures, showReadOnlyModal, onDisconnect}: UseMergeProviderCardDetailsParams) { const {translate, datetimeToRelative} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx new file mode 100644 index 000000000000..cf10b7f71f45 --- /dev/null +++ b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx @@ -0,0 +1,205 @@ +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItem from '@components/MenuItem'; +import MenuItemField from '@components/MenuItem/presets/MenuItemField'; +import MenuItemSectionRoot from '@components/MenuItem/presets/MenuItemSectionRoot'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import usePrevious from '@hooks/usePrevious'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useScreenBoundDynamicRoute from '@hooks/useScreenBoundDynamicRoute'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; + +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; + +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; + +import CONST from '@src/CONST'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +import React, {useEffect} from 'react'; +import {View} from 'react-native'; + +import type {ReceiptPartnerName, ReceiptPartnerRowComponent} from './connectionRows/types'; + +import UberConnectionRow from './connectionRows/UberConnectionRow'; +import useReceiptPartnersSettings from './useReceiptPartnersSettings'; + +const CONNECTION_ROWS: Record = { + [CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER]: UberConnectionRow, +}; + +const receiptPartnerNames = Object.values(CONST.POLICY.RECEIPT_PARTNERS.NAME); + +type WorkspaceReceiptPartnersPageProps = PlatformStackScreenProps; + +function WorkspaceReceiptPartnersPage({route}: WorkspaceReceiptPartnersPageProps) { + const policyID = route.params.policyID; + const icons = useMemoizedLazyExpensifyIcons(['Mail']); + const {translate} = useLocalize(); + const buildDynamicRoute = useScreenBoundDynamicRoute(); + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {policy, integrations, isAutoInvite, isAutoRemove, isUberConnected, canWriteMoreFeatures, withReadOnlyFallback, toggleUberAutoInvite, toggleUberAutoRemove} = + useReceiptPartnersSettings(policyID); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.receiptPartners'); + const isLoading = policy?.isLoading; + const centralBillingAccountEmail = !!integrations?.uber?.centralBillingAccountEmail; + // Track focus and connection change to route to the invite flow once after successful connection + const prevIsUberConnected = usePrevious(isUberConnected); + + // When Uber connection status flips from false -> true, navigate to the invite flow once + useEffect(() => { + if (!isUberConnected || prevIsUberConnected || !canWriteMoreFeatures) { + return; + } + Navigation.navigate(buildDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE.getRoute(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER))); + }, [prevIsUberConnected, isUberConnected, policyID, canWriteMoreFeatures, buildDynamicRoute]); + + return ( + + {isLoading ? ( + + ) : ( + + + + +
+ {receiptPartnerNames.map((name) => { + const ConnectionRow = CONNECTION_ROWS[name]; + return ( + + ); + })} + {isUberConnected && ( + <> + + + + + + + + + + + {centralBillingAccountEmail && ( + + + + Navigation.navigate( + ROUTES.WORKSPACE_RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT.getRoute( + policyID, + CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER, + ), + ) + : undefined + } + > + + {canWriteMoreFeatures && } + + + + + )} + {canWriteMoreFeatures && ( + + + Navigation.navigate( + buildDynamicRoute(DYNAMIC_ROUTES.WORKSPACE_RECEIPT_PARTNERS_INVITE_EDIT.getRoute(CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER)), + ) + } + > + + + + + + {translate('workspace.receiptPartners.uber.manageInvites')} + + + + + + + + )} + + )} +
+
+
+
+ )} +
+ ); +} + +export default withUnifiedConnectionsBeta(WorkspaceReceiptPartnersPage); diff --git a/src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx b/src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx new file mode 100644 index 000000000000..ca2bbc2654de --- /dev/null +++ b/src/pages/workspace/receiptPartners/connectionRows/UberConnectionRow.tsx @@ -0,0 +1,182 @@ +/** + * Uber for Business row on the receipt partners page. Shows the partner, a claim-offer badge and either a setup + * button or, once connected, a three-dots menu to re-enter credentials or disconnect. + */ +import UserAvatar from '@components/Avatar/UserAvatar'; +import Badge from '@components/Badge'; +import Button from '@components/Button'; +import FormHelpMessage from '@components/FormHelpMessage'; +import MenuItem from '@components/MenuItem'; +import MenuItemSectionRoot from '@components/MenuItem/presets/MenuItemSectionRoot'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; + +import useConfirmModal from '@hooks/useConfirmModal'; +import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; +import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import Navigation from '@navigation/Navigation'; + +import getSynchronizationErrorMessage from '@pages/workspace/receiptPartners/utils'; + +import {openExternalLink} from '@userActions/Link'; +import {openPolicyReceiptPartnersPage, removePolicyReceiptPartnersConnection} from '@userActions/Policy/Policy'; + +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {AnchorPosition} from '@src/styles'; + +import type {ComponentRef} from 'react'; + +import React, {useRef} from 'react'; +import {View} from 'react-native'; + +import type {ReceiptPartnerRowProps} from './types'; + +const UBER = CONST.POLICY.RECEIPT_PARTNERS.NAME.UBER; + +function UberConnectionRow({policyID}: ReceiptPartnerRowProps) { + const icons = useMemoizedLazyExpensifyIcons(['Key', 'NewWindow', 'Trashcan']); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isOffline} = useNetwork(); + const {showConfirmModal} = useConfirmModal(); + const overflowMenuAnchorRef = useRef>(null); + const policy = usePolicy(policyID); + const uber = policy?.receiptPartners?.uber; + const {getReceiptPartnersIntegrationData, shouldShowEnterCredentialsError, isUberConnected} = useGetReceiptPartnersIntegrationData(policyID); + const {canWrite, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.MORE_FEATURES); + const integrationData = getReceiptPartnersIntegrationData(UBER); + + if (!integrationData) { + return null; + } + + const hasError = !!integrationData.errorFields || shouldShowEnterCredentialsError; + + const openUberConnect = () => openExternalLink(`${CONST.UBER_CONNECT_URL}?${uber?.connectFormData}`); + + const disconnect = () => { + showConfirmModal({ + title: translate('workspace.moreFeatures.receiptPartnersWarningModal.featureEnabledTitle'), + prompt: translate('workspace.moreFeatures.receiptPartnersWarningModal.description'), + confirmText: translate('workspace.accounting.disconnect'), + cancelText: translate('common.cancel'), + buttonVariant: CONST.BUTTON_VARIANT.DANGER, + }).then(({action}) => { + if (action !== ModalActions.CONFIRM) { + return; + } + removePolicyReceiptPartnersConnection(policyID, UBER, uber); + openPolicyReceiptPartnersPage(policyID); + }); + }; + + const getOverflowMenuAnchorPosition = () => { + if (shouldUseNarrowLayout) { + return Promise.resolve({horizontal: 0, vertical: 0}); + } + return new Promise((resolve) => { + overflowMenuAnchorRef.current?.measureInWindow((x, y, width, height) => { + resolve({horizontal: x + width, vertical: y + height}); + }); + }); + }; + + let overflowMenuItems: PopoverMenuItem[] = []; + if (canWrite && shouldShowEnterCredentialsError) { + overflowMenuItems = [ + { + icon: icons.Key, + text: translate('workspace.accounting.enterCredentials'), + onSelected: openUberConnect, + shouldCallAfterModalHide: true, + disabled: isOffline, + iconRight: icons.NewWindow, + }, + ]; + } else if (canWrite && isUberConnected) { + overflowMenuItems = [ + { + icon: icons.Trashcan, + text: translate('workspace.accounting.disconnect'), + onSelected: disconnect, + shouldCallAfterModalHide: true, + }, + ]; + } + + const badge = ( + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_CLAIM_OFFER.getRoute(policyID, UBER)) : undefined} + pressable={canWrite} + badgeStyles={shouldUseNarrowLayout ? [styles.alignSelfStart, styles.ml13, styles.mt2] : [styles.ml0, hasError && styles.mr1]} + /> + ); + + return ( + + + + + + + {integrationData.title} + {integrationData.description} + + + {!shouldUseNarrowLayout && badge} + {hasError && } + {overflowMenuItems.length > 0 ? ( + + + + ) : ( + + )} + + + {shouldUseNarrowLayout && badge} + {shouldShowEnterCredentialsError && ( + + )} + + ); +} + +export default UberConnectionRow; diff --git a/src/pages/workspace/receiptPartners/connectionRows/types.ts b/src/pages/workspace/receiptPartners/connectionRows/types.ts new file mode 100644 index 000000000000..8d63b4da0344 --- /dev/null +++ b/src/pages/workspace/receiptPartners/connectionRows/types.ts @@ -0,0 +1,16 @@ +/** Contract between the receipt partners page and the per-partner connection rows it renders */ +import type CONST from '@src/CONST'; + +import type {ComponentType} from 'react'; +import type {ValueOf} from 'type-fest'; + +type ReceiptPartnerName = ValueOf; + +/** Props every partner's connection row receives from the receipt partners page */ +type ReceiptPartnerRowProps = { + policyID: string; +}; + +type ReceiptPartnerRowComponent = ComponentType; + +export type {ReceiptPartnerName, ReceiptPartnerRowProps, ReceiptPartnerRowComponent}; diff --git a/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts b/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts index 1a5ab1c1ab16..8198698fb49f 100644 --- a/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts +++ b/src/pages/workspace/receiptPartners/useReceiptPartnersSettings.ts @@ -23,7 +23,7 @@ type UseReceiptPartnersSettingsOptions = { onDisconnectConfirmed?: (disconnect: () => void) => void; }; -/** State and actions for the workspace's receipt partner connections, used by the Connections receipt partners panel */ +/** State and actions for the workspace's receipt partner connections, shared by the Receipt partners page and its Connections panel */ function useReceiptPartnersSettings(policyID: string, {onDisconnectConfirmed}: UseReceiptPartnersSettingsOptions = {}) { const icons = useMemoizedLazyExpensifyIcons(['Key', 'NewWindow', 'Trashcan']); const {translate} = useLocalize(); diff --git a/src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx b/src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx new file mode 100644 index 000000000000..33a4f1aeb2ba --- /dev/null +++ b/src/pages/workspace/recruiting/WorkspaceRecruitingPage.tsx @@ -0,0 +1,60 @@ +import RenderHTML from '@components/RenderHTML'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; +import usePermissions from '@hooks/usePermissions'; +import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; + +import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; +import MergeConnectionsPageBase from '@pages/workspace/merge/MergeConnectionsPageBase'; + +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; + +import React from 'react'; +import {View} from 'react-native'; + +import {getRecruitingCards} from './utils'; + +type WorkspaceRecruitingPageProps = PlatformStackScreenProps; + +function WorkspaceRecruitingPage({ + route: { + params: {policyID}, + }, +}: WorkspaceRecruitingPageProps) { + const {translate, formatPhoneNumber} = useLocalize(); + const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['Download']); + const policy = usePolicy(policyID); + const policyEmployeePersonalDetails = usePersonalDetailsByLogins([...Object.keys(policy?.employeeList ?? {})]); + const {openConciergeAnywhere} = useOpenConciergeAnywhere(); + const {isBetaEnabled} = usePermissions(); + + const cards = getRecruitingCards({policy, policyEmployeePersonalDetails, policyID, icons, translate, formatPhoneNumber}); + + return ( + + openConciergeAnywhere()} + /> +
+ } + /> + ); +} + +export default withUnifiedConnectionsBeta(WorkspaceRecruitingPage); diff --git a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx index 29d98ba54b2f..9685f36e6111 100644 --- a/src/pages/workspace/rules/RulesRequireFieldsPage.tsx +++ b/src/pages/workspace/rules/RulesRequireFieldsPage.tsx @@ -222,7 +222,7 @@ function RulesRequireFieldsPage({ if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)); + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); return; } @@ -272,7 +272,7 @@ function RulesRequireFieldsPage({ if (action !== ModalActions.CONFIRM) { return; } - Navigation.navigate(ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(policyID)); + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); return; } diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index ee3520dc6ab9..f808b2c8c4de 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -786,11 +786,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const subtitleText = useMemo(() => { const emptyTagsSubtitle = hasAccountingConnections - ? translate( - 'workspace.tags.emptyTags.subtitleWithAccounting', - `${environmentURL}/${ROUTES.WORKSPACE_CONNECTIONS.getRoute(policyID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)}`, - canWriteTags, - ) + ? translate('workspace.tags.emptyTags.subtitleWithAccounting', `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`, canWriteTags) : translate('workspace.tags.emptyTags.subtitleHTML'); return ( diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 0e4c607bda5e..0d663f74ae2c 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -3,6 +3,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useIsUnifiedConnectionsBetaEnabled from '@hooks/useIsUnifiedConnectionsBetaEnabled'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -128,6 +129,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { const {translate} = useLocalize(); const {isBetaEnabledOrUnknown} = usePermissions(); const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING); + const isUnifiedConnectionsBetaEnabled = useIsUnifiedConnectionsBetaEnabled(); const {accountID, email = ''} = useCurrentUserPersonalDetails(); const getReviewWorkspaceSettingsTaskCompletion = useReviewWorkspaceSettingsTaskCompletion(); const [priorFirstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL); @@ -191,8 +193,8 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { case CONST.UPGRADE_FEATURE_INTRO_MAPPING.travelSubmit.id: return Navigation.goBack(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); case CONST.UPGRADE_FEATURE_INTRO_MAPPING.hr.id: - // Connections upgrades for HR and passes itself as the page to return to - return Navigation.goBack(route.params.backTo ?? ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); + // Connections also upgrades for HR, and passes itself as the page to return to + return Navigation.goBack((isUnifiedConnectionsBetaEnabled ? route.params.backTo : undefined) ?? ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); default: return route.params.backTo ? Navigation.goBack(route.params.backTo) : Navigation.goBack(); } @@ -333,10 +335,10 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { enablePerDiem(policyID, true, perDiemCustomUnit?.customUnitID, false); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.hr.id: - enablePolicyHR(policyID, true, false); + enablePolicyHR(policyID, true, !isUnifiedConnectionsBetaEnabled); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.recruiting.id: - enablePolicyRecruiting(policyID, true, false); + enablePolicyRecruiting(policyID, true, !isUnifiedConnectionsBetaEnabled); break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id: case CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvalSubmit.id: diff --git a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx index a79eb8ac168f..fdc91f331572 100644 --- a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx +++ b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx @@ -6,6 +6,7 @@ import {ModalProvider} from '@components/Modal/Global/ModalContext'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; +import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; @@ -25,6 +26,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type Beta from '@src/types/onyx/Beta'; import {PortalProvider} from '@gorhom/portal'; import {NavigationContainer} from '@react-navigation/native'; @@ -44,6 +46,8 @@ jest.mock('@components/Modal/ReanimatedModal', () => { return MockReanimatedModal; }); +jest.mock('@hooks/useIsPolicyConnectedToUberReceiptPartner', () => ({__esModule: true, default: jest.fn(() => false)})); + jest.mock('@libs/CardUtils', () => { const actual: typeof CardUtils = jest.requireActual('@libs/CardUtils'); return { @@ -118,6 +122,7 @@ const isSmartLimitEnabledMock = jest.mocked(CardUtils.isSmartLimitEnabled); const getCompanyFeedsMock = jest.mocked(CardUtils.getCompanyFeeds); const hasAccountingConnectionsMock = jest.mocked(PolicyUtils.hasAccountingConnections); const hasAccountingFeatureConnectionMock = jest.mocked(PolicyUtils.hasAccountingFeatureConnection); +const useIsUberConnectedMock = jest.mocked(useIsPolicyConnectedToUberReceiptPartner); const navigateSpy = jest.spyOn(Navigation, 'navigate').mockImplementation(() => undefined); const navigateToConciergeChatSpy = jest.spyOn(ReportActions, 'navigateToConciergeChat').mockImplementation(() => Promise.resolve()); @@ -155,6 +160,7 @@ describe('WorkspaceMoreFeaturesPage', () => { getCompanyFeedsMock.mockReturnValue({}); hasAccountingConnectionsMock.mockReturnValue(false); hasAccountingFeatureConnectionMock.mockReturnValue(false); + useIsUberConnectedMock.mockReturnValue(false); }); afterEach(async () => { @@ -342,6 +348,45 @@ describe('WorkspaceMoreFeaturesPage', () => { }); }); + describe('Accounting toggle (locked when an integration is connected)', () => { + it('locks the Accounting switch when the policy has an active connection', async () => { + await TestHelper.signInWithTestUser(); + hasAccountingConnectionsMock.mockReturnValue(true); + await act(async () => { + // The test user is on the 'all' beta, which includes the unified Connections beta that hides this toggle + await Onyx.merge(ONYXKEYS.BETAS, []); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); + }); + + renderPage({policyID: POLICY_ID}); + await waitForBatchedUpdatesWithAct(); + + await expect(findLockedSwitch('workspace.moreFeatures.connections.subtitle')).resolves.toBeOnTheScreen(); + }); + + it('routes confirm to the accounting page', async () => { + await TestHelper.signInWithTestUser(); + hasAccountingConnectionsMock.mockReturnValue(true); + await act(async () => { + // The test user is on the 'all' beta, which includes the unified Connections beta that hides this toggle + await Onyx.merge(ONYXKEYS.BETAS, []); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); + }); + + renderPage({policyID: POLICY_ID}); + await waitForBatchedUpdatesWithAct(); + fireEvent.press(await findLockedSwitch('workspace.moreFeatures.connections.subtitle')); + + await waitFor(() => { + expect(screen.getByText(TestHelper.translateLocal('workspace.moreFeatures.connectionsWarningModal.disconnectText'))).toBeOnTheScreen(); + }); + fireEvent.press(await screen.findByLabelText(TestHelper.translateLocal('workspace.moreFeatures.connectionsWarningModal.manageSettings'))); + await waitForBatchedUpdatesWithAct(); + + expect(navigateSpy).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); + }); + }); + describe('Concierge-routed disable flows', () => { it('opens Concierge chat when the user confirms the Expensify Card disable warning', async () => { await TestHelper.signInWithTestUser(); @@ -388,22 +433,66 @@ describe('WorkspaceMoreFeaturesPage', () => { }); }); - describe('Integrate section', () => { - it('does not show the integration toggles', async () => { - // Given a workspace + describe('Receipt partners (informational warning, no confirm action)', () => { + it('opens the disconnect-Uber info modal without a Cancel button when locked', async () => { await TestHelper.signInWithTestUser(); + useIsUberConnectedMock.mockReturnValue(true); await act(async () => { + // The test user is on the 'all' beta, which includes the unified Connections beta that hides this toggle + await Onyx.merge(ONYXKEYS.BETAS, []); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); }); - // When More Features renders renderPage({policyID: POLICY_ID}); await waitForBatchedUpdatesWithAct(); + fireEvent.press(await findLockedSwitch('workspace.moreFeatures.receiptPartners.subtitle')); + + await waitFor(() => { + expect(screen.getByText(TestHelper.translateLocal('workspace.moreFeatures.receiptPartnersWarningModal.disconnectText'))).toBeOnTheScreen(); + }); + expect(screen.queryByText(TestHelper.translateLocal('common.cancel'))).toBeNull(); - // Then the Accounting toggle is gone, because Connections turns each feature on as its first integration connects - expect(screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.connections.subtitle')), 'i')})).toBeNull(); + fireEvent.press(await screen.findByLabelText(TestHelper.translateLocal('workspace.moreFeatures.receiptPartnersWarningModal.confirmText'))); + await waitForBatchedUpdatesWithAct(); + + expect(navigateSpy).not.toHaveBeenCalled(); + expect(navigateToConciergeChatSpy).not.toHaveBeenCalled(); }); }); + + describe('Integrate section', () => { + const renderWithBetas = async (betas: Beta[]) => { + await TestHelper.signInWithTestUser(); + await act(async () => { + await Onyx.merge(ONYXKEYS.BETAS, betas); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID})); + }); + renderPage({policyID: POLICY_ID}); + await waitForBatchedUpdatesWithAct(); + }; + + const accountingSwitchQuery = () => + screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.connections.subtitle')), 'i')}); + + it('shows the Accounting toggle without the unified Connections beta', async () => { + // Given a workspace + // When More Features renders without the beta + await renderWithBetas([]); + + // Then the integration features can still be turned on here, since their pages are only reachable once they are on + expect(accountingSwitchQuery()).toBeOnTheScreen(); + }); + + it('hides the integration toggles with the unified Connections beta', async () => { + // Given a workspace + // When More Features renders with the beta + await renderWithBetas([CONST.BETAS.UNIFIED_CONNECTIONS]); + + // Then the toggles are gone, because Connections turns each feature on as its first integration connects + expect(accountingSwitchQuery()).toBeNull(); + }); + }); + describe('Vendors row (visibility gated on a supported integration)', () => { const renderWithVendorMatching = async (connections: Record, isBetaEnabled = true) => { await TestHelper.signInWithTestUser(); diff --git a/tests/unit/AccountingConnectionConfirmationTest.tsx b/tests/unit/AccountingConnectionConfirmationTest.tsx index 66de76a14d5c..eaa05033e0a0 100644 --- a/tests/unit/AccountingConnectionConfirmationTest.tsx +++ b/tests/unit/AccountingConnectionConfirmationTest.tsx @@ -87,7 +87,8 @@ jest.mock('@hooks/useHasReusablePoliciesConnectedTo', () => ({ default: () => false, })); -jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: () => false})); +let mockEnabledBetas: string[] = []; +jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: (beta: string) => mockEnabledBetas.includes(beta)})); const mockEnablePolicyFeatureForConnection = jest.fn(); jest.mock('@userActions/Policy/Policy', () => ({ @@ -164,12 +165,16 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { // Then the user is asked first, and the setup flow stays unmounted so it cannot start behind the prompt expect(mockShowConfirmModal).toHaveBeenCalledTimes(1); + expect(getShowConfirmModalOption('title')).toBe(`workspace.accounting.connectTitle:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.quickbooksOnline}`); + expect(getShowConfirmModalOption('prompt')).toBe(`workspace.accounting.connectPrompt:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.quickbooksOnline}`); + expect(getShowConfirmModalOption('confirmText')).toBe('workspace.accounting.setup'); expect(getShowConfirmModalOption('cancelText')).toBe('common.cancel'); + expect(getShowConfirmModalOption('buttonVariant')).toBe(CONST.BUTTON_VARIANT.SUCCESS); expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); }); it('should reuse one prompt when the flow is started again while the prompt is still unanswered', async () => { - // Given a prompt already raised for a flow that needs a disconnect. `WorkspaceConnectionsPage` starts the flow + // Given a prompt already raised for a flow that needs a disconnect. `PolicyAccountingPage` starts the flow // from a `useFocusEffect` that re-fires whenever `startIntegrationFlow` is re-created, so the same flow can be // started twice before the user has answered. const ref = renderProvider(); @@ -195,6 +200,25 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { expect(screen.getByTestId(SETUP_FLOW_TEST_ID)).toBeOnTheScreen(); }); + it('should use the Intuit Enterprise Suite display name when the integration is one', async () => { + // Given a workspace with an integration connected + const ref = renderProvider(); + + // When the flow being started is the Intuit Enterprise Suite variant, which shares QBO's connection name + await act(async () => { + ref.current?.startIntegrationFlow({ + name: CONST.POLICY.CONNECTIONS.NAME.QBO, + isIntuitEnterpriseSuite: true, + shouldDisconnectIntegrationBeforeConnecting: true, + integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.XERO, + }); + await waitForBatchedUpdates(); + }); + + // Then the prompt names the suite rather than QuickBooks Online, so the user recognises what they bought + expect(getShowConfirmModalOption('title')).toBe('workspace.accounting.connectTitle:workspace.accounting.intuitEnterpriseSuite'); + }); + it('should disconnect the old connection and release the setup flow on confirm', async () => { // Given a prompt raised for a flow that needs the existing connection dropped const ref = renderProvider(); @@ -298,53 +322,63 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); }); - it('should ask to replace the connected integration, naming the one that would be removed', async () => { - // Given a workspace with an accounting integration connected - const ref = renderProvider(); + describe('with the unified Connections beta', () => { + beforeEach(() => { + mockEnabledBetas = [CONST.BETAS.UNIFIED_CONNECTIONS]; + }); - // When a flow that has to disconnect it first is started - await startFlowNeedingDisconnect(ref); + afterEach(() => { + mockEnabledBetas = []; + }); - // Then the prompt is about replacing that connection, because Connections lists every integration side by side - expect(getShowConfirmModalOption('title')).toBe('workspace.connections.replaceConnectionTitle'); - expect(getShowConfirmModalOption('prompt')).toBe(`workspace.connections.replaceConnectionPrompt:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.xero}`); - expect(getShowConfirmModalOption('confirmText')).toBe('common.replace'); - expect(getShowConfirmModalOption('buttonVariant')).toBe(CONST.BUTTON_VARIANT.DANGER); - expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); - }); + it('should ask to replace the connected integration, naming the one that would be removed', async () => { + // Given a workspace with an accounting integration connected + const ref = renderProvider(); + + // When a flow that has to disconnect it first is started + await startFlowNeedingDisconnect(ref); - it('should name the connected Intuit Enterprise Suite when that is the connection being replaced', async () => { - // Given a workspace whose QuickBooks Online connection is the Intuit Enterprise Suite variant - const ref = renderProvider({ - ...policy, - connections: createMock({quickbooksOnline: {config: {credentials: {scope: CONST.POLICY.CONNECTIONS.INTUIT_ENTERPRISE_SUITE_SCOPE}}}}), + // Then the prompt is about replacing that connection, because Connections lists every integration side by side + expect(getShowConfirmModalOption('title')).toBe('workspace.connections.replaceConnectionTitle'); + expect(getShowConfirmModalOption('prompt')).toBe(`workspace.connections.replaceConnectionPrompt:${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.xero}`); + expect(getShowConfirmModalOption('confirmText')).toBe('common.replace'); + expect(getShowConfirmModalOption('buttonVariant')).toBe(CONST.BUTTON_VARIANT.DANGER); + expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); }); - // When a different integration is started, which means replacing that connection - await act(async () => { - ref.current?.startIntegrationFlow({ - name: CONST.POLICY.CONNECTIONS.NAME.XERO, - shouldDisconnectIntegrationBeforeConnecting: true, - integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.QBO, + it('should name the connected Intuit Enterprise Suite when that is the connection being replaced', async () => { + // Given a workspace whose QuickBooks Online connection is the Intuit Enterprise Suite variant + const ref = renderProvider({ + ...policy, + connections: createMock({quickbooksOnline: {config: {credentials: {scope: CONST.POLICY.CONNECTIONS.INTUIT_ENTERPRISE_SUITE_SCOPE}}}}), }); - await waitForBatchedUpdates(); + + // When a different integration is started, which means replacing that connection + await act(async () => { + ref.current?.startIntegrationFlow({ + name: CONST.POLICY.CONNECTIONS.NAME.XERO, + shouldDisconnectIntegrationBeforeConnecting: true, + integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.QBO, + }); + await waitForBatchedUpdates(); + }); + + // Then the prompt names the suite rather than QuickBooks Online, so the user recognizes what they would lose + expect(getShowConfirmModalOption('prompt')).toBe('workspace.connections.replaceConnectionPrompt:workspace.accounting.intuitEnterpriseSuite'); }); - // Then the prompt names the suite rather than QuickBooks Online, so the user recognizes what they would lose - expect(getShowConfirmModalOption('prompt')).toBe('workspace.connections.replaceConnectionPrompt:workspace.accounting.intuitEnterpriseSuite'); - }); + it('should turn the Accounting feature on when a flow starts, since Connections has no toggle for it', async () => { + // Given a workspace with the Accounting feature off, which Connections has no toggle for + const ref = renderProvider({...policy, areConnectionsEnabled: false}); - it('should turn the Accounting feature on when a flow starts, since Connections has no toggle for it', async () => { - // Given a workspace with the Accounting feature off, which Connections has no toggle for - const ref = renderProvider({...policy, areConnectionsEnabled: false}); + // When a connect flow is started + await act(async () => { + ref.current?.startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.QBO}); + await waitForBatchedUpdates(); + }); - // When a connect flow is started - await act(async () => { - ref.current?.startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.QBO}); - await waitForBatchedUpdates(); + // Then the feature is turned on without navigating away, so the integration can be used once it connects + expect(mockEnablePolicyFeatureForConnection).toHaveBeenCalledWith(POLICY_ID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); }); - - // Then the feature is turned on without navigating away, so the integration can be used once it connects - expect(mockEnablePolicyFeatureForConnection).toHaveBeenCalledWith(POLICY_ID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); }); }); diff --git a/tests/unit/CompanyCardExportTest.ts b/tests/unit/CompanyCardExportTest.ts index 63f3ef72aedd..bef186d9eae0 100644 --- a/tests/unit/CompanyCardExportTest.ts +++ b/tests/unit/CompanyCardExportTest.ts @@ -99,7 +99,7 @@ function createCard(nvpExportAccount?: string): Card { const translate: LocaleContextProps['translate'] = translateLocal; const themeStyles = createMock({}); -const basePath = ROUTES.WORKSPACE_CONNECTIONS_ACCOUNTING.getRoute(MOCK_POLICY_ID); +const basePath = ROUTES.POLICY_ACCOUNTING.getRoute(MOCK_POLICY_ID); describe('getExportMenuItem - QBD credit card account resolution', () => { it('resolves account by ID when NVP contains a QBD ListID (Classic-saved)', () => { diff --git a/tests/unit/ConnectionsPanelGatesTest.tsx b/tests/unit/ConnectionsPanelGatesTest.tsx index 885c969754f9..197cdc840249 100644 --- a/tests/unit/ConnectionsPanelGatesTest.tsx +++ b/tests/unit/ConnectionsPanelGatesTest.tsx @@ -18,6 +18,7 @@ const NOT_FOUND = 'Not found'; const LOADING = 'Loading'; const POLICY_ID = 'policy-1'; +let mockIsUnifiedConnectionsBetaEnabled = true; let mockConnectedIntegration: string | undefined; let mockIsUberConnected = false; let mockHasConnectionsDataBeenFetched: boolean | undefined; @@ -35,6 +36,7 @@ jest.mock('@components/FullscreenLoadingIndicator', () => { const {default: MockText} = jest.requireActual<{default: typeof TextComponent}>('@components/Text'); return {__esModule: true, default: () => Loading}; }); +jest.mock('@hooks/useIsUnifiedConnectionsBetaEnabled', () => () => mockIsUnifiedConnectionsBetaEnabled); jest.mock('@hooks/useWorkspaceDocumentTitle', () => () => {}); jest.mock('@hooks/useScreenBoundDynamicRoute', () => () => jest.fn()); jest.mock('@hooks/useLocalize', () => () => ({translate: (key: string) => key})); @@ -83,22 +85,35 @@ function buildHRCard(isConnected: boolean): MergeProviderCardDescriptor { describe('Connections panel gates', () => { beforeEach(() => { + mockIsUnifiedConnectionsBetaEnabled = true; mockConnectedIntegration = undefined; mockIsUberConnected = false; mockHasConnectionsDataBeenFetched = true; }); describe('ConnectionsAccountingPage', () => { + it('should block the panel for a user without the beta, even with an integration connected', () => { + // Given a connected workspace viewed by a user without the unified Connections beta + mockIsUnifiedConnectionsBetaEnabled = false; + mockConnectedIntegration = CONST.POLICY.CONNECTIONS.NAME.QBO; + + // When the panel opens from a link + render(); + + // Then it shows Not Found, since this user still has the Accounting page + expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); + }); + it('should block the panel when no accounting integration is connected', () => { - // Given a workspace with nothing connected + // Given a beta user on a workspace with nothing connected render(); // Then the panel shows Not Found, since it only holds a connected integration's settings expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); }); - it('should open the panel with an integration connected', () => { - // Given a workspace connected to QuickBooks Online + it('should open the panel for a beta user with an integration connected', () => { + // Given a beta user on a workspace connected to QuickBooks Online mockConnectedIntegration = CONST.POLICY.CONNECTIONS.NAME.QBO; // When the panel opens @@ -111,7 +126,7 @@ describe('Connections panel gates', () => { describe('ConnectionsMergePageBase', () => { it('should block the panel when no provider of the category is connected', () => { - // Given a workspace with no HR provider connected + // Given a beta user on a workspace with no HR provider connected render( { }); it('should show a loading screen until the connections are fetched', () => { - // Given a user deep linking to a workspace whose connections haven't loaded yet + // Given a beta user deep linking to a workspace whose connections haven't loaded yet mockHasConnectionsDataBeenFetched = undefined; // When the panel opens before any provider shows as connected @@ -143,7 +158,7 @@ describe('Connections panel gates', () => { }); it('should open the panel once a provider is connected', () => { - // Given a workspace with Gusto connected + // Given a beta user on a workspace with Gusto connected render( { describe('ConnectionsReceiptPartnersPage', () => { it('should block the panel when Uber is not connected', () => { - // Given a workspace without Uber + // Given a beta user on a workspace without Uber render(); // Then the panel shows Not Found expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); }); + + it('should block the panel for a user without the beta', () => { + // Given Uber is connected but the user doesn't have the beta + mockIsUnifiedConnectionsBetaEnabled = false; + mockIsUberConnected = true; + render(); + + // Then the panel shows Not Found, since this user still has the Receipt partners page + expect(screen.getByText(NOT_FOUND)).toBeOnTheScreen(); + }); }); }); diff --git a/tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx b/tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx new file mode 100644 index 000000000000..f1b9acae0807 --- /dev/null +++ b/tests/unit/PolicyAccountingPageAutoStartFlowTest.tsx @@ -0,0 +1,261 @@ +import {fireEvent, render, screen, within} from '@testing-library/react-native'; + +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import type {MenuItemProps} from '@components/MenuItem'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; + +import Navigation from '@libs/Navigation/Navigation'; + +import PolicyAccountingPage from '@pages/workspace/accounting/PolicyAccountingPage'; + +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import type {ConnectionName} from '@src/types/onyx/Policy'; +import type Policy from '@src/types/onyx/Policy'; + +import type * as ReactNavigation from '@react-navigation/native'; +import type {View} from 'react-native'; + +import React from 'react'; + +import createRandomPolicy from '../utils/collections/policies'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const POLICY_ID = 'policy-1'; + +type RouteParams = { + newConnectionName?: ConnectionName; + integrationToDisconnect?: ConnectionName; + shouldDisconnectIntegrationBeforeConnecting?: boolean; + isIntuitEnterpriseSuite?: string; +}; + +let mockRouteParams: RouteParams = {}; + +const mockStartIntegrationFlow = jest.fn(); +const mockShowReadOnlyModal = jest.fn(); +const mockPopoverAnchorRefs = { + current: Object.fromEntries( + [...CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES, ...Object.values(CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES)].map((name) => [name, {current: null}]), + ), +}; +let mockCanWriteAccounting = true; +let mockEnabledBetas: string[] = []; + +jest.mock('@hooks/usePermissions', () => ({ + __esModule: true, + default: () => ({isBetaEnabled: (beta: string) => mockEnabledBetas.includes(beta)}), +})); + +// The real `useFocusEffect` re-runs its callback whenever the callback's identity changes while the screen is focused. +// `useEffect(cb, [cb])` reproduces exactly that, which is the behaviour the guard under test has to survive. +jest.mock('@react-navigation/native', () => { + const actualNavigation = jest.requireActual('@react-navigation/native'); + const {useEffect: mockUseEffect} = jest.requireActual('react'); + return { + ...actualNavigation, + useFocusEffect: (callback: () => void) => { + mockUseEffect(callback, [callback]); + }, + useRoute: () => ({params: mockRouteParams}), + useNavigation: () => ({}), + useIsFocused: () => true, + }; +}); + +// `Navigation.setParams` clears `newConnectionName` through react-navigation's state update, which lands in a later +// render than the synchronous call. The mock therefore only records the requested params; each test applies them when +// it wants that state update to land, which is what lets the two orderings below be told apart. +let mockPendingParams: RouteParams | undefined; +jest.mock('@libs/Navigation/Navigation', () => ({ + __esModule: true, + default: { + setParams: jest.fn((params: RouteParams) => { + mockPendingParams = params; + }), + navigate: jest.fn(), + goBack: jest.fn(), + isNavigationReady: () => Promise.resolve(), + }, +})); + +/** Lands the pending `Navigation.setParams` update, as a real navigation state change eventually would. */ +function landPendingParamsUpdate() { + mockRouteParams = {...mockRouteParams, ...mockPendingParams}; + mockPendingParams = undefined; +} + +jest.mock('@pages/workspace/accounting/AccountingContext', () => ({ + __esModule: true, + AccountingContextProvider: ({children}: {children: React.ReactNode}) => children, + useAccountingActions: () => ({startIntegrationFlow: mockStartIntegrationFlow}), + useAccountingState: () => ({activeIntegration: undefined, popoverAnchorRefs: mockPopoverAnchorRefs}), +})); + +jest.mock('@pages/workspace/withPolicyConnections', () => ({ + __esModule: true, + default: (Component: React.ComponentType<{policy: Policy}>) => Component, +})); + +jest.mock('@pages/workspace/AccessOrNotFoundWrapper', () => ({ + __esModule: true, + default: ({children}: {children: React.ReactNode}) => children, +})); + +jest.mock('@components/ScreenWrapper', () => ({ + __esModule: true, + default: ({children}: {children: React.ReactNode}) => children, +})); + +jest.mock('@components/ScrollView', () => ({ + __esModule: true, + default: ({children}: {children: React.ReactNode}) => children, +})); + +jest.mock('@components/HeaderWithBackButton', () => ({__esModule: true, default: () => null})); +jest.mock('@components/MenuItemList', () => ({__esModule: true, default: () => null})); +jest.mock('@components/MenuItem', () => { + const {View: MockView} = jest.requireActual<{View: typeof View}>('react-native'); + return { + __esModule: true, + default: ({title, rightComponent}: MenuItemProps) => {rightComponent}, + }; +}); +jest.mock('@components/Section', () => ({__esModule: true, default: ({children}: {children: React.ReactNode}) => children})); +jest.mock('@components/CollapsibleSection', () => ({__esModule: true, default: () => null})); +jest.mock('@components/ThreeDotsMenu', () => ({__esModule: true, default: () => null})); +jest.mock('@components/ActivityIndicator', () => ({__esModule: true, default: () => null})); + +jest.mock('@libs/PolicyUtils', () => { + const actual = jest.requireActual>('@libs/PolicyUtils'); + return { + ...actual, + isControlPolicy: () => true, + }; +}); + +jest.mock('@hooks/usePolicyFeatureWriteAccess', () => ({ + __esModule: true, + default: () => ({canWrite: mockCanWriteAccounting, showReadOnlyModal: mockShowReadOnlyModal}), +})); + +// The real `withPolicyConnections` HOC reads `policy` from Onyx and strips it from the component's public props. It is +// mocked to an identity wrapper above, so the component under test takes `policy` directly. +// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test-only: the HOC that would inject `policy` is mocked out, so it is passed as a prop here +const PolicyAccountingPageUnderTest = PolicyAccountingPage as unknown as React.ComponentType<{policy: Policy}>; + +function buildPolicy(overrides: Partial = {}): Policy { + return {...createRandomPolicy(1, CONST.POLICY.TYPE.CORPORATE, 'Test workspace'), id: POLICY_ID, ...overrides}; +} + +function AccountingTestWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + +describe('PolicyAccountingPage auto-started connect flow', () => { + beforeAll(() => IntlStore.load(CONST.LOCALES.EN)); + + beforeEach(() => { + jest.clearAllMocks(); + mockCanWriteAccounting = true; + mockEnabledBetas = []; + mockPendingParams = undefined; + mockRouteParams = { + newConnectionName: CONST.POLICY.CONNECTIONS.NAME.QBO, + integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.XERO, + shouldDisconnectIntegrationBeforeConnecting: true, + }; + }); + + it('should start the flow once when the effect re-runs before the cleared param has landed', async () => { + // Given the page opened by a route that asks for a connect flow, which starts it and asks for the param to be + // cleared so it cannot be acted on twice + const {rerender} = render(, {wrapper: AccountingTestWrapper}); + await waitForBatchedUpdates(); + + expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); + expect(Navigation.setParams).toHaveBeenCalled(); + + // When anything re-creates `startIntegrationFlow` and re-runs the effect before that clear has landed, which a + // policy update does. This is the window the guard exists for: `newConnectionName` is still set here. + rerender(); + await waitForBatchedUpdates(); + + // Then the flow is not started again, because a second start is what used to stack a second confirmation + // prompt the user had to dismiss twice + expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); + }); + + it('should start the flow again for a later round-trip that asks for the same integration', async () => { + // Given a connect flow that was started from the route param and then let the clear land, so nothing is + // pending any more + const {rerender} = render(, {wrapper: AccountingTestWrapper}); + await waitForBatchedUpdates(); + + expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); + + landPendingParamsUpdate(); + rerender(); + await waitForBatchedUpdates(); + + expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(1); + + // When the user comes back later and asks for the very same integration again + mockRouteParams = { + newConnectionName: CONST.POLICY.CONNECTIONS.NAME.QBO, + integrationToDisconnect: CONST.POLICY.CONNECTIONS.NAME.XERO, + shouldDisconnectIntegrationBeforeConnecting: true, + }; + rerender(); + await waitForBatchedUpdates(); + + // Then it is honoured rather than swallowed as a repeat of the first run, or the connect flow would silently + // do nothing the second time round + expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(2); + }); + + it.each([{enabledBetas: []}, {enabledBetas: ['campfire']}])('should offer Campfire with beta enrollment $enabledBetas', async ({enabledBetas}) => { + // Given an eligible workspace with no accounting connection + mockEnabledBetas = enabledBetas; + mockRouteParams = {}; + + // When the admin connects Campfire from the accounting page + render(, {wrapper: AccountingTestWrapper}); + await waitForBatchedUpdates(); + fireEvent.press(within(screen.getByTestId('Campfire')).getByRole('button')); + + // Then Campfire starts without enabling other integrations + expect(mockStartIntegrationFlow).toHaveBeenCalledWith({name: CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE, isIntuitEnterpriseSuite: undefined}); + expect(screen.queryByTestId(CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.businessCentral)).toBeNull(); + }); + + it('should keep Campfire connection changes read-only without accounting write access', async () => { + // Given a workspace whose accounting settings are read-only + mockCanWriteAccounting = false; + mockRouteParams = {}; + + // When the user presses the Campfire connect button + render(, {wrapper: AccountingTestWrapper}); + await waitForBatchedUpdates(); + fireEvent.press(within(screen.getByTestId('Campfire')).getByRole('button')); + + // Then the read-only explanation appears without starting a connection + expect(mockShowReadOnlyModal).toHaveBeenCalledTimes(1); + expect(mockStartIntegrationFlow).not.toHaveBeenCalled(); + }); + + it('should preserve Business Central beta access alongside Campfire', async () => { + // Given an eligible workspace enrolled in the Business Central beta + mockEnabledBetas = [CONST.BETAS.BUSINESS_CENTRAL]; + mockRouteParams = {}; + + // When viewing the available accounting integrations + render(, {wrapper: AccountingTestWrapper}); + await waitForBatchedUpdates(); + + // Then the enrolled integration and Campfire are both available + expect(screen.getByTestId(CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.businessCentral)).toBeOnTheScreen(); + expect(screen.getByTestId('Campfire')).toBeOnTheScreen(); + }); +}); diff --git a/tests/unit/ConnectionsAccountingPageQBOTokenExpiryTest.tsx b/tests/unit/PolicyAccountingPageQBOTokenExpiryTest.tsx similarity index 96% rename from tests/unit/ConnectionsAccountingPageQBOTokenExpiryTest.tsx rename to tests/unit/PolicyAccountingPageQBOTokenExpiryTest.tsx index ed4a3018badc..77b52432932d 100644 --- a/tests/unit/ConnectionsAccountingPageQBOTokenExpiryTest.tsx +++ b/tests/unit/PolicyAccountingPageQBOTokenExpiryTest.tsx @@ -8,6 +8,7 @@ import type TextComponent from '@components/Text'; import DateUtils from '@libs/DateUtils'; +import PolicyAccountingPage from '@pages/workspace/accounting/PolicyAccountingPage'; import ConnectionsAccountingPage from '@pages/workspace/connections/ConnectionsAccountingPage'; import CONST from '@src/CONST'; @@ -111,6 +112,8 @@ jest.mock('@hooks/usePolicyFeatureWriteAccess', () => ({ // mocked to an identity wrapper above, so the component under test takes `policy` directly. // The Accounting page and the Connections accounting panel share the connected integration's logic, so both are covered. const PAGES_UNDER_TEST = [ + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test-only: the HOC that would inject `policy` is mocked out, so it is passed as a prop here + {pageName: 'PolicyAccountingPage', PageUnderTest: PolicyAccountingPage as unknown as React.ComponentType<{policy: Policy}>}, // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test-only: the HOC that would inject `policy` is mocked out, so it is passed as a prop here {pageName: 'ConnectionsAccountingPage', PageUnderTest: ConnectionsAccountingPage as unknown as React.ComponentType<{policy: Policy}>}, ]; diff --git a/tests/unit/SearchRouterNavigationTest.ts b/tests/unit/SearchRouterNavigationTest.ts index f2a2427053c6..8f2ffc545e48 100644 --- a/tests/unit/SearchRouterNavigationTest.ts +++ b/tests/unit/SearchRouterNavigationTest.ts @@ -203,7 +203,9 @@ const workspaceIcons = { Users: mockIcon, Hashtag: mockIcon, Document: mockIcon, + Sync: mockIcon, Connect: mockIcon, + Receipt: mockIcon, Briefcase: mockIcon, Folder: mockIcon, Tag: mockIcon, @@ -220,6 +222,7 @@ const workspaceIcons = { Gear: mockIcon, Bolt: mockIcon, Bot: mockIcon, + UserPlus: mockIcon, }; function createWorkspacePolicy(id: string, name: string, overrides: Partial = {}): Policy { @@ -702,7 +705,7 @@ describe('Workspace Search Router navigation source', () => { jest.clearAllMocks(); }); - const buildItems = (policies: Policy[], isOffline = false) => + const buildItems = (policies: Policy[], isOffline = false, isUnifiedConnectionsBetaEnabled = false) => buildWorkspaceNavigationItems({ policies: Object.fromEntries(policies.map((policy) => [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy])), policyCategories: undefined, @@ -710,7 +713,9 @@ describe('Workspace Search Router navigation source', () => { icons: workspaceIcons, isOffline, isVendorMatchingBetaEnabled: false, + isRecruitingBetaEnabled: false, isCompanyAgentsBetaEnabled: false, + isUnifiedConnectionsBetaEnabled, shouldUseNarrowLayout: false, convertToDisplayString: () => '$0.00', getItemText: (item) => { @@ -751,14 +756,14 @@ describe('Workspace Search Router navigation source', () => { const items = buildItems([createWorkspacePolicy('1', 'Beta Workspace', {isHREnabled: true}), createWorkspacePolicy('2', 'Alpha Workspace', {isHREnabled: true})]); expect(buildNavigationSuggestions('hr', [items], localeCompare).map((item) => item.keyForList)).toEqual([ - `workspace_2_${SCREENS.WORKSPACE.CONNECTIONS}`, - `workspace_1_${SCREENS.WORKSPACE.CONNECTIONS}`, + `workspace_2_${SCREENS.WORKSPACE.HR}`, + `workspace_1_${SCREENS.WORKSPACE.HR}`, ]); }); - it('matches the Connections row by the name of a page it replaced', () => { - // Given a workspace with the HR feature on, which has no row of its own - const items = buildItems([createWorkspacePolicy('1', 'Alpha Workspace', {isHREnabled: true})]); + it('matches the Connections row by the name of a page it replaced when the unified Connections beta is on', () => { + // Given a workspace with the HR feature on, viewed with the beta on so HR has no row of its own + const items = buildItems([createWorkspacePolicy('1', 'Alpha Workspace', {isHREnabled: true})], false, true); // When searching for the old HR page by its short name const suggestions = buildNavigationSuggestions('hr', [items], localeCompare); @@ -841,9 +846,11 @@ describe('Workspace Search Router navigation source', () => { expect.objectContaining({ policy: activePolicy, isVendorMatchingBetaEnabled: true, + isRecruitingBetaEnabled: true, }), ); expect(mockIsBetaEnabled).toHaveBeenCalledWith(CONST.BETAS.VENDOR_MATCHING); + expect(mockIsBetaEnabled).toHaveBeenCalledWith(CONST.BETAS.MERGE_ATS); }); }); diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts index 372ef6694b44..7a4a3fd7cf63 100644 --- a/tests/unit/WorkflowUtilsTest.ts +++ b/tests/unit/WorkflowUtilsTest.ts @@ -3585,7 +3585,7 @@ describe('WorkflowUtils', () => { (approvalMode) => { expect(getApprovalWorkflowSource(buildPolicyWithConnectedATS(approvalMode), POLICY_ID)).toEqual({ providerName: 'Greenhouse', - settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_RECRUITING.getRoute(POLICY_ID), + settingsRoute: ROUTES.WORKSPACE_RECRUITING.getRoute(POLICY_ID), }); }, ); @@ -3621,7 +3621,7 @@ describe('WorkflowUtils', () => { expect(getApprovalWorkflowSource(policy, POLICY_ID)).toEqual({ providerName: 'Workday', - settingsRoute: ROUTES.WORKSPACE_CONNECTIONS_HR.getRoute(POLICY_ID), + settingsRoute: ROUTES.WORKSPACE_HR.getRoute(POLICY_ID), }); }); }); diff --git a/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx b/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx index 9279cd8d79ce..ae56b376e691 100644 --- a/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx +++ b/tests/unit/WorkspaceConnectionsPageAutoStartFlowTest.tsx @@ -23,11 +23,9 @@ type RouteParams = { integrationToDisconnect?: ConnectionName; shouldDisconnectIntegrationBeforeConnecting?: boolean; isIntuitEnterpriseSuite?: string; - tab?: string; }; let mockRouteParams: RouteParams = {}; -let mockActiveTabKey: string | undefined; const mockStartIntegrationFlow = jest.fn(); @@ -97,13 +95,7 @@ jest.mock('@components/ScrollView', () => ({ })); jest.mock('@components/SidePanel/SidePanelButton', () => ({__esModule: true, default: () => null})); -jest.mock('@components/TabSelector/TabSelectorBase', () => ({ - __esModule: true, - default: ({activeTabKey}: {activeTabKey?: string}) => { - mockActiveTabKey = activeTabKey; - return null; - }, -})); +jest.mock('@components/TabSelector/TabSelectorBase', () => ({__esModule: true, default: () => null})); jest.mock('@components/RenderHTML', () => ({__esModule: true, default: () => null})); jest.mock('@pages/workspace/connections/ConnectionsGrid', () => ({__esModule: true, default: () => null})); jest.mock('@pages/workspace/connections/useAccountingConnectionListings', () => ({__esModule: true, default: () => []})); @@ -194,33 +186,6 @@ describe('WorkspaceConnectionsPage auto-started connect flow', () => { expect(mockStartIntegrationFlow).toHaveBeenCalledTimes(2); }); }); - -describe('WorkspaceConnectionsPage tab from the route', () => { - it('should open on the tab the link asks for', async () => { - // Given a link to Connections that asks for the Accounting tab, like the onboarding accounting task - mockRouteParams = {tab: CONST.TAB.CONNECTIONS.ACCOUNTING}; - - // When the page opens - render(); - await waitForBatchedUpdates(); - - // Then the Accounting tab is selected, so the user lands on the integrations the link was about - expect(mockActiveTabKey).toBe(CONST.TAB.CONNECTIONS.ACCOUNTING); - }); - - it('should fall back to All for a tab it does not know', async () => { - // Given a link with a tab value that is not one of the page's tabs - mockRouteParams = {tab: 'unknown'}; - - // When the page opens - render(); - await waitForBatchedUpdates(); - - // Then the page opens on All instead of an empty selection - expect(mockActiveTabKey).toBe(CONST.TAB.CONNECTIONS.ALL); - }); -}); - describe('WorkspaceConnectionsPage data fetched on open', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx b/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx index ebc84a8787db..0ccaa1268ee9 100644 --- a/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx +++ b/tests/unit/components/QuickbooksOnlineSetupPageWebTest.tsx @@ -57,10 +57,10 @@ describe('QuickbooksOnlineSetupPage (web)', () => { jest.clearAllMocks(); }); - it('returns to the workspace Connections page', () => { + it('returns to the workspace accounting page', () => { renderQuickbooksOnlineSetupPage(); - expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID)); + expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); }); it('renders nothing', () => { diff --git a/tests/unit/components/XeroSetupPageWebTest.tsx b/tests/unit/components/XeroSetupPageWebTest.tsx index 05e236149839..06c449ace7c3 100644 --- a/tests/unit/components/XeroSetupPageWebTest.tsx +++ b/tests/unit/components/XeroSetupPageWebTest.tsx @@ -57,10 +57,10 @@ describe('XeroSetupPage (web)', () => { jest.clearAllMocks(); }); - it('returns to the workspace Connections page', () => { + it('returns to the workspace accounting page', () => { renderXeroSetupPage(); - expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID)); + expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); }); it('renders nothing', () => { diff --git a/tests/unit/getWorkspaceMenuItemsTest.ts b/tests/unit/getWorkspaceMenuItemsTest.ts index def36b238a38..209529ae1562 100644 --- a/tests/unit/getWorkspaceMenuItemsTest.ts +++ b/tests/unit/getWorkspaceMenuItemsTest.ts @@ -16,7 +16,9 @@ const icons = { Users: mockIcon, Hashtag: mockIcon, Document: mockIcon, + Sync: mockIcon, Connect: mockIcon, + Receipt: mockIcon, Briefcase: mockIcon, Folder: mockIcon, Tag: mockIcon, @@ -33,6 +35,7 @@ const icons = { Gear: mockIcon, Bolt: mockIcon, Bot: mockIcon, + UserPlus: mockIcon, }; function buildPolicy(role: Policy['role']): Policy { @@ -150,7 +153,7 @@ describe('getWorkspaceMenuItems', () => { expect(items.find((item) => item.translationKey === 'workspace.common.profile')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); - it('shows an accounting sync error on Connections only when a sync is not in progress', () => { + it('shows an accounting sync error only when a sync is not in progress', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), areConnectionsEnabled: true, @@ -178,8 +181,23 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(buildItems(false).find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); - expect(buildItems(true).find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBeUndefined(); + expect(buildItems(false).find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + expect(buildItems(true).find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBeUndefined(); + }); + + it('shows an error indicator when receipt partner credentials require attention', () => { + const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), receiptPartners: {enabled: true}}); + + const items = getWorkspaceMenuItems({ + policy, + policyID: policy.id, + currentUserLogin, + icons, + shouldShowEnterCredentialsError: true, + convertToDisplayString: () => '', + }); + + expect(items.find((item) => item.translationKey === 'workspace.common.receiptPartners')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); it('shows an error indicator when company cards require attention', () => { @@ -299,7 +317,7 @@ describe('getWorkspaceMenuItems', () => { expect(items.find((item) => item.translationKey === 'workspace.common.rules')?.brickRoadIndicator).toBeUndefined(); }); - it('shows an information indicator on Connections when Merge HR setup is incomplete', () => { + it('shows an information indicator when Merge HR setup is incomplete', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), isHREnabled: true, @@ -320,10 +338,10 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); + expect(items.find((item) => item.translationKey === 'workspace.common.hr')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); }); - it('shows an information indicator on Connections when the QBO refresh token is about to expire', () => { + it('shows an information indicator on Accounting when the QBO refresh token is about to expire', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), areConnectionsEnabled: true, @@ -343,10 +361,10 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); + expect(items.find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO); }); - it('keeps the error indicator on Connections when a sync error exists even if the QBO refresh token is about to expire', () => { + it('keeps the error indicator on Accounting when a sync error exists even if the QBO refresh token is about to expire', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), areConnectionsEnabled: true, @@ -366,10 +384,10 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + expect(items.find((item) => item.translationKey === 'workspace.common.accounting')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); }); - it('shows an error indicator on Connections when the Merge HR connection has an authentication error', () => { + it('shows an error indicator when the Merge HR connection has an authentication error', () => { const policy = createMock({ ...buildPolicy(CONST.POLICY.ROLE.ADMIN), isHREnabled: true, @@ -389,7 +407,85 @@ describe('getWorkspaceMenuItems', () => { convertToDisplayString: () => '', }); - expect(items.find((item) => item.translationKey === 'workspace.common.connections')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + expect(items.find((item) => item.translationKey === 'workspace.common.hr')?.brickRoadIndicator).toBe(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + }); + + it('hides the Recruiting row when the Merge ATS beta is disabled', () => { + const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), isRecruitingEnabled: true}); + + const buildItems = (isRecruitingBetaEnabled: boolean) => + getWorkspaceMenuItems({ + policy, + policyID: policy.id, + currentUserLogin, + icons, + isRecruitingBetaEnabled, + convertToDisplayString: () => '', + }); + + expect(buildItems(false).find((item) => item.translationKey === 'workspace.common.recruiting')).toBeUndefined(); + + const recruitingItem = buildItems(true).find((item) => item.translationKey === 'workspace.common.recruiting'); + expect(recruitingItem?.getRoute()).toBe(ROUTES.WORKSPACE_RECRUITING.getRoute(policy.id)); + expect(recruitingItem?.screenName).toBe(SCREENS.WORKSPACE.RECRUITING); + }); + + it('shows the Recruiting row when an ATS connection exists even without the policy flag', () => { + const policy = createMock({ + ...buildPolicy(CONST.POLICY.ROLE.ADMIN), + isRecruitingEnabled: undefined, + connections: { + [CONST.POLICY.CONNECTIONS.NAME.MERGE_ATS]: { + config: {integration: 'greenhouse'}, + lastSync: {syncStatus: CONST.MERGE.SYNC_STATUS.DONE}, + }, + }, + }); + + const items = getWorkspaceMenuItems({ + policy, + policyID: policy.id, + currentUserLogin, + icons, + isRecruitingBetaEnabled: true, + convertToDisplayString: () => '', + }); + + expect(items.find((item) => item.translationKey === 'workspace.common.recruiting')).toBeDefined(); + }); + + it('hides the Recruiting row on a workspace whose plan cannot access the feature', () => { + const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), type: CONST.POLICY.TYPE.TEAM, isRecruitingEnabled: true}); + + const items = getWorkspaceMenuItems({ + policy, + policyID: policy.id, + currentUserLogin, + icons, + isRecruitingBetaEnabled: true, + convertToDisplayString: () => '', + }); + + expect(items.find((item) => item.translationKey === 'workspace.common.recruiting')).toBeUndefined(); + }); + + it('does not highlight a newly enabled Recruiting feature while the Merge ATS beta is disabled', () => { + const policy = createMock({ + ...buildPolicy(CONST.POLICY.ROLE.ADMIN), + isRecruitingEnabled: true, + pendingFields: {isRecruitingEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }); + + const items = getWorkspaceMenuItems({ + policy, + policyID: policy.id, + currentUserLogin, + icons, + previousPendingFields: {}, + convertToDisplayString: () => '', + }); + + expect(items.filter((item) => item.highlighted)).toHaveLength(0); }); it('preserves the complete enabled Workspace menu order and presentation data', () => { @@ -427,6 +523,7 @@ describe('getWorkspaceMenuItems', () => { currentUserLogin, icons, isVendorMatchingBetaEnabled: true, + isRecruitingBetaEnabled: true, convertToDisplayString, }); @@ -435,7 +532,11 @@ describe('getWorkspaceMenuItems', () => { 'workspace.common.members', 'workspace.common.rooms', 'common.reports', - 'workspace.common.connections', + 'workspace.common.accounting', + 'workspace.common.hr', + 'workspace.common.recruiting', + 'workspace.common.receiptPartners', + 'workspace.common.mcp', 'workspace.common.categories', 'workspace.common.tags', 'workspace.common.taxes', @@ -456,7 +557,11 @@ describe('getWorkspaceMenuItems', () => { ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id), ROUTES.WORKSPACE_ROOMS.getRoute(policy.id), ROUTES.WORKSPACE_REPORTS.getRoute(policy.id), - ROUTES.WORKSPACE_CONNECTIONS.getRoute(policy.id), + ROUTES.POLICY_ACCOUNTING.getRoute(policy.id), + ROUTES.WORKSPACE_HR.getRoute(policy.id), + ROUTES.WORKSPACE_RECRUITING.getRoute(policy.id), + ROUTES.WORKSPACE_RECEIPT_PARTNERS.getRoute(policy.id), + ROUTES.WORKSPACE_MCP.getRoute(policy.id), ROUTES.WORKSPACE_CATEGORIES.getRoute(policy.id), ROUTES.WORKSPACE_TAGS.getRoute(policy.id), ROUTES.WORKSPACE_TAXES.getRoute(policy.id), @@ -477,7 +582,11 @@ describe('getWorkspaceMenuItems', () => { SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.ROOMS, SCREENS.WORKSPACE.REPORTS, - SCREENS.WORKSPACE.CONNECTIONS, + SCREENS.WORKSPACE.ACCOUNTING.ROOT, + SCREENS.WORKSPACE.HR, + SCREENS.WORKSPACE.RECRUITING, + SCREENS.WORKSPACE.RECEIPT_PARTNERS, + SCREENS.WORKSPACE.MCP, SCREENS.WORKSPACE.CATEGORIES, SCREENS.WORKSPACE.TAGS, SCREENS.WORKSPACE.TAXES, @@ -499,7 +608,7 @@ describe('getWorkspaceMenuItems', () => { }); }); -describe('getWorkspaceMenuItems Connections row', () => { +describe('getWorkspaceMenuItems with the unified Connections beta', () => { it('replaces the separate integration rows with a single Connections row', () => { // Given an admin on a workspace with every integration feature turned on const policy = createMock({ @@ -511,12 +620,14 @@ describe('getWorkspaceMenuItems Connections row', () => { isMCPEnabled: true, }); - // When the Workspace menu is built + // When the Workspace menu is built with the beta on const items = getWorkspaceMenuItems({ policy, policyID: policy.id, currentUserLogin, icons, + isRecruitingBetaEnabled: true, + isUnifiedConnectionsBetaEnabled: true, convertToDisplayString: () => '', }); @@ -552,12 +663,13 @@ describe('getWorkspaceMenuItems Connections row', () => { receiptPartners: {enabled: false}, }); - // When the Workspace menu is built + // When the Workspace menu is built with the beta on const items = getWorkspaceMenuItems({ policy, policyID: policy.id, currentUserLogin, icons, + isUnifiedConnectionsBetaEnabled: true, convertToDisplayString: () => '', }); @@ -569,13 +681,14 @@ describe('getWorkspaceMenuItems Connections row', () => { // Given a workspace whose receipt partner credentials need attention const policy = createMock({...buildPolicy(CONST.POLICY.ROLE.ADMIN), receiptPartners: {enabled: true}}); - // When the Workspace menu is built + // When the Workspace menu is built with the beta on const items = getWorkspaceMenuItems({ policy, policyID: policy.id, currentUserLogin, icons, shouldShowEnterCredentialsError: true, + isUnifiedConnectionsBetaEnabled: true, convertToDisplayString: () => '', }); diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index eb821235a3ea..654739866231 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -390,7 +390,7 @@ describe('useGettingStartedItems', () => { await waitForBatchedUpdates(); const connectItem = result.current.items.find((item) => item.key === 'connectAccounting'); - expect(connectItem?.route).toBe(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING)); + expect(connectItem?.route).toBe(ROUTES.WORKSPACE_ACCOUNTING.getRoute(POLICY_ID)); }); it('should be not completed when workspace has no accounting connection', async () => { diff --git a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx index 2ba935ea07e0..17820abd1498 100644 --- a/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx +++ b/tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx @@ -546,9 +546,7 @@ describe('GettingStartedSection', () => { const row = screen.getByText(/homePage\.gettingStartedSection\.connectAccounting:/); fireEvent.press(row); - expect(Navigation.navigate).toHaveBeenCalledWith( - ROUTES.WORKSPACE_CONNECTIONS.getRoute(TEST_POLICY_ID, undefined, undefined, undefined, undefined, CONST.TAB.CONNECTIONS.ACCOUNTING), - ); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_ACCOUNTING.getRoute(TEST_POLICY_ID)); }); it('navigates to workspace categories when "Customize categories" row is pressed', async () => { diff --git a/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx b/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx index 4dea5275cabe..4027823e98ee 100644 --- a/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx +++ b/tests/unit/pages/workspace/accounting/netsuite/NetSuiteSetupPageTest.tsx @@ -89,13 +89,13 @@ describe('NetSuiteSetupPage', () => { expect(await screen.findByTestId('netsuite-webview')).toBeOnTheScreen(); }); - it('returns to the Connections page on back, skipping the token input wizard underneath', async () => { + it('returns to the accounting page on back, skipping the token input wizard underneath', async () => { renderNetSuiteSetupPage(ACCOUNT_ID); expect(await screen.findByTestId('netsuite-webview')).toBeOnTheScreen(); mockHeaderProps.current?.onBackButtonPress?.(); - expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.WORKSPACE_CONNECTIONS.getRoute(POLICY_ID)); + expect(mockedGoBack).toHaveBeenCalledWith(ROUTES.POLICY_ACCOUNTING.getRoute(POLICY_ID)); }); it('renders nothing when the account ID is missing from the route', () => { diff --git a/tests/unit/useAccountingConnectionListingsTest.ts b/tests/unit/useAccountingConnectionListingsTest.ts index d1a7c67a75e0..bdc53ce368dd 100644 --- a/tests/unit/useAccountingConnectionListingsTest.ts +++ b/tests/unit/useAccountingConnectionListingsTest.ts @@ -14,8 +14,8 @@ let mockCanWrite = true; let mockConnectedIntegration: ConnectionName | undefined; jest.mock('@hooks/useLocalize', () => () => ({translate: (key: string) => key, datetimeToRelative: () => '', getLocalDateFromDatetime: jest.fn()})); -let mockEnabledBetas: string[] = []; -jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: (beta: string) => mockEnabledBetas.includes(beta)})); +jest.mock('@hooks/usePermissions', () => () => ({isBetaEnabled: () => false})); +jest.mock('@hooks/useIsUnifiedConnectionsBetaEnabled', () => () => true); jest.mock('@hooks/useOnyx', () => () => [undefined]); jest.mock('@hooks/useThemeStyles', () => () => ({})); jest.mock('@hooks/useLazyAsset', () => ({useMemoizedLazyExpensifyIcons: () => ({})})); @@ -50,7 +50,6 @@ describe('useAccountingConnectionListings', () => { jest.clearAllMocks(); mockCanWrite = true; mockConnectedIntegration = undefined; - mockEnabledBetas = []; }); it('should start the connect flow for a workspace with no accounting integration', () => { @@ -104,27 +103,4 @@ describe('useAccountingConnectionListings', () => { // Then QuickBooks Online carries a healthy status, so it shows in the connected section with Configure expect(qbo?.status?.isBroken).toBe(false); }); - it.each([{enabledBetas: []}, {enabledBetas: ['campfire']}])('should offer Campfire with beta enrollment $enabledBetas', ({enabledBetas}) => { - // Given a workspace with no accounting connection - mockEnabledBetas = enabledBetas; - - // When the accounting listings are built - const campfire = findListing(CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE); - - // Then Campfire is offered, because it no longer needs a beta, and Business Central stays hidden without its beta - expect(campfire).toBeDefined(); - expect(findListing(CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL)).toBeUndefined(); - }); - - it('should offer Business Central alongside Campfire to a workspace in the Business Central beta', () => { - // Given a workspace enrolled in the Business Central beta - mockEnabledBetas = [CONST.BETAS.BUSINESS_CENTRAL]; - - // When the accounting listings are built - const businessCentral = findListing(CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL); - - // Then both integrations are available - expect(businessCentral).toBeDefined(); - expect(findListing(CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE)).toBeDefined(); - }); }); diff --git a/tests/unit/withUnifiedConnectionsBetaTest.tsx b/tests/unit/withUnifiedConnectionsBetaTest.tsx new file mode 100644 index 000000000000..55082759b98a --- /dev/null +++ b/tests/unit/withUnifiedConnectionsBetaTest.tsx @@ -0,0 +1,53 @@ +import {render, screen} from '@testing-library/react-native'; + +import Text from '@components/Text'; + +import withUnifiedConnectionsBeta from '@pages/workspace/connections/withUnifiedConnectionsBeta'; + +import React from 'react'; + +let mockIsUnifiedConnectionsBetaEnabled = false; +jest.mock('@hooks/useIsUnifiedConnectionsBetaEnabled', () => ({ + __esModule: true, + default: () => mockIsUnifiedConnectionsBetaEnabled, +})); + +jest.mock('@pages/workspace/connections/WorkspaceConnectionsPage', () => { + const {default: MockText} = jest.requireActual<{default: typeof Text}>('@components/Text'); + return { + __esModule: true, + default: () => Connections page, + }; +}); + +function OldIntegrationPage({label}: {label: string}) { + return {label}; +} + +const OldIntegrationPageWithBeta = withUnifiedConnectionsBeta(OldIntegrationPage); + +describe('withUnifiedConnectionsBeta', () => { + it('should render the wrapped page while the beta is off', () => { + // Given a user without the unified Connections beta + mockIsUnifiedConnectionsBetaEnabled = false; + + // When an old integration page renders + render(); + + // Then the old page shows as before, since only beta users moved to Connections + expect(screen.getByText('Accounting page')).toBeOnTheScreen(); + expect(screen.queryByText('Connections page')).not.toBeOnTheScreen(); + }); + + it('should render the Connections page in its place while the beta is on', () => { + // Given a user with the unified Connections beta + mockIsUnifiedConnectionsBetaEnabled = true; + + // When an old link opens an integration page + render(); + + // Then Connections shows instead, since the old page is no longer in the workspace menu for this user + expect(screen.getByText('Connections page')).toBeOnTheScreen(); + expect(screen.queryByText('Accounting page')).not.toBeOnTheScreen(); + }); +}); From 576f5ea68ced6f26fc7aa5981b97e1b23c03faa2 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 23 Oct 2026 20:15:05 +0530 Subject: [PATCH 8/8] Open the accounting setup a frame after turning Connections on, so its page doesn't send the user to More features before the change lands Signed-off-by: krishna2323 --- .../accounting/AccountingContext/index.tsx | 15 +++++-- .../AccountingConnectionConfirmationTest.tsx | 39 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/accounting/AccountingContext/index.tsx b/src/pages/workspace/accounting/AccountingContext/index.tsx index 613063ec86eb..90c22534e904 100644 --- a/src/pages/workspace/accounting/AccountingContext/index.tsx +++ b/src/pages/workspace/accounting/AccountingContext/index.tsx @@ -11,7 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import {getAccountingIntegrationDisplayName} from '@libs/AccountingUtils'; import {removePolicyConnection} from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; -import {isControlPolicy, tryNavigateToSubmitWorkspaceUpgrade} from '@libs/PolicyUtils'; +import {isControlPolicy, isPolicyFeatureEnabled, tryNavigateToSubmitWorkspaceUpgrade} from '@libs/PolicyUtils'; import {getAccountingIntegrationData} from '@pages/workspace/accounting/utils'; @@ -158,7 +158,8 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider ); // Connections has no feature toggle, so turn it on here. The Control upgrade only enables the integration's own feature. - if (isUnifiedConnectionsBetaEnabled) { + const shouldEnableConnections = isUnifiedConnectionsBetaEnabled && !isPolicyFeatureEnabled(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + if (shouldEnableConnections) { enablePolicyFeatureForConnection(policy, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); } @@ -169,10 +170,16 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider ); return; } - setActiveIntegration({ + const activeIntegrationToStart = { ...newActiveIntegration, key: Math.random(), - }); + }; + // The setup pages redirect to More features while Connections is off, so they open a frame later, once the policy shows it on + if (shouldEnableConnections) { + requestAnimationFrame(() => setActiveIntegration(activeIntegrationToStart)); + } else { + setActiveIntegration(activeIntegrationToStart); + } const integrationToDisconnect = newActiveIntegration.integrationToDisconnect; if (!newActiveIntegration.shouldDisconnectIntegrationBeforeConnecting || !integrationToDisconnect) { diff --git a/tests/unit/AccountingConnectionConfirmationTest.tsx b/tests/unit/AccountingConnectionConfirmationTest.tsx index eaa05033e0a0..b592efb347ad 100644 --- a/tests/unit/AccountingConnectionConfirmationTest.tsx +++ b/tests/unit/AccountingConnectionConfirmationTest.tsx @@ -53,6 +53,7 @@ jest.mock('@libs/actions/connections', () => ({ jest.mock('@libs/PolicyUtils', () => ({ __esModule: true, isControlPolicy: () => true, + isPolicyFeatureEnabled: (policyToCheck: Policy | undefined) => !!policyToCheck?.areConnectionsEnabled, tryNavigateToSubmitWorkspaceUpgrade: () => false, })); @@ -380,5 +381,43 @@ describe('AccountingContextProvider connect-confirmation prompt', () => { // Then the feature is turned on without navigating away, so the integration can be used once it connects expect(mockEnablePolicyFeatureForConnection).toHaveBeenCalledWith(POLICY_ID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); }); + + it('should open the setup flow a frame after turning the Accounting feature on', async () => { + // Given a workspace with the Accounting feature off + const ref = renderProvider({...policy, areConnectionsEnabled: false}); + + // When a connect flow is started + act(() => { + ref.current?.startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.QBO}); + }); + + // Then the setup flow waits, because its pages redirect to More features if they render before the feature is on + expect(mockEnablePolicyFeatureForConnection).toHaveBeenCalledWith(POLICY_ID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + expect(screen.queryByTestId(SETUP_FLOW_TEST_ID)).not.toBeOnTheScreen(); + + // When the next frame runs + await act(async () => { + await new Promise((resolve) => { + requestAnimationFrame(resolve); + }); + }); + + // Then the setup flow opens + expect(screen.getByTestId(SETUP_FLOW_TEST_ID)).toBeOnTheScreen(); + }); + + it('should open the setup flow right away when the Accounting feature is already on', () => { + // Given a workspace with the Accounting feature already on + const ref = renderProvider({...policy, areConnectionsEnabled: true}); + + // When a connect flow is started + act(() => { + ref.current?.startIntegrationFlow({name: CONST.POLICY.CONNECTIONS.NAME.QBO}); + }); + + // Then setup opens without waiting a frame, because there is nothing to turn on first + expect(mockEnablePolicyFeatureForConnection).not.toHaveBeenCalled(); + expect(screen.getByTestId(SETUP_FLOW_TEST_ID)).toBeOnTheScreen(); + }); }); });