From 336f3e2ed6ff4ff63c03a57b1308090fe37078ff Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 22:03:21 +0000 Subject: [PATCH 01/27] Extend useDocumentTitle hook to all remaining pages Add useDocumentTitle to 27 pages that were missing browser tab titles: - Top-level tabs: Home, Search, Workspaces, Report - Workspace pages: Overview, Workflows, Members, Accounting, More Features, Taxes, Categories, Tags, Reports, Company Cards, Per Diem, Receipt Partners, Distance Rates, Travel, Invoices, Expensify Card, Rules, Time Tracking - Domain pages: Initial, SAML, Admins, Members, Groups Each page now sets document.title with the appropriate context (e.g., "PolicyName - Categories" for workspace pages, "domain.com - SAML" for domain pages). Co-authored-by: Yuwen Memon --- src/pages/Search/SearchPage.tsx | 3 +++ src/pages/domain/Admins/DomainAdminsPage.tsx | 5 ++++- src/pages/domain/DomainInitialPage.tsx | 2 ++ src/pages/domain/DomainSamlPage.tsx | 2 ++ src/pages/domain/Groups/DomainGroupsPage.tsx | 5 ++++- src/pages/domain/Members/DomainMembersPage.tsx | 2 ++ src/pages/home/HomePage.tsx | 3 +++ src/pages/inbox/ReportScreen.tsx | 2 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 2 ++ src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 2 ++ src/pages/workspace/WorkspaceOverviewPage.tsx | 2 ++ src/pages/workspace/WorkspacesListPage.tsx | 3 +++ src/pages/workspace/accounting/PolicyAccountingPage.tsx | 2 ++ src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 ++ .../workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 2 ++ .../workspace/distanceRates/PolicyDistanceRatesPage.tsx | 2 ++ .../workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx | 6 ++++++ src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx | 4 ++++ src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx | 2 ++ .../receiptPartners/WorkspaceReceiptPartnersPage.tsx | 2 ++ src/pages/workspace/reports/WorkspaceReportsPage.tsx | 2 ++ src/pages/workspace/rules/PolicyRulesPage.tsx | 4 ++++ src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 ++ src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 2 ++ .../workspace/timeTracking/WorkspaceTimeTrackingPage.tsx | 4 ++++ src/pages/workspace/travel/PolicyTravelPage.tsx | 2 ++ src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx | 2 ++ 27 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 2c5dbe320d31..172296fba80a 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -23,6 +23,7 @@ import useAllTransactions from '@hooks/useAllTransactions'; import useBulkPayOptions from '@hooks/useBulkPayOptions'; import useConfirmModal from '@hooks/useConfirmModal'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useFilterFormValues from '@hooks/useFilterFormValues'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -226,6 +227,8 @@ function SearchPage({route}: SearchPageProps) { updateAdvancedFilters(formValues, true); }, [formValues]); + useDocumentTitle(translate('common.search')); + useConfirmReadyToOpenApp(); useEffect(() => { diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 46e10add49bc..967d90c47f40 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,8 +1,9 @@ -import {adminAccountIDsSelector, adminPendingActionSelector, technicalContactSettingsSelector} from '@selectors/Domain'; +import {adminAccountIDsSelector, adminPendingActionSelector, domainNameSelector, technicalContactSettingsSelector} from '@selectors/Domain'; import React from 'react'; import Badge from '@components/Badge'; import Button from '@components/Button'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -23,7 +24,9 @@ type DomainAdminsPageProps = PlatformStackScreenProps { diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 84fa8adcd2ca..77e17c12d051 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -8,6 +8,7 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useDefaultFundID from '@hooks/useDefaultFundID'; import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -84,6 +85,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate} = useLocalize(); + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.moreFeatures')}` : ''); const {isBetaEnabled} = usePermissions(); const hasAccountingConnection = hasAccountingConnections(policy); const isAccountingEnabled = !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections); diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 6d0dd62cd5e0..1dcc07006755 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -14,6 +14,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import Section from '@components/Section'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDefaultFundID from '@hooks/useDefaultFundID'; @@ -95,6 +96,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa // When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx. const policy = policyDraft?.id ? policyDraft : policyProp; const policyID = policy?.id; + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.profile')}` : ''); const defaultFundID = useDefaultFundID(policyID); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const isBankAccountVerified = !!cardSettings?.paymentBankAccountID; diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 3ed37e8cf06f..33eaec677759 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -24,6 +24,7 @@ import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; import useCardFeeds from '@hooks/useCardFeeds'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -157,6 +158,8 @@ function WorkspacesListPage() { const [adminAccess] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); + useDocumentTitle(translate('common.workspaces')); + // This hook preloads the screens of adjacent tabs to make changing tabs faster. usePreloadFullScreenNavigators(); diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 5abcdae3a07d..a64404b4aa47 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -21,6 +21,7 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useEnvironment from '@hooks/useEnvironment'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -74,6 +75,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate, datetimeToRelative: getDatetimeToRelative, getLocalDateFromDatetime} = useLocalize(); + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.accounting')}` : ''); const {environment} = useEnvironment(); const oldDotEnvironmentURL = getOldDotURLFromEnvironment(environment); const {isOffline} = useNetwork(); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 8e59ab3b46ee..e0b0788826c0 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -24,6 +24,7 @@ import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Switch from '@components/Switch'; import Text from '@components/Text'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; @@ -79,6 +80,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const isMobileSelectionModeEnabled = useMobileSelectionMode(); const policyData = usePolicyData(policyId); const {policy, categories: policyCategories} = policyData; + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.categories')}` : ''); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 5aaaa7fc2124..dfee233dc481 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import DecisionModal from '@components/DecisionModal'; import useAssignCard from '@hooks/useAssignCard'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useCompanyCards from '@hooks/useCompanyCards'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -28,6 +29,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const policy = usePolicy(policyID); + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.companyCards')}` : ''); const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; const companyCards = useCompanyCards({policyID}); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 348d755f5d58..fea44a8ad75c 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -18,6 +18,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import Switch from '@components/Switch'; import Text from '@components/Text'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyAsset, useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -70,6 +71,7 @@ function PolicyDistanceRatesPage({ const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const policy = usePolicy(policyID); + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.distanceRates')}` : ''); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const canSelectMultiple = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx index 252fdba24625..e192a489be98 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx @@ -1,8 +1,11 @@ import React, {useCallback, useEffect} from 'react'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useDefaultFundID from '@hooks/useDefaultFundID'; +import useDocumentTitle from '@hooks/useDocumentTitle'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import {updateSelectedExpensifyCardFeed} from '@libs/actions/Card'; import {filterInactiveCards} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -19,6 +22,9 @@ type WorkspaceExpensifyCardPageProps = PlatformStackScreenProps(null); const policy = usePolicy(policyID); + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.receiptPartners')}` : ''); const {getReceiptPartnersIntegrationData, shouldShowEnterCredentialsError, isUberConnected} = useGetReceiptPartnersIntegrationData(policyID); const [selectedPartner, setSelectedPartner] = useState(null); const isLoading = policy?.isLoading; diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index 1677038c8988..0bbbb14e696b 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -17,6 +17,7 @@ import Section from '@components/Section'; import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -64,6 +65,7 @@ function WorkspaceReportFieldsPage({ const {translate, localeCompare} = useLocalize(); const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); const policy = usePolicy(policyID); + useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.reports')}` : ''); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx index 04dc9e1da121..4fdb15529559 100644 --- a/src/pages/workspace/rules/PolicyRulesPage.tsx +++ b/src/pages/workspace/rules/PolicyRulesPage.tsx @@ -1,7 +1,9 @@ import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {openPolicyRulesPage} from '@libs/actions/Policy/Rules'; @@ -19,6 +21,8 @@ type PolicyRulesPageProps = PlatformStackScreenProps([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const isMobileSelectionModeEnabled = useMobileSelectionMode(); diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx index 1a057a7cef36..851b108fca90 100644 --- a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx +++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx @@ -1,7 +1,9 @@ import React from 'react'; import {View} from 'react-native'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -17,6 +19,8 @@ type WorkspaceTimeTrackingPageProps = PlatformStackScreenProps Date: Tue, 24 Feb 2026 22:07:32 +0000 Subject: [PATCH 02/27] Fix: Sort imports to match Prettier configuration Co-authored-by: Yuwen Memon --- src/pages/Search/SearchPage.tsx | 2 +- src/pages/inbox/ReportScreen.tsx | 2 +- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- src/pages/workspace/WorkspaceOverviewPage.tsx | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 2 +- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- .../workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 2 +- src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx | 4 ++-- src/pages/workspace/reports/WorkspaceReportsPage.tsx | 2 +- src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 172296fba80a..52a754fc132c 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -23,8 +23,8 @@ import useAllTransactions from '@hooks/useAllTransactions'; import useBulkPayOptions from '@hooks/useBulkPayOptions'; import useConfirmModal from '@hooks/useConfirmModal'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useFilterFormValues from '@hooks/useFilterFormValues'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index a14767a8b725..257c69e02f02 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -24,9 +24,9 @@ import useShowWideRHPVersion from '@components/WideRHPContextProvider/useShowWid import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper'; import useActionListContextValue from '@hooks/useActionListContextValue'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet'; import {useCurrentReportIDState} from '@hooks/useCurrentReportID'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useIsAnonymousUser from '@hooks/useIsAnonymousUser'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 77e17c12d051..82bfbfdfb524 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -8,8 +8,8 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useDefaultFundID from '@hooks/useDefaultFundID'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 1dcc07006755..3d57313c61e8 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -14,10 +14,10 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import Section from '@components/Section'; import useCardFeeds from '@hooks/useCardFeeds'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDefaultFundID from '@hooks/useDefaultFundID'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 33eaec677759..c5af6cc3b591 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -24,8 +24,8 @@ import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler'; import useCardFeeds from '@hooks/useCardFeeds'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index e0b0788826c0..19a8ec5c7816 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -24,9 +24,9 @@ import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Switch from '@components/Switch'; import Text from '@components/Text'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index dfee233dc481..bda26c06608f 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import DecisionModal from '@components/DecisionModal'; import useAssignCard from '@hooks/useAssignCard'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useCompanyCards from '@hooks/useCompanyCards'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index 25e64308da8c..55115e609a79 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -1,13 +1,13 @@ import React from 'react'; import {View} from 'react-native'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useDocumentTitle from '@hooks/useDocumentTitle'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; -import usePolicy from '@hooks/usePolicy'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import CONST from '@src/CONST'; diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index 0bbbb14e696b..d7285e64bcab 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -16,8 +16,8 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; -import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useDocumentTitle from '@hooks/useDocumentTitle'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 3207d671bf0c..b8734a511f6f 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -16,8 +16,8 @@ import SearchBar from '@components/SearchBar'; import Section from '@components/Section'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import useConfirmModal from '@hooks/useConfirmModal'; +import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; From 17c76b1eadcb8abe49f818f9c16eedfa80e19608 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Tue, 24 Feb 2026 22:26:00 +0000 Subject: [PATCH 03/27] Fix: Update WalletStatementPage for new addEncryptedAuthTokenToURL signature The addEncryptedAuthTokenToURL function signature changed to require encryptedAuthToken as an explicit parameter. Updated the caller in WalletStatementPage to pass the token from useSession(), matching the pattern used by other updated callers in the codebase. Co-authored-by: Yuwen Memon --- src/pages/wallet/WalletStatementPage.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/wallet/WalletStatementPage.tsx b/src/pages/wallet/WalletStatementPage.tsx index 897cc8ba3ad0..72602eab77c3 100644 --- a/src/pages/wallet/WalletStatementPage.tsx +++ b/src/pages/wallet/WalletStatementPage.tsx @@ -3,6 +3,7 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useState} from 'react'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useSession} from '@components/OnyxListItemProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import WalletStatementModal from '@components/WalletStatementModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -29,6 +30,8 @@ type WalletStatementPageProps = PlatformStackScreenProps setIsDownloading(false)); + fileDownload(translate, addEncryptedAuthTokenToURL(pdfURL, encryptedAuthToken, true), downloadFileName, '', isMobileSafari()).finally(() => setIsDownloading(false)); return; } generateStatementPDF(yearMonth); - }, [baseURL, currentUserLogin, isWalletStatementGenerating, translate, walletStatement, yearMonth]); + }, [baseURL, currentUserLogin, encryptedAuthToken, isWalletStatementGenerating, translate, walletStatement, yearMonth]); // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { From 30870d4120de7c4e0002c9c6f51616e0d121a640 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Wed, 25 Feb 2026 01:35:41 +0000 Subject: [PATCH 04/27] Extract useWorkspaceDocumentTitle and useDomainDocumentTitle hooks Address CONSISTENCY-3 review feedback by extracting shared helpers to eliminate duplicated title formatting across 18 workspace pages and 4 domain pages. Co-authored-by: Yuwen Memon --- src/hooks/useDomainDocumentTitle.ts | 14 ++++++++++++++ src/hooks/useWorkspaceDocumentTitle.ts | 14 ++++++++++++++ src/pages/domain/Admins/DomainAdminsPage.tsx | 4 ++-- src/pages/domain/DomainSamlPage.tsx | 4 ++-- src/pages/domain/Groups/DomainGroupsPage.tsx | 4 ++-- src/pages/domain/Members/DomainMembersPage.tsx | 4 ++-- src/pages/workspace/WorkspaceMembersPage.tsx | 4 ++-- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 4 ++-- src/pages/workspace/WorkspaceOverviewPage.tsx | 4 ++-- .../workspace/accounting/PolicyAccountingPage.tsx | 4 ++-- .../categories/WorkspaceCategoriesPage.tsx | 4 ++-- .../companyCards/WorkspaceCompanyCardsPage.tsx | 4 ++-- .../distanceRates/PolicyDistanceRatesPage.tsx | 4 ++-- .../expensifyCard/WorkspaceExpensifyCardPage.tsx | 6 ++---- .../workspace/invoices/WorkspaceInvoicesPage.tsx | 4 ++-- .../workspace/perDiem/WorkspacePerDiemPage.tsx | 4 ++-- .../WorkspaceReceiptPartnersPage.tsx | 4 ++-- .../workspace/reports/WorkspaceReportsPage.tsx | 4 ++-- src/pages/workspace/rules/PolicyRulesPage.tsx | 4 ++-- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 4 ++-- src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 4 ++-- .../timeTracking/WorkspaceTimeTrackingPage.tsx | 4 ++-- src/pages/workspace/travel/PolicyTravelPage.tsx | 4 ++-- .../workspace/workflows/WorkspaceWorkflowsPage.tsx | 4 ++-- 24 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 src/hooks/useDomainDocumentTitle.ts create mode 100644 src/hooks/useWorkspaceDocumentTitle.ts diff --git a/src/hooks/useDomainDocumentTitle.ts b/src/hooks/useDomainDocumentTitle.ts new file mode 100644 index 000000000000..d151f988ca3f --- /dev/null +++ b/src/hooks/useDomainDocumentTitle.ts @@ -0,0 +1,14 @@ +import type {TranslationPaths} from '@src/languages/types'; +import useDocumentTitle from './useDocumentTitle'; +import useLocalize from './useLocalize'; + +/** + * Sets the browser document title for domain pages using the format "domainName - PageTitle". + * Falls back to empty string (default title) if domain name is not yet available. + */ +function useDomainDocumentTitle(domainName: string | undefined, titleKey: TranslationPaths) { + const {translate} = useLocalize(); + useDocumentTitle(domainName ? `${domainName} - ${translate(titleKey)}` : ''); +} + +export default useDomainDocumentTitle; diff --git a/src/hooks/useWorkspaceDocumentTitle.ts b/src/hooks/useWorkspaceDocumentTitle.ts new file mode 100644 index 000000000000..27a58237b0f6 --- /dev/null +++ b/src/hooks/useWorkspaceDocumentTitle.ts @@ -0,0 +1,14 @@ +import type {TranslationPaths} from '@src/languages/types'; +import useDocumentTitle from './useDocumentTitle'; +import useLocalize from './useLocalize'; + +/** + * Sets the browser document title for workspace pages using the format "PolicyName - PageTitle". + * Falls back to empty string (default title) if policy name is not yet available. + */ +function useWorkspaceDocumentTitle(policyName: string | undefined, titleKey: TranslationPaths) { + const {translate} = useLocalize(); + useDocumentTitle(policyName ? `${policyName} - ${translate(titleKey)}` : ''); +} + +export default useWorkspaceDocumentTitle; diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 967d90c47f40..9e0ee57135e9 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -3,7 +3,7 @@ import React from 'react'; import Badge from '@components/Badge'; import Button from '@components/Button'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useDomainDocumentTitle from '@hooks/useDomainDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -26,7 +26,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const {domainAccountID} = route.params; const [domainName] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: domainNameSelector}); const {translate} = useLocalize(); - useDocumentTitle(domainName ? `${domainName} - ${translate('domain.domainAdmins')}` : ''); + useDomainDocumentTitle(domainName, 'domain.domainAdmins'); const styles = useThemeStyles(); const theme = useTheme(); const {shouldUseNarrowLayout} = useResponsiveLayout(); diff --git a/src/pages/domain/DomainSamlPage.tsx b/src/pages/domain/DomainSamlPage.tsx index adad03f7d2a3..548cf501f6bd 100644 --- a/src/pages/domain/DomainSamlPage.tsx +++ b/src/pages/domain/DomainSamlPage.tsx @@ -10,7 +10,7 @@ import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useDomainDocumentTitle from '@hooks/useDomainDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -46,7 +46,7 @@ function DomainSamlPage({route}: DomainSamlPageProps) { const isSamlEnabled = !!domainSettings?.samlEnabled; const isSamlRequired = !!domainSettings?.samlRequired; const domainName = domain ? Str.extractEmailDomain(domain.email) : undefined; - useDocumentTitle(domainName ? `${domainName} - ${translate('domain.saml')}` : ''); + useDomainDocumentTitle(domainName, 'domain.saml'); const doesDomainExist = !!domain; const samlFeatures: FeatureListItem[] = useMemo( diff --git a/src/pages/domain/Groups/DomainGroupsPage.tsx b/src/pages/domain/Groups/DomainGroupsPage.tsx index ac667e39c7ad..38e7f0c268fb 100644 --- a/src/pages/domain/Groups/DomainGroupsPage.tsx +++ b/src/pages/domain/Groups/DomainGroupsPage.tsx @@ -8,7 +8,7 @@ import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/ListItem/TableListItem'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import Text from '@components/Text'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useDomainDocumentTitle from '@hooks/useDomainDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -30,7 +30,7 @@ function DomainGroupsPage({route}: DomainGroupsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [domainName] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: domainNameSelector}); - useDocumentTitle(domainName ? `${domainName} - ${translate('domain.groups.title')}` : ''); + useDomainDocumentTitle(domainName, 'domain.groups.title'); const illustrations = useMemoizedLazyIllustrations(['Members']); const {shouldUseNarrowLayout} = useResponsiveLayout(); diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 63023644f04c..ec54dd4a8571 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -6,7 +6,7 @@ import type {DomainMemberBulkActionType, DropdownOption} from '@components/Butto import DecisionModal from '@components/DecisionModal'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useDomainDocumentTitle from '@hooks/useDomainDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -53,7 +53,7 @@ function DomainMembersPage({route}: DomainMembersPageProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`); const [domainName] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {selector: domainNameSelector}); - useDocumentTitle(domainName ? `${domainName} - ${translate('domain.domainMembers')}` : ''); + useDomainDocumentTitle(domainName, 'domain.domainMembers'); // We need to use isSmallScreenWidth here because the DecisionModal is opening from RHP and ShouldUseNarrowLayout layout will not work in this place. // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 3346ba617a6b..1ee885d4a546 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -20,7 +20,7 @@ import Text from '@components/Text'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -114,7 +114,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const {translate, formatPhoneNumber, localeCompare} = useLocalize(); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('common.members')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'common.members'); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); const filterEmployees = useCallback( (employee: PolicyEmployee | undefined) => { diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 82bfbfdfb524..ee86beb8b89b 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -9,7 +9,7 @@ import Section from '@components/Section'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; import useDefaultFundID from '@hooks/useDefaultFundID'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -85,7 +85,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate} = useLocalize(); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.moreFeatures')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.moreFeatures'); const {isBetaEnabled} = usePermissions(); const hasAccountingConnection = hasAccountingConnections(policy); const isAccountingEnabled = !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections); diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 3d57313c61e8..df3cac711246 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -17,7 +17,7 @@ import useCardFeeds from '@hooks/useCardFeeds'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDefaultFundID from '@hooks/useDefaultFundID'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -96,7 +96,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa // When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx. const policy = policyDraft?.id ? policyDraft : policyProp; const policyID = policy?.id; - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.profile')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.profile'); const defaultFundID = useDefaultFundID(policyID); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const isBankAccountVerified = !!cardSettings?.paymentBankAccountID; diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index a64404b4aa47..8ef3fc79868b 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -21,7 +21,7 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useEnvironment from '@hooks/useEnvironment'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -75,7 +75,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate, datetimeToRelative: getDatetimeToRelative, getLocalDateFromDatetime} = useLocalize(); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.accounting')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.accounting'); const {environment} = useEnvironment(); const oldDotEnvironmentURL = getOldDotURLFromEnvironment(environment); const {isOffline} = useNetwork(); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 19a8ec5c7816..485f9f31d383 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -26,7 +26,7 @@ import Text from '@components/Text'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -80,7 +80,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const isMobileSelectionModeEnabled = useMobileSelectionMode(); const policyData = usePolicyData(policyId); const {policy, categories: policyCategories} = policyData; - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.categories')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.categories'); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index bda26c06608f..8169521a8e81 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import DecisionModal from '@components/DecisionModal'; import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -29,7 +29,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.companyCards')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.companyCards'); const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; const companyCards = useCompanyCards({policyID}); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index fea44a8ad75c..6aef72ce187e 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -18,7 +18,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import Switch from '@components/Switch'; import Text from '@components/Text'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyAsset, useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -71,7 +71,7 @@ function PolicyDistanceRatesPage({ const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.distanceRates')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.distanceRates'); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const canSelectMultiple = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx index e192a489be98..b9d7ae3b8f5f 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx @@ -1,8 +1,7 @@ import React, {useCallback, useEffect} from 'react'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useDefaultFundID from '@hooks/useDefaultFundID'; -import useDocumentTitle from '@hooks/useDocumentTitle'; -import useLocalize from '@hooks/useLocalize'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; @@ -22,9 +21,8 @@ type WorkspaceExpensifyCardPageProps = PlatformStackScreenProps(null); const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.receiptPartners')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.receiptPartners'); const {getReceiptPartnersIntegrationData, shouldShowEnterCredentialsError, isUberConnected} = useGetReceiptPartnersIntegrationData(policyID); const [selectedPartner, setSelectedPartner] = useState(null); const isLoading = policy?.isLoading; diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index d7285e64bcab..d57bd8f85430 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -16,7 +16,7 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -65,7 +65,7 @@ function WorkspaceReportFieldsPage({ const {translate, localeCompare} = useLocalize(); const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.reports')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.reports'); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx index 4fdb15529559..3b8e0d088c11 100644 --- a/src/pages/workspace/rules/PolicyRulesPage.tsx +++ b/src/pages/workspace/rules/PolicyRulesPage.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; @@ -22,7 +22,7 @@ function PolicyRulesPage({route}: PolicyRulesPageProps) { const {translate} = useLocalize(); const {policyID} = route.params; const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.rules')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.rules'); const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const illustrations = useMemoizedLazyIllustrations(['Rules']); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index cdb66cee4b75..173d444a1ce5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -23,7 +23,7 @@ import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Switch from '@components/Switch'; import Text from '@components/Text'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -96,7 +96,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const {backTo, policyID} = route.params; const policyData = usePolicyData(policyID); const {policy, tags: policyTags} = policyData; - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.tags')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.tags'); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const {environmentURL} = useEnvironment(); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 629ad72a91e9..dce645a4a678 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -18,7 +18,7 @@ import CustomListHeader from '@components/SelectionListWithModal/CustomListHeade import Switch from '@components/Switch'; import Text from '@components/Text'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -66,7 +66,7 @@ function WorkspaceTaxesPage({ const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.taxes')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.taxes'); const [selectedTaxesIDs, setSelectedTaxesIDs] = useState([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const isMobileSelectionModeEnabled = useMobileSelectionMode(); diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx index 851b108fca90..b649c3323007 100644 --- a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx +++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {View} from 'react-native'; -import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,7 +20,7 @@ function WorkspaceTimeTrackingPage({route}: WorkspaceTimeTrackingPageProps) { const {policyID} = route.params; const {translate} = useLocalize(); const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.moreFeatures.timeTracking.title')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.moreFeatures.timeTracking.title'); const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['Clock']); const {shouldUseNarrowLayout} = useResponsiveLayout(); diff --git a/src/pages/workspace/travel/PolicyTravelPage.tsx b/src/pages/workspace/travel/PolicyTravelPage.tsx index 93d51279a0f4..efe7e27427c4 100644 --- a/src/pages/workspace/travel/PolicyTravelPage.tsx +++ b/src/pages/workspace/travel/PolicyTravelPage.tsx @@ -5,7 +5,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -41,7 +41,7 @@ function WorkspaceTravelPage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const policy = usePolicy(policyID); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.travel')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.travel'); const illustrations = useMemoizedLazyIllustrations(['Luggage'] as const); const workspaceAccountID = useWorkspaceAccountID(policyID); diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index b8734a511f6f..4a0fb591f95a 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -17,7 +17,7 @@ import Section from '@components/Section'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; import useConfirmModal from '@hooks/useConfirmModal'; -import useDocumentTitle from '@hooks/useDocumentTitle'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -82,7 +82,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply a correct padding style // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - useDocumentTitle(policy?.name ? `${policy.name} - ${translate('workspace.common.workflows')}` : ''); + useWorkspaceDocumentTitle(policy?.name, 'workspace.common.workflows'); const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; const [cardFeeds] = useCardFeeds(policy?.id); const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`); From 82a3f70559c1bfa9eec12807b321959d1f6e9387 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Wed, 25 Feb 2026 01:44:02 +0000 Subject: [PATCH 05/27] Gate useDocumentTitle on screen focus to prevent title overwrites from preloaded screens usePreloadFullScreenNavigators mounts screens in the React tree with display:none, causing their useEffect hooks to fire. This meant preloaded tabs (Workspaces, Settings) could overwrite document.title even when the user was viewing a different tab. Switching from useEffect to useFocusEffect ensures setPageTitle only runs when the screen actually has focus. Co-authored-by: Yuwen Memon --- src/hooks/useDocumentTitle.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/hooks/useDocumentTitle.ts b/src/hooks/useDocumentTitle.ts index 87db95bfddad..fee445f7d63f 100644 --- a/src/hooks/useDocumentTitle.ts +++ b/src/hooks/useDocumentTitle.ts @@ -1,15 +1,18 @@ -import {useEffect} from 'react'; +import {useCallback} from 'react'; +import {useFocusEffect} from '@react-navigation/native'; import {setPageTitle} from '@libs/UnreadIndicatorUpdater/updateUnread'; function useDocumentTitle(title: string) { - useEffect(() => { - setPageTitle(title); + useFocusEffect( + useCallback(() => { + setPageTitle(title); - // Reset to default title when component unmounts - return () => { - setPageTitle(''); - }; - }, [title]); + // Reset to default title when screen loses focus or unmounts + return () => { + setPageTitle(''); + }; + }, [title]), + ); } export default useDocumentTitle; From 3ea905c54f2e119ab3d2e281bee4e6e9007c5c77 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Wed, 25 Feb 2026 01:46:31 +0000 Subject: [PATCH 06/27] Fix: Sort imports to match Prettier configuration Co-authored-by: Yuwen Memon --- src/hooks/useDocumentTitle.ts | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- src/pages/workspace/WorkspaceOverviewPage.tsx | 2 +- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 2 +- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 2 +- src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx | 2 +- src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx | 2 +- src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx | 2 +- .../workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx | 2 +- src/pages/workspace/reports/WorkspaceReportsPage.tsx | 2 +- src/pages/workspace/rules/PolicyRulesPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 2 +- src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx | 2 +- src/pages/workspace/travel/PolicyTravelPage.tsx | 2 +- src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/hooks/useDocumentTitle.ts b/src/hooks/useDocumentTitle.ts index fee445f7d63f..6c56c4031242 100644 --- a/src/hooks/useDocumentTitle.ts +++ b/src/hooks/useDocumentTitle.ts @@ -1,5 +1,5 @@ -import {useCallback} from 'react'; import {useFocusEffect} from '@react-navigation/native'; +import {useCallback} from 'react'; import {setPageTitle} from '@libs/UnreadIndicatorUpdater/updateUnread'; function useDocumentTitle(title: string) { diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 250fe82b8c8a..366f0405d4ed 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -22,7 +22,6 @@ import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; @@ -33,6 +32,7 @@ import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import { clearAddMemberError, diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 5397e2c2105d..b4c4ba4ec947 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -11,7 +11,6 @@ import useCardFeeds from '@hooks/useCardFeeds'; import useDefaultFundID from '@hooks/useDefaultFundID'; import useIsPolicyConnectedToUberReceiptPartner from '@hooks/useIsPolicyConnectedToUberReceiptPartner'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -20,6 +19,7 @@ import usePolicyData from '@hooks/usePolicyData'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {enablePolicyTravel} from '@libs/actions/Policy/Travel'; import {filterInactiveCards, getAllCardsForWorkspace, getCompanyFeeds, isSmartLimitEnabled as isSmartLimitEnabledUtil} from '@libs/CardUtils'; import {getLatestErrorField} from '@libs/ErrorUtils'; diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 2f08a3df0eff..7e54849dd314 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -19,7 +19,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDefaultFundID from '@hooks/useDefaultFundID'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePayAndDowngrade from '@hooks/usePayAndDowngrade'; @@ -29,6 +28,7 @@ import usePrivateSubscription from '@hooks/usePrivateSubscription'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolationOfWorkspace from '@hooks/useTransactionViolationOfWorkspace'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {close} from '@libs/actions/Modal'; import {clearInviteDraft, clearWorkspaceOwnerChangeFlow, isApprover as isApproverUserAction, requestWorkspaceOwnerChange} from '@libs/actions/Policy/Member'; import { diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 60d97d7db38a..e9adda2426e0 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -25,13 +25,13 @@ import useEnvironment from '@hooks/useEnvironment'; import useExpensifyCardFeeds from '@hooks/useExpensifyCardFeeds'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {isAuthenticationError, isConnectionInProgress, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections'; import {shouldShowQBOReimbursableExportDestinationAccountError} from '@libs/actions/connections/QuickbooksOnline'; import {isExpensifyCardFullySetUp} from '@libs/CardUtils'; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index dfb0d83bf61a..b60494717d77 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -28,7 +28,6 @@ import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; @@ -40,6 +39,7 @@ import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index f050a0dc7057..3edfbaf1529e 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -4,10 +4,10 @@ import useAssignCard from '@hooks/useAssignCard'; import useCompanyCards from '@hooks/useCompanyCards'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index fabfcc73fe77..a563f6d330a8 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -26,11 +26,11 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolation from '@hooks/useTransactionViolation'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import { clearCreateDistanceRateItemAndError, diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index e4f53e58d8a6..d5f6d4e67205 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -3,9 +3,9 @@ import {View} from 'react-native'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index eab52156c9de..3ae2f76263e4 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -26,10 +26,10 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {convertAmountToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx index 8b1e715ac012..4695aeeb3c3b 100644 --- a/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx +++ b/src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx @@ -21,9 +21,9 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; 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'; diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index 0be2bcbc602c..1a3e5f30a336 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -22,8 +22,8 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; import {clearPolicyTitleFieldError, enablePolicyReportFields, setPolicyPreventMemberCreatedTitle} from '@libs/actions/Policy/Policy'; import {getLatestErrorField} from '@libs/ErrorUtils'; diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx index 541678f282fd..6714e1c6eb62 100644 --- a/src/pages/workspace/rules/PolicyRulesPage.tsx +++ b/src/pages/workspace/rules/PolicyRulesPage.tsx @@ -3,9 +3,9 @@ import {View} from 'react-native'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {openPolicyRulesPage} from '@libs/actions/Policy/Rules'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index a3138300a706..961747aa9b2a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -25,7 +25,6 @@ import Text from '@components/Text'; import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; @@ -36,6 +35,7 @@ import useSearchBackPress from '@hooks/useSearchBackPress'; import useSearchResults from '@hooks/useSearchResults'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import { diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index d7f2b37a0f30..d8f6324b2ebd 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -25,9 +25,9 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useSearchResults from '@hooks/useSearchResults'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {clearTaxRateError, deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; diff --git a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx index a685bb6412c2..fb8638894274 100644 --- a/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx +++ b/src/pages/workspace/timeTracking/WorkspaceTimeTrackingPage.tsx @@ -3,9 +3,9 @@ import {View} from 'react-native'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; diff --git a/src/pages/workspace/travel/PolicyTravelPage.tsx b/src/pages/workspace/travel/PolicyTravelPage.tsx index 29e391c715c0..17eb6b75037f 100644 --- a/src/pages/workspace/travel/PolicyTravelPage.tsx +++ b/src/pages/workspace/travel/PolicyTravelPage.tsx @@ -12,9 +12,9 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {openPolicyTravelPage} from '@libs/actions/TravelInvoicing'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 2aeb9405bcb8..d367154143a7 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -20,12 +20,12 @@ import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import { clearPolicyErrorField, isCurrencySupportedForDirectReimbursement, From 3937719cdcad83626785c8a921dc2a78128b83bb Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Wed, 25 Feb 2026 19:22:42 +0000 Subject: [PATCH 07/27] Fix: Reset SearchPage, ReportScreen, and WorkspaceOverviewPage to main and re-apply useDocumentTitle The merge commit incorrectly resolved conflicts in these files, keeping older versions that used the removed useSearchContext API and dropped recently added functionality (clientID, TransactionNavigationUtils). Reset all three to main's version and re-applied only the intended useDocumentTitle additions. Co-authored-by: Yuwen Memon --- src/pages/Search/SearchPage.tsx | 183 +++++++++++++----- src/pages/inbox/ReportScreen.tsx | 27 ++- src/pages/workspace/WorkspaceOverviewPage.tsx | 27 +++ 3 files changed, 191 insertions(+), 46 deletions(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index bbe8966d81d7..1ee06cd51744 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -15,7 +15,7 @@ import type {PaymentMethodType} from '@components/KYCWall/types'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; -import {useSearchContext} from '@components/Search/SearchContext'; +import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import type {SearchHeaderOptionValue} from '@components/Search/SearchPageHeader/SearchPageHeader'; import type {PaymentData, SearchParams} from '@components/Search/types'; import {usePlaybackActionsContext} from '@components/VideoPlayerContexts/PlaybackContext'; @@ -40,11 +40,12 @@ import useSelfDMReport from '@hooks/useSelfDMReport'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; -import {deleteAppReport, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; +import {deleteAppReport, markAsManuallyExported, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter, searchInServer} from '@libs/actions/Report'; import { approveMoneyRequestOnSearch, bulkDeleteReports, exportSearchItemsToCSV, + exportToIntegrationOnSearch, getExportTemplates, getLastPolicyBankAccountID, getLastPolicyPaymentMethod, @@ -67,9 +68,10 @@ import {getTransactionsAndReportsFromSearch} from '@libs/MergeTransactionUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; -import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; +import {getConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; +import {getSecondaryExportReportActions, isMergeActionForSelectedTransactions} from '@libs/ReportSecondaryActionUtils'; import { + getIntegrationIcon, getReportOrDraftReport, isBusinessInvoiceRoom, isCurrentUserSubmitter, @@ -105,17 +107,8 @@ function SearchPage({route}: SearchPageProps) { const {isOffline} = useNetwork(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const { - selectedTransactions, - clearSelectedTransactions, - selectedReports, - lastSearchType, - setLastSearchType, - areAllMatchingItemsSelected, - selectAllMatchingItems, - currentSearchKey, - currentSearchResults, - } = useSearchContext(); + const {selectedTransactions, selectedReports, lastSearchType, areAllMatchingItemsSelected, currentSearchKey, currentSearchResults} = useSearchStateContext(); + const {clearSelectedTransactions, setLastSearchType, selectAllMatchingItems} = useSearchActionsContext(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMobileSelectionModeEnabled = useMobileSelectionMode(clearSelectedTransactions); const allTransactions = useAllTransactions(); @@ -165,6 +158,12 @@ function SearchPage({route}: SearchPageProps) { 'SmartScan', 'MoneyBag', 'ArrowSplit', + 'QBOSquare', + 'XeroSquare', + 'NetSuiteSquare', + 'IntacctSquare', + 'QBDSquare', + 'Pencil', ] as const); const lastNonEmptySearchResults = useRef(undefined); @@ -204,10 +203,12 @@ function SearchPage({route}: SearchPageProps) { const report = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const chatReportID = report?.chatReportID; const chatReport = chatReportID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined; + const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; + const invoiceReceiverPolicy = invoiceReceiverPolicyID ? currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`] : undefined; return ( report && - !canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, false) && - canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, true) + !canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, false, undefined, invoiceReceiverPolicy) && + canIOUBePaid(report, chatReport, selectedPolicy, bankAccountList, undefined, true, undefined, invoiceReceiverPolicy) ); }); }, [currentSearchResults?.data, selectedPolicyIDs, selectedReportIDs, selectedTransactionReportIDs, bankAccountList]); @@ -389,6 +390,7 @@ function SearchPage({route}: SearchPageProps) { selectedTransactionsKeys, translate, clearSelectedTransactions, + setIsDownloadErrorModalVisible, showConfirmModal, hash, selectAllMatchingItems, @@ -739,21 +741,10 @@ function SearchPage({route}: SearchPageProps) { const typeExpenseReport = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT; - // Gets the list of options for the export sub-menu // Gets the list of options for the export sub-menu const getExportOptions = () => { // We provide the basic and expense level export options by default - const exportOptions: PopoverMenuItem[] = [ - { - text: translate('export.basicExport'), - icon: expensifyIcons.Table, - onSelected: () => { - handleBasicExport(); - }, - shouldCloseModalOnSelect: true, - shouldCallAfterModalHide: true, - }, - ]; + const exportOptions: PopoverMenuItem[] = []; // Determine if only full reports are selected by comparing the reportIDs of the selected transactions and the reportIDs of the selected reports const areFullReportsSelected = selectedTransactionReportIDs.length === selectedReportIDs.length && selectedTransactionReportIDs.every((id) => selectedReportIDs.includes(id)); @@ -765,8 +756,119 @@ function SearchPage({route}: SearchPageProps) { // the selected expenses are the only expenses of their parent expense report include the report level export option. const includeReportLevelExport = ((typeExpenseReport || typeInvoice) && areFullReportsSelected) || (typeExpense && !typeExpenseReport && isAllOneTransactionReport); - // Collect a list of export templates available to the user from their account, policy, and custom integrations templates const policy = selectedPolicyIDs.length === 1 ? policies?.[`${ONYXKEYS.COLLECTION.POLICY}${selectedPolicyIDs.at(0)}`] : undefined; + const connectedIntegration = getConnectedIntegration(policy); + const isReportsTab = typeExpenseReport; + + const canReportBeExported = (report: (typeof selectedReports)[0]) => { + if (!report.reportID) { + return false; + } + + const reportPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const completeReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; + + if (!completeReport) { + return false; + } + + const reportExportOptions = getSecondaryExportReportActions( + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, + currentUserPersonalDetails?.login ?? '', + completeReport, + bankAccountList, + reportPolicy, + ); + + return reportExportOptions.includes(CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION); + }; + + const canExportAllReports = isReportsTab && selectedReportIDs.length > 0 && includeReportLevelExport && selectedReports.every(canReportBeExported); + + if (canExportAllReports && connectedIntegration) { + const connectionNameFriendly = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectedIntegration]; + const integrationIcon = getIntegrationIcon(connectedIntegration, expensifyIcons); + + const handleExportAction = (exportAction: () => void) => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + const exportedReportNames: string[] = []; + let areAnyReportsExported = false; + + for (const reportID of selectedReportIDs) { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + + if (!report?.isExportedToIntegration) { + continue; + } + + areAnyReportsExported = true; + + if (report.reportName) { + exportedReportNames.push(report.reportName); + } + } + + if (areAnyReportsExported) { + showConfirmModal({ + title: translate('workspace.exportAgainModal.title'), + prompt: translate('workspace.exportAgainModal.description', { + connectionName: connectedIntegration, + reportName: exportedReportNames.join('\n'), + }), + confirmText: translate('workspace.exportAgainModal.confirmText'), + cancelText: translate('workspace.exportAgainModal.cancelText'), + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } + + if (hash) { + clearSelectedTransactions(); + exportAction(); + } + }); + } else if (hash) { + exportAction(); + clearSelectedTransactions(); + } + }; + + exportOptions.push( + { + text: connectionNameFriendly, + icon: integrationIcon, + onSelected: () => handleExportAction(() => exportToIntegrationOnSearch(hash, selectedReportIDs, connectedIntegration)), + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + displayInDefaultIconColor: true, + additionalIconStyles: styles.integrationIcon, + }, + { + text: translate('workspace.common.markAsExported'), + icon: integrationIcon, + onSelected: () => handleExportAction(() => markAsManuallyExported(selectedReportIDs, connectedIntegration)), + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + displayInDefaultIconColor: true, + additionalIconStyles: styles.integrationIcon, + }, + ); + } + + exportOptions.push({ + text: translate('export.basicExport'), + icon: expensifyIcons.Table, + onSelected: () => { + handleBasicExport(); + }, + shouldCloseModalOnSelect: true, + shouldCallAfterModalHide: true, + }); + const exportTemplates = getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, translate, policy, includeReportLevelExport); for (const template of exportTemplates) { exportOptions.push({ @@ -1074,19 +1176,7 @@ function SearchPage({route}: SearchPageProps) { hash, selectedTransactions, queryJSON?.type, - expensifyIcons.Export, - expensifyIcons.ArrowRight, - expensifyIcons.Table, - expensifyIcons.ThumbsUp, - expensifyIcons.ThumbsDown, - expensifyIcons.Send, - expensifyIcons.MoneyBag, - expensifyIcons.Stopwatch, - expensifyIcons.ArrowCollapse, - expensifyIcons.DocumentMerge, - expensifyIcons.ArrowSplit, - expensifyIcons.Trashcan, - expensifyIcons.Exclamation, + expensifyIcons, translate, areAllMatchingItemsSelected, isOffline, @@ -1101,6 +1191,9 @@ function SearchPage({route}: SearchPageProps) { policies, integrationsExportTemplates, csvExportLayouts, + currentUserPersonalDetails.accountID, + currentUserPersonalDetails?.login, + bankAccountList, handleBasicExport, beginExportWithTemplate, handleApproveWithDEWCheck, @@ -1113,8 +1206,8 @@ function SearchPage({route}: SearchPageProps) { onBulkPaySelected, areAllTransactionsFromSubmitter, dismissedHoldUseExplanation, - currentUserPersonalDetails.accountID, localeCompare, + allReports, firstTransaction, firstTransactionPolicy, handleDeleteSelectedTransactions, @@ -1122,6 +1215,8 @@ function SearchPage({route}: SearchPageProps) { styles.colorMuted, styles.fontWeightNormal, styles.textWrap, + styles.integrationIcon, + showConfirmModal, ]); const {initScanRequest, PDFValidationComponent, ErrorModal} = useReceiptScanDrop(); diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index 93bcb31d7d72..ee7aa811a848 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -90,6 +90,7 @@ import { isValidReportIDFromPath, } from '@libs/ReportUtils'; import {cancelSpan, cancelSpansByPrefix} from '@libs/telemetry/activeSpans'; +import {doesDeleteNavigateBackUrlIncludeDuplicatesReview, getParentReportActionDeletionStatus} from '@libs/TransactionNavigationUtils'; import {isNumeric} from '@libs/ValidationUtils'; import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@navigation/types'; import {setShouldShowComposeInput} from '@userActions/Composer'; @@ -174,11 +175,12 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const {currentReportID: currentReportIDValue} = useCurrentReportIDState(); + const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true}); + const [parentReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportOnyx?.parentReportID}`, {allowStaleData: true}); const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`); const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID); const [accountManagerReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(accountManagerReportID)}`); const [userLeavingStatus = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportIDFromRoute}`); - const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true}); const [reportNameValuePairsOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportIDFromRoute}`, {allowStaleData: true}); const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {allowStaleData: true}); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true}); @@ -479,12 +481,23 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr () => !!linkedAction && isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), [currentUserAccountID, linkedAction], ); + const {isParentActionMissingAfterLoad, isParentActionDeleted} = getParentReportActionDeletionStatus({ + parentReportID: report?.parentReportID, + parentReportActionID: report?.parentReportActionID, + parentReportAction, + parentReportMetadata, + isOffline, + }); + const isDeletedTransactionThread = isReportTransactionThread(report) && (isParentActionDeleted || isParentActionMissingAfterLoad); const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); useEffect(() => { if (!isFocused || !deleteTransactionNavigateBackUrl) { return; } + if (doesDeleteNavigateBackUrlIncludeDuplicatesReview(deleteTransactionNavigateBackUrl)) { + return; + } // Clear the URL after all interactions are processed to ensure all updates are completed before hiding the skeleton // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { @@ -510,12 +523,20 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const shouldShowNotFoundPage = useMemo((): boolean => { const isInvalidReportPath = !!currentReportIDFormRoute && !isValidReportIDFromPath(currentReportIDFormRoute); const isLoading = isLoadingApp !== false || isLoadingReportData || !!reportMetadata?.isLoadingInitialReportActions; - const reportExists = !!reportID || isOptimisticDelete || userLeavingStatus; + const reportExists = !!reportID || (!isDeletedTransactionThread && isOptimisticDelete) || userLeavingStatus; if (shouldShowNotFoundLinkedAction) { return true; } + if (deleteTransactionNavigateBackUrl) { + return false; + } + + if (isDeletedTransactionThread) { + return true; + } + if (isInvalidReportPath) { return true; } @@ -539,6 +560,8 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr userLeavingStatus, currentReportIDFormRoute, firstRender, + deleteTransactionNavigateBackUrl, + isDeletedTransactionThread, ]); const createOneTransactionThreadReport = useCallback(() => { diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index d245ae16798e..5133c48185db 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -148,6 +148,12 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa } Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_DESCRIPTION.getRoute(policyID)); }, [policyID]); + const onPressClientID = useCallback(() => { + if (!policyID) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_CLIENT_ID.getRoute(policyID)); + }, [policyID]); const onPressShare = useCallback(() => { if (!policyID) { return; @@ -654,6 +660,27 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa /> )} + {!!account?.isApprovedAccountant && ( + { + if (!policy?.id) { + return; + } + clearPolicyErrorField(policy.id, 'clientID'); + }} + > + + + )} Date: Thu, 26 Feb 2026 01:44:45 +0000 Subject: [PATCH 08/27] Remove unrelated changes, keep only document-title feature Reset all files to main and re-apply only the useDocumentTitle hook changes. This removes merge artifacts and unrelated refactors that were inadvertently included in this PR. Co-authored-by: Yuwen Memon --- .claude/agents/code-inline-reviewer.md | 62 ++-- .claude/commands/review-code-pr.md | 11 +- .claude/schemas/code-review-output.json | 27 ++ .../javascript/authorChecklist/index.js | 14 + .../javascript/awaitStagingDeploys/index.js | 14 + .../javascript/checkAndroidStatus/index.js | 14 + .../javascript/checkDeployBlockers/index.js | 14 + .../javascript/checkSVGCompression/index.js | 14 + .../createOrUpdateStagingDeploy/index.js | 14 + .../javascript/formatCodeCovComment/index.js | 14 + .../javascript/getArtifactInfo/index.js | 14 + .../getDeployPullRequestList/index.js | 14 + .../javascript/getPreviousVersion/index.js | 14 + .../javascript/getPullRequestDetails/index.js | 14 + .../getPullRequestIncrementalChanges/index.js | 14 + .../javascript/isStagingDeployLocked/index.js | 14 + .../markPullRequestsAsDeployed/index.js | 14 + .../javascript/postTestBuildComment/index.js | 14 + .../javascript/proposalPoliceComment/index.js | 14 + .../reopenIssueWithComment/index.js | 14 + .../javascript/reviewerChecklist/index.js | 14 + .../javascript/verifySignedCommits/index.js | 14 + .../javascript/waitForPreviousRuns/index.js | 14 + .github/libs/GithubUtils.ts | 17 +- .github/workflows/claude-review.yml | 29 +- .github/workflows/generateTranslations.yml | 4 +- Mobile-Expensify | 2 +- android/app/build.gradle | 4 +- contributingGuides/CONTRIBUTING.md | 2 - contributingGuides/OBSERVABILITY_METRICS.md | 14 +- .../reports-and-expenses/Create-an-Expense.md | 20 +- .../reports-and-expenses/Distance-Expenses.md | 143 ++++++---- ios/NewExpensify/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- ios/ShareViewController/Info.plist | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/generateTranslations.ts | 164 ++++++++++- scripts/runPrettier.sh | 42 --- src/CONST/index.ts | 5 +- src/ONYXKEYS.ts | 2 - src/ROUTES.ts | 6 - src/SCREENS.ts | 1 - src/components/Banner.tsx | 3 +- src/components/EnvironmentContext.tsx | 92 ------ src/components/MoneyReportHeader.tsx | 4 +- .../MoneyRequestReportActionsList.tsx | 12 +- .../MoneyRequestReportNavigation.tsx | 61 +--- .../MoneyRequestReportView.tsx | 13 +- .../NavigationTabBar/SearchTabButton.tsx | 14 +- .../ExportWithDropdownMenu.tsx | 2 +- src/components/Search/index.tsx | 53 ++-- src/components/ThemeStylesProvider.tsx | 21 -- .../VideoPlayerContexts/FullScreenContext.tsx | 38 --- src/hooks/useArchivedReportsIdSet.ts | 23 +- src/hooks/useSearchSections.ts | 69 +++++ src/libs/API/parameters/ReportExportParams.ts | 2 +- .../ModalStackNavigators/index.tsx | 1 - .../helpers/getAdaptedStateFromPath.ts | 33 ++- src/libs/Navigation/linkingConfig/config.ts | 3 - src/libs/Navigation/types.ts | 5 - .../ForegroundNotifications/index.ios.ts | 11 +- .../nonPersonalAndWorkspaceCardList.ts | 7 +- .../configs/personalAndWorkspaceCardList.ts | 7 +- .../OnyxDerived/configs/reportAttributes.ts | 12 +- src/libs/actions/OnyxDerived/index.ts | 27 +- src/libs/actions/OnyxDerived/types.ts | 1 - src/libs/actions/Policy/Member.ts | 22 +- src/libs/actions/Report/index.ts | 48 ++-- src/libs/actions/Search.ts | 100 ++++--- src/libs/telemetry/markOpenReportEnd.ts | 9 +- src/pages/Search/SearchPage.tsx | 164 ++--------- .../domain/Members/DomainMembersPage.tsx | 3 +- src/pages/inbox/ReportScreen.tsx | 4 +- src/pages/inbox/report/ReportActionsView.tsx | 12 +- .../inbox/report/ReportDetailsExportPage.tsx | 2 +- .../WorkspaceInviteMessageApproverPage.tsx | 181 ------------ src/pages/workspace/WorkspaceMembersPage.tsx | 8 +- .../accounting/PolicyAccountingPage.tsx | 21 +- .../categories/WorkspaceCategoriesPage.tsx | 14 +- ...orkspaceCompanyCardsTableHeaderButtons.tsx | 5 +- .../distanceRates/PolicyDistanceRatesPage.tsx | 12 +- .../WorkspaceInviteMessageComponent.tsx | 40 +-- .../WorkspaceReceiptPartnersPage.tsx | 8 +- .../workspace/taxes/WorkspaceTaxesPage.tsx | 16 +- src/pages/workspace/withPolicy.tsx | 1 - .../workflows/WorkspaceWorkflowsPage.tsx | 5 +- .../theme/context/ThemeStylesContext.ts | 16 -- tests/actions/PolicyMemberTest.ts | 264 ------------------ ...WorkspaceInviteMessageApproverPageTest.tsx | 213 -------------- tests/unit/CardFeedErrorsDerivedValueTest.ts | 1 - tests/unit/ForegroundNotificationsTest.ts | 84 ++++++ tests/unit/OnyxDerivedTest.tsx | 6 +- .../hooks/useArchivedReportsIdSet.test.ts | 79 ++++++ 94 files changed, 1183 insertions(+), 1516 deletions(-) create mode 100644 .claude/schemas/code-review-output.json delete mode 100755 scripts/runPrettier.sh delete mode 100644 src/components/EnvironmentContext.tsx delete mode 100644 src/components/ThemeStylesProvider.tsx delete mode 100644 src/components/VideoPlayerContexts/FullScreenContext.tsx create mode 100644 src/hooks/useSearchSections.ts delete mode 100644 src/pages/workspace/WorkspaceInviteMessageApproverPage.tsx delete mode 100644 src/styles/theme/context/ThemeStylesContext.ts delete mode 100644 tests/ui/WorkspaceInviteMessageApproverPageTest.tsx create mode 100644 tests/unit/ForegroundNotificationsTest.ts create mode 100644 tests/unit/hooks/useArchivedReportsIdSet.test.ts diff --git a/.claude/agents/code-inline-reviewer.md b/.claude/agents/code-inline-reviewer.md index 05334cef22da..8c665b9b8b6c 100644 --- a/.claude/agents/code-inline-reviewer.md +++ b/.claude/agents/code-inline-reviewer.md @@ -2,7 +2,7 @@ name: code-inline-reviewer description: Reviews code and creates inline comments for specific rule violations. -tools: Glob, Grep, Read, TodoWrite, Bash, BashOutput, KillBash +tools: Glob, Grep, Read, Bash, BashOutput model: inherit --- @@ -41,49 +41,26 @@ Each rule file contains: - **For large files (>5000 lines):** Use the Grep tool with Search Patterns from each rule's Review Metadata to locate potential violations. Focus on changed portions shown in the diff. - **For smaller files:** You may read the full file using the Read tool - **If a Read fails with token limit error:** Immediately switch to using Grep with targeted patterns for the rules you're checking - - For each rule: evaluate the Condition and "DO NOT flag" exceptions. Mark the rule as checked on the checklist. A single rule **can produce multiple violations** — flag each separately. -5. **For each violation found, immediately create an inline comment** using the available GitHub inline comment tool. Do not batch — create the comment as soon as you confirm a violation. -6. **Required parameters for each inline comment:** - - `path`: Full file path (e.g., "src/components/ReportActionsList.tsx") +3. **Search strategy for large files:** Use the search patterns defined in each rule's "Search patterns" field to efficiently locate potential violations with Grep. +4. **Return your findings as structured JSON output.** Your response must be a JSON object matching this schema: + ```json + { "violations": [ { "ruleId": "...", "path": "...", "line": ..., "body": "..." } ] } + ``` + - `ruleId`: The rule ID (e.g., `PERF-1`, `CONSISTENCY-2`) + - `path`: Full file path (e.g., `src/components/ReportActionsList.tsx`) - `line`: Line number where the issue occurs - - `body`: Concise and actionable description of the violation and fix, following the below Comment Format -7. **Each comment must reference exactly one Rule ID.** -8. **Output must consist exclusively of calls to createInlineComment.sh in the required format.** No other text, Markdown, or prose is allowed. -9. **If no violations are found, add a reaction to the PR**: - Add a +1 reaction to the PR using the `addPrReaction` script (available in PATH from `.claude/scripts/`). The script takes ONLY the PR number as argument - it always adds a "+1" reaction, so do NOT pass any reaction type or emoji. -10. **Add reaction if and only if**: - - You examined EVERY changed line in EVERY changed file (via diff + targeted grep/read) - - You checked EVERY changed file against ALL rules - - You found ZERO violations matching the exact rule criteria - - You verified no false negatives by checking each rule systematically - If you found even ONE violation or have ANY uncertainty do NOT add the reaction - create inline comments instead. -11. **DO NOT invent new rules, stylistic preferences, or commentary outside the listed rules.** -12. **DO NOT describe what you are doing, create comments with a summary, explanations, extra content, comments on rules that are NOT violated or ANYTHING ELSE.** - Only inline comments regarding rules violations are allowed. If no violations are found, add a reaction instead of creating any comment. - EXCEPTION: If you believe something MIGHT be a Rule violation but are uncertain, err on the side of creating an inline comment with your concern rather than skipping it. -13. **Reality check before posting**: Before creating each inline comment, re-read the specific code one more time and confirm the violation is real. If upon re-reading you realize the code is actually correct, **do NOT post the comment** — silently skip it and move on. Never post a comment that flags a violation and then concludes it is not actually a problem. - -## Tool Usage Example - -For each violation, call the createInlineComment.sh script like this: - -```bash -createInlineComment.sh 'src/components/ReportActionsList.tsx' '' 128 -``` - -**IMPORTANT**: Always use single quotes around the body argument to properly handle special characters and quotes. - -If ZERO violations are found, use the Bash tool to add a reaction to the PR body: - -```bash -addPrReaction.sh -``` - -**IMPORTANT**: Always use the `addPrReaction.sh` script (available in PATH from `.claude/scripts/`) instead of calling `gh api` directly. + - `body`: Concise and actionable description of the violation and fix, formatted per the Comment Format below +5. **Each violation must reference exactly one Rule ID.** +6. **If no violations are found, return an empty violations array:** `{ "violations": [] }` +7. **Do NOT post comments, call scripts, or add reactions.** Only return the structured JSON. +8. **DO NOT invent new rules, stylistic preferences, or commentary outside the listed rules.** +9. **DO NOT describe what you are doing or add extra content.** + EXCEPTION: If you believe something MIGHT be a Rule violation but are uncertain, err on the side of including it in the violations array rather than skipping it. +10. **Reality check before posting**: Before creating each inline comment, re-read the specific code one more time and confirm the violation is real. If upon re-reading you realize the code is actually correct, **do NOT post the comment** — silently skip it and move on. Never post a comment that flags a violation and then concludes it is not actually a problem. ## Comment Format -Build the docs link by mapping the ruleId to its rule filename: +Use this format for the `body` field of each violation: ``` ### ❌ [(docs)](https://github.com/Expensify/App/blob/main/.claude/skills/coding-standards/rules/.md) @@ -92,8 +69,3 @@ Build the docs link by mapping the ruleId to its rule filename: ``` - -For example, a PERF-1 violation links to: -`https://github.com/Expensify/App/blob/main/.claude/skills/coding-standards/rules/perf-1-no-spread-in-renderitem.md` - -**CRITICAL**: You must actually call the createInlineComment.sh script for each violation. Don't just describe what you found - create the actual inline comments! diff --git a/.claude/commands/review-code-pr.md b/.claude/commands/review-code-pr.md index 63132922c18a..f2cf70927608 100644 --- a/.claude/commands/review-code-pr.md +++ b/.claude/commands/review-code-pr.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(addPrReaction.sh:*),Bash(createInlineComment.sh:*) +allowed-tools: Bash(gh pr diff:*),Bash(gh pr view:*) description: Review a code contribution pull request --- @@ -8,10 +8,13 @@ Perform a comprehensive PR review using a specialized subagent: ## Inline Review Use the code-inline-reviewer agent to: - Scan all changed source code files -- Create inline comments for specific review rule violations -- Focus on line-specific, actionable feedback +- Detect review rule violations with line-specific, actionable feedback -Run the agent and ensure its feedback is posted to the PR. +Run the agent. It will return structured JSON with any violations found. + +## Output +Return the subagent's violations JSON as your structured output unchanged. +Do NOT post comments or reactions yourself - the workflow handles that. Keep feedback concise. diff --git a/.claude/schemas/code-review-output.json b/.claude/schemas/code-review-output.json new file mode 100644 index 000000000000..47c57b275012 --- /dev/null +++ b/.claude/schemas/code-review-output.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "properties": { + "violations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ruleId": { + "type": "string" + }, + "path": { + "type": "string" + }, + "line": { + "type": "integer" + }, + "body": { + "type": "string" + } + }, + "required": ["ruleId", "path", "line", "body"] + } + } + }, + "required": ["violations"] +} diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 09d7caf01972..f06248d685c5 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -16018,6 +16018,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index 884b6664704a..669c735a6689 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12796,6 +12796,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js index a931693ffd9e..838721c2f5e7 100644 --- a/.github/actions/javascript/checkAndroidStatus/index.js +++ b/.github/actions/javascript/checkAndroidStatus/index.js @@ -737539,6 +737539,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 2784558fbd67..030b822567c8 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -12063,6 +12063,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/checkSVGCompression/index.js b/.github/actions/javascript/checkSVGCompression/index.js index e88674933578..0228ad14e048 100644 --- a/.github/actions/javascript/checkSVGCompression/index.js +++ b/.github/actions/javascript/checkSVGCompression/index.js @@ -20588,6 +20588,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 680985a79d52..e4d963cab04b 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -12479,6 +12479,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/formatCodeCovComment/index.js b/.github/actions/javascript/formatCodeCovComment/index.js index 09aa763760f7..96d651816c61 100644 --- a/.github/actions/javascript/formatCodeCovComment/index.js +++ b/.github/actions/javascript/formatCodeCovComment/index.js @@ -12250,6 +12250,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index cc31072587dc..c014d874de19 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -12024,6 +12024,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 3acf0d80147a..925474c3b9d0 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -12405,6 +12405,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index baf9ae8c960f..95981e3b1aba 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -12216,6 +12216,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 21b61c79450b..90c710f712f6 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -12153,6 +12153,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/getPullRequestIncrementalChanges/index.js b/.github/actions/javascript/getPullRequestIncrementalChanges/index.js index cc5f27429486..faf0b165802e 100644 --- a/.github/actions/javascript/getPullRequestIncrementalChanges/index.js +++ b/.github/actions/javascript/getPullRequestIncrementalChanges/index.js @@ -12255,6 +12255,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index a38570890192..8cd81bde5531 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -12024,6 +12024,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index dbac1332f2aa..d1b92659a5dc 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -13476,6 +13476,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 5f2b86314d98..5098a0ec5d75 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -12153,6 +12153,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 80ee22dc686d..a53e8c37fb2c 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -12327,6 +12327,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 6a435c0e7f6e..d8cb4e7f56be 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -12034,6 +12034,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 541981928362..98c03e9eae8c 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -12126,6 +12126,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index f6b4cad4c305..729f0e532e87 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -12066,6 +12066,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/actions/javascript/waitForPreviousRuns/index.js b/.github/actions/javascript/waitForPreviousRuns/index.js index 9d50eb439131..d8f5428b370c 100644 --- a/.github/actions/javascript/waitForPreviousRuns/index.js +++ b/.github/actions/javascript/waitForPreviousRuns/index.js @@ -12075,6 +12075,20 @@ class GithubUtils { }) .then(({ data: pullRequestComment }) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber) { + const { data: pullRequest } = await this.octokit.pulls.get({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + pull_number: pullRequestNumber, + }); + const { data: comparison } = await this.octokit.repos.compareCommits({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } static getAllReviewComments(pullRequestNumber) { return this.paginate(this.octokit.pulls.listReviews, { owner: CONST_1.default.GITHUB_OWNER, diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index 979080a6b4cc..1da5fdc58ae6 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -493,6 +493,21 @@ class GithubUtils { .then(({data: pullRequestComment}) => pullRequestComment.body); } + static async getPullRequestMergeBaseSHA(pullRequestNumber: number): Promise { + const {data: pullRequest} = await this.octokit.pulls.get({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + pull_number: pullRequestNumber, + }); + const {data: comparison} = await this.octokit.repos.compareCommits({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + base: pullRequest.base.ref, + head: pullRequest.head.sha, + }); + return comparison.merge_base_commit.sha; + } + static getAllReviewComments(pullRequestNumber: number): Promise { return this.paginate( this.octokit.pulls.listReviews, @@ -701,7 +716,7 @@ class GithubUtils { /** * Get the contents of a file from the API at a given ref as a string. */ - static async getFileContents(path: string, ref = CONST.DEFAULT_BASE_REF): Promise { + static async getFileContents(path: string, ref: string = CONST.DEFAULT_BASE_REF): Promise { const {data} = await this.octokit.repos.getContent({ owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 29bc53910719..3d742cb27e1e 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -48,7 +48,12 @@ jobs: "$GITHUB_WORKSPACE/.claude/skills/coding-standards/rules" \ "$GITHUB_WORKSPACE/.claude/allowed-rules.txt" + - name: Load code review JSON schema + id: schema + run: echo "json=$(jq -c . "$GITHUB_WORKSPACE/.claude/schemas/code-review-output.json")" >> "$GITHUB_OUTPUT" + - name: Run Claude Code (code) + id: code-review if: steps.filter.outputs.code == 'true' uses: anthropics/claude-code-action@ea36d6abdedc17fc2a671b36060770b208a6f8f1 # v1.0.51 with: @@ -58,7 +63,29 @@ jobs: prompt: "/review-code-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}" claude_args: | --model claude-opus-4-6 - --allowedTools "Task,Glob,Grep,Read,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(addPrReaction.sh:*),Bash(createInlineComment.sh:*)" + --allowedTools "Task,Glob,Grep,Read,Bash(gh pr diff:*),Bash(gh pr view:*)" --json-schema '${{ steps.schema.outputs.json }}' + + - name: Post code review results + if: steps.code-review.outcome == 'success' && steps.filter.outputs.code == 'true' + env: + GH_TOKEN: ${{ github.token }} + STRUCTURED_OUTPUT: ${{ steps.code-review.outputs.structured_output }} + run: | + if [ -z "$STRUCTURED_OUTPUT" ]; then + echo "::error::Claude Code Action returned empty structured output" + exit 1 + fi + COUNT=$(echo "$STRUCTURED_OUTPUT" | jq '.violations | length') + if [ "$COUNT" -eq 0 ]; then + addPrReaction.sh "$PR_NUMBER" + else + echo "$STRUCTURED_OUTPUT" | jq -c '.violations[]' | while IFS= read -r v; do + PATH_ARG=$(echo "$v" | jq -r '.path') + BODY_ARG=$(echo "$v" | jq -r '.body') + LINE_ARG=$(echo "$v" | jq -r '.line') + createInlineComment.sh "$PATH_ARG" "$BODY_ARG" "$LINE_ARG" || true + done + fi - name: Run Claude Code (docs) if: steps.filter.outputs.docs == 'true' diff --git a/.github/workflows/generateTranslations.yml b/.github/workflows/generateTranslations.yml index 2782c447ae06..b699d374a674 100644 --- a/.github/workflows/generateTranslations.yml +++ b/.github/workflows/generateTranslations.yml @@ -65,13 +65,13 @@ jobs: GITHUB_TOKEN: ${{ github.token }} PULL_REQUEST_NUMBER: ${{ github.event_name == 'workflow_dispatch' && steps.pr-data.outputs.PR_NUMBER || '' }} - - name: Fetch base ref + - name: Fetch base ref for fallback if: steps.check-en-changes.outputs.HAS_CHANGES == 'true' run: git fetch --no-tags --depth=1 --no-recurse-submodules origin +refs/heads/${{ steps.pr-data.outputs.BASE_REF }}:refs/heads/${{ steps.pr-data.outputs.BASE_REF }} - name: Run generateTranslations for added translations if: steps.check-en-changes.outputs.HAS_CHANGES == 'true' - run: npx ts-node ./scripts/generateTranslations.ts --verbose --yes --compare-ref=${{ steps.pr-data.outputs.BASE_REF }} + run: npx ts-node ./scripts/generateTranslations.ts --verbose --yes --pr-number=${{ steps.pr-data.outputs.PR_NUMBER }} --compare-ref=${{ steps.pr-data.outputs.BASE_REF }} env: GITHUB_TOKEN: ${{ github.token }} OPENAI_API_KEY: ${{ secrets.PROPOSAL_POLICE_API_KEY }} diff --git a/Mobile-Expensify b/Mobile-Expensify index 50345b0bf1f6..b8486d2e7fca 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 50345b0bf1f6df72a777b621c78b7aefc4359174 +Subproject commit b8486d2e7fcaa704c6dc4093b0f8f8a557be4567 diff --git a/android/app/build.gradle b/android/app/build.gradle index 76d14285afd4..4d37c481853b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,8 +111,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009032600 - versionName "9.3.26-0" + versionCode 1009032604 + versionName "9.3.26-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index f10ea80df932..35f83c434f4c 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -66,8 +66,6 @@ If a PR causes a regression at any point within the regression period (starting - payments will be issued 7 days after all regressions are fixed (ie: deployed to production) - a 50% penalty will be applied to the Contributor and [Contributor+](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md) for each regression on an issue -The 168 hours (aka 7 days) will be measured by calculating the time between when the PR is merged, and when a bug is posted to the #expensify-bugs Slack channel. - ## Finding Jobs A job could be fixing a bug or working on a new feature. There are two ways you can find a job that you can contribute to: diff --git a/contributingGuides/OBSERVABILITY_METRICS.md b/contributingGuides/OBSERVABILITY_METRICS.md index 3bb0e548b84d..c5b73935e462 100644 --- a/contributingGuides/OBSERVABILITY_METRICS.md +++ b/contributingGuides/OBSERVABILITY_METRICS.md @@ -48,18 +48,22 @@ This document lists all implemented telemetry metrics in the Expensify App. ### Navigate to Reports Tab -**Constant**: `CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB` -**Sentry Name**: `ManualNavigateToReportsTab` +**Constant**: `CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS` +**Sentry Name**: `ManualNavigateToReports` **Threshold**: 400ms (P90) -**What's Measured**: Time from clicking search tab to results rendered -**Start**: User clicks search/reports tab ([`src/components/Navigation/NavigationTabBar/index.tsx`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/components/Navigation/NavigationTabBar/index.tsx#L175)) +**What's Measured**: Time from clicking search tab to results rendered (either list or skeleton) +**Start**: User clicks search/reports tab ([`src/components/Navigation/NavigationTabBar/SearchTabButton.tsx`](https://github.com/Expensify/App/blob/42c42d7fb1984adde1d96ef2285d3c8e1177a4aa/src/components/Navigation/NavigationTabBar/SearchTabButton.tsx#L47)) **End**: -- User sees: Search results list displayed +- User sees: Search results list displayed (warm) - Technical: Search results layout complete (onLayout event) - Search results data loaded from Onyx - Results sorted and sectioned - List layout rendered ([`src/components/Search/index.tsx`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/components/Search/index.tsx#L961)) +- User sees: Search skeleton displayed (cold) +- Technical: Search skeleton layout complete (onLayoutSkeleton event) + - Skeleton layout rendered ([`src/components/Search/index.tsx`](https://github.com/Expensify/App/blob/e8d4f62021987e5821d69ce483349562918a948a/src/components/Search/index.tsx#L1162)) + ### Navigate to Inbox Tab **Constant**: `CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB` diff --git a/docs/articles/new-expensify/reports-and-expenses/Create-an-Expense.md b/docs/articles/new-expensify/reports-and-expenses/Create-an-Expense.md index bfcba3dca02b..aec4f7a1432c 100644 --- a/docs/articles/new-expensify/reports-and-expenses/Create-an-Expense.md +++ b/docs/articles/new-expensify/reports-and-expenses/Create-an-Expense.md @@ -1,7 +1,7 @@ --- title: Create an Expense description: Learn how to create and submit expenses in Expensify using SmartScan, manual entry, distance tracking, or time expenses. -keywords: [create expense, submit expense, SmartScan, manual expense, distance expense, time expense, create time expense, log time, track hours, expense report, submit to workspace, submit to individual, split expense, scan receipts, bulk upload] +keywords: [create expense, submit expense, SmartScan, manual expense, distance expense, time expense, create time expense, log time, track hours, expense report, submit to workspace, submit to individual, split expense, scan receipts, bulk upload, GPS, GPS tracking, GPS mileage] internalScope: Audience is submitters, approvers, and Workspace Admins. Covers how to create and submit expenses to a workspace or individual using SmartScan, manual entry, distance tracking, or Time expenses. Does not cover credit card import or Time Tracking --- @@ -45,7 +45,7 @@ You can create an expense by scanning a receipt, entering details manually, or t --- -## How to manually create an expense +## How to manually create a cash expense 1. Click the **➕ Create** button. 2. Select **Create Expense** then **Manual**. @@ -56,9 +56,7 @@ You can create an expense by scanning a receipt, entering details manually, or t --- -## How to create a distance expense - -To create a distance expense: +## How to create a Distance expense from a map (Web and Mobile) 1. Click the **➕ Create** button. 2. Select **Track distance**. @@ -68,6 +66,18 @@ To create a distance expense: 6. Choose the recipient and add expense details like description, category, tags, tax, date, and set whether the expense is reimbursable. 7. Click **Create expense**. +## How to create a Distance expense from a GPS (Mobile only) + +1. Tap the **➕ Create** button. +2. Select **Track distance**. +3. Select **GPS** and tap **Start**. +4. Drive to your destination — tracking runs in the background. +5. Tap **Stop** when you arrive and confirm by selecting **Stop GPS tracking**. +6. Review the route summary, then tap **Next**. +7. Review the expense details, then tap **Create expense**. + +For more details on all distance methods including manual entry, see [Distance Expenses](https://help.expensify.com/articles/new-expensify/reports-and-expenses/Distance-Expenses). + --- ## How to create a time expense diff --git a/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md b/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md index 2e915dbc290b..5dbddde2b551 100644 --- a/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md +++ b/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md @@ -1,105 +1,134 @@ --- title: Distance Expenses -description: Learn how to create a distance expense and how the reimbursement rate is determined in New Expensify. -keywords: [New Expensify, distance expense, mileage reimbursement, create expense, distance rate, workspace rate, personal policy, map route, reimbursement rate, manual mileage, manual distance, global create, track distance] -date: 2025-06-18T00:00:00Z +description: Learn how to create a Distance expense using GPS tracking, map-based routes, or manual entry, and how the reimbursement rate is determined in New Expensify. +keywords: [New Expensify, distance expense, mileage reimbursement, create expense, distance rate, workspace rate, map route, reimbursement rate, manual mileage, manual distance, global create, track distance, GPS, GPS tracking, start GPS, track route, track mileage, mileage tracking, calculate mileage reimbursement, mileage rate] +internalScope: Audience is all members. Covers creating Distance expenses using GPS tracking, map-based routes, and manual entry, plus how reimbursement rates are applied. Does not cover configuring Workspace distance rates in detail or broader report submission workflows. --- -Expensify makes it easy to submit mileage expenses using a built-in map, whether you're on the web, desktop, or mobile app. This guide explains how to create a distance expense and how the reimbursement rate is determined. +# Distance Expenses + +Expensify offers three ways to create a Distance expense: **GPS tracking** on mobile, **map-based routes** using start and end locations, or **manual entry** by typing in the distance. This guide explains each method and how the reimbursement rate is determined. --- -# Create and Send a Distance Expense +## How to create a GPS Distance expense (Mobile only) + +GPS tracking lets Expensify record your actual driving route in the background while you drive. When you stop, the app generates a receipt with a map of the route you took. + +To create an expense using GPS tracking based on the distance traveled: + +1. Tap the **➕ Create** button. +2. Select **Track distance**. +3. Select **GPS** from the top row. +4. Tap **Start** and drive to your destination — tracking runs in the background. +5. Tap **Stop** when you arrive and confirm by selecting **Stop GPS tracking**. +6. Review the route summary showing your start and end addresses, then tap **Next**. +7. Review the expense details, then tap **Create expense**. -To submit a distance-based expense using the Start and End locations of your trip: +**Note:** GPS tracking is available on iOS and Android only. On Web, you'll see a prompt to download the mobile app when selecting the GPS option. -1. From the navigation tabs, click the green **+** button and select **Create expense**. -2. Select **Distance** from the top row. -3. Enter your **Start** and **Finish** locations. - - To include additional stops, click **Add stop**. -4. Click **Next**. -5. Choose a workspace from your recent options or search to select the correct one. -6. On the confirmation screen, review and confirm: - - **Distance** - - **Amount** - - **Date** - - (Optional) Add a **description**, **category**, or **tag** -7. Click **Create expense** to submit the mileage expense for approval. +--- -To submit a distance-based expense by entering the distance manually: +## How to create a map-based Distance expense (Web and Mobile) -1. Click the green “+” sign and select **Track distance** from any page in the app or directly -from an expense chat -2. Choose the **Manual** tab -3. Enter the number of miles or kilometers you need to be reimbursed for -4. Review the calculated reimbursement amount -5. Tap **Next** to continue -6. Confirm the expense details, recipient, category, and add a receipt if needed -7. Submit the expense +To create an expense using distance between the starting and ending locations of your trip: -A Workspace Admin will be notified of the expense and, depending on your workspace settings, can reimburse you through Expensify or another method. +1. Select the **➕ Create** button and select **Track distance**. +2. Select **Map** from the top row. +3. Enter the **Start** and **Stop** locations. + - To include additional stops, select **Add stop**. +4. Select **Next**. +5. On the confirmation screen, review and confirm: + - Distance + - Amount + - Date + - (Optional) Add a description, category, or tag. +6. Select **Create expense**. --- -# How Reimbursement Rates Are Determined +## How to create a manual Distance expense (Web and Mobile) + +To create an expense by inputting a distance manually: -## Submitting to a workspace +1. Select the **➕ Create** button and select **Track distance**. +2. Select **Manual** from the top row. +3. Enter the number of miles or kilometers you need to be reimbursed for. +4. Select **Next**. +5. On the confirmation screen, review and confirm: + - Distance + - Amount + - Date + - (Optional) Add a description, category, tag or receipt. +6. Select **Create expense**. -If you're submitting the expense to a workspace: +Once a Distance expense is created, it can be submitted on a report. To learn how to add expenses to a report, see [Create and Submit Reports](https://help.expensify.com/articles/new-expensify/reports-and-expenses/Create-and-Submit-Reports). + +--- -- You'll choose from the distance rates that are enabled on that workspace. -- The **Workspace Admin** manages available rates. -- The **unit** (miles or kilometers) depends on the workspace’s distance unit setting. +## How reimbursement rates are set for Distance expenses + +### Distance expenses created on a Workspace + +If you are creating expenses on a Workspace: + +- Workspace Admins set and manage the reimbursement rates for the workspace's distance unit (miles or kilometers). +- When creating a Distance expense, the available reimbursement rates will show for selection. [Learn how to manage distance rates as a Workspace Admin](https://help.expensify.com/articles/new-expensify/reports-and-expenses/Managing-Distance-Rates) -## Not submitting to a workspace +### Distance expenses created for personal tracking + +If you're creating expenses outside of a Workspace: -If you're not submitting to a workspace (e.g., personal tracking): +- Expensify sets a default rate based on your payment currency. + - Example: For USD, the rate is based on the current IRS reimbursement rate and uses miles. + - Distance default rates are updated annually. -- Expensify will apply a **default rate** based on your **reporting currency**. - - Example: For USD, the rate is based on the current IRS reimbursement rate and uses **miles**. - - Other currencies use sensible defaults determined by Expensify’s internal research. -- You can’t customize this rate unless you upgrade to a workspace. -- Default distance rates are updated annually. +**Note:** Only Workspace Admins can set a custom distance rate. It's not possible to set a custom distance rate for personal expenses outside of a Workspace. --- # FAQ -## Is there an easy way to reuse recent locations? +## Can I reuse recent locations? -Yes! When selecting the **Start** and **Finish** addresses, recently used locations will appear for quick selection. +Yes! When selecting the **Start** and **Stop** addresses, recently used locations will appear for quick selection. ## How do I create a round-trip expense? -To create a round-trip distance expense, enter the same location for both the starting point and destination, and add one or more waypoints in between. +To create a round-trip Distance expense, enter the same location for both the starting point and destination, and add one or more waypoints in between. For example, if you're starting and ending in San Francisco but making a stop in Los Angeles, enter: **San Francisco → Los Angeles → San Francisco** -## How is the expense calculated? +## How are Distance expense amounts calculated? + +The expense amount is automatically calculated by multiplying the distance by the Workspace’s distance rate. If no Workspace is assigned to the expense, a default rate is applied based on your default currency. Distance expenses are rounded to two decimal places. + +## Can I edit a Distance expense after I’ve created it? + +Yes! You can edit the expense before it is approved. To learn how to edit an expense, see [Managing Expenses in a Report](https://help.expensify.com/articles/new-expensify/reports-and-expenses/Managing-Expenses-in-a-Report). -The amount is automatically calculated by multiplying the distance by the workspace’s rate. If no workspace is assigned to the expense, a default rate is applied based on your default currency. Distance expenses are rounded to two decimal places. +## Can I update the Distance expense unit or rate? -## Can I edit an expense after I’ve created it? +The distance unit and rate can only be updated by a Workspace Admin on the Workspace. It is not possible to adjust the distance rate or unit at the expense level. -Yes! You can edit the expense before it is approved. +## What happens if a Distance expense is moved to a different Workspace? -## Can I split a distance expense? +When a Distance expense is moved to another Workspace, it keeps its original unit and rate. -Only map-based distance expenses can be split, manual distance expenses are not available for splitting. If you need a manual distance expense split with another user, you can each create your own expense for half the distance each. +If the rate isn’t valid in the new Workspace, the expense will show a “Rate not valid for this workspace” violation. Selecting a valid rate will update the expense. -## Can I update the distance unit or rate? +## Do I need to keep the mobile app open during GPS tracking? -The distance unit and rate can only be updated at the workspace level, not at the expense level. If you need these updated, you will need to speak with an admin. +No. GPS tracking runs in the background on your mobile device. A notification confirms that tracking is active, so you can use other apps or lock your phone while driving. -## Can I move my distance expense to a new workspace? +## What does the GPS Distance expense receipt look like? -Yes, you can do this before the expenses have been approved. However, be aware that doing so will automatically update the distance rate and unit based on the new workspace. +The GPS receipt shows a map of your actual route driven, along with the total distance and calculated reimbursement amount. It looks similar to a map-based distance receipt, but reflects the path you actually took rather than a suggested route. -## What if I enter 0 miles or kilometers? +## Can I use GPS tracking on web or desktop? -You cannot create a distance expense for 0 miles or kilometers. If you do, you’ll see this error: -**“Please enter a valid distance before continuing.”** +No. GPS tracking requires the iOS or Android mobile app because it uses your device's location services. On web or desktop, you'll see a prompt to download the mobile app when selecting GPS. You can still use map-based or manual distance entry on any platform. diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 1c084f7b56e4..cef0e7dff94f 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.26.0 + 9.3.26.4 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2159cb83168b..d96ea0231488 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.3.26 CFBundleVersion - 9.3.26.0 + 9.3.26.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index 3064e1b72b33..a366bb8a0402 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.3.26 CFBundleVersion - 9.3.26.0 + 9.3.26.4 NSExtension NSExtensionAttributes diff --git a/package-lock.json b/package-lock.json index f05b5136bc06..20d87e5f5807 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.26-0", + "version": "9.3.26-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.26-0", + "version": "9.3.26-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8a4218324f51..38224345bf9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.26-0", + "version": "9.3.26-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index c2065d0e901c..8eb21d27f75b 100755 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -2,6 +2,7 @@ /* * This script uses src/languages/en.ts as the source of truth, and leverages ChatGPT to generate translations for other languages. */ +import {execFileSync} from 'child_process'; import * as dotenv from 'dotenv'; import fs from 'fs'; // eslint-disable-next-line you-dont-need-lodash-underscore/get @@ -9,6 +10,7 @@ import get from 'lodash/get'; import path from 'path'; import type {TemplateExpression} from 'typescript'; import ts from 'typescript'; +import GitHubUtils from '@github/libs/GithubUtils'; import decodeUnicode from '@libs/StringUtils/decodeUnicode'; import dedent from '@libs/StringUtils/dedent'; import hashStr from '@libs/StringUtils/hash'; @@ -19,6 +21,7 @@ import type {TranslationPaths} from '@src/languages/types'; import CLI from './utils/CLI'; import COLORS from './utils/COLORS'; import Git from './utils/Git'; +import type {DiffResult} from './utils/Git'; import Prettier from './utils/Prettier'; import PromisePool from './utils/PromisePool'; import ChatGPTTranslator from './utils/Translator/ChatGPTTranslator'; @@ -66,6 +69,23 @@ class TranslationGenerator { */ private static readonly CONTEXT_REGEX = /^\s*(?:\/{2}|\*|\/\*)?\s*@context\s+([^\n*/]+)/; + /** + * Regex to validate a full 40-character git SHA hash. + */ + private static readonly GIT_SHA_REGEX = /^[a-fA-F0-9]{40}$/; + + /** + * HTTP 406 Not Acceptable - returned by GitHub API when diff is too large. + */ + private static readonly HTTP_STATUS_DIFF_TOO_LARGE = 406; + + /** + * Extracts a readable error message from an unknown error type. + */ + private static getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); + } + /** * The languages to generate translations for. */ @@ -116,6 +136,26 @@ class TranslationGenerator { */ private readonly isIncremental: boolean; + /** + * PR number for GitHub API diff (CI mode). + * When provided, uses GitHub API for accurate PR-specific diff instead of git diff. + * Requires GITHUB_TOKEN environment variable to be set. + */ + private readonly prNumber: number; + + /** + * The git ref (SHA or branch) that the diff was computed against. + * Used by extractRemovedPaths to get the old version of en.ts at the correct commit. + * Set in buildPathsFromGitDiff: merge-base SHA (local or CI). + */ + private diffBase: string; + + /** + * Whether the GitHub API was successfully used for the diff. + * When false (fallback to git diff or local mode), extractRemovedPaths uses Git.show instead of the GitHub API. + */ + private useGitHubAPI: boolean; + /** * CLI instance for user prompts. */ @@ -128,6 +168,7 @@ class TranslationGenerator { namedArgs: { locales: {description: string; default: TranslationTargetLocale[]; parse: (val: string) => TranslationTargetLocale[]}; 'compare-ref': {description: string; default: string; parse: (val: string) => string}; + 'pr-number': {description: string; default: number; parse: (val: string) => number}; paths: {description: string; parse: (val: string) => Set; supersedes: string[]; required: false}; }; }>; @@ -190,6 +231,17 @@ class TranslationGenerator { return val; }, }, + 'pr-number': { + description: 'PR number to get diff from GitHub API (CI only, requires GITHUB_TOKEN). When provided, uses GitHub API for accurate PR-specific diff instead of git diff.', + default: 0, + parse: (val: string): number => { + const parsed = parseInt(val, 10); + if (Number.isNaN(parsed) || parsed <= 0) { + throw new Error(`Invalid PR number: "${val}". Please provide a valid positive integer.`); + } + return parsed; + }, + }, paths: { description: 'Comma-separated list of specific translation paths to retranslate (e.g., "common.save,errors.generic").', parse: (val: string): Set => { @@ -217,11 +269,18 @@ class TranslationGenerator { this.targetLanguages = this.cli.namedArgs.locales; this.compareRef = this.cli.namedArgs['compare-ref']; + this.prNumber = this.cli.namedArgs['pr-number']; + this.diffBase = this.compareRef; + this.useGitHubAPI = false; this.pathsToAdd = new Set(); this.pathsToModify = this.cli.namedArgs.paths ?? new Set(); this.pathsToRemove = new Set(); this.verbose = this.cli.flags.verbose; - this.isIncremental = this.pathsToModify.size > 0 || !!this.compareRef; + this.isIncremental = this.pathsToModify.size > 0 || !!this.compareRef || !!this.prNumber; + + if (this.prNumber && !process.env.GITHUB_TOKEN) { + throw new Error('GITHUB_TOKEN environment variable is required when using --pr-number'); + } const sourceCode = fs.readFileSync(enSourceFile, 'utf8'); this.sourceFile = ts.createSourceFile(enSourceFile, sourceCode, ts.ScriptTarget.Latest, true); @@ -245,8 +304,7 @@ class TranslationGenerator { const translations = new Map>(); if (this.isIncremental && this.pathsToModify.size === 0) { - // If compareRef is provided (and no specific paths), use git diff to find changed lines and build dot-notation paths - this.buildPathsFromGitDiff(); + await this.buildPathsFromGitDiff(); } if (this.verbose) { @@ -834,18 +892,91 @@ class TranslationGenerator { /** * Build dot-notation paths from git diff by analyzing changed lines. + * Uses GitHub API diff when --pr-number is provided (CI mode), falls back to git diff otherwise. */ - private buildPathsFromGitDiff(): void { + private async buildPathsFromGitDiff(): Promise { try { // Get the relative path from the git repo root const relativePath = path.relative(process.cwd(), path.join(this.languagesDir, 'en.ts')); - // Run git diff to find changed lines - const diffResult = Git.diff(this.compareRef, undefined, relativePath); + let diffResult: DiffResult; + + if (this.prNumber) { + // CI mode: Use GitHub API for accurate PR-specific diff + try { + if (this.verbose) { + console.log(`🌐 Using GitHub API diff for PR #${this.prNumber}`); + } + + // GitHub's PR diff is a three-dot diff (merge-base...head), so we need the + // actual merge-base commit, not the tip of the base branch. + this.diffBase = await GitHubUtils.getPullRequestMergeBaseSHA(this.prNumber); + this.useGitHubAPI = true; + + if (this.verbose) { + console.log(`📌 PR merge-base SHA: ${this.diffBase.slice(0, 8)}`); + } + + const prDiff = await GitHubUtils.getPullRequestDiff(this.prNumber); + const parsedDiff = Git.parseDiff(prDiff); + + // Filter to only en.ts + const enTsFile = parsedDiff.files.find((f) => f.filePath === relativePath); + diffResult = { + files: enTsFile ? [enTsFile] : [], + hasChanges: !!enTsFile, + }; + + if (this.verbose) { + const enTsMatchCount = diffResult.hasChanges ? 1 : 0; + console.log(`📄 GitHub API diff: ${parsedDiff.files.length} files total, ${enTsMatchCount} matching en.ts`); + } + } catch (apiError: unknown) { + // Fallback to git diff if GitHub API fails (e.g., 406 for large diffs >20K lines) + const isLargeDiff = apiError !== null && typeof apiError === 'object' && 'status' in apiError && apiError.status === TranslationGenerator.HTTP_STATUS_DIFF_TOO_LARGE; + + if (isLargeDiff) { + console.warn('⚠️ GitHub API diff too large (>20K lines or >1MB). Falling back to git diff.'); + } else { + console.warn(`⚠️ GitHub API failed: ${TranslationGenerator.getErrorMessage(apiError)}. Falling back to git diff.`); + } + + this.useGitHubAPI = false; + if (this.compareRef) { + this.diffBase = this.compareRef; + diffResult = Git.diff(this.compareRef, undefined, relativePath); + } else { + throw new Error('GitHub API failed and no --compare-ref provided for fallback'); + } + } + } else { + // Local mode: Use git merge-base for accurate diff + if (!this.compareRef) { + throw new Error('--compare-ref is required when --pr-number is not provided for incremental translation'); + } + + this.diffBase = this.compareRef; + try { + const mergeBaseResult = execFileSync('git', ['merge-base', this.compareRef, 'HEAD'], {encoding: 'utf8'}); + const mergeBase = mergeBaseResult.trim(); + if (mergeBase && TranslationGenerator.GIT_SHA_REGEX.test(mergeBase)) { + this.diffBase = mergeBase; + if (this.verbose) { + console.log(`🔀 Using merge-base ${mergeBase.slice(0, 8)} (common ancestor of ${this.compareRef} and HEAD)`); + } + } + } catch (mergeBaseError: unknown) { + // If merge-base fails (e.g., shallow clone), fall back to direct diff + if (this.verbose) { + console.log(`⚠️ Could not calculate merge-base (${TranslationGenerator.getErrorMessage(mergeBaseError)}), using ${this.compareRef} directly`); + } + } + diffResult = Git.diff(this.diffBase, undefined, relativePath); + } if (!diffResult.hasChanges) { if (this.verbose) { - console.log('🔍 No changes detected in git diff'); + console.log('🔍 No changes detected in diff'); } return; } @@ -868,7 +999,7 @@ class TranslationGenerator { // For removed paths, we need to traverse the old version of en.ts if (changedLines.removedLines.size > 0) { - this.extractRemovedPaths(changedLines.removedLines); + await this.extractRemovedPaths(changedLines.removedLines, relativePath); } // Handle the case where the same path has both additions and removals (treat as modified, not deleted) @@ -911,8 +1042,8 @@ class TranslationGenerator { console.log(`➕ Paths to add: ${Array.from(this.pathsToAdd).join(', ')}`); console.log(`🗑️ Paths to remove: ${Array.from(this.pathsToRemove).join(', ')}`); } - } catch (error) { - throw new Error('Error building paths from git diff, giving up on --compare-ref incremental translation'); + } catch (error: unknown) { + throw new Error(`Error building paths from diff, giving up on incremental translation: ${TranslationGenerator.getErrorMessage(error)}`); } } @@ -1066,12 +1197,17 @@ class TranslationGenerator { /** * Extract removed paths by traversing the old version of en.ts. + * Uses diffBase (merge-base SHA or PR base SHA) to get the file at the correct commit + * that the diff was computed against, ensuring line numbers align. */ - private extractRemovedPaths(removedLines: Set): void { + private async extractRemovedPaths(removedLines: Set, relativePath: string): Promise { try { - // Get the old version of en.ts from the compare ref - const relativePath = path.relative(process.cwd(), this.sourceFile.fileName); - const oldEnContent = Git.show(this.compareRef, relativePath); + let oldEnContent: string; + if (this.useGitHubAPI) { + oldEnContent = await GitHubUtils.getFileContents(relativePath, this.diffBase); + } else { + oldEnContent = Git.show(this.diffBase, relativePath); + } const oldSourceFile = ts.createSourceFile(this.sourceFile.fileName, oldEnContent, ts.ScriptTarget.Latest, true); const oldTranslationsNode = this.findTranslationsNode(oldSourceFile); diff --git a/scripts/runPrettier.sh b/scripts/runPrettier.sh deleted file mode 100755 index 5df1552115da..000000000000 --- a/scripts/runPrettier.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# -# Prettier can intermittently fail due to a stale cache. This wrapper runs -# prettier once, and if it fails, clears the cache and retries. - -set -eu - -TOP="$(realpath "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/..")" -readonly TOP -source "${TOP}/scripts/shellUtils.sh" - -declare -r PRETTIER_CACHE_DIR="${TOP}/node_modules/.cache/prettier" - -function run_prettier() { - npx prettier --experimental-cli --write . "$@" -} - -function clear_cache() { - if [[ -d "${PRETTIER_CACHE_DIR}" ]]; then - info "Clearing prettier cache at ${PRETTIER_CACHE_DIR}" - rm -rf "${PRETTIER_CACHE_DIR}" - fi -} - -function main() { - if run_prettier "$@"; then - success "Prettier finished successfully" - return - fi - - error "Prettier failed — clearing cache and retrying" - clear_cache - - if run_prettier "$@"; then - success "Prettier finished successfully after clearing cache" - else - error "Prettier failed again after clearing cache" - exit 1 - fi -} - -main "$@" diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 975f0ccf33d4..41e00122aebb 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1778,9 +1778,7 @@ const CONST = { // Span names SPAN_OPEN_REPORT: 'ManualOpenReport', SPAN_APP_STARTUP: 'ManualAppStartup', - SPAN_NAVIGATE_TO_REPORTS_TAB: 'ManualNavigateToReportsTab', - SPAN_NAVIGATE_TO_REPORTS_TAB_RENDER: 'ManualNavigateToReportsTabRender', - SPAN_ON_LAYOUT_SKELETON_REPORTS: 'ManualOnLayoutSkeletonReports', + SPAN_NAVIGATE_TO_REPORTS: 'ManualNavigateToReports', SPAN_NAVIGATE_TO_INBOX_TAB: 'ManualNavigateToInboxTab', SPAN_OD_ND_TRANSITION: 'ManualOdNdTransition', SPAN_OD_ND_TRANSITION_LOGGED_OUT: 'ManualOdNdTransitionLoggedOut', @@ -1852,6 +1850,7 @@ const CONST = { ATTRIBUTE_ROUTE_TO: 'route_to', ATTRIBUTE_MIN_DURATION: 'min_duration', ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually', + ATTRIBUTE_IS_WARM: 'is_warm', ATTRIBUTE_SKELETON_PREFIX: 'skeleton.', ATTRIBUTE_SCENARIO: 'scenario', ATTRIBUTE_HAS_RECEIPT: 'has_receipt', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b9ad1ab7cdb8..a50016afc090 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -674,7 +674,6 @@ const ONYXKEYS = { WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', WORKSPACE_INVITE_ROLE_DRAFT: 'workspaceInviteRoleDraft_', - WORKSPACE_INVITE_APPROVER_DRAFT: 'workspaceInviteApproverDraft_', REPORT: 'report_', REPORT_NAME_VALUE_PAIRS: 'reportNameValuePairs_', REPORT_DRAFT: 'reportDraft_', @@ -1160,7 +1159,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT]: string; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_APPROVER_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: OnyxTypes.ReportNameValuePairs; [ONYXKEYS.COLLECTION.REPORT_DRAFT]: OnyxTypes.Report; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0f4204255bcd..13de6a81ded2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1536,12 +1536,6 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`workspaces/${policyID}/invite-message/role`, backTo)}` as const, }, - WORKSPACE_INVITE_MESSAGE_APPROVER: { - route: 'workspaces/:policyID/invite-message/approver', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`workspaces/${policyID}/invite-message/approver`, backTo)}` as const, - }, WORKSPACE_OVERVIEW: { route: 'workspaces/:policyID/overview', getRoute: (policyID: string | undefined, backTo?: string) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ba2f1a24b3bd..f8e33acb4f54 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -672,7 +672,6 @@ const SCREENS = { INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', INVITE_MESSAGE_ROLE: 'Workspace_Invite_Message_Role', - INVITE_MESSAGE_APPROVER: 'Workspace_Invite_Message_Approver', CATEGORIES: 'Workspace_Categories', TAGS: 'Workspace_Tags', TAGS_SETTINGS: 'Tags_Settings', diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index b5ba853f3b98..f652653f910f 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -121,7 +121,7 @@ function Banner({ {shouldShowButton && (