diff --git a/Mobile-Expensify b/Mobile-Expensify
index 42ad9da8bfde..fdf508981aff 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 42ad9da8bfde582b57b2e66dc30e22757e06deea
+Subproject commit fdf508981aff5189c5c17c67493a3d1c4535c1a6
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c7a7c6045cb9..a5b9d35d6bf0 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 1009040500
- versionName "9.4.5-0"
+ versionCode 1009040600
+ versionName "9.4.6-0"
// 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/assets/images/receipt-search.svg b/assets/images/receipt-search.svg
new file mode 100644
index 000000000000..bbf36cf9b1df
--- /dev/null
+++ b/assets/images/receipt-search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs
index f41e62edc95c..86e05b589d15 100644
--- a/config/eslint/eslint.config.mjs
+++ b/config/eslint/eslint.config.mjs
@@ -157,11 +157,6 @@ const restrictedImportPaths = [
];
const restrictedImportPatterns = [
- {
- group: ['**/TransitionTracker', './TransitionTracker', '../TransitionTracker'],
- message:
- "TransitionTracker is an internal primitive. Please use higher-level APIs (Navigation with 'afterTransition'/'waitForTransition', KeyboardUtils, useConfirmModal). See contributingGuides/INTERACTION_MANAGER.md.",
- },
{
group: ['**/assets/animations/**/*.json'],
message: "Do not import animations directly. Please use the '@components/LottieAnimations' import instead.",
@@ -188,6 +183,26 @@ const restrictedReportNameImportPatterns = [
},
];
+// `isPaidGroupPolicy` is BILLING/paid-only (Collect/Control). Existing usages are grandfathered via
+// eslint-seatbelt; this only flags NEW imports so they make a conscious choice: for workspace feature
+// gating (violations, report fields, workspace chat, report creation, expense-workspace usability) use
+// `isGroupPolicy` / `isReportInGroupPolicy` instead, otherwise free group plans like Submit (submit2026)
+// are wrongly excluded and access bugs return.
+const restrictedPaidGroupPolicyImportPatterns = [
+ {
+ group: ['**/PolicyUtils', '**/libs/PolicyUtils'],
+ importNames: ['isPaidGroupPolicy'],
+ message:
+ 'isPaidGroupPolicy is billing/paid-only (Collect/Control). For workspace feature gating use isGroupPolicy so free group plans like Submit are not excluded. If this is genuinely a billing/paid-only check, keep it and disable this line with a reason.',
+ },
+ {
+ group: ['**/ReportUtils', '**/libs/ReportUtils'],
+ importNames: ['isPaidGroupPolicy', 'isPaidGroupPolicyExpenseReport'],
+ message:
+ 'isPaidGroupPolicy / isPaidGroupPolicyExpenseReport are billing/paid-only. For feature gating use isReportInGroupPolicy / isGroupPolicyExpenseReport so Submit workspaces are not excluded. If this is genuinely billing/paid-only, keep it and disable this line with a reason.',
+ },
+];
+
const config = defineConfig([
expensifyConfig,
typescriptEslint.configs.recommendedTypeChecked,
@@ -255,8 +270,6 @@ const config = defineConfig([
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'],
rules: {
- '@lwc/lwc/no-async-await': 'off',
-
// TypeScript specific rules
'@typescript-eslint/prefer-enum-initializers': 'error',
'@typescript-eslint/no-var-requires': 'off',
@@ -336,7 +349,6 @@ const config = defineConfig([
'es/no-optional-chaining': 'off',
'@typescript-eslint/no-deprecated': ['error', {allow: ['translateFn']}],
'arrow-body-style': 'off',
- 'no-continue': 'off',
'no-empty': ['error', {allowEmptyCatch: true}],
// Import specific rules
@@ -715,7 +727,7 @@ const config = defineConfig([
'error',
{
paths: restrictedImportPaths,
- patterns: [...restrictedImportPatterns, ...restrictedReportNameImportPatterns],
+ patterns: [...restrictedImportPatterns, ...restrictedReportNameImportPatterns, ...restrictedPaidGroupPolicyImportPatterns],
},
],
},
diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv
index 57557c64e0ce..7efae31cde1c 100644
--- a/config/eslint/eslint.seatbelt.tsv
+++ b/config/eslint/eslint.seatbelt.tsv
@@ -68,7 +68,7 @@
"../../src/Expensify.tsx" "react-hooks/set-state-in-effect" 1
"../../src/GlobalModals.tsx" "no-restricted-syntax" 2
"../../src/ONYXKEYS.ts" "no-restricted-syntax" 2
-"../../src/ROUTES.ts" "@typescript-eslint/no-deprecated/getUrlWithBackToParam" 132
+"../../src/ROUTES.ts" "@typescript-eslint/no-deprecated/getUrlWithBackToParam" 139
"../../src/ROUTES.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/TIMEZONES.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/AccountingConnectionConfirmationModal.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
@@ -109,6 +109,7 @@
"../../src/components/AvatarSelector.tsx" "no-restricted-syntax" 2
"../../src/components/AvatarWithImagePicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/AvatarWithImagePicker.tsx" "react-hooks/set-state-in-effect" 2
+"../../src/components/BookTravelButton.tsx" "no-restricted-imports" 1
"../../src/components/Button/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/Button/validateSubmitShortcut/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/ButtonComposed/Button.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -249,7 +250,7 @@
"../../src/components/MapView/MapViewImpl.web.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/MapView/MapViewImpl.web.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/MapView/ToggleDistanceUnitButton/index.android.tsx" "no-restricted-syntax" 1
-"../../src/components/MenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../src/components/MenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/components/Modal/BaseModal.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/Modal/Global/ConfirmModalWrapper.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
"../../src/components/Modal/Global/ModalContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -276,10 +277,9 @@
"../../src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "no-restricted-imports" 1
"../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "react-hooks/refs" 3
"../../src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx" "react-hooks/set-state-in-effect" 3
-"../../src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
+"../../src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -296,8 +296,8 @@
"../../src/components/MultifactorAuthentication/config/scenarios/ChangePIN.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/MultifactorAuthentication/config/scenarios/DefaultUserInterface.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/Navigation/NavigationTabBar/SearchTabButton.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/components/Navigation/NavigationTabBar/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/components/Navigation/QuickCreationActionsBar/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/components/Navigation/QuickCreationActionsBar/index.tsx" "no-restricted-imports" 1
"../../src/components/Navigation/TopLevelNavigationTabBar/index.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/NavigationDeferredMount.tsx" "no-restricted-imports" 1
"../../src/components/NumberWithSymbolForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -377,6 +377,8 @@
"../../src/components/Search/SearchList/ListItem/ChatListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../src/components/Search/SearchList/ListItem/GroupHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/components/Search/SearchList/ListItem/TaskListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -408,7 +410,6 @@
"../../src/components/Search/hooks/useOptimisticSearchTracking.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/components/Search/hooks/useStableOptimisticSortedData.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/Search/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 14
-"../../src/components/Search/index.tsx" "no-restricted-imports" 1
"../../src/components/Search/index.tsx" "react-hooks/preserve-manual-memoization" 2
"../../src/components/Search/index.tsx" "react-hooks/refs" 5
"../../src/components/Search/index.tsx" "react-hooks/set-state-in-effect" 1
@@ -423,6 +424,7 @@
"../../src/components/SelectionList/utils/getListboxRole/index.web.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/SettlementButton/AnimatedSettlementButton.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/SettlementButton/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 11
+"../../src/components/SettlementButton/index.tsx" "no-restricted-imports" 1
"../../src/components/SidePanel/SidePanelOverlay.tsx" "no-restricted-syntax" 1
"../../src/components/SidePanel/SidePanelReport/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/SignInButtons/IconButton.tsx" "no-restricted-syntax" 1
@@ -446,11 +448,10 @@
"../../src/components/Table/TableContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/components/Table/TableFilterButtons/buildFilterItems.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/Table/middlewares/filtering.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
-"../../src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
+"../../src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/TagPicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/TaxPicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/TestDrive/Modal/AdminTestDriveModal.tsx" "no-restricted-imports" 1
-"../../src/components/TestDrive/Modal/EmployeeTestDriveModal.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/components/TestDrive/TestDriveDemo.tsx" "no-restricted-imports" 1
"../../src/components/TextInput/BaseTextInput/implementation/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/TextInput/BaseTextInput/implementation/index.native.tsx" "react-hooks/refs" 2
@@ -503,6 +504,7 @@
"../../src/hooks/useAdvancedSearchFilters.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useAnimatedHighlightStyle/index.ts" "react-hooks/set-state-in-effect" 2
"../../src/hooks/useAssignCard.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/hooks/useAutoCreateTrackWorkspace.ts" "no-restricted-imports" 1
"../../src/hooks/useAutoFocusInput.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/hooks/useAutoUpdateTimezone.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useAutocompleteSuggestions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -514,7 +516,9 @@
"../../src/hooks/useCachedImageSource.ts" "react-hooks/set-state-in-effect" 1
"../../src/hooks/useCancellationType.ts" "react-hooks/refs" 2
"../../src/hooks/useCancellationType.ts" "react-hooks/set-state-in-effect" 1
+"../../src/hooks/useCardFeedsForActivePolicies.ts" "no-restricted-imports" 1
"../../src/hooks/useCardFeedsForDisplay.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/hooks/useCardFeedsForDisplay.ts" "no-restricted-imports" 1
"../../src/hooks/useCreateReport.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useDebounce.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useDebounceNonReactive.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -526,18 +530,18 @@
"../../src/hooks/useDomainGroupFilter.ts" "react-hooks/set-state-in-effect" 1
"../../src/hooks/useDragAndDrop/types.ts" "@typescript-eslint/no-deprecated/React.MutableRefObject" 1
"../../src/hooks/useDynamicBackPath.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/hooks/useExpenseActions.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 3
+"../../src/hooks/useExpenseActions.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/hooks/useExportActions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useExportedToFilterOptions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useFilesValidation.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
"../../src/hooks/useFilterFormValues.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/hooks/useGeographicalStateAndCountryFromRoute.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
-"../../src/hooks/useHRSyncResultsModal.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useHtmlPaste/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/hooks/useImportSpreadsheetConfirmModal.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useInitial.ts" "react-hooks/refs" 4
"../../src/hooks/useIsBlockedToAddFeed.ts" "react-hooks/set-state-in-effect" 1
"../../src/hooks/useIsOwnWorkspaceChatRef.ts" "react-hooks/refs" 2
+"../../src/hooks/useIsPaidPolicyAdmin.ts" "no-restricted-imports" 1
"../../src/hooks/useKeyboardShortcut.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useLazyAsset.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../src/hooks/useLazyAsset.ts" "react-hooks/refs" 2
@@ -647,6 +651,7 @@
"../../src/libs/Middleware/Pagination.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../src/libs/Middleware/SaveResponseInOnyx.ts" "no-restricted-syntax" 1
"../../src/libs/ModifiedExpenseMessage.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
+"../../src/libs/MoneyRequestReportUtils.ts" "no-restricted-imports" 1
"../../src/libs/Navigation/AppNavigator/AuthScreens.tsx" "no-restricted-syntax" 1
"../../src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx" "no-restricted-syntax" 6
"../../src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx" "react-hooks/refs" 1
@@ -657,6 +662,7 @@
"../../src/libs/Navigation/AppNavigator/KeyboardShortcutsHandler/ShortcutsOverviewHandler.tsx" "no-restricted-syntax" 1
"../../src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../src/libs/Navigation/AppNavigator/Navigators/Overlay/BaseOverlay.tsx" "no-restricted-syntax" 2
+"../../src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx" "no-restricted-imports" 1
"../../src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx" "no-restricted-syntax" 1
"../../src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx" "no-restricted-imports" 1
@@ -665,9 +671,8 @@
"../../src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/AppNavigator/Navigators/TabNavigator.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/AppNavigator/Navigators/TestToolsModalNavigator.tsx" "no-restricted-syntax" 1
-"../../src/libs/Navigation/AppNavigator/Navigators/WorkspaceNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
-"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts" "@typescript-eslint/no-unsafe-type-assertion" 17
+"../../src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts" "@typescript-eslint/no-unsafe-type-assertion" 15
"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/restoreTabNavigatorRoutes.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
@@ -688,7 +693,6 @@
"../../src/libs/Navigation/guards/OnboardingGuard.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-deprecated/escape" 1
"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/libs/Navigation/helpers/dismissModalAndOpenReportInInboxTab.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/libs/Navigation/helpers/dynamicRoutesUtils/getDynamicRouteAdaptedState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/helpers/dynamicRoutesUtils/getDynamicRouteQueryParams.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -725,6 +729,7 @@
"../../src/libs/NextStepUtils.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 2
"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "@typescript-eslint/no-deprecated/translateLocal" 1
"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "no-restricted-imports" 1
"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "no-restricted-syntax" 2
"../../src/libs/Notification/PushNotification/parsePushNotificationPayload.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../src/libs/Notification/PushNotification/shouldShowPushNotification.ts" "no-restricted-syntax" 2
@@ -758,10 +763,11 @@
"../../src/libs/ReportActionItemEventHandler/index.android.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-deprecated/reportAction.sequenceNumber" 1
"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-deprecated/reportAction?.originalMessage" 2
-"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 110
+"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 111
"../../src/libs/ReportActionsUtils.ts" "rulesdir/no-onyx-connect" 3
"../../src/libs/ReportNameUtils.ts" "@typescript-eslint/no-deprecated/translateLocal" 12
"../../src/libs/ReportNameUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/libs/ReportPrimaryActionUtils.ts" "no-restricted-imports" 1
"../../src/libs/ReportTitleUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-deprecated/getPolicy" 28
"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-deprecated/translateLocal" 34
@@ -770,6 +776,7 @@
"../../src/libs/SearchAutocompleteUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/libs/SearchQueryUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 73
"../../src/libs/SearchUIUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 69
+"../../src/libs/SearchUIUtils.ts" "no-restricted-imports" 1
"../../src/libs/SelectionScraper/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/libs/SidebarUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Sound/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
@@ -782,6 +789,7 @@
"../../src/libs/TransactionPreviewUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/TransactionUtils/index.ts" "@typescript-eslint/no-deprecated/translateLocal" 5
"../../src/libs/TransactionUtils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
+"../../src/libs/UnreadIndicatorUpdater/index.ts" "no-restricted-imports" 1
"../../src/libs/UnreadIndicatorUpdater/index.ts" "no-restricted-syntax" 1
"../../src/libs/Url.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/libs/UserAvatarUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
@@ -800,6 +808,7 @@
"../../src/libs/actions/Chronos.ts" "no-restricted-syntax" 1
"../../src/libs/actions/ClearReportActionErrors.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/actions/CompanyCards.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../src/libs/actions/CompanyCards.ts" "no-restricted-imports" 1
"../../src/libs/actions/CompanyCards.ts" "no-restricted-syntax" 5
"../../src/libs/actions/Delegate.ts" "no-restricted-syntax" 4
"../../src/libs/actions/Domain.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
@@ -823,6 +832,7 @@
"../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/libs/actions/IOU/PayMoneyRequest.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 3
"../../src/libs/actions/IOU/PayMoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/libs/actions/IOU/PayMoneyRequest.ts" "no-restricted-imports" 1
"../../src/libs/actions/IOU/PayMoneyRequest.ts" "no-restricted-syntax" 1
"../../src/libs/actions/IOU/PerDiem.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 1
"../../src/libs/actions/IOU/PerDiem.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
@@ -834,6 +844,7 @@
"../../src/libs/actions/IOU/RejectMoneyRequest.ts" "no-restricted-syntax" 1
"../../src/libs/actions/IOU/ReportWorkflow.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 7
"../../src/libs/actions/IOU/ReportWorkflow.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/libs/actions/IOU/ReportWorkflow.ts" "no-restricted-imports" 1
"../../src/libs/actions/IOU/ReportWorkflow.ts" "no-restricted-syntax" 1
"../../src/libs/actions/IOU/SearchUpdate.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/libs/actions/IOU/SendInvoice.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -864,13 +875,13 @@
"../../src/libs/actions/ImportTransactions.ts" "no-restricted-syntax" 1
"../../src/libs/actions/InputFocus/index.web.ts" "no-restricted-imports" 1
"../../src/libs/actions/InputFocus/index.web.ts" "no-restricted-syntax" 1
-"../../src/libs/actions/Link.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/libs/actions/Link.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/libs/actions/Link.ts" "no-restricted-syntax" 3
"../../src/libs/actions/MapboxToken.ts" "no-restricted-syntax" 2
"../../src/libs/actions/MergeAccounts.ts" "no-restricted-syntax" 1
"../../src/libs/actions/MergeTransaction.ts" "@typescript-eslint/no-deprecated/getPolicyTagsData" 1
"../../src/libs/actions/MergeTransaction.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/libs/actions/MergeTransaction.ts" "no-restricted-imports" 1
"../../src/libs/actions/MergeTransaction.ts" "no-restricted-syntax" 1
"../../src/libs/actions/OdometerTransactionUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/actions/Onboarding.ts" "no-restricted-syntax" 1
@@ -889,6 +900,7 @@
"../../src/libs/actions/Policy/Category.ts" "no-restricted-syntax" 5
"../../src/libs/actions/Policy/DistanceRate.ts" "no-restricted-syntax" 2
"../../src/libs/actions/Policy/Member.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/libs/actions/Policy/Member.ts" "no-restricted-imports" 1
"../../src/libs/actions/Policy/Member.ts" "no-restricted-syntax" 8
"../../src/libs/actions/Policy/Member.ts" "rulesdir/no-onyx-connect" 1
"../../src/libs/actions/Policy/PerDiem.ts" "no-restricted-syntax" 1
@@ -896,11 +908,14 @@
"../../src/libs/actions/Policy/Policy.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 1
"../../src/libs/actions/Policy/Policy.ts" "@typescript-eslint/no-deprecated/translateLocal" 1
"../../src/libs/actions/Policy/Policy.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
+"../../src/libs/actions/Policy/Policy.ts" "no-restricted-imports" 2
"../../src/libs/actions/Policy/Policy.ts" "no-restricted-syntax" 10
"../../src/libs/actions/Policy/Policy.ts" "rulesdir/no-onyx-connect" 2
+"../../src/libs/actions/Policy/ReportField.ts" "no-restricted-imports" 1
"../../src/libs/actions/Policy/ReportField.ts" "no-restricted-syntax" 4
"../../src/libs/actions/Policy/Rules.ts" "no-restricted-syntax" 3
"../../src/libs/actions/Policy/Tag.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/libs/actions/Policy/Tag.ts" "no-restricted-imports" 1
"../../src/libs/actions/Policy/Tag.ts" "no-restricted-syntax" 4
"../../src/libs/actions/Policy/Travel.ts" "no-restricted-syntax" 2
"../../src/libs/actions/PolicyConnections.ts" "no-restricted-syntax" 1
@@ -908,7 +923,6 @@
"../../src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts" "no-restricted-syntax" 1
"../../src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts" "no-restricted-syntax" 1
"../../src/libs/actions/Report/MarkAllMessageAsRead.tsx" "no-restricted-syntax" 1
-"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 3
"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/reportAction.originalMessage" 1
"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 20
@@ -921,7 +935,6 @@
"../../src/libs/actions/Search.ts" "@typescript-eslint/no-unsafe-type-assertion" 19
"../../src/libs/actions/Search.ts" "no-restricted-syntax" 1
"../../src/libs/actions/Session/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
-"../../src/libs/actions/Session/index.ts" "no-restricted-imports" 1
"../../src/libs/actions/Session/index.ts" "no-restricted-syntax" 9
"../../src/libs/actions/Session/index.ts" "rulesdir/no-onyx-connect" 2
"../../src/libs/actions/SplitExpenses.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -931,6 +944,7 @@
"../../src/libs/actions/Subscription.ts" "no-restricted-syntax" 1
"../../src/libs/actions/Tab.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/actions/Task.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
+"../../src/libs/actions/Task.ts" "no-restricted-imports" 1
"../../src/libs/actions/Task.ts" "no-restricted-syntax" 7
"../../src/libs/actions/TaxRate.ts" "no-restricted-syntax" 1
"../../src/libs/actions/TeachersUnite.ts" "no-restricted-syntax" 1
@@ -943,6 +957,7 @@
"../../src/libs/actions/TravelInvoicing.ts" "no-restricted-syntax" 3
"../../src/libs/actions/UnreportedExpenses.tsx" "no-restricted-syntax" 1
"../../src/libs/actions/User.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
+"../../src/libs/actions/User.ts" "no-restricted-imports" 1
"../../src/libs/actions/User.ts" "no-restricted-syntax" 8
"../../src/libs/actions/VacationDelegate.ts" "no-restricted-syntax" 2
"../../src/libs/actions/Wallet.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
@@ -960,8 +975,10 @@
"../../src/libs/actions/connections/QuickbooksOnline.ts" "no-restricted-syntax" 2
"../../src/libs/actions/connections/SageIntacct.ts" "no-restricted-syntax" 2
"../../src/libs/actions/connections/Xero.ts" "no-restricted-syntax" 2
+"../../src/libs/actions/connections/index.ts" "no-restricted-imports" 1
"../../src/libs/actions/connections/index.ts" "no-restricted-syntax" 3
"../../src/libs/actions/getCompanyCardBankConnection/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../src/libs/actions/getCompanyCardBankConnection/index.tsx" "no-restricted-imports" 1
"../../src/libs/actions/getCompanyCardBankConnection/index.tsx" "no-restricted-syntax" 2
"../../src/libs/actions/replaceOptimisticReportWithActualReport.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/libs/actions/replaceOptimisticReportWithActualReport.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1032,10 +1049,14 @@
"../../src/pages/MissingPersonalDetails/subPages/Address.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../src/pages/MissingPersonalDetails/subPages/Address.tsx" "react-hooks/refs" 4
"../../src/pages/MultifactorAuthentication/RevokePage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
+"../../src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx" "no-restricted-imports" 1
"../../src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx" "no-restricted-imports" 1
"../../src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx" "react-hooks/preserve-manual-memoization" 2
"../../src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/OnboardingWorkspaceConfirmation/BaseOnboardingWorkspaceConfirmation.tsx" "no-restricted-imports" 1
"../../src/pages/OnboardingWorkspaceCurrency/BaseOnboardingWorkspaceCurrency.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/OnboardingWorkspaceOptional/BaseOnboardingWorkspaceOptional.tsx" "no-restricted-imports" 1
"../../src/pages/ProfilePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/ReimbursementAccount/AddressFormFields.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/ReimbursementAccount/EnterSignerInfo/index.tsx" "@typescript-eslint/no-deprecated/useSubStep" 1
@@ -1069,6 +1090,7 @@
"../../src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/BankAccountValidationForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/ReimbursementAccount/USD/USDVerifiedBankAccountFlowPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/pages/ReimbursementAccount/utils/getSubStepValues.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../src/pages/ReportDescriptionPage.tsx" "no-restricted-imports" 1
"../../src/pages/ReportDescriptionPage.tsx" "no-restricted-syntax" 1
"../../src/pages/ReportParticipantDetailsPage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
"../../src/pages/ReportParticipantDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1095,6 +1117,7 @@
"../../src/pages/ShareCodePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/TeachersUnite/SaveTheWorldPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/TransactionDuplicate/Confirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/TransactionDuplicate/Confirmation.tsx" "no-restricted-imports" 1
"../../src/pages/TransactionDuplicate/Confirmation.tsx" "react-hooks/refs" 12
"../../src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/TransactionMerge/ConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1102,6 +1125,7 @@
"../../src/pages/TransactionMerge/MergeTransactionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/TransactionMerge/MergeTransactionsListContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/Travel/DynamicTravelTerms.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/Travel/DynamicTravelUpgrade.tsx" "no-restricted-imports" 1
"../../src/pages/Travel/TravelUpgrade.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/Travel/WorkspaceConfirmationForTravelPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/UnreportedExpenseListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1110,9 +1134,11 @@
"../../src/pages/domain/Saml/SamlLoginSectionContent.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
"../../src/pages/home/ForYouSection/EmptyState.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts" "no-restricted-imports" 1
"../../src/pages/home/SpendOverTimeSection/useSpendOverTimeData.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/home/TimeSensitiveSection/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/home/TimeSensitiveSection/items/FixCompanyCardConnection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/pages/home/YourSpendSection/useYourSpendData.ts" "no-restricted-imports" 1
"../../src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx" "no-restricted-imports" 1
"../../src/pages/inbox/ReactionListWrapper.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/inbox/ReportFetchHandler.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2
@@ -1149,7 +1175,6 @@
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/refs" 5
"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/inbox/report/ReportActionsList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/pages/inbox/report/ReportActionsList.tsx" "no-restricted-imports" 1
"../../src/pages/inbox/report/ReportActionsList.tsx" "react-hooks/refs" 2
"../../src/pages/inbox/report/TripSummary.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/inbox/report/UserTypingEventListener.tsx" "no-restricted-imports" 1
@@ -1166,24 +1191,23 @@
"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/QuickActionMenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
+"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx" "no-restricted-imports" 1
"../../src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/inbox/sidebar/SidebarLinksData.tsx" "react-hooks/refs" 1
-"../../src/pages/inbox/usePusherDraftPacing.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/iou/MoneyRequestAmountForm.tsx" "react-hooks/set-state-in-effect" 3
"../../src/pages/iou/SplitExpensePage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/pages/iou/SplitExpensePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/iou/SplitExpensePage.tsx" "react-hooks/set-state-in-effect" 3
"../../src/pages/iou/SplitList/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/iou/request/MoneyRequestAttendeeSelector.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/pages/iou/request/MoneyRequestAttendeeSelector.tsx" "no-restricted-imports" 1
"../../src/pages/iou/request/ParticipantSearchResults.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/pages/iou/request/step/DistanceManualTabContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../src/pages/iou/request/step/DynamicIOURequestStepCategoryPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
-"../../src/pages/iou/request/step/DynamicIOURequestStepUpgradePage.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
-"../../src/pages/iou/request/step/DynamicIOURequestStepUpgradePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/iou/request/step/IOURequestEditReport.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/iou/request/step/IOURequestEditReportCommon.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
"../../src/pages/iou/request/step/IOURequestEditReportCommon.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/iou/request/step/IOURequestStepAmount.tsx" "react-hooks/set-state-in-effect" 1
+"../../src/pages/iou/request/step/IOURequestStepCategory.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/pages/iou/request/step/IOURequestStepConfirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/pages/iou/request/step/IOURequestStepConfirmation.tsx" "react-hooks/preserve-manual-memoization" 1
"../../src/pages/iou/request/step/IOURequestStepDestination.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
@@ -1209,6 +1233,8 @@
"../../src/pages/iou/request/step/IOURequestStepSubrate.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/pages/iou/request/step/IOURequestStepSubrate.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/iou/request/step/IOURequestStepSubrate.tsx" "react-hooks/set-state-in-effect" 1
+"../../src/pages/iou/request/step/IOURequestStepUpgrade.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
+"../../src/pages/iou/request/step/IOURequestStepUpgrade.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/iou/request/step/IOURequestStepWaypoint.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/iou/request/step/StepScreenWrapper.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/iou/request/step/confirmation/submitDismissStrategies.ts" "no-restricted-imports" 1
@@ -1286,6 +1312,7 @@
"../../src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/settings/Wallet/WalletPage/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/pages/settings/Wallet/WalletPage/index.tsx" "no-restricted-imports" 1
"../../src/pages/settings/Wallet/WalletPage/index.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/signin/LoginForm/BaseLoginForm.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/pages/signin/SAMLSignInPage/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1298,7 +1325,6 @@
"../../src/pages/signin/SignInPageLayout/FooterRow/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx" "react-hooks/set-state-in-effect" 4
"../../src/pages/tasks/NewTaskPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
-"../../src/pages/tasks/TaskAssigneeSelectorModal.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2
"../../src/pages/tasks/TaskShareDestinationSelectorModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/wallet/WalletStatementPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1306,6 +1332,7 @@
"../../src/pages/workspace/WorkspaceConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/workspace/WorkspaceInitialPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/workspace/WorkspaceMembersPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/workspace/WorkspaceMembersPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/WorkspaceMembersPage.tsx" "react-hooks/exhaustive-deps" 1
"../../src/pages/workspace/WorkspaceMembersPage.tsx" "react-hooks/refs" 1
"../../src/pages/workspace/WorkspaceNewRoomPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
@@ -1366,12 +1393,16 @@
"../../src/pages/workspace/accounting/qbd/export/DynamicQuickbooksDesktopOutOfPocketExpenseEntitySelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx" "no-restricted-syntax" 2
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx" "rulesdir/no-default-id-values" 1
+"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx" "no-restricted-syntax" 3
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx" "rulesdir/no-default-id-values" 1
+"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx" "no-restricted-syntax" 3
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx" "rulesdir/no-default-id-values" 1
+"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx" "no-restricted-syntax" 1
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx" "rulesdir/no-default-id-values" 1
+"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx" "no-restricted-syntax" 3
"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/accounting/qbo/advanced/DynamicQuickbooksOnlineAccountingMethodPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1382,15 +1413,18 @@
"../../src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx" "no-restricted-syntax" 2
"../../src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx" "rulesdir/no-default-id-values" 1
+"../../src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx" "no-restricted-syntax" 2
"../../src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx" "no-restricted-syntax" 2
"../../src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/accounting/utils.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../src/pages/workspace/accounting/xero/XeroImportPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/xero/XeroImportPage.tsx" "no-restricted-syntax" 1
"../../src/pages/workspace/accounting/xero/XeroImportPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/accounting/xero/XeroMapTrackingCategoryConfigurationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx" "no-restricted-syntax" 4
"../../src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1399,10 +1433,11 @@
"../../src/pages/workspace/accounting/xero/export/DynamicXeroPurchaseBillStatusSelectorPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx" "no-restricted-syntax" 2
"../../src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx" "rulesdir/no-default-id-values" 1
+"../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "no-restricted-syntax" 3
"../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/categories/CategorySettingsPage.tsx" "react-hooks/preserve-manual-memoization" 1
-"../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "react-hooks/set-state-in-effect" 2
+"../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/workspace/companyCards/BankConnection/index.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/workspace/companyCards/WorkspaceCompanyCardAddWorkEmailPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1420,7 +1455,6 @@
"../../src/pages/workspace/copyPolicySettings/CopyPolicySettingsConfirmPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx" "react-hooks/preserve-manual-memoization" 1
-"../../src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx" "react-hooks/preserve-manual-memoization" 2
"../../src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1434,10 +1468,10 @@
"../../src/pages/workspace/hr/HRProviderCard.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/hr/utils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/pages/workspace/members/ImportMembersPage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/members/ImportMembersPage.tsx" "no-restricted-syntax" 1
"../../src/pages/workspace/members/ImportMembersPage.tsx" "rulesdir/no-default-id-values" 1
"../../src/pages/workspace/members/ImportedMembersConfirmationPage.tsx" "react-hooks/refs" 7
-"../../src/pages/workspace/members/ImportedMembersPage.tsx" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx" "react-hooks/set-state-in-effect" 1
"../../src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
@@ -1456,6 +1490,7 @@
"../../src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 3
"../../src/pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/upgrade/UpgradeIntro.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx" "no-restricted-imports" 1
"../../src/pages/workspace/withPolicy.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../src/pages/workspace/withPolicyAndFullscreenLoading.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1467,6 +1502,7 @@
"../../src/selectors/AdvancedSearchFiltersForm.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/selectors/Domain.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/selectors/Policy.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../src/selectors/Policy.ts" "no-restricted-imports" 1
"../../src/setup/addUtilsToWindow.ts" "@typescript-eslint/no-unsafe-type-assertion" 20
"../../src/setup/moduleInitPolyfill.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/setup/telemetry/index.web.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1551,19 +1587,29 @@
"../../tests/actions/BankAccountsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/actions/CopyPolicySettingsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 18
"../../tests/actions/DomainTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 20
+"../../tests/actions/EnforceActionExportRestrictions.ts" "no-restricted-imports" 1
"../../tests/actions/IOU/BuildOnyxDataForMoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 17
+"../../tests/actions/IOU/CreateDraftTransactionTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOU/GetMoneyRequestInformationTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../tests/actions/IOU/MoneyRequestBuilderTest.ts" "no-restricted-imports" 1
+"../../tests/actions/IOU/MoneyRequestSettersTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOU/PerDiemTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
"../../tests/actions/IOU/RequestMoneyTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
+"../../tests/actions/IOU/RequestMoneyTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOU/SearchUpdateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../tests/actions/IOU/SearchUpdateTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOU/SplitReportTotalsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 10
+"../../tests/actions/IOU/SplitReportTotalsTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOUTest/BulkEditTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 25
"../../tests/actions/IOUTest/DeleteMoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/actions/IOUTest/DuplicateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 23
"../../tests/actions/IOUTest/GetUpdateMoneyRequestParamsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/actions/IOUTest/HoldTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../tests/actions/IOUTest/HoldTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOUTest/PayMoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
+"../../tests/actions/IOUTest/PayMoneyRequestTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOUTest/ReceiptTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 9
+"../../tests/actions/IOUTest/ReceiptTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOUTest/RejectMoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/actions/IOUTest/ReportWorkflowTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 64
"../../tests/actions/IOUTest/SendInvoiceTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 7
@@ -1571,27 +1617,37 @@
"../../tests/actions/IOUTest/SplitSelfDMTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/actions/IOUTest/SplitTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 21
"../../tests/actions/IOUTest/TrackExpenseTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 10
+"../../tests/actions/IOUTest/TrackExpenseTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOUTest/UpdateMoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
-"../../tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../tests/actions/IOUTest/UpdateMoneyRequestTest.ts" "no-restricted-imports" 1
+"../../tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/actions/MergeTransactionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 10
+"../../tests/actions/OdometerTransactionUtilsTest.ts" "no-restricted-imports" 1
"../../tests/actions/OnyxUpdateManagerTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/actions/PersonalDetailsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 10
"../../tests/actions/PolicyCategoryTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/actions/PolicyMemberTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 9
"../../tests/actions/PolicyProfileTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../tests/actions/PolicyProfileTest.ts" "no-restricted-imports" 1
"../../tests/actions/PolicyTagTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/actions/PolicyTaxTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/actions/PolicyTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 86
+"../../tests/actions/PolicyTest.ts" "no-restricted-imports" 1
"../../tests/actions/QueuedOnyxUpdatesTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/actions/ReimbursementAccountTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../tests/actions/ReplaceOptimisticReportWithActualReportTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/actions/ReportFieldTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../tests/actions/ReportFieldTest.ts" "no-restricted-imports" 1
"../../tests/actions/ReportPreviewActionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 28
+"../../tests/actions/ReportPreviewActionUtilsTest.ts" "no-restricted-imports" 2
"../../tests/actions/ReportTest.ts" "@typescript-eslint/no-deprecated/buildNextStepNew" 1
"../../tests/actions/ReportTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 80
+"../../tests/actions/ReportTest.ts" "no-restricted-imports" 1
"../../tests/actions/SessionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 12
"../../tests/actions/SubscriptionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/actions/TaskTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 44
+"../../tests/actions/TaskTest.ts" "no-restricted-imports" 1
+"../../tests/actions/TransactionTest.ts" "no-restricted-imports" 1
"../../tests/actions/UserTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 45
"../../tests/actions/WorkflowTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 13
"../../tests/actions/connections/NetSuite.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
@@ -1625,7 +1681,7 @@
"../../tests/perf-test/ReportUtils.perf-test.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/perf-test/SelectionList.perf-test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/perf-test/useFilterFormValues.perf-test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 15
-"../../tests/ui/AgentsPromoBannersTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../tests/ui/AgentsPromoBannersTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/ui/AssignCardFeed.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/ui/AuthScreensInitHandlerTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7
"../../tests/ui/AvatarSelector.test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7
@@ -1655,6 +1711,7 @@
"../../tests/ui/MoneyReportViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/ui/MoneyRequestReportFooter.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
+"../../tests/ui/MoneyRequestReportPreview.test.tsx" "no-restricted-imports" 1
"../../tests/ui/MoneyRequestViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/ui/MonthListItemHeaderTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/ui/MultifactorAuthenticationRevokePageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 8
@@ -1694,6 +1751,7 @@
"../../tests/ui/WorkspaceInviteMessageApproverPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/ui/WorkspaceMembersTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/ui/WorkspaceMoreFeaturesPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../tests/ui/WorkspaceMoreFeaturesPageTest.tsx" "no-restricted-imports" 1
"../../tests/ui/WorkspaceOnboarding.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/ui/WorkspaceTagsTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/ui/WorkspaceUpgradeTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1712,9 +1770,9 @@
"../../tests/ui/components/SettlementButtonTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/ui/components/TimePickerTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/ui/hooks/useHtmlPasteTest/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 10
-"../../tests/unit/APITest.ts" "@typescript-eslint/no-unsafe-type-assertion" 74
+"../../tests/unit/APITest.ts" "@typescript-eslint/no-unsafe-type-assertion" 61
"../../tests/unit/AddExistingExpenseSearchTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
-"../../tests/unit/AgentActionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 61
+"../../tests/unit/AgentActionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 53
"../../tests/unit/AgentZeroStatusContextTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../tests/unit/AvatarButtonWithIconTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/AvatarUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1727,7 +1785,7 @@
"../../tests/unit/CIGitLogicTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../tests/unit/CLIVariadicTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/CardFeedErrorsDerivedValueTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../tests/unit/CardFeedUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 16
+"../../tests/unit/CardFeedUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/CardUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 152
"../../tests/unit/CardsSectionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/CategoryUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1758,14 +1816,17 @@
"../../tests/unit/FlashListTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/FocusTrapForModalTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/FormulaTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 78
+"../../tests/unit/FormulaTest.ts" "no-restricted-imports" 1
"../../tests/unit/FraudProtectionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/GitTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../tests/unit/GithubUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/HomePage/YourSpendSection/YourSpendSectionTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
+"../../tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts" "no-restricted-imports" 1
"../../tests/unit/HrUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 61
"../../tests/unit/HybridAppActionsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/IOUUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 11
+"../../tests/unit/IOUUtilsTest.ts" "no-restricted-imports" 1
"../../tests/unit/ImageSVGCachePolicyTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/ImportTransactions.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 17
"../../tests/unit/LHNAvatarPipelineTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1776,7 +1837,8 @@
"../../tests/unit/MentionUserRendererTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 8
"../../tests/unit/MentionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/unit/MiddlewareTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 20
-"../../tests/unit/ModifiedExpenseMessageTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
+"../../tests/unit/ModifiedExpenseMessageTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
+"../../tests/unit/ModifiedExpenseMessageTest.ts" "no-restricted-imports" 1
"../../tests/unit/MoneyRequestReportTransactionListActiveTransactionIDsTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../tests/unit/MoneyRequestReportUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
"../../tests/unit/MoneyRequestUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
@@ -1808,6 +1870,7 @@
"../../tests/unit/PopoverMenuV2Test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/unit/PressResponderTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/QuickActionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 7
+"../../tests/unit/QuickActionUtilsTest.ts" "no-restricted-imports" 2
"../../tests/unit/ReceiptAlternativeMethodsTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/unit/RefreshCardFeedConnectionPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/unit/ReimbursementAccountUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1816,13 +1879,17 @@
"../../tests/unit/ReportActionsListPaddingViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/unit/ReportActionsUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 35
"../../tests/unit/ReportLayoutUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../tests/unit/ReportNameUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 22
+"../../tests/unit/ReportNameUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 23
"../../tests/unit/ReportPrimaryActionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 168
-"../../tests/unit/ReportSecondaryActionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 450
+"../../tests/unit/ReportPrimaryActionUtilsTest.ts" "no-restricted-imports" 1
+"../../tests/unit/ReportSecondaryActionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 466
+"../../tests/unit/ReportSecondaryActionUtilsTest.ts" "no-restricted-imports" 2
"../../tests/unit/ReportSubmitUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/ReportTitleUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
+"../../tests/unit/ReportTitleUtilsTest.ts" "no-restricted-imports" 1
"../../tests/unit/ReportUtilsGetIconsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../tests/unit/ReportUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 98
+"../../tests/unit/ReportUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 105
+"../../tests/unit/ReportUtilsTest.ts" "no-restricted-imports" 1
"../../tests/unit/ReportWelcomeTextTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/RequestConflictUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 11
"../../tests/unit/RequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1831,7 +1898,7 @@
"../../tests/unit/Search/SearchListRenderCountTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/Search/SearchQueryUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 9
"../../tests/unit/Search/SearchSingleSelectionPickerTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../tests/unit/Search/SearchUIUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 133
+"../../tests/unit/Search/SearchUIUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 136
"../../tests/unit/Search/buildCardFilterDataTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 22
"../../tests/unit/Search/buildSubstitutionsMapTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/Search/getSpendOverTimeStateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1846,7 +1913,8 @@
"../../tests/unit/SelectFeaturesEmptyTagGroupTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/SettlementButtonUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/unit/SidebarTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
-"../../tests/unit/SidebarUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 31
+"../../tests/unit/SidebarUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 35
+"../../tests/unit/SidebarUtilsTest.ts" "no-restricted-imports" 1
"../../tests/unit/SplitExpenseAutoAdjustmentTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/SubscriptionUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/SuggestedFollowupTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -1869,6 +1937,7 @@
"../../tests/unit/ViolationUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 20
"../../tests/unit/WhisperContentMentionContextTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/WorkflowUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
+"../../tests/unit/WorkspaceReportFieldUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/WorkspacesSettingsUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 15
"../../tests/unit/awaitStagingDeploysTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
"../../tests/unit/canEditFieldOfMoneyRequestTest.ts" "@typescript-eslint/no-deprecated/randomReportAction.originalMessage" 3
@@ -1920,7 +1989,7 @@
"../../tests/unit/hooks/useDraftMessageVideoAttributeCache.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/hooks/useEditComposerToggle.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/hooks/useEditMessage.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
-"../../tests/unit/hooks/useExpenseSubmission.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 3
+"../../tests/unit/hooks/useExpenseSubmission.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/hooks/useExpensifyCardFeedsForFeedSelector.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/hooks/useExportedToFilterOptions.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 5
"../../tests/unit/hooks/useFilterFeedDataTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1930,7 +1999,6 @@
"../../tests/unit/hooks/useFreeTrial.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
"../../tests/unit/hooks/useGettingStartedItems.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 11
"../../tests/unit/hooks/useHasAnyAdminExpensifyCardFeed.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
-"../../tests/unit/hooks/useHoldRejectActionsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/hooks/useIsAllowedToIssueCompanyCard.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
"../../tests/unit/hooks/useIsBlockedToAddFeed.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 11
"../../tests/unit/hooks/useIsSidebarRouteActive.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
@@ -1978,10 +2046,12 @@
"../../tests/unit/markPullRequestsAsDeployedTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/unit/navigateAfterExpenseCreateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 8
"../../tests/unit/navigateAfterOnboardingTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
+"../../tests/unit/navigateAfterOnboardingTest.ts" "no-restricted-imports" 1
"../../tests/unit/navigateToWorkspacesPageTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 14
+"../../tests/unit/navigateToWorkspacesPageTest.ts" "no-restricted-imports" 1
"../../tests/unit/pages/home/GettingStartedSection/GettingStartedSectionTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 11
-"../../tests/unit/pages/inbox/ConciergeDraftContext.test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/pages/inbox/report/ReportActionEditMessageContext.test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
+"../../tests/unit/pages/inbox/report/ReportActionEditMessageContext.test.tsx" "no-restricted-imports" 1
"../../tests/unit/pages/settings/AddAgentAvatarPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/pages/settings/AddAgentPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 11
"../../tests/unit/pages/settings/AgentsListRowTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
@@ -2017,6 +2087,7 @@
"../../tests/unit/useGroupDraftRestoreTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 14
"../../tests/unit/useIndicatorStatusTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/useLastWorkspaceNumberTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
+"../../tests/unit/useMarkAsReadTest.ts" "no-restricted-imports" 1
"../../tests/unit/usePersonalDetailSearchSelectorTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../tests/unit/usePolicyIndicatorChecksTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 11
"../../tests/unit/usePopoverPositionTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md
index 449f5ff7a125..8e0cd9a65e59 100644
--- a/contributingGuides/CONTRIBUTING.md
+++ b/contributingGuides/CONTRIBUTING.md
@@ -4,10 +4,13 @@ Welcome! Thanks for checking out the New Expensify app and taking the time to co
## Getting Started
If you would like to become an Expensify contributor, the first step is to read this document in its **entirety**. The second step is to review the README guidelines [here](https://github.com/Expensify/App/blob/main/README.md) to understand our coding philosophy and for a general overview of the code repository (i.e. how to run the app locally, testing, storage, our app philosophy, etc). Please read both documents before asking questions, as it may be covered within the documentation.
+### MelvinBot issues (read this)
+Many App jobs use [MelvinBot](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_WORK_WITH_MELVINBOT.md) (Melvin), our AI agent that posts the first proposal on an issue. **Do not open a pull request until a proposal is accepted.** Contributors, C+, and internal engineers each have specific responsibilities on these jobs. See **[How to Work with MelvinBot](./HOW_TO_WORK_WITH_MELVINBOT.md)** before working on any issue where Melvin has posted a proposal.
+
### Test Accounts
You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. Do not use Expensify employee or customer accounts for testing.
-**Notes**:
+**Notes**:
1. When creating test accounts:
- Include a `+` (plus sign) in the email address (e.g., matt+1@gmail.com). This marks the account and their associated workspaces as test accounts in Expensify, ensuring Expensify Guides are not assigned to help with account setup.
@@ -21,7 +24,7 @@ You can generate multiple test accounts by using a `+` postfix, for example if y
#### High Traffic Accounts
-All internal engineers, contributors, and C+ members are required to test with a “high traffic” account against the staging or production web servers. Use these Google forms to manage your high-traffic accounts. You'll need to authenticate via Google first.
+All internal engineers, contributors, and C+ members are required to test with a “high traffic” account against the staging or production web servers. Use these Google forms to manage your high-traffic accounts. You'll need to authenticate via Google first.
1. [Make an account high-traffic](https://docs.google.com/forms/d/e/1FAIpQLScpiS0Mo-HA5xHPsvDow79yTsMBgF0wjuqc0K37lTK5fheB8Q/viewform)
2. [Remove a high-traffic account](https://docs.google.com/forms/d/e/1FAIpQLSd9_FDav83pnhhtu1KGAKIpf2yttQ_0Bvq1b9nuFM1-wbL11Q/viewform)
@@ -40,7 +43,7 @@ We have a shared Slack channel called #expensify-open-source — this channel is
That said, we have a small issue with adding users at the moment and we’re working with Slack to try and get this resolved. If you would like to join, [fill out this form](https://forms.gle/Q7hnhUJPnQCK7Fe56) with your email and Upwork profile link. Once resolved, we’ll add you.
-Note: Do not send direct messages to the Expensify team in Slack or Expensify Chat, they will not be able to respond.
+Note: Do not send direct messages to the Expensify team in Slack or Expensify Chat, they will not be able to respond.
Note: if you are hired for an Upwork job and have any job-specific questions, please ask in the GitHub issue or pull request. This will ensure that the person addressing your question has as much context as possible.
@@ -48,9 +51,9 @@ Note: if you are hired for an Upwork job and have any job-specific questions, pl
If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue.
## Payment for Contributions
-We hire and pay external contributors via [Upwork.com](https://www.upwork.com). If you'd like to be paid for contributing, please create an Upwork account, apply for an available job in [GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22), and finally apply for the job in Upwork once your proposal gets selected in GitHub. Please make sure your Upwork profile is **fully verified** before applying, otherwise you run the risk of not being paid. If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted.
+We hire and pay external contributors via [Upwork.com](https://www.upwork.com). If you'd like to be paid for contributing, please create an Upwork account, apply for an available job in [GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22), and finally apply for the job in Upwork once your proposal gets selected in GitHub. Please make sure your Upwork profile is **fully verified** before applying, otherwise you run the risk of not being paid. If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted.
-Please add your Upwork profile link in your GitHub Bio to help ensure prompt payment. If you're using Slack or Expensify for discussions, please add your Upwork profile link **and** your GitHub username in your Slack Title and Expensify Status.
+Please add your Upwork profile link in your GitHub Bio to help ensure prompt payment. If you're using Slack or Expensify for discussions, please add your Upwork profile link **and** your GitHub username in your Slack Title and Expensify Status.
Payment for your contributions will be made no less than 7 days after the pull request is deployed to production to allow for [regression](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions) testing. If you have not received payment after 8 days of the PR being deployed to production, and there are no [regressions](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions), please add a comment to the issue mentioning the team member assigned to pay (Look for the melvin-bot "Triggered auto assignment to... (`Awaiting Payment`)" to see who this is).
@@ -58,13 +61,13 @@ New contributors are limited to working on one job at a time, **do not submit pr
Please be aware that compensation for any support in solving an issue is provided **entirely at Expensify’s discretion**. Personal time or resources applied towards investigating a proposal **will not guarantee compensation**. Compensation is only guaranteed to those who **[propose a solution and get hired for that job](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#propose-a-solution-for-the-job)**. We understand there may be cases where a selected proposal may take inspiration from a previous proposal. Unfortunately, it’s not possible for us to evaluate every individual case and we have no process that can efficiently do so. Issues with higher rewards come with higher risk factors so try to keep things civil and make the best proposal you can. Once again, **any information provided may not necessarily lead to you getting hired for that issue or compensated in any way.**
-**Important:** Payment amounts are variable, dependent on if there are any [regressions](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions). Your PR will be reviewed by a [Contributor+ (C+)](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md) team member and an internal engineer. All tests must pass and all code must pass lint checks before a merge.
+**Important:** Payment amounts are variable, dependent on if there are any [regressions](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions). Your PR will be reviewed by a [Contributor+ (C+)](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md) team member and an internal engineer. All tests must pass and all code must pass lint checks before a merge.
Contributors are eligible to be paid via Expensify 18 months after they were first assigned to a job. More details at [PAYMENT_VIA_EXPENSIFY.md](https://github.com/Expensify/App/blob/main/contributingGuides/PAYMENT_VIA_EXPENSIFY.md)
### Regressions
-If a PR causes a regression at any point within the regression period (starting when the code is merged and ending 168 hours (that's 7 days) after being deployed to production):
+If a PR causes a regression at any point within the regression period (starting when the code is merged and ending 168 hours (that's 7 days) after being deployed to production):
- 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
@@ -88,7 +91,7 @@ In short, a good problem statement makes no mention whatsoever of the desired so
**Bad:**
> **Problem:** We don't have a car
->
+>
> **Solution:** Buy a car.
@@ -152,8 +155,9 @@ This helps future investigators understand the history and current status of err
3. If you cannot reproduce the problem, pause on this step and add a comment to the issue explaining where you are stuck or that you don't think the issue can be reproduced.
### Propose a solution for the job
-4. Proposals must only be posted after the `Help Wanted` label is added. Any proposals submitted beforehand will be ignored and not reviewed. Do not post proposals in Slack.
-5. Contributors should **not** submit proposals on issues when they have assigned issues or PRs that are awaiting an action from them. If so, they will be in violation of Rule #1 (Get Shit Done) in our [Code of Conduct](https://github.com/Expensify/App/blob/main/CODE_OF_CONDUCT.md) and will receive a warning. Multiple warnings can lead to removal from the program.
+4. Proposals must only be posted after the `Help Wanted` label is added. Any proposals submitted beforehand will be ignored and not reviewed. Do not post proposals in Slack. MelvinBot is an exception: it may post a proposal before `Help Wanted` is applied.
+ - On issues where [MelvinBot](./HOW_TO_WORK_WITH_MELVINBOT.md) has posted a proposal, review Melvin's proposal using the same standards as contributor proposals. Melvin's proposal is not guaranteed to be the earliest, but it should always be reviewed first because we are spending the money to run it. Follow first-come-first-serve review order for all other proposals after `Help Wanted`. **Do not open a PR** until a proposal is accepted. See [How to Work with MelvinBot](./HOW_TO_WORK_WITH_MELVINBOT.md) for more details.
+5. Contributors should **not** submit proposals on issues when they have assigned issues or PRs that are awaiting an action from them. If so, they will be in violation of Rule #1 (Get Shit Done) in our [Code of Conduct](https://github.com/Expensify/App/blob/main/CODE_OF_CONDUCT.md) and will receive a warning. Multiple warnings can lead to removal from the program.
6. After you reproduce the issue, complete the [proposal template here](./PROPOSAL_TEMPLATE.md) and post it as a comment in the corresponding GitHub issue (linked in the Upwork job).
- Note: Before submitting a proposal on an issue, be sure to read any other existing proposals. ALL NEW PROPOSALS MUST BE DIFFERENT FROM EXISTING PROPOSALS. The *difference* should be important, meaningful or considerable.
7. Refrain from leaving additional comments until someone from the Contributor-Plus team and / or someone from Expensify provides feedback on your proposal (do not create a pull request yet).
@@ -197,18 +201,18 @@ This helps future investigators understand the history and current status of err
### Completing the final checklist
20. Once your PR has been deployed to production, a checklist will automatically be commented in the GH issue. You're required to complete the steps that have your name mentioned before payment will be issued.
-21. The items requiring your completion consist of:
+21. The items requiring your completion consist of:
1. Proposing steps to take for a regression test to ensure the bug doesn't occur again (For information on how to successfully complete this, head [here](https://github.com/Expensify/App/blob/main/contributingGuides/REGRESSION_TEST_BEST_PRACTICES.md)).
2. Identifying and noting the offending PR that caused the bug (if any).
3. Commenting on the offending PR to note the bug it caused and why (if applicable).
- 4. Starting a conversation on if any additional steps should be taken to prevent further bugs similar to the one fixed from occurring again.
-22. Once the above items have been successfully completed, then payments will begin to be issued.
+ 4. Starting a conversation on if any additional steps should be taken to prevent further bugs similar to the one fixed from occurring again.
+22. Once the above items have been successfully completed, then payments will begin to be issued.
### Timeline expectations and asking for help along the way
- If you have made a change to your pull request and are ready for another review, leave a comment that says "Updated" on the pull request itself.
- Please keep the conversation in GitHub, and do not ping individual reviewers in Slack or Upwork to get their attention.
- Pull Request reviews can sometimes take a few days. If your pull request has not been addressed after four days, please let us know via the #expensify-open-source Slack channel.
-- On occasion, our engineers will need to focus on a feature release and choose to place a hold on the review of your PR.
+- On occasion, our engineers will need to focus on a feature release and choose to place a hold on the review of your PR.
### Important note about JavaScript Style
- Read our official [JavaScript and React style guide](https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md). Please refer to our Style Guide before asking for a review.
@@ -219,9 +223,9 @@ This helps future investigators understand the history and current status of err
### For external agencies that Expensify partners with
Follow all the above above steps and processes. When you find a job you'd like to work on:
- Post “I’m from [agency], I’d like to work on this job”
- - If no proposals have been submitted by other contributors, BugZero (BZ) team member or an internal engineer will assign the issue to you.
- - If there are existing proposals, BZ will put the issue on hold. [Contributor+](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md) will review the existing proposals. If a contributor’s proposal is accepted then the contributor will be assigned to the issue. If not the issue will be assigned to the agency-employee.
-- Once assigned follow the steps [here](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#propose-a-solution-for-the-job) to submit your proposal
+ - If no proposals have been submitted by other contributors, BugZero (BZ) team member or an internal engineer will assign the issue to you.
+ - If there are existing proposals, BZ will put the issue on hold. [Contributor+](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md) will review the existing proposals. If a contributor’s proposal is accepted then the contributor will be assigned to the issue. If not the issue will be assigned to the agency-employee.
+- Once assigned follow the steps [here](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#propose-a-solution-for-the-job) to submit your proposal
## Guide on Acronyms used within Expensify Communication
During communication with Expensify, you will come across a variety of acronyms used by our team. While acronyms can be useful, they cease to be the moment they are not known to the receiver. As such, we wanted to create a list here of our most commonly used acronyms and what they're referring to. Lastly, please never hesitate to ask in Slack or the GH issue if there are any that are not understood/known!
@@ -231,8 +235,8 @@ During communication with Expensify, you will come across a variety of acronyms
- **LHN:** Left Hand Navigation (Primary navigation modal in Expensify Chat, docked on the left-hand side)
- **OP:** Original Post (Most commonly the post in E/App GH issues that reports the bug)
- **FAB** Floating Action Button (the + Button that is used to launch flows like 'New Chat', 'Request Money')
-- **GBR:** Green Brick Road (UX Design Principle that utilizes green indicators on action items to encourage the user down the optimal path for a given process or task)
-- **RBR:** Red Brick Road (UX Design Principle that utilizes red indicators on action items to encourage the user down the optimal path for handling and discovering errors)
+- **GBR:** Green Brick Road (UX Design Principle that utilizes green indicators on action items to encourage the user down the optimal path for a given process or task)
+- **RBR:** Red Brick Road (UX Design Principle that utilizes red indicators on action items to encourage the user down the optimal path for handling and discovering errors)
- **VBA:** Verified Bank Account (Bank account that has been verified as real and belonging to the correct business/individual)
- **NAB:** Not a Blocker (An issue that doesn't block progress, but would be nice to not have)
- **IOU:** I owe you (used to describe payment requests between users)
@@ -241,3 +245,4 @@ During communication with Expensify, you will come across a variety of acronyms
- **QA:** Quality Assurance
- **GH:** GitHub
- **LGTM:*** Looks good to me
+- **MelvinBot:** Expensify's AI agent for App GitHub issues (posts proposals, implements accepted solutions). See [How to Work with MelvinBot](./HOW_TO_WORK_WITH_MELVINBOT.md).
diff --git a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md
index 9993755274e5..9922324c1ea8 100644
--- a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md
+++ b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md
@@ -2,6 +2,7 @@
C+ are contributors who are experienced at working with Expensify and have gained the confidence of the internal Expensify team. Accordingly, they are allocated additional opportunities and responsibilities:
- They review proposed solutions in external GitHub issues, validate them, and make recommendations to an internal Contributor Manager Engineer (CME).
- Once a proposal has been accepted by a CME, the C+ will be the first person to review the pull request associated with that proposal.
+ - On [MelvinBot issues](./HOW_TO_WORK_WITH_MELVINBOT.md), the C+ reviews Melvin's proposal, recommends it to the CME, asks Melvin to implement after CME approval, and owns the PR as the human author before CME PR review.
## Why would someone want to be a C+
- C+ are compensated the same price as the contributor for reviewing proposals and the associated PR. (ie. if a job is listed at $250, that’s how much the C+ will make if they review both the proposals and PR). If regressions are found that should have* been caught after the PR has been approved, C+ payment is reduced by 50% for each regression found.
diff --git a/contributingGuides/HOW_TO_WORK_WITH_MELVINBOT.md b/contributingGuides/HOW_TO_WORK_WITH_MELVINBOT.md
new file mode 100644
index 000000000000..da3b0101a5d9
--- /dev/null
+++ b/contributingGuides/HOW_TO_WORK_WITH_MELVINBOT.md
@@ -0,0 +1,105 @@
+# Working with MelvinBot on App Issues
+
+MelvinBot (Melvin) is Expensify's AI agent for App GitHub issues. On many open-source jobs, Melvin posts the first proposal automatically. This guide explains how contributors, Contributor+ (C+), and internal engineers work together on those issues.
+
+Clearly documenting the process helps us move efficiently by preventing all of us from wasting time on incorrect solutions. Internal engineering approval is required on Melvinbot proposals because we usually have the most context to know whether a solution aligns with our best practices.
+
+For general contributor workflow (proposals, payment, PR standards), see [CONTRIBUTING.md](./CONTRIBUTING.md). For C+ responsibilities, see [HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md](./HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md).
+
+## Roles
+
+| Role | Responsibility on Melvin issues |
+|------|----------------------------------|
+| **MelvinBot** | Posts the first proposal on the issue. Implements the accepted solution when asked. Opens a draft PR with MelvinBot as the GitHub author. |
+| **Contributor** | May submit their own proposal if they have a meaningfully different approach. Must not open a PR until a proposal is accepted and they are hired on the job. |
+| **Contributor+ (C+)** | Reviews proposals (Melvin's and any contributor proposals) using the same standards as any other job. Recommends acceptance to the CME. After the CME approves, asks Melvin to implement, then owns the PR as the human author before sending it for final review. |
+| **Contributor Manager Engineer (CME)** | Reviews and approves proposals (same as any other App job). After implementation, reviews and merges the PR. |
+
+Only Expensify employees, C+ members, and backend contributors can trigger Melvin with `@MelvinBot` comments on GitHub.
+
+## Workflow overview
+
+```
+Issue opened → Melvin posts proposal → C+ reviews proposal(s)
+ → C+ recommends acceptance (🎀👀🎀) → CME approves proposal
+ → C+ asks Melvin to implement (only after CME approval)
+ → Not accepted: C+ or CME explains why
+→ Melvin opens draft PR → C+ tweaks, tests, posts PR body/checklist in a PR comment, asks Melvin to apply it, self-reviews (including Reviewer checklist)
+→ C+ submits PR for review as human author → CME reviews and merges (PR body and/or C+ PR comment)
+```
+
+## Phase 1: Proposal review
+
+Follow the **same proposal review process** as any other App job, but remember to review the MelvinBot proposal first, and then review in first-come-first-serve review order for proposals posted after the `Help Wanted` is applied (see [CONTRIBUTING.md](./CONTRIBUTING.md#propose-a-solution-for-the-job)).
+
+1. Use the [proposal template](./PROPOSAL_TEMPLATE.md) criteria: clear root cause, concrete solution, no code diffs.
+2. C+ reviews Melvin's proposal with the same rigor as a contributor proposal. Do not approve proposals that lack a satisfying root-cause explanation.
+3. Contributors may still post proposals if they have a **meaningfully different** approach (see [CONTRIBUTING.md](./CONTRIBUTING.md#propose-a-solution-for-the-job)).
+4. **No one opens a pull request** until a proposal is accepted by the CME (same as any other App job; see [CONTRIBUTING.md](./CONTRIBUTING.md#propose-a-solution-for-the-job)).
+5. If Melvin's proposal is not acceptable, the C+ can review proposals from other contributors, or iterate with Melvin by tagging `@MelvinBot` on the issue, explaining the needed changes, and asking it to update its proposal or post a new one until it is satisfactory.
+
+### C+ reviews and recommends
+
+The C+ reviews Melvin's proposal with the same rigor as a contributor proposal. Follow the [proposal template](./PROPOSAL_TEMPLATE.md) review instructions.
+
+When the C+ is satisfied with Melvin's proposal, they recommend it to the CME by posting `🎀👀🎀` on the issue. That triggers CME assignment for the job.
+
+### After the CME approves the proposal
+
+Once the assigned CME has approved Melvin's proposal (the same acceptance step as for any contributor proposal), the C+ comments on the issue asking Melvin to implement. Include a link to the specific proposal comment so Melvin implements the latest version (Melvin may post a new proposal when asked to update). For example:
+
+```
+@MelvinBot please implement your proposal at https://github.com/Expensify/App/issues/12345#issuecomment-6789012345
+```
+
+Adjust the wording if needed, but the comment must mention `@MelvinBot`, link to the proposal comment, and clearly request implementation.
+
+## Phase 2: C+ owns the pull request
+
+Melvin opens a draft PR linked to the issue. MelvinBot remains the GitHub author but the C+ is the human author. Most C+ members will **not be able to edit the PR description directly** (GitHub only allows the author or users with write access to edit a pull request body).
+
+Before requesting final review, the assigned C+ must:
+
+1. **Manually tweak the PR** if Melvin's implementation needs corrections. Comment on the pull request, tag `@MelvinBot`, and explain the needed changes so Melvin can update the code.
+2. **Test the change** on all required platforms (see [CONTRIBUTING.md](./CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)).
+3. **Complete every step** in the PR Author Checklist (see [Updating the PR description and checklist](#updating-the-pr-description-and-checklist) below). Screenshots and videos may live only in the Reviewer Checklist section of your PR comment; you do not need to duplicate them in the Author Checklist.
+4. **Self-review the code** against [PR Review Guidelines](./PR_REVIEW_GUIDELINES.md), [PR Authoring & Reviewing Best Practices](./PR_AUTHOR_REVIEWER_BEST_PRACTICES.md), and complete the [Reviewer Checklist](./REVIEWER_CHECKLIST.md) as part of that self-review.
+
+The C+ is accountable for the PR quality, the same as any contributor who authored a PR.
+
+### Updating the PR description and checklist
+
+C+ members who cannot edit the PR body directly should use this workaround:
+
+1. On the **pull request**, post a comment with the exact content you want in the PR description. Put the full body inside a `` block so the PR thread stays readable. Include the complete [PR template](https://github.com/Expensify/App/blob/main/.github/PULL_REQUEST_TEMPLATE.md) when possible: Explanation of Change, Fixed Issues, Tests, Offline tests, QA Steps, a fully checked PR Author Checklist, and Screenshots/Videos sections as needed.
+2. In the same comment, ask Melvin to copy that content into the pull request body, for example:
+
+```
+@MelvinBot please set the PR body to the content in the details section above
+```
+
+To update only the checklist, etc, post the section in a `` block and ask Melvin to set just that portion of the PR body.
+
+3. Confirm Melvin updated the PR description before marking the pull request ready for review.
+
+The C+ PR comment is the **source of truth** for what was tested and checked. CMEs may review that comment directly when the PR body is incomplete, out of date, or hard to verify.
+
+## Phase 3: CME review and merge
+
+After the C+ submits the PR for review:
+
+1. A **CME** (internal engineer) is assigned to review and merge.
+2. The CME follows the normal internal review process. Review the **PR body** when Melvin has applied the C+ content, or failing that, review the C+ **PR comment** (with the `` block) directly for testing steps and checklist completion.
+3. Payment and regression checklists follow the standard rules in [CONTRIBUTING.md](./CONTRIBUTING.md).
+
+### No additional contributor or C+ PR review
+
+We intentionally do **not** add a second round of contributor or C+ PR review on top of the CME review. Prior discussions did not surface a problem that extra review would solve, and an additional review step would add cost and delay without clear benefit. The C+ self-review before submission, combined with CME review, is the intended quality gate.
+
+## Related documentation
+
+- [CONTRIBUTING.md](./CONTRIBUTING.md) — full contributor workflow
+- [PROPOSAL_TEMPLATE.md](./PROPOSAL_TEMPLATE.md) — proposal format and C+ review instructions
+- [Reviewer Checklist](./REVIEWER_CHECKLIST.md) — C+ completes this during self-review before submitting for CME review
+- [AI Etiquette](./AI_ETIQUETTE.md) — accountability for AI-assisted work
+- [AI Reviewer philosophy](./philosophies/AI-REVIEWER.md) — automated PR feedback vs human review
diff --git a/contributingGuides/INTERACTION_MANAGER.md b/contributingGuides/INTERACTION_MANAGER.md
index 08008b0b42ad..5d152fc04532 100644
--- a/contributingGuides/INTERACTION_MANAGER.md
+++ b/contributingGuides/INTERACTION_MANAGER.md
@@ -29,7 +29,37 @@ On top of TransitionTracker, existing APIs gain transition-aware callbacks:
This makes the code self-descriptive: instead of a generic `runAfterInteractions`, each call site says exactly what it's waiting for and why.
-> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `useConfirmModal`, etc.) rather than importing TransitionTracker directly.
+### What TransitionTracker actually tracks
+
+TransitionTracker registers transitions **only** in the following cases:
+
+1. **Navigation transitions** — screen push/pop/replace animations (via `transitionStart`/`transitionEnd` events in `ScreenLayout`)
+2. **Modal transitions** — modal open/close animations (via `ReanimatedModal`)
+3. **Keyboard transitions** — keyboard dismiss animations (via `KeyboardUtils.dismiss`)
+
+### When to use `runAfterTransitions` directly
+
+Prefer higher-level APIs (`Navigation` with `afterTransition`/`waitForTransition`, `KeyboardUtils`, `useConfirmModal`) whenever possible. Use `TransitionTracker.runAfterTransitions` directly **only** when:
+
+- There is no specific navigation/keyboard/modal action you can attach a callback to, or
+- There are too many concurrent actions and it's impossible to bind the callback to a particular one.
+
+### When NOT to use `runAfterTransitions`
+
+If you are not waiting for any of the tracked events (navigation, modal, keyboard), adding `runAfterTransitions` is meaningless - TransitionTracker will have no active transitions and the callback will fire synchronously in the same tick. For example, scroll events, layout animations, or Lottie playback are **not** tracked, so wrapping post-scroll logic in `runAfterTransitions` provides no benefit.
+
+### How `runAfterTransitions` works
+
+1. If there are ongoing transitions at the time of the call, the callback is enqueued and waits until all current transitions complete.
+2. If there are no ongoing transitions, the callback executes in the same tick.
+
+### The same-tick race condition
+
+Sometimes a transition starts in the same tick as the `runAfterTransitions` call. Because TransitionTracker hasn't registered the transition yet, the callback fires immediately - before the transition even begins.
+
+To fix this, pass the `waitForUpcomingTransition` flag along with the callback. This tells TransitionTracker to wait for a transition that hasn't started yet, ensuring the callback truly runs after the transition completes.
+
+> **Important:** `waitForUpcomingTransition` waits up to 1 second for a transition to begin. If no transition starts within that window, the callback fires anyway — effectively acting as a 1-second timeout. When using this flag, always make sure that TransitionTracker actually picks up the transition (i.e. the transition is registered within the 1-second window). Otherwise you're not waiting for a real transition, just adding an arbitrary delay.
## How
The migration is split into 9 issues. Current status of the migration can be found in the parent Github issue [here](https://github.com/Expensify/App/issues/71913).
diff --git a/cspell.json b/cspell.json
index 8f866b8f6afb..80ef1951f2c2 100644
--- a/cspell.json
+++ b/cspell.json
@@ -203,6 +203,8 @@
"Marqeta",
"McAfee",
"Michelina",
+ "Melvin",
+ "MelvinBot",
"Menlo",
"Microtransaction",
"Miniwarehouses",
diff --git a/docs/articles/expensify-classic/connections/VAT-IT.md b/docs/articles/expensify-classic/connections/VAT-IT.md
index 29465eaeb874..f55b6e7d6e9f 100644
--- a/docs/articles/expensify-classic/connections/VAT-IT.md
+++ b/docs/articles/expensify-classic/connections/VAT-IT.md
@@ -27,13 +27,13 @@ Businesses can reclaim VAT on various expenses, including:
# How VAT Refunds Work
-VAT refund regulations vary by country and expense type, making the process complex. Global VaTax simplifies this by handling:
+VAT refund regulations vary by country and expense type, making the process complex. VAT IT simplifies this by handling:
- VAT analysis and calculation
- Expense classification based on VAT eligibility
- Report preparation and submission to tax authorities
-By syncing Expensify with Global VaTax, businesses can automate VAT reporting and reclaim eligible expenses efficiently.
+By syncing Expensify with VAT IT, businesses can automate VAT reporting and reclaim eligible expenses efficiently.
---
@@ -52,6 +52,6 @@ Once VAT IT prepares your VAT reclaim documents in the required languages, they
# Tracking Your VAT Reclaim
-After submission, you can monitor the progress of your VAT reclaim via the **Submission Analysis Report** in Global VaTax.
+After submission, you can monitor the progress of your VAT reclaim via the **Submission Analysis Report** in VAT IT.
By integrating Expensify with VAT IT, businesses can maximize VAT recovery while reducing administrative burden.
diff --git a/docs/articles/new-expensify/connect-credit-cards/Assign-Company-Cards.md b/docs/articles/new-expensify/connect-credit-cards/Assign-Company-Cards.md
index 5ccfe82e069b..2ce67abacb34 100644
--- a/docs/articles/new-expensify/connect-credit-cards/Assign-Company-Cards.md
+++ b/docs/articles/new-expensify/connect-credit-cards/Assign-Company-Cards.md
@@ -28,15 +28,19 @@ If you haven’t set up a feed yet, learn how to set one up:
3. Select the card connection name.
4. Click **Assign card** on the card you want to assign.
5. Enter the assignee's name, email address, or phone number.
-6. Review the details and click **Assign card** to finalize the assignment.
+6. If needed, update the **Transaction start date** to choose the date from which Expensify starts importing transactions for the card.
+7. Click **Assign card** to finalize the assignment.
---
## What happens after you assign company cards
- Posted transactions from the assigned card import automatically into the member's account as expenses.
+ - Expensify imports transactions from the **Transaction start date** onward.
- The imported company card expenses are added to reports and submitted according to Workspace rules.
+---
+
# FAQ
## Can I assign a company card to someone who is not a member of the Workspace?
@@ -50,3 +54,7 @@ Yes. A member can have multiple company cards assigned. Each card’s transactio
## Can members unassign their own company cards?
No. Only Workspace Admins and their Copilots can assign or reassign company cards.
+
+## Can I change the transaction start date on an assigned card?
+
+Yes. You can update the **Transaction start date** at any time from the assigned card's details. The **Transaction start date** determines the date from which Expensify imports transactions for the card.
diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md
index b7d71247a78b..1c55091863b7 100644
--- a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md
+++ b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md
@@ -75,8 +75,8 @@ To manage automation and other connection preferences:
- **Sync reimbursed reports**:
- If marked as paid in QuickBooks Online, the report will show as reimbursed in Expensify
- If reimbursed via ACH in Expensify, the status will sync to paid in QuickBooks
-- **QuickBooks bill payment account**: Select where paid expense reports are stored
-- **QuickBooks invoice collections account**: Select where paid invoices are stored
+- **QuickBooks bill payment account**: Select where payments for expense reports are recorded
+- **QuickBooks invoice collections account**: Select where payments for invoices are recorded
---
diff --git a/docs/articles/new-expensify/domains/Domain-Migration.md b/docs/articles/new-expensify/domains/Domain-Migration.md
index 04f7247bc12d..7d06e6adbdcc 100644
--- a/docs/articles/new-expensify/domains/Domain-Migration.md
+++ b/docs/articles/new-expensify/domains/Domain-Migration.md
@@ -21,7 +21,7 @@ This is the recommended approach when possible.
Concierge can typically perform an automatic Domain Migration if:
-- The old domain or new domain is verified
+- The old domain is verified
- Email addresses match a 1:1 format, such as `name@olddomain.com` to `name@newdomain.com`
- Members have not already created separate accounts under the new email address
@@ -65,7 +65,8 @@ Yes. If done correctly, members keep their full expense history, reports, and ap
## Do I need to verify the new domain before migrating?
-Yes. You must claim and verify the new domain before transferring domain control or updating member login emails.
+- If you are migrating your domain automatically with assistance from Concierge, you will not need to verify the new domain. It is preferred that you do not add or verify the new domain in Expensify.
+- If you are migrating your domain manually, you will need to verify the new domain.
## What happens to the Expensify Card during Domain Migration?
diff --git a/docs/articles/new-expensify/reports-and-expenses/Getting-Started-with-the-Spend-Page.md b/docs/articles/new-expensify/reports-and-expenses/Getting-Started-with-the-Spend-Page.md
index 819b9d95edf9..dd39d268f9c1 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Getting-Started-with-the-Spend-Page.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Getting-Started-with-the-Spend-Page.md
@@ -104,9 +104,9 @@ Filters help you narrow down expenses or reports so you can find exactly what yo
You can filter your **expenses** and **reports** using dropdowns, search bars, and chips.
-- **Expenses:** can be filtered by `Date`, `Merchant`, `Category`, `Tag`, `Amount`, `Reimbursable`, `Billable`, `Status` (e.g. Unreported, Deleted)
+- **Expenses:** can be filtered by `Date`, `Merchant`, `Category`, `Tag`, `Amount`, `Reimbursable`, `Billable`, `Status` (e.g. Unreported, Deleted), and `Exported to`.
-- **Reports**: can be filtered by `Submitter`, `Workspace`, `Report Status` (e.g. Draft, Outstanding, Approved, Paid), `Date`
+- **Reports**: can be filtered by `Submitter`, `Workspace`, `Report Status` (e.g. Draft, Outstanding, Approved, Paid), `Date`, and `Exported to`.
To view other available filters, click into the **search box** on the **Spend** page while viewing **Reports** or **Expenses** under **Explore**.
diff --git a/docs/articles/new-expensify/reports-and-expenses/How-to-Export-Expenses.md b/docs/articles/new-expensify/reports-and-expenses/How-to-Export-Expenses.md
index e17e44e77517..b69b1547a41f 100644
--- a/docs/articles/new-expensify/reports-and-expenses/How-to-Export-Expenses.md
+++ b/docs/articles/new-expensify/reports-and-expenses/How-to-Export-Expenses.md
@@ -38,16 +38,12 @@ You'll receive the exported CSV file in a message from Concierge.
## What export templates can I choose from?
-Expensify offers pre-built export templates, or you can build your own custom export template. All available templates will appear in the menu when you export an expense.
+Expensify offers pre-built export templates, or you can build your own custom export template.
- **Basic export** - Essential fields including date, amount, merchant, category, and receipt URL.
- **All Data - expense level** - One row per expense with all available data fields.
- - **custom templates** - Any custom template created by you or your Workspace Admin, if available.
-
- **Custom templates** - Any custom template created by you or your Workspace Admin, if available.
-**Note** Currently, it's not possible to build custom export templates on New Expensify, they can only be created on Expensify Classic. However, once built they will be available on New Expensify when exporting expenses. [Learn how to build a custom export template in Expensify Classic](/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports#create-a-custom-export-template).
-
## Where do I find the exported file?
For the Basic Export template, the file downloads directly to your device. For all other templates, Concierge sends the file to you in a direct message. Open your Concierge chat in the **Inbox** to find it.
@@ -58,8 +54,6 @@ If your Workspace Admin has created custom export templates, you can select one
## Why don't I see certain export templates?
-Report-level templates (such as All Data - Report Level Export) only appear when exporting from **Spend > Reports** page with full reports selected. When exporting individual expenses from the Expenses page, only expense-level templates are available.
-
If all selected expenses are deleted, only the **Basic export** template is available. Other export templates require a report, and deleted expenses are not associated with any report.
## The data looks wrong in Excel. How can I fix it?
diff --git a/docs/articles/new-expensify/reports-and-expenses/Use-Search-Operators-to-Filter-and-Analyze.md b/docs/articles/new-expensify/reports-and-expenses/Use-Search-Operators-to-Filter-and-Analyze.md
index 13f38e6c5ed3..f06cbb0880b4 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Use-Search-Operators-to-Filter-and-Analyze.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Use-Search-Operators-to-Filter-and-Analyze.md
@@ -77,6 +77,7 @@ You can use the following operators to filter reports:
- `report-id:` – unique report reference
- `status:` – draft, outstanding, approved, paid, done
- `submitted:` / `approved:` / `paid:` / `exported:` – supports absolute or relative dates, and comparisons for date ranges (e.g., `submitted>=2024-01-01 submitted<=2024-01-31`)
+- `exported-to:` – filter by where reports or expenses were exported, such as a connected accounting integration.
- `title:` – report title
- `total:` – total amount with relative comparisons
- `withdrawn:` – ACH withdrawal date
@@ -84,7 +85,7 @@ You can use the following operators to filter reports:
- `action:` – blocking report action, e.g. `action:approve`
**Example query:**
-`status:paid exported<=2026-01-01`
+`status:paid exported<=2026-01-01 exported-to:xero`
---
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index f21fb37f5600..785a864e6500 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.4.5
+ 9.4.6
CFBundleSignature
????
CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.4.5.0
+ 9.4.6.0
FullStory
OrgId
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index f0541fc79c1c..e721b9d49276 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.4.5
+ 9.4.6
CFBundleVersion
- 9.4.5.0
+ 9.4.6.0
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 1d385f71f0f0..480cdc510747 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -4088,7 +4088,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNSVG (15.12.1):
+ - RNSVG (15.15.5):
- boost
- DoubleConversion
- fast_float
@@ -4114,10 +4114,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNSVG/common (= 15.12.1)
+ - RNSVG/common (= 15.15.5)
- SocketRocket
- Yoga
- - RNSVG/common (15.12.1):
+ - RNSVG/common (15.15.5):
- boost
- DoubleConversion
- fast_float
@@ -4808,7 +4808,7 @@ SPEC CHECKSUMS:
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
- hermes-engine: c57209f580d9916632aeb0aa3851b1b90b47859f
+ hermes-engine: a0c087309de5ae9b2297a2cd8acfa9c53de099ee
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
@@ -4938,7 +4938,7 @@ SPEC CHECKSUMS:
RNScreens: 2b6107925ee4e14a9b2eb0dfb52fe25223aa64d7
RNSentry: f73f4da92e4c20841ab16e1fa22fc289bc2f9f4e
RNShare: 1c1fde2c4134b9cf220ffebbd6df9c414036d382
- RNSVG: 74eb75bd44d62ba9969941e80d8f9832971c681f
+ RNSVG: 14e21dacbded6cfa51becd6076c5921aa52a6992
RNWorklets: 165079e6aa8caad7a05156ec2161fd4c52f9b745
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist
index 44d8056abf00..bef8c163e2df 100644
--- a/ios/ShareViewController/Info.plist
+++ b/ios/ShareViewController/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.4.5
+ 9.4.6
CFBundleVersion
- 9.4.5.0
+ 9.4.6.0
NSExtension
NSExtensionAttributes
diff --git a/knip.json b/knip.json
index 2df7e9791c2d..1f6121995c89 100644
--- a/knip.json
+++ b/knip.json
@@ -28,7 +28,7 @@
".storybook/**/*.{js,ts,tsx}",
".github/actions/javascript/**/*.ts"
],
- "ignore": [".github/actions/**/index.js", "tests/perf-test/**", "web/snippets/gib.js"],
+ "ignore": [".github/actions/**/index.js", "tests/perf-test/**", "web/snippets/gib.js", "src/libs/actions/connections/FinancialForce.ts"],
"eslint": {
"config": ["config/eslint/eslint.config.mjs", "eslint.changed.config.mjs"]
},
diff --git a/package-lock.json b/package-lock.json
index 620ab9bee353..c66c94d2fc9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.4.5-0",
+ "version": "9.4.6-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.4.5-0",
+ "version": "9.4.6-0",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -120,7 +120,7 @@
"react-native-localize": "^3.5.4",
"react-native-nitro-modules": "0.35.0",
"react-native-nitro-sqlite": "9.6.0",
- "react-native-onyx": "3.0.80",
+ "react-native-onyx": "3.0.83",
"react-native-pager-view": "8.0.0",
"react-native-pdf": "7.0.2",
"react-native-permissions": "^5.4.0",
@@ -132,7 +132,7 @@
"react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.25.0",
"react-native-share": "11.0.2",
- "react-native-svg": "15.12.1",
+ "react-native-svg": "15.15.5",
"react-native-tab-view": "^4.3.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "5.1.0",
@@ -243,7 +243,7 @@
"dotenv": "^16.0.3",
"eslint": "^9.36.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "3.0.2",
+ "eslint-config-expensify": "3.0.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-file-progress": "3.0.1",
"eslint-plugin-jest": "^29.0.1",
@@ -23200,14 +23200,13 @@
}
},
"node_modules/eslint-config-expensify": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-3.0.2.tgz",
- "integrity": "sha512-ulH1Gl3Uu2fh0E918Rt2R76nMidY1BuxezBBtT20+v2K8O/bFs5v83oX1cwvg3Kz97anhf64aqpA2h2KIG8p9w==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-3.0.3.tgz",
+ "integrity": "sha512-xPsrTYCE/6AthZ5rIyXCh5HjNdUXNrDnfpz3AoUmbepTAa6XS51ulZjuyoyGnAfy092RLuKi2dXgOZLmkHN1CA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/eslint-parser": "^7.28.4",
- "@lwc/eslint-plugin-lwc": "^3.2.0",
"@typescript-eslint/parser": "^8.44.1",
"@typescript-eslint/utils": "^8.44.1",
"confusing-browser-globals": "^1.0.11",
@@ -35673,9 +35672,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "3.0.80",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.80.tgz",
- "integrity": "sha512-rXcHgLi0V7LkkPsim7P+ULQKHCWzWqER+Hx4fDmt+omwtVD3n0LpuV63Fq91/g8cEl6PT4okmYseMVDUUSQPLA==",
+ "version": "3.0.83",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.83.tgz",
+ "integrity": "sha512-/OIwvQeXkVaKNsdHWbS1T/16o0iWOlX5KOMF6USKq3ePRVfhBzEOczNgtchAZ5MDStqnZMLNbOsTLwFjLJu56w==",
"license": "MIT",
"dependencies": {
"ascii-table": "0.0.9",
@@ -35942,14 +35941,13 @@
}
},
"node_modules/react-native-svg": {
- "version": "15.12.1",
- "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
- "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
+ "version": "15.15.5",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.5.tgz",
+ "integrity": "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
- "css-tree": "^1.1.3",
- "warn-once": "0.1.1"
+ "css-tree": "^1.1.3"
},
"peerDependencies": {
"react": "*",
diff --git a/package.json b/package.json
index a6238f2779c4..f6370118ed66 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.4.5-0",
+ "version": "9.4.6-0",
"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.",
@@ -193,7 +193,7 @@
"react-native-localize": "^3.5.4",
"react-native-nitro-modules": "0.35.0",
"react-native-nitro-sqlite": "9.6.0",
- "react-native-onyx": "3.0.80",
+ "react-native-onyx": "3.0.83",
"react-native-pager-view": "8.0.0",
"react-native-pdf": "7.0.2",
"react-native-permissions": "^5.4.0",
@@ -205,7 +205,7 @@
"react-native-safe-area-context": "5.6.2",
"react-native-screens": "4.25.0",
"react-native-share": "11.0.2",
- "react-native-svg": "15.12.1",
+ "react-native-svg": "15.15.5",
"react-native-tab-view": "^4.3.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "5.1.0",
@@ -316,7 +316,7 @@
"dotenv": "^16.0.3",
"eslint": "^9.36.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "3.0.2",
+ "eslint-config-expensify": "3.0.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-file-progress": "3.0.1",
"eslint-plugin-jest": "^29.0.1",
diff --git a/patches/react-native-draggable-flatlist/details.md b/patches/react-native-draggable-flatlist/details.md
index 8afd4dc971c9..0f72087f6309 100644
--- a/patches/react-native-draggable-flatlist/details.md
+++ b/patches/react-native-draggable-flatlist/details.md
@@ -14,3 +14,11 @@
- Upstream PR/issue: https://github.com/computerjazz/react-native-draggable-flatlist/pull/544
- E/App issue: 🛑
- PR Introducing Patch: [#55066](https://github.com/Expensify/App/pull/55066)
+
+
+### [react-native-draggable-flatlist+4.0.3+003+fix-ios-autoscroll-feedback.patch](react-native-draggable-flatlist+4.0.3+003+fix-ios-autoscroll-feedback.patch)
+
+- Reason: On iOS, `scrollToOffset({animated: true})` does not emit intermediate `onScroll` events, so the `scrollOffset` shared value never updates mid-scroll. This blocks the autoscroll feedback loop (`hasScrolledToTarget` stays false), preventing waypoint reordering beyond the visible viewport. The fix manually advances `scrollOffset` after each `scrollToOffset` call to keep the loop alive.
+- Upstream PR/issue: https://github.com/computerjazz/react-native-draggable-flatlist/issues/509
+- E/App issue: [#87362](https://github.com/Expensify/App/issues/87362)
+- PR Introducing Patch: [#90617](https://github.com/Expensify/App/pull/90617)
diff --git a/patches/react-native-draggable-flatlist/react-native-draggable-flatlist+4.0.3+003+fix-ios-autoscroll-feedback.patch b/patches/react-native-draggable-flatlist/react-native-draggable-flatlist+4.0.3+003+fix-ios-autoscroll-feedback.patch
new file mode 100644
index 000000000000..4e331c5b7383
--- /dev/null
+++ b/patches/react-native-draggable-flatlist/react-native-draggable-flatlist+4.0.3+003+fix-ios-autoscroll-feedback.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useAutoScroll.tsx b/node_modules/react-native-draggable-flatlist/src/hooks/useAutoScroll.tsx
+index 4e30bca..b1a2f3e 100644
+--- a/node_modules/react-native-draggable-flatlist/src/hooks/useAutoScroll.tsx
++++ b/node_modules/react-native-draggable-flatlist/src/hooks/useAutoScroll.tsx
+@@ -89,6 +89,8 @@ export function useAutoScroll() {
+ function scrollToInternal(offset: number) {
+ if (flatlistRef && "current" in flatlistRef) {
+ flatlistRef.current?.scrollToOffset({ offset, animated: true });
++ // Manually advance scrollOffset so the autoscroll feedback loop is not blocked on iOS.
++ scrollOffset.value = offset;
+ }
+ }
+
diff --git a/patches/react-native/details.md b/patches/react-native/details.md
index 4d5f5e2dc67c..58b04ce0b32e 100644
--- a/patches/react-native/details.md
+++ b/patches/react-native/details.md
@@ -284,3 +284,11 @@
- Upstream PR/issue: https://github.com/facebook/react-native/issues/53128
- E/App issue: https://github.com/Expensify/App/issues/91292
- PR introducing patch: https://github.com/Expensify/App/pull/91736
+
+
+### [react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch](react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch)
+
+- Reason: Fixes a fatal iOS crash (APP-8BM) in HybridApp where `RCTNetworking`'s default URL-request-handler provider builds its handler list using an Objective-C array literal (`@[...]`) with `[moduleRegistry moduleForName:"BlobModule"]` at index 3. During OldDot↔NewDot bridge transitions, the `__weak _turboModuleRegistry` in `RCTModuleRegistry` is zeroed by ARC at the start of `TurboModuleManager` dealloc — before `[RCTNetworking invalidate]` clears the handler cache — leaving a window where a concurrent in-flight network request calls `prioritizedHandlers`, finds the cache empty, and tries to rebuild it with a nil `BlobModule`. Since `@[…]` compiles to `+[NSArray arrayWithObjects:count:]` which raises `NSInvalidArgumentException` on any nil element, the crash is fatal. The fix replaces the literal with an `NSMutableArray` built from the three always-non-nil handlers (`RCTHTTPRequestHandler`, `RCTDataRequestHandler`, `RCTFileRequestHandler`) and conditionally appends `BlobModule` only when the registry lookup is non-nil, turning a guaranteed crash into a graceful "no blob handler for this window".
+- Upstream PR/issue: 🛑
+- E/App issue: https://github.com/Expensify/App/issues/92413
+- PR introducing patch: https://github.com/Expensify/App/pull/92918
\ No newline at end of file
diff --git a/patches/react-native/react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch b/patches/react-native/react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch
new file mode 100644
index 000000000000..d0a50ce8c581
--- /dev/null
+++ b/patches/react-native/react-native+0.83.1+038+fix-nil-BlobModule-crash-APP-8BM.patch
@@ -0,0 +1,24 @@
+diff --git a/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm b/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
+index fabfff3..cefa0fa 100644
+--- a/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
++++ b/node_modules/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm
+@@ -103,11 +103,16 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled)
+ initWithHandlersProvider:^NSArray> *(RCTModuleRegistry *moduleRegistry) {
+ NSArray *URLRequestHandlerModules =
+ extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTURLRequestHandler));
+- return [@[
++ NSMutableArray> *defaultHandlers = [NSMutableArray arrayWithObjects:
+ [RCTHTTPRequestHandler new],
+ [RCTDataRequestHandler new],
+ [RCTFileRequestHandler new],
+- [moduleRegistry moduleForName:"BlobModule"],
+- ] arrayByAddingObjectsFromArray:URLRequestHandlerModules];
++ nil
++ ];
++ id blobModule = [moduleRegistry moduleForName:"BlobModule"];
++ if (blobModule != nil) {
++ [defaultHandlers addObject:blobModule];
++ }
++ return [defaultHandlers arrayByAddingObjectsFromArray:URLRequestHandlerModules];
+ }];
+ }
diff --git a/rock.config.mjs b/rock.config.mjs
index 3f441f1ac5a3..fd354e8f6832 100644
--- a/rock.config.mjs
+++ b/rock.config.mjs
@@ -22,7 +22,7 @@ export default {
android: platformAndroid({sourceDir: isHybrid ? './Mobile-Expensify/Android' : './android'}),
},
fingerprint: {
- extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/compute-patches-hash.sh'],
+ extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/compute-patches-hash.sh', 'patches', ...(isHybrid ? ['Mobile-Expensify/patches'] : [])],
env: ['USE_WEB_PROXY', 'PUSHER_DEV_SUFFIX', 'SECURE_NGROK_URL', 'NGROK_URL', 'USE_NGROK', 'FORCE_NATIVE_BUILD'],
ignorePaths: ['Mobile-Expensify/Android/assets/app/shared/bundle.js'],
},
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 93e47db3b810..23dcba5a2b1f 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -259,6 +259,7 @@ const CONST = {
COMPOSER_FOCUS_DELAY: 150,
MAX_TRANSITION_DURATION_MS: 1000,
MAX_TRANSITION_START_WAIT_MS: 1000,
+ EXPENSE_REPORT_DELETE_DELAY_MS: 300,
ANIMATION_DIRECTION: {
IN: 'in',
OUT: 'out',
@@ -369,6 +370,8 @@ const CONST = {
API_TRANSACTION_TAG_MAX_LENGTH: 255,
+ TRANSACTION_TAG_AND_CATEGORY_PICKER_MAX_TITLE_LINES: 5,
+
AUTO_AUTH_STATE: {
NOT_STARTED: 'not-started',
SIGNING_IN: 'signing-in',
@@ -1645,6 +1648,7 @@ const CONST = {
MARK_REIMBURSED_FROM_INTEGRATION: 'ACTIONMARKEDREIMBURSEDFROMINTEGRATION', // OldDot Action
MERGED_WITH_CASH_TRANSACTION: 'MERGEDWITHCASHTRANSACTION',
MODIFIED_EXPENSE: 'MODIFIEDEXPENSE',
+ CONCIERGE_AUTO_MATCH_VENDOR: 'CONCIERGEAUTOMATCHVENDOR',
MOVED: 'MOVED',
MOVED_TRANSACTION: 'MOVEDTRANSACTION',
UNREPORTED_TRANSACTION: 'UNREPORTEDTRANSACTION',
@@ -1742,6 +1746,7 @@ const CONST = {
UPDATE_AUTO_REPORTING_FREQUENCY: 'POLICYCHANGELOG_UPDATE_AUTOREPORTING_FREQUENCY',
UPDATE_BUDGET: 'POLICYCHANGELOG_UPDATE_BUDGET',
UPDATE_CATEGORY: 'POLICYCHANGELOG_UPDATE_CATEGORY',
+ UPDATE_CATEGORY_TAX_RATE: 'POLICYCHANGELOG_UPDATE_CATEGORY_TAX_RATE',
UPDATE_CATEGORIES: 'POLICYCHANGELOG_UPDATE_CATEGORIES',
UPDATE_CURRENCY: 'POLICYCHANGELOG_UPDATE_CURRENCY',
UPDATE_CUSTOM_UNIT: 'POLICYCHANGELOG_UPDATE_CUSTOM_UNIT',
@@ -1962,6 +1967,7 @@ const CONST = {
MESSAGE_KEY: {
WAITING_TO_ADD_TRANSACTIONS: 'waitingToAddTransactions',
WAITING_TO_SUBMIT: 'waitingToSubmit',
+ WAITING_TO_MARK_AS_DONE: 'waitingToMarkAsDone',
NO_FURTHER_ACTION: 'noFurtherAction',
WAITING_FOR_SUBMITTER_ACCOUNT: 'waitingForSubmitterAccount',
WAITING_FOR_AUTOMATIC_SUBMIT: 'waitingForAutomaticSubmit',
@@ -2328,6 +2334,11 @@ const CONST = {
GSD: 'gsd',
DEFAULT: 'default',
},
+ INBOX_TAB: {
+ ALL: 'all',
+ TODO: 'todo',
+ UNREAD: 'unread',
+ },
THEME: {
DEFAULT: 'system',
FALLBACK: 'dark',
@@ -2995,6 +3006,7 @@ const CONST = {
REPORT_EXPORT_STATUS: 'reportExportStatus',
TAX_NON_BILLABLE: 'taxNonBillable',
EXPORT_FOREIGN_CURRENCY: 'exportForeignCurrency',
+ COMPANY: 'company',
},
CERTINIA_EXPORT_STATUS: {
@@ -3089,6 +3101,7 @@ const CONST = {
MANAGER: 'manager',
CUSTOM: 'custom',
},
+ COOKIE_CLEAR_DELAY_MS: 500,
SYNC_STATUS: {
SYNCING: 'SYNCING',
DONE: 'DONE',
@@ -3683,6 +3696,8 @@ const CONST = {
IOU: {
MAX_RECENT_REPORTS_TO_SHOW: 5,
+ MAX_RECENT_ATTENDEES: 40,
+
// This will guranatee that the quantity input will not exceed 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER).
QUANTITY_MAX_LENGTH: 12,
// This is the transactionID used when going through the create expense flow so that it mimics a real transaction (like the edit flow)
@@ -3967,6 +3982,7 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
MAKE_AUDITOR: 'makeAuditor',
+ MAKE_CARD_ADMIN: 'makeCardAdmin',
},
BULK_ACTION_TYPES: {
DELETE: 'delete',
@@ -3981,7 +3997,6 @@ const CONST = {
ARE_DISTANCE_RATES_ENABLED: 'areDistanceRatesEnabled',
ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled',
ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled',
- ARE_INVOICE_FIELDS_ENABLED: 'areInvoiceFieldsEnabled',
ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled',
ARE_RECEIPT_PARTNERS_ENABLED: 'receiptPartners',
ARE_COMPANY_CARDS_ENABLED: 'areCompanyCardsEnabled',
@@ -4035,7 +4050,6 @@ const CONST = {
AUTOREPORTING_FREQUENCY: 'autoReportingFrequency',
AUTOREPORTING_OFFSET: 'autoReportingOffset',
GENERAL_SETTINGS: 'generalSettings',
- ADD_AGENT: 'addAgent',
},
EXPENSE_REPORT_RULES: {
PREVENT_SELF_APPROVAL: 'preventSelfApproval',
@@ -4282,6 +4296,12 @@ const CONST = {
FAKE_P2P_ID: '_FAKE_P2P_ID_',
MILES_TO_KILOMETERS: 1.609344,
KILOMETERS_TO_MILES: 0.621371,
+ RATE_STATUS: {
+ ACTIVE: 'active',
+ FUTURE: 'future',
+ EXPIRED: 'expired',
+ INACTIVE: 'inactive',
+ },
RATE_FIELD: {
START_DATE: 'startDate',
END_DATE: 'endDate',
@@ -7248,14 +7268,6 @@ const CONST = {
description: 'workspace.upgrade.reportFields.description' as const,
icon: 'Pencil',
},
- invoiceFields: {
- id: 'invoiceFields' as const,
- alias: 'invoice-fields',
- name: 'Invoice Fields',
- title: 'workspace.upgrade.invoiceFields.title' as const,
- description: 'workspace.upgrade.invoiceFields.description' as const,
- icon: 'Pencil',
- },
policyPreventMemberChangingTitle: {
id: 'policyPreventMemberChangingTitle' as const,
alias: 'policy-prevent-member-changing-title',
@@ -7429,12 +7441,12 @@ const CONST = {
description: 'workspace.upgrade.distanceRates.description' as const,
icon: 'CarIce',
},
- auditor: {
- id: 'auditor' as const,
- alias: 'auditor',
- name: 'Auditor',
- title: 'workspace.upgrade.auditor.title' as const,
- description: 'workspace.upgrade.auditor.description' as const,
+ controlPolicyRoles: {
+ id: 'controlPolicyRoles' as const,
+ alias: 'control-policy-roles',
+ name: 'Control policy roles',
+ title: 'workspace.upgrade.controlPolicyRoles.title' as const,
+ description: 'workspace.upgrade.controlPolicyRoles.description' as const,
icon: 'BlueShield',
},
reports: {
@@ -7517,10 +7529,6 @@ const CONST = {
LIST: 'dropdown',
FORMULA: 'formula',
},
- REPORT_FIELD_TARGETS: {
- EXPENSE: 'expense',
- INVOICE: 'invoice',
- },
NAVIGATION_ACTIONS: {
RESET: 'RESET',
@@ -7753,7 +7761,6 @@ const CONST = {
HAS_FILTER_NEGATION: 'hasFilterNegation',
},
CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal',
- AGENTS_WORKFLOWS_BANNER: 'agentsWorkflowsBanner',
AGENTS_RULES_BANNER: 'agentsRulesBanner',
SMART_BANNER_HEIGHT: 152,
@@ -7900,9 +7907,6 @@ const CONST = {
HIGH_CONTRAST_MODE_SWITCHER: {
TOGGLE: 'HighContrastModeSwitcher-Toggle',
},
- AGENTS_WORKFLOWS_BANNER: {
- DISMISS: 'AgentsWorkflowsBanner-Dismiss',
- },
AGENTS_RULES_BANNER: {
CTA: 'AgentsRulesBanner-CTA',
DISMISS: 'AgentsRulesBanner-Dismiss',
@@ -8464,6 +8468,7 @@ const CONST = {
BULK_ACTIONS_DROPDOWN: 'WorkspaceTaxes-BulkActionsDropdown',
},
DISTANCE_RATES: {
+ ROW: 'WorkspaceDistanceRates-Row',
ADD_BUTTON: 'WorkspaceDistanceRates-AddButton',
MORE_DROPDOWN: 'WorkspaceDistanceRates-MoreDropdown',
BULK_ACTIONS_DROPDOWN: 'WorkspaceDistanceRates-BulkActionsDropdown',
@@ -8684,6 +8689,8 @@ const CONST = {
DELEGATE_ITEM: 'SettingsSecurity-DelegateItem',
DELEGATE_CHANGE_ACCESS: 'SettingsSecurity-DelegateChangeAccess',
DELEGATE_REMOVE: 'SettingsSecurity-DelegateRemove',
+ DELEGATOR_ITEM: 'SettingsSecurity-DelegatorItem',
+ DELEGATOR_REMOVE: 'SettingsSecurity-DelegatorRemove',
},
SETTINGS_WALLET: {
ADD_BANK_ACCOUNT: 'SettingsWallet-AddBankAccount',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index f22984a038b1..4e43a9a5e2c4 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -172,6 +172,9 @@ const ONYXKEYS = {
/** Contains the user preference for the LHN priority mode */
NVP_PRIORITY_MODE: 'nvp_priorityMode',
+ /** Contains the user preference for the active inbox tab filter */
+ NVP_INBOX_TAB: 'nvp_inboxTab',
+
/** Contains the users's block expiration (if they have one) */
NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge',
@@ -599,25 +602,6 @@ const ONYXKEYS = {
/** Stores the information about currently edited advanced approval workflow */
APPROVAL_WORKFLOW: 'approvalWorkflow',
- /**
- * Workflow saves the user committed while a freshly-created agent was still pending. Keyed
- * by `${policyID}:${firstApproverEmail}`. WorkspaceWorkflowsPage overlays these entries on
- * top of the regular workflows so the new agent shows up faded in the approver card, and
- * a watcher fires the actual `updateApprovalWorkflow` once the agent gets its real email
- * and clears the entry. This lets the admin "Save" before CREATE_AGENT resolves without
- * the modal blocking on a `This field is required` validation error.
- */
- DEFERRED_AGENT_WORKFLOW_SAVES: 'deferredAgentWorkflowSaves',
-
- /**
- * Maps optimistic agent account IDs (randomly generated) to the real, server-assigned IDs returned
- * by CREATE_AGENT. The server echoes this mapping in the response's `onyxData` (and queues
- * it on the owner's account channel) so the WorkspaceWorkflowsPage and Edit Approvers
- * reconciliation can swap the pending approver to the real agent without falling back to
- * matching by prompt — which is ambiguous when multiple agents share the same prompt text.
- */
- OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING: 'optimisticAgentAccountIDMapping',
-
/** Stores the user search value for persistence across the screens */
ROOM_MEMBERS_USER_SEARCH_PHRASE: 'roomMembersUserSearchPhrase',
@@ -1214,6 +1198,7 @@ const ONYXKEYS = {
TODOS: 'todos',
RAM_ONLY_SORTED_REPORT_ACTIONS: 'sortedReportActions',
OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID: 'openAndSubmittedReportsByPolicyID',
+ FLAGGED_EXPENSES: 'flaggedExpenses',
},
/** Stores HybridApp specific state required to interoperate with OldDot */
@@ -1496,6 +1481,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.BETA_CONFIGURATION]: OnyxTypes.BetaConfiguration;
[ONYXKEYS.NVP_MUTED_PLATFORMS]: Partial>;
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf;
+ [ONYXKEYS.NVP_INBOX_TAB]: ValueOf;
[ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge;
[ONYXKEYS.QUEUE_FLUSHED_DATA]: AnyOnyxUpdate[];
[ONYXKEYS.TRANSACTIONS_PENDING_3DS_REVIEW]: OnyxTypes.TransactionsPending3DSReview;
@@ -1639,8 +1625,6 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[];
[ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string;
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
- [ONYXKEYS.DEFERRED_AGENT_WORKFLOW_SAVES]: Record;
- [ONYXKEYS.OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING]: Record;
[ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet;
[ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA]: OnyxTypes.ImportedSpreadsheetMemberData[];
[ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE]: ValueOf;
@@ -1707,6 +1691,7 @@ type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.TODOS]: OnyxTypes.TodosDerivedValue;
[ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: OnyxTypes.SortedReportActionsDerivedValue;
[ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID]: OnyxTypes.OpenAndSubmittedReportsByPolicyIDDerivedValue;
+ [ONYXKEYS.DERIVED.FLAGGED_EXPENSES]: OnyxTypes.FlaggedExpensesDerivedValue;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index f8e65e8cc72b..03d6054d2a70 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -180,126 +180,6 @@ const DYNAMIC_ROUTES = {
entryScreens: ['*'],
getRoute: (accountID: number) => `avatar/${accountID}` as const,
},
- MONEY_REQUEST_STEP_TAX_RATE: {
- path: 'money-request/tax-rate/:action/:iouType/:transactionID/:reportID?',
- entryScreens: [
- SCREENS.MONEY_REQUEST.STEP_CONFIRMATION,
- SCREENS.REPORT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- ],
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID?: string) => {
- if (!transactionID || !reportID) {
- Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_TAX_RATE dynamic route');
- }
-
- return `money-request/tax-rate/${action as string}/${iouType as string}/${transactionID}${reportID ? `/${reportID}` : ''}` as const;
- },
- },
- MONEY_REQUEST_STEP_TAX_AMOUNT: {
- path: 'money-request/tax-amount/:action/:iouType/:transactionID/:reportID?',
- entryScreens: [
- SCREENS.MONEY_REQUEST.STEP_CONFIRMATION,
- SCREENS.REPORT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- ],
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID?: string) => {
- if (!transactionID || !reportID) {
- Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_TAX_AMOUNT dynamic route');
- }
-
- return `money-request/tax-amount/${action as string}/${iouType as string}/${transactionID}${reportID ? `/${reportID}` : ''}` as const;
- },
- },
- MONEY_REQUEST_STEP_CATEGORY: {
- path: 'money-request/category/:action/:iouType/:transactionID/:reportID/:reportActionID?',
- entryScreens: [
- SCREENS.MONEY_REQUEST.STEP_CONFIRMATION,
- SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT,
- SCREENS.REPORT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- ],
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => {
- if (!transactionID || !reportID) {
- Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_CATEGORY dynamic route');
- }
-
- return `money-request/category/${action as string}/${iouType as string}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}` as const;
- },
- },
- MONEY_REQUEST_ATTENDEE: {
- path: 'money-request/attendees/:action/:iouType/:transactionID/:reportID',
- entryScreens: [
- SCREENS.MONEY_REQUEST.STEP_CONFIRMATION,
- SCREENS.REPORT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- ],
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined) => {
- if (!transactionID || !reportID) {
- Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_ATTENDEE dynamic route');
- }
-
- return `money-request/attendees/${action as string}/${iouType as string}/${transactionID}/${reportID}` as const;
- },
- },
- MONEY_REQUEST_ACCOUNTANT: {
- path: 'money-request/accountant/:action/:iouType/:transactionID/:reportID',
- entryScreens: [
- SCREENS.MONEY_REQUEST.STEP_CONFIRMATION,
- SCREENS.REPORT,
- SCREENS.REPORT_DETAILS.DYNAMIC_ROOT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- ],
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined) => {
- if (!transactionID || !reportID) {
- Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_ACCOUNTANT dynamic route');
- }
-
- return `money-request/accountant/${action as string}/${iouType as string}/${transactionID}/${reportID}` as const;
- },
- },
- MONEY_REQUEST_UPGRADE: {
- path: 'money-request/upgrade/:action/:iouType/:transactionID/:reportID/:upgradePath?',
- entryScreens: [
- SCREENS.MONEY_REQUEST.STEP_CONFIRMATION,
- SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT,
- SCREENS.HOME,
- SCREENS.INBOX,
- SCREENS.REPORT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- SCREENS.SEARCH.ROOT,
- SCREENS.SEARCH.TRANSACTIONS_CHANGE_REPORT_SEARCH_RHP,
- ],
- getRoute: (params: {action: IOUAction; iouType: IOUType; transactionID: string; reportID: string; upgradePath?: string; shouldSubmitExpense?: boolean}) => {
- const {action, iouType, transactionID, reportID, upgradePath, shouldSubmitExpense} = params;
- const upgradePathParam = upgradePath ? `/${upgradePath}` : '';
- const basePath = `money-request/upgrade/${action as string}/${iouType as string}/${transactionID}/${reportID}${upgradePathParam}` as const;
-
- if (shouldSubmitExpense) {
- return `${basePath}?shouldSubmitExpense=true` as const;
- }
-
- return basePath;
- },
- queryParams: ['shouldSubmitExpense'],
- },
NEW_REPORT_WORKSPACE_SELECTION: {
path: 'new-report-workspace-selection',
entryScreens: ['*'],
@@ -342,6 +222,14 @@ const DYNAMIC_ROUTES = {
path: 'certinia/advanced',
entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.ROOT],
},
+ POLICY_ACCOUNTING_CERTINIA_REPORT_EXPORT_STATUS: {
+ path: 'certinia-report-status/select',
+ entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_EXPORT],
+ },
+ POLICY_ACCOUNTING_CERTINIA_COMPANY_SELECTOR: {
+ path: 'certinia/company',
+ entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.ROOT],
+ },
POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: {
path: 'vendor/select',
entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_NETSUITE_EXPORT_EXPENSES],
@@ -1427,14 +1315,11 @@ const ROUTES = {
SETTINGS_AGENTS: 'settings/agents',
SETTINGS_AGENTS_ADD: {
route: 'settings/agents/new',
- getRoute: ({policyID, workflowApproverEmail}: {policyID?: string; workflowApproverEmail?: string} = {}) => {
+ getRoute: ({policyID}: {policyID?: string} = {}) => {
const params = new URLSearchParams();
if (policyID) {
params.set('policyID', policyID);
}
- if (workflowApproverEmail) {
- params.set('workflowApproverEmail', workflowApproverEmail);
- }
const query = params.toString();
return `settings/agents/new${query ? `?${query}` : ''}` as const;
},
@@ -1749,6 +1634,26 @@ const ROUTES = {
return getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}/${reportActionID ? `${reportActionID}/` : ''}${pageIndex}`, backTo);
},
},
+ MONEY_REQUEST_STEP_TAX_RATE: {
+ route: ':action/:iouType/taxRate/:transactionID/:reportID?',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_TAX_RATE route');
+ }
+
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/taxRate/${transactionID}/${reportID}`, backTo);
+ },
+ },
+ MONEY_REQUEST_STEP_TAX_AMOUNT: {
+ route: ':action/:iouType/taxAmount/:transactionID/:reportID?',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_TAX_AMOUNT route');
+ }
+
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/taxAmount/${transactionID}/${reportID}`, backTo);
+ },
+ },
MONEY_REQUEST_STEP_CATEGORY_CREATE: {
route: ':action/:iouType/category/new/:transactionID/:reportID/:reportActionID?',
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string, backTo = '') => {
@@ -1759,6 +1664,60 @@ const ROUTES = {
return getUrlWithBackToParam(`${action as string}/${iouType as string}/category/new/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo);
},
},
+ MONEY_REQUEST_STEP_CATEGORY: {
+ route: ':action/:iouType/category/:transactionID/:reportID/:reportActionID?',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_CATEGORY route');
+ }
+
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo);
+ },
+ },
+ MONEY_REQUEST_ATTENDEE: {
+ route: ':action/:iouType/attendees/:transactionID/:reportID',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_ATTENDEE route');
+ }
+
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo);
+ },
+ },
+ MONEY_REQUEST_ACCOUNTANT: {
+ route: ':action/:iouType/accountant/:transactionID/:reportID',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_ACCOUNTANT route');
+ }
+
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/accountant/${transactionID}/${reportID}`, backTo);
+ },
+ },
+ MONEY_REQUEST_UPGRADE: {
+ route: ':action/:iouType/upgrade/:transactionID/:reportID/:upgradePath?',
+ getRoute: (params: {action: IOUAction; iouType: IOUType; transactionID: string; reportID: string; backTo?: string; shouldSubmitExpense?: boolean; upgradePath?: string}) => {
+ const {action, iouType, transactionID, reportID, backTo = '', shouldSubmitExpense = false, upgradePath} = params;
+ const upgradePathParam = upgradePath ? `/${upgradePath}` : '';
+ const baseURL = `${action as string}/${iouType as string}/upgrade/${transactionID}/${reportID}${upgradePathParam}` as const;
+
+ if (shouldSubmitExpense) {
+ return getUrlWithBackToParam(`${baseURL}?shouldSubmitExpense=${shouldSubmitExpense}` as const, backTo);
+ }
+
+ return getUrlWithBackToParam(baseURL, backTo);
+ },
+ },
+ MONEY_REQUEST_STEP_VENDOR: {
+ route: ':action/:iouType/vendor/:transactionID/:reportID/:reportActionID?',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_VENDOR route');
+ }
+
+ return `${action as string}/${iouType as string}/vendor/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}` as const;
+ },
+ },
MONEY_REQUEST_STEP_DESTINATION: {
route: ':action/:iouType/destination/:transactionID/:reportID',
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
@@ -2353,11 +2312,7 @@ const ROUTES = {
},
WORKSPACE_WORKFLOWS_APPROVALS_EDIT: {
route: 'workspaces/:policyID/workflows/approvals/:firstApproverEmail/edit',
- getRoute: (policyID: string, firstApproverEmail: string, seedApproverEmail?: string, seedApproverAccountID?: number) =>
- getUrlWithParams(`workspaces/${policyID}/workflows/approvals/${encodeURIComponent(firstApproverEmail)}/edit`, {
- seedApproverEmail,
- seedApproverAccountID: seedApproverAccountID !== undefined ? String(seedApproverAccountID) : undefined,
- }),
+ getRoute: (policyID: string, firstApproverEmail: string) => `workspaces/${policyID}/workflows/approvals/${encodeURIComponent(firstApproverEmail)}/edit` as const,
},
WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM: {
route: 'workspaces/:policyID/workflows/approvals/expenses-from',
@@ -2380,11 +2335,6 @@ const ROUTES = {
route: 'workspaces/:policyID/workflows/approvals/over-limit-approver',
getRoute: (policyID: string, approverIndex: number) => `workspaces/${policyID}/workflows/approvals/over-limit-approver?approverIndex=${approverIndex}` as const,
},
- WORKSPACE_WORKFLOWS_ADD_AGENT: {
- route: 'workspaces/:policyID/workflows/add-agent',
- getRoute: ({policyID, workflowApproverEmail}: {policyID: string; workflowApproverEmail?: string}) =>
- getUrlWithParams(`workspaces/${policyID}/workflows/add-agent`, {workflowApproverEmail}),
- },
WORKSPACE_WORKFLOWS_PAYER: {
route: 'workspaces/:policyID/workflows/payer',
getRoute: (policyId: string) => `workspaces/${policyId}/workflows/payer` as const,
@@ -2428,35 +2378,6 @@ const ROUTES = {
route: 'workspaces/:policyID/invoices/company-website',
getRoute: (policyID: string) => `workspaces/${policyID}/invoices/company-website` as const,
},
- WORKSPACE_INVOICE_FIELDS_CREATE: {
- route: 'workspaces/:policyID/invoices/newInvoiceField',
- getRoute: (policyID: string) => `workspaces/${policyID}/invoices/newInvoiceField` as const,
- },
- WORKSPACE_INVOICE_FIELDS_SETTINGS: {
- route: 'workspaces/:policyID/invoices/:reportFieldID/edit',
- getRoute: (policyID: string, reportFieldID: string) => `workspaces/${policyID}/invoices/${encodeURIComponent(reportFieldID)}/edit` as const,
- },
- WORKSPACE_INVOICE_FIELDS_LIST_VALUES: {
- route: 'workspaces/:policyID/invoices/listValues/:reportFieldID?',
- getRoute: (policyID: string, reportFieldID?: string) => `workspaces/${policyID}/invoices/listValues/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const,
- },
- WORKSPACE_INVOICE_FIELDS_ADD_VALUE: {
- route: 'workspaces/:policyID/invoices/addValue/:reportFieldID?',
- getRoute: (policyID: string, reportFieldID?: string) => `workspaces/${policyID}/invoices/addValue/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const,
- },
- WORKSPACE_INVOICE_FIELDS_VALUE_SETTINGS: {
- route: 'workspaces/:policyID/invoices/:valueIndex/:reportFieldID?',
- getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) =>
- `workspaces/${policyID}/invoices/${valueIndex}/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const,
- },
- WORKSPACE_INVOICE_FIELDS_EDIT_VALUE: {
- route: 'workspaces/:policyID/invoices/newInvoiceField/:valueIndex/edit',
- getRoute: (policyID: string, valueIndex: number) => `workspaces/${policyID}/invoices/newInvoiceField/${valueIndex}/edit` as const,
- },
- WORKSPACE_INVOICE_FIELDS_EDIT_INITIAL_VALUE: {
- route: 'workspaces/:policyID/invoices/:reportFieldID/edit/initialValue',
- getRoute: (policyID: string, reportFieldID: string) => `workspaces/${policyID}/invoices/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const,
- },
WORKSPACE_MEMBERS: {
route: 'workspaces/:policyID/members',
getRoute: (policyID: string | undefined) => {
@@ -4019,6 +3940,33 @@ const ROUTES = {
return `workspaces/${policyID}/accounting/certinia/advanced` as const;
},
},
+ POLICY_ACCOUNTING_CERTINIA_TAGS_MAPPING: {
+ route: 'workspaces/:policyID/accounting/certinia/import/tags-mapping',
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_CERTINIA_TAGS_MAPPING route');
+ }
+ return `workspaces/${policyID}/accounting/certinia/import/tags-mapping` as const;
+ },
+ },
+ POLICY_ACCOUNTING_CERTINIA_REPORT_EXPORT_STATUS: {
+ route: 'workspaces/:policyID/accounting/certinia/export/report-status',
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_CERTINIA_REPORT_EXPORT_STATUS route');
+ }
+ return `workspaces/${policyID}/accounting/certinia/export/report-status` as const;
+ },
+ },
+ POLICY_ACCOUNTING_CERTINIA_COMPANY_SELECTOR: {
+ route: 'workspaces/:policyID/accounting/certinia/company',
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_CERTINIA_COMPANY_SELECTOR route');
+ }
+ return `workspaces/${policyID}/accounting/certinia/company` as const;
+ },
+ },
ADD_EXISTING_EXPENSE: {
route: 'search/r/:reportID/add-existing-expense/:backToReport?',
getRoute: (reportID: string | undefined, backToReport?: string) => `search/r/${reportID}/add-existing-expense/${backToReport ?? ''}` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 6066e17d3967..cc9c10d594ad 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -373,9 +373,9 @@ const SCREENS = {
STEP_CONFIRMATION: 'Money_Request_Step_Confirmation',
STEP_CONFIRMATION_VERIFY_ACCOUNT: 'Money_Request_Step_Confirmation_Verify_Account',
START: 'Money_Request_Start',
- DYNAMIC_STEP_UPGRADE: 'Dynamic_Money_Request_Step_Upgrade',
+ STEP_UPGRADE: 'Money_Request_Step_Upgrade',
STEP_AMOUNT: 'Money_Request_Step_Amount',
- DYNAMIC_STEP_CATEGORY: 'Dynamic_Money_Request_Step_Category',
+ STEP_CATEGORY: 'Money_Request_Step_Category',
STEP_CATEGORY_CREATE: 'Money_Request_Step_Category_Create',
STEP_DATE: 'Money_Request_Step_Date',
STEP_DESCRIPTION: 'Money_Request_Step_Description',
@@ -386,8 +386,9 @@ const SCREENS = {
STEP_SCAN: 'Money_Request_Step_Scan',
STEP_TAG: 'Money_Request_Step_Tag',
STEP_WAYPOINT: 'Money_Request_Step_Waypoint',
- DYNAMIC_STEP_TAX_AMOUNT: 'Dynamic_Money_Request_Step_Tax_Amount',
- DYNAMIC_STEP_TAX_RATE: 'Dynamic_Money_Request_Step_Tax_Rate',
+ STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount',
+ STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate',
+ STEP_VENDOR: 'Money_Request_Step_Vendor',
RECEIPT_VIEW: 'Money_Request_Receipt_View',
STEP_SEND_FROM: 'Money_Request_Step_Send_From',
STEP_COMPANY_INFO: 'Money_Request_Step_Company_Info',
@@ -396,8 +397,8 @@ const SCREENS = {
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
RECEIPT: 'Money_Request_Receipt',
STATE_SELECTOR: 'Money_Request_State_Selector',
- DYNAMIC_STEP_ATTENDEES: 'Dynamic_Money_Request_Attendee',
- DYNAMIC_STEP_ACCOUNTANT: 'Dynamic_Money_Request_Step_Accountant',
+ STEP_ATTENDEES: 'Money_Request_Attendee',
+ STEP_ACCOUNTANT: 'Money_Request_Accountant',
STEP_DESTINATION: 'Money_Request_Destination',
STEP_TIME: 'Money_Request_Time',
STEP_SUBRATE: 'Money_Request_SubRate',
@@ -701,6 +702,9 @@ const SCREENS = {
CERTINIA_EXPORT_DATE: 'Policy_Accounting_Certinia_Export_Date',
CERTINIA_DEFAULT_VENDOR: 'Policy_Accounting_Certinia_Default_Vendor',
CERTINIA_ADVANCED: 'Policy_Accounting_Certinia_Advanced',
+ CERTINIA_TAGS_MAPPING: 'Policy_Accounting_Certinia_Tags_Mapping',
+ CERTINIA_REPORT_EXPORT_STATUS: 'Policy_Accounting_Certinia_Report_Export_Status',
+ CERTINIA_COMPANY_SELECTOR: 'Policy_Accounting_Certinia_Company_Selector',
CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation',
CARD_RECONCILIATION_SAGE_INTACCT_AUTO_SYNC: 'Policy_Accounting_Card_Reconciliation_Sage_Intacct_Auto_Sync',
DYNAMIC_RECONCILIATION_ACCOUNT_SETTINGS: 'Dynamic_Policy_Accounting_Reconciliation_Account_Settings',
@@ -766,13 +770,6 @@ const SCREENS = {
INVOICES_VERIFY_ACCOUNT: 'Workspace_Invoices_Verify_Account',
INVOICES_COMPANY_NAME: 'Workspace_Invoices_Company_Name',
INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website',
- INVOICE_FIELDS_CREATE: 'Workspace_InvoiceFields_Create',
- INVOICE_FIELDS_SETTINGS: 'Workspace_InvoiceFields_Settings',
- INVOICE_FIELDS_LIST_VALUES: 'Workspace_InvoiceFields_ListValues',
- INVOICE_FIELDS_ADD_VALUE: 'Workspace_InvoiceFields_AddValue',
- INVOICE_FIELDS_VALUE_SETTINGS: 'Workspace_InvoiceFields_ValueSettings',
- INVOICE_FIELDS_EDIT_VALUE: 'Workspace_InvoiceFields_EditValue',
- INVOICE_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_InvoiceFields_EditInitialValue',
MEMBERS: 'Workspace_Members',
ROOMS: 'Workspace_Rooms',
ROOM_CREATE: 'Workspace_Room_Create',
@@ -829,7 +826,6 @@ const SCREENS = {
WORKFLOWS_APPROVALS_APPROVER_CHANGE: 'Workspace_Workflows_Approvals_Approver_Change',
WORKFLOWS_APPROVALS_APPROVAL_LIMIT: 'Workspace_Workflows_Approvals_Approval_Limit',
WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER: 'Workspace_Workflows_Approvals_Over_Limit_Approver',
- WORKFLOWS_ADD_AGENT: 'Workspace_Workflows_Add_Agent',
WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency',
WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset',
DESCRIPTION: 'Workspace_Overview_Description',
diff --git a/src/components/AnimatedSubmitButton/index.tsx b/src/components/AnimatedSubmitButton/index.tsx
index 945d5333fc9b..6e3e010aae1e 100644
--- a/src/components/AnimatedSubmitButton/index.tsx
+++ b/src/components/AnimatedSubmitButton/index.tsx
@@ -28,9 +28,12 @@ type AnimatedSubmitButtonProps = WithSentryLabel & {
// Whether the button should be disabled
isDisabled?: boolean;
+
+ /** Whether to show "Mark as done" copy instead of "Submit" copy for track-intent users */
+ isMarkAsDone?: boolean;
};
-function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunning, onAnimationFinish, isDisabled, sentryLabel}: AnimatedSubmitButtonProps) {
+function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunning, onAnimationFinish, isDisabled, sentryLabel, isMarkAsDone}: AnimatedSubmitButtonProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const isAnimationRunning = isSubmittingAnimationRunning;
@@ -120,7 +123,7 @@ function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunn
>
void;
- /** A function that is called when the Add agent pill is pressed */
- onAddAgentPress?: () => void;
-
- /**
- * Called when the X is clicked on an approver row that carries `approver.errors`. The page
- * uses this to discard the failed optimistic agent (clears the deferred-save entry, the
- * optimistic personal detail / prompt, and the policy-level addAgent error).
- */
- onDismissApproverError?: (approver: Approver) => void;
-
- /**
- * Whether the Add agent pill is allowed on this card. It is still gated by the
- * customAgent beta on top of this flag.
- */
- canAddAgent?: boolean;
-
/** Currency used for formatting approval limits */
currency?: string;
@@ -67,25 +49,18 @@ function ApprovalWorkflowSection({
approvalWorkflow,
onPress,
onShowAllMembersPress,
- onAddAgentPress,
- onDismissApproverError,
- canAddAgent = false,
currency = CONST.CURRENCY.USD,
isDisabled = false,
hrProviderName,
isHRAdvancedMode = false,
hrFinalApproverEmail,
}: ApprovalWorkflowSectionProps) {
- const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'Bot', 'Lightbulb', 'Pencil', 'Users', 'UserCheck']);
+ const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'Lightbulb', 'Pencil', 'Users', 'UserCheck']);
const styles = useThemeStyles();
const theme = useTheme();
const {translate, toLocaleOrdinal, localeCompare} = useLocalize();
const {convertToDisplayString} = useCurrencyListActions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {isBetaEnabled} = usePermissions();
- const isCustomAgentEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT);
- const shouldShowAddAgentButton = canAddAgent && isCustomAgentEnabled && !isDisabled && !!onAddAgentPress;
-
const approverTitle = (index: number) => {
if (isHRAdvancedMode) {
const isLast = index === approvalWorkflow.approvers.length - 1;
@@ -179,7 +154,6 @@ function ApprovalWorkflowSection({
key={`approver-${approver.email || approver.accountID}-${index}`}
pendingAction={approver.pendingAction}
errors={approver.errors}
- onClose={approver.errors && onDismissApproverError ? () => onDismissApproverError(approver) : undefined}
>
@@ -225,16 +199,6 @@ function ApprovalWorkflowSection({
accessibilityLabel={translate('workflowsPage.editWorkflowAction')}
sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.APPROVAL_WORKFLOW_SECTION}
/>
- {shouldShowAddAgentButton && (
-
- )}
)}
diff --git a/src/components/BlockingViews/ForceFullScreenView/index.tsx b/src/components/BlockingViews/ForceFullScreenView/index.tsx
index f856794e4a57..a222d69f6816 100644
--- a/src/components/BlockingViews/ForceFullScreenView/index.tsx
+++ b/src/components/BlockingViews/ForceFullScreenView/index.tsx
@@ -1,4 +1,4 @@
-import {useRoute} from '@react-navigation/native';
+import {useIsFocused, useRoute} from '@react-navigation/native';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import {useFullScreenBlockingViewActions} from '@components/FullScreenBlockingViewContextProvider';
@@ -9,16 +9,17 @@ function ForceFullScreenView({children, shouldForceFullScreen = false}: ForceFul
const route = useRoute();
const styles = useThemeStyles();
const {addRouteKey, removeRouteKey} = useFullScreenBlockingViewActions();
+ const isFocused = useIsFocused();
useEffect(() => {
- if (!shouldForceFullScreen) {
+ if (!shouldForceFullScreen || !isFocused) {
return;
}
addRouteKey(route.key);
return () => removeRouteKey(route.key);
- }, [addRouteKey, removeRouteKey, route.key, shouldForceFullScreen]);
+ }, [addRouteKey, removeRouteKey, route.key, shouldForceFullScreen, isFocused]);
if (shouldForceFullScreen) {
return {children} ;
diff --git a/src/components/CategoryPicker/index.tsx b/src/components/CategoryPicker/index.tsx
index 909a4f7a8353..7253482ff633 100644
--- a/src/components/CategoryPicker/index.tsx
+++ b/src/components/CategoryPicker/index.tsx
@@ -92,7 +92,7 @@ function CategoryPicker({selectedCategory, policyID, onSubmit, addBottomSafeArea
addBottomSafeAreaPadding={addBottomSafeAreaPadding}
style={{listItemTitleStyles: styles.w100}}
isRowMultilineSupported
- titleNumberOfLines={3}
+ titleNumberOfLines={CONST.TRANSACTION_TAG_AND_CATEGORY_PICKER_MAX_TITLE_LINES}
/>
);
}
diff --git a/src/components/Charts/PieChart/PieChartContent.tsx b/src/components/Charts/PieChart/PieChartContent.tsx
index 9f60500949eb..8dac9f1946f0 100644
--- a/src/components/Charts/PieChart/PieChartContent.tsx
+++ b/src/components/Charts/PieChart/PieChartContent.tsx
@@ -131,7 +131,7 @@ function PieChartContent({data, isLoading, valueUnit, valueUnitPosition, onSlice
const renderLegendItem = (slice: PieSlice) => {
return (
{
tooltipPosition.set(slice.tooltipPosition);
diff --git a/src/components/ConnectToHRFlow/index.ios.tsx b/src/components/ConnectToHRFlow/index.ios.tsx
new file mode 100644
index 000000000000..f2d1f800e9f0
--- /dev/null
+++ b/src/components/ConnectToHRFlow/index.ios.tsx
@@ -0,0 +1,90 @@
+import {openAuthSessionAsync} from 'expo-web-browser';
+import React, {useEffect, useRef, useState} from 'react';
+import {StyleSheet, View} from 'react-native';
+import ActivityIndicator from '@components/ActivityIndicator';
+import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Modal from '@components/Modal';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getShortLivedAuthTokenURL} from '@userActions/Link';
+import CONST from '@src/CONST';
+import type ConnectToHRFlowProps from './types';
+
+function ConnectToHRFlow({setupLink, onDone}: ConnectToHRFlowProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const hasOpened = useRef(false);
+ const isDismissed = useRef(false);
+ const isMounted = useRef(true);
+ const [isModalOpen, setIsModalOpen] = useState(true);
+
+ const dismiss = () => {
+ if (isDismissed.current || !isMounted.current) {
+ return;
+ }
+ isDismissed.current = true;
+ setIsModalOpen(false);
+ };
+
+ const openSession = () => {
+ if (hasOpened.current) {
+ return;
+ }
+ hasOpened.current = true;
+
+ getShortLivedAuthTokenURL(setupLink)
+ // CONST.DEEPLINK_BASE_URL is used as a sentinel so ASWebAuthenticationSession
+ // auto-dismisses when the flow redirects back to the app via deep link.
+ .then((url) => {
+ if (isDismissed.current) {
+ return;
+ }
+ return openAuthSessionAsync(url, CONST.DEEPLINK_BASE_URL, {preferEphemeralSession: true});
+ })
+ .finally(dismiss);
+ };
+
+ const {isOffline} = useNetwork({onReconnect: openSession});
+
+ useEffect(() => {
+ if (isOffline) {
+ return;
+ }
+ openSession();
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only the initial setupLink should be used; re-opening the auth session on prop change is not desired
+ }, []);
+
+ useEffect(
+ () => () => {
+ isMounted.current = false;
+ },
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ConnectToHRFlow;
diff --git a/src/components/ConnectToHRFlow/index.native.tsx b/src/components/ConnectToHRFlow/index.native.tsx
index db54e473fa04..27fbeac343eb 100644
--- a/src/components/ConnectToHRFlow/index.native.tsx
+++ b/src/components/ConnectToHRFlow/index.native.tsx
@@ -1,25 +1,58 @@
-import React, {useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import type {WebViewNavigation} from 'react-native-webview';
import {WebView} from 'react-native-webview';
+import type {WebViewOpenWindowEvent} from 'react-native-webview/lib/WebViewTypes';
import ActivityIndicator from '@components/ActivityIndicator';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import useLocalize from '@hooks/useLocalize';
-import useOnyx from '@hooks/useOnyx';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getShortLivedAuthTokenURL} from '@userActions/Link';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type ConnectToHRFlowProps from './types';
-function ConnectToHRFlow({setupLink}: ConnectToHRFlowProps) {
+function ConnectToHRFlow({setupLink, onDone}: ConnectToHRFlowProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const webViewRef = useRef(null);
const [isWebViewOpen, setIsWebViewOpen] = useState(true);
- const [session] = useOnyx(ONYXKEYS.SESSION);
+ const [popupUrl, setPopupUrl] = useState(null);
+ const [isPopupVisible, setIsPopupVisible] = useState(false);
+ const [authenticatedUrl, setAuthenticatedUrl] = useState(null);
+ const [cookiesCleared, setCookiesCleared] = useState(false);
+ const hasFetched = useRef(false);
+ const cookieTimerRef = useRef(null);
+
+ const fetchAuthUrl = () => {
+ if (hasFetched.current) {
+ return;
+ }
+ hasFetched.current = true;
+ getShortLivedAuthTokenURL(setupLink).then(setAuthenticatedUrl);
+ };
+
+ const {isOffline} = useNetwork({onReconnect: fetchAuthUrl});
+
+ useEffect(() => {
+ if (isOffline) {
+ return;
+ }
+ fetchAuthUrl();
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only fetch once on mount when online
+ }, []);
+
+ useEffect(
+ () => () => {
+ if (!cookieTimerRef.current) {
+ return;
+ }
+ clearTimeout(cookieTimerRef.current);
+ },
+ [],
+ );
const renderLoading = () => (
@@ -30,40 +63,87 @@ function ConnectToHRFlow({setupLink}: ConnectToHRFlowProps) {
);
+ const handleOpenWindow = (event: WebViewOpenWindowEvent) => {
+ setPopupUrl(event.nativeEvent.targetUrl);
+ setIsPopupVisible(true);
+ };
+
+ const dismiss = () => {
+ setIsWebViewOpen(false);
+ onDone?.();
+ };
+
+ const handleBackPress = () => {
+ if (isPopupVisible) {
+ // Keep the popup WebView mounted (hidden) so in-flight OAuth redirects
+ // can complete and shared cookies remain intact for Merge Link.
+ setIsPopupVisible(false);
+ return;
+ }
+ dismiss();
+ };
+
const handleNavigationStateChange = (navState: WebViewNavigation) => {
if (!navState.url.includes(ROUTES.CONNECTION_COMPLETE)) {
return;
}
- setIsWebViewOpen(false);
+
+ dismiss();
};
- const authToken = session?.authToken ?? null;
+ const isReady = cookiesCleared && !!authenticatedUrl;
return (
setIsWebViewOpen(false)}
+ onClose={handleBackPress}
fullscreen
isVisible={isWebViewOpen}
type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE}
>
setIsWebViewOpen(false)}
- shouldDisplayHelpButton={false}
+ onBackButtonPress={handleBackPress}
/>
-
+
+ {!cookiesCleared && (
+ {
+ // Brief delay to ensure the incognito WebView has fully cleared cookies
+ // before mounting the main WebView. No deterministic completion signal is
+ // available from the incognito session teardown.
+ cookieTimerRef.current = setTimeout(() => setCookiesCleared(true), CONST.MERGE_HR.COOKIE_CLEAR_DELAY_MS);
+ }}
+ style={styles.opacity0}
+ />
+ )}
+ {!isReady && renderLoading()}
+ {isReady && (
+ // Not using incognito here so the popup WebView can share cookies
+ // with the main flow (required for OAuth handoffs).
+
+ )}
+ {!!popupUrl && (
+
+
+
+ )}
+
);
diff --git a/src/components/ConnectToHRFlow/types.ts b/src/components/ConnectToHRFlow/types.ts
index 56f5e1be2809..85b230c23ed8 100644
--- a/src/components/ConnectToHRFlow/types.ts
+++ b/src/components/ConnectToHRFlow/types.ts
@@ -1,6 +1,9 @@
type ConnectToHRFlowProps = {
/** The URL to open for the HR provider's connection flow */
setupLink: string;
+
+ /** Called when the flow is dismissed or completed so the parent can unmount the component */
+ onDone?: () => void;
};
export default ConnectToHRFlowProps;
diff --git a/src/components/DatePicker/index.tsx b/src/components/DatePicker/index.tsx
index 8964cde7658e..64563214a97f 100644
--- a/src/components/DatePicker/index.tsx
+++ b/src/components/DatePicker/index.tsx
@@ -2,6 +2,7 @@ import {format, setYear} from 'date-fns';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {InteractionManager, View} from 'react-native';
+import type {TextInputKeyPressEvent} from 'react-native';
import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useAccessibilityAnnouncement from '@hooks/useAccessibilityAnnouncement';
@@ -10,6 +11,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import {isNumeric} from '@libs/ValidationUtils';
import {setDraftValues} from '@userActions/FormActions';
import CONST from '@src/CONST';
import DatePickerModal from './DatePickerModal';
@@ -53,8 +55,6 @@ function DatePicker({
// Whether the user currently intends the picker to be open. Lets a deferred measurement skip opening if the
// picker was dismissed before it resolved.
const openIntentRef = useRef(false);
- // Whether the initial autoFocus has already opened the picker, so later focuses don't reopen it.
- const hasAutoOpenedRef = useRef(false);
const {inputCallbackRef: autoFocusCallbackRef, cancelAutoFocus} = useAutoFocusInput();
const autoFocusCallbackRefRef = useRef(autoFocusCallbackRef);
@@ -116,24 +116,16 @@ function DatePicker({
setIsModalVisible(false);
}, []);
- const openDatePickerOnPress = useCallback(() => {
- if (!shouldDeferShowUntilPositioned) {
- return;
- }
- showDatePickerModal();
- }, [shouldDeferShowUntilPositioned, showDatePickerModal]);
-
- const handleInputFocus = useCallback(() => {
- if (!shouldDeferShowUntilPositioned) {
+ const handleInputKeyPress = useCallback(
+ (event: TextInputKeyPressEvent) => {
+ if (!isNumeric(event.nativeEvent.key)) {
+ return;
+ }
+ event.preventDefault();
showDatePickerModal();
- return;
- }
- if (!autoFocus || hasAutoOpenedRef.current) {
- return;
- }
- hasAutoOpenedRef.current = true;
- showDatePickerModal();
- }, [shouldDeferShowUntilPositioned, autoFocus, showDatePickerModal]);
+ },
+ [showDatePickerModal],
+ );
const handleDateSelected = (newDate: string) => {
onTouched?.();
@@ -191,14 +183,16 @@ function DatePicker({
iconContainerStyle={styles.pr0}
label={label}
accessibilityLabel={label}
- role={CONST.ROLE.PRESENTATION}
+ role={CONST.ROLE.COMBOBOX}
+ accessibilityState={{expanded: isModalVisible}}
value={selectedDate}
placeholder={placeholder ?? translate('common.dateFormat')}
errorText={errorText}
inputStyle={styles.pointerEventsNone}
disabled={disabled}
- onPress={openDatePickerOnPress}
- onFocus={handleInputFocus}
+ onPress={() => showDatePickerModal()}
+ onSubmitEditing={() => showDatePickerModal()}
+ onKeyPress={handleInputKeyPress}
textInputContainerStyles={isModalVisible ? styles.borderColorFocus : {}}
shouldHideClearButton={shouldHideClearButton}
onClearInput={handleClear}
diff --git a/src/components/HighContrastModeSwitcher.tsx b/src/components/HighContrastModeSwitcher.tsx
index 22332e1b220e..0fdf3581724f 100644
--- a/src/components/HighContrastModeSwitcher.tsx
+++ b/src/components/HighContrastModeSwitcher.tsx
@@ -18,10 +18,11 @@ function HighContrastModeSwitcher() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
+ const [highContrastIntent] = useOnyx(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT);
const icons = useMemoizedLazyExpensifyIcons(['Moon']);
const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
- const isHighContrast = isHighContrastTheme(currentTheme);
+ const isHighContrast = highContrastIntent ?? isHighContrastTheme(currentTheme);
const label = translate(isHighContrast ? 'themePage.disableHighContrast' : 'themePage.enableHighContrast');
const toggleHighContrast = () => {
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index 078f2de84e42..219348dc4a20 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -207,6 +207,7 @@ import ReceiptMultiple from '@assets/images/receipt-multiple.svg';
import ReceiptPlaceholderPlus from '@assets/images/receipt-placeholder-plus.svg';
import ReceiptPlus from '@assets/images/receipt-plus.svg';
import ReceiptScan from '@assets/images/receipt-scan.svg';
+import ReceiptSearch from '@assets/images/receipt-search.svg';
import ReceiptSlash from '@assets/images/receipt-slash.svg';
import Receipt from '@assets/images/receipt.svg';
import RemoveMembers from '@assets/images/remove-members.svg';
@@ -451,6 +452,7 @@ const Expensicons = {
ReceiptPlaceholderPlus,
ReceiptPlus,
ReceiptScan,
+ ReceiptSearch,
ReceiptSlash,
RemoveMembers,
ReportCopy,
diff --git a/src/components/InteractiveStepSubHeader.tsx b/src/components/InteractiveStepSubHeader.tsx
index f73b17f9ba41..2c0fd86ba028 100644
--- a/src/components/InteractiveStepSubHeader.tsx
+++ b/src/components/InteractiveStepSubHeader.tsx
@@ -3,6 +3,7 @@ import React, {useImperativeHandle, useState} from 'react';
import type {ViewStyle} from 'react-native';
import {View} from 'react-native';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
@@ -21,6 +22,9 @@ type InteractiveStepSubHeaderProps = {
/** The index of the step to start with */
startStepIndex?: number;
+ /** Description of the current step, appended to its accessibility label */
+ currentStepAccessibilityDescription: string;
+
/** Reference to the outer element */
ref?: ForwardedRef;
};
@@ -39,8 +43,9 @@ type InteractiveStepSubHeaderHandle = {
const MIN_AMOUNT_FOR_EXPANDING = 3;
const MIN_AMOUNT_OF_STEPS = 2;
-function InteractiveStepSubHeader({stepNames, startStepIndex = 0, onStepSelected, ref}: InteractiveStepSubHeaderProps) {
+function InteractiveStepSubHeader({stepNames, startStepIndex = 0, currentStepAccessibilityDescription, onStepSelected, ref}: InteractiveStepSubHeaderProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
const containerWidthStyle: ViewStyle = stepNames.length < MIN_AMOUNT_FOR_EXPANDING ? styles.mnw60 : styles.mnw100;
if (stepNames.length < MIN_AMOUNT_OF_STEPS) {
@@ -68,11 +73,7 @@ function InteractiveStepSubHeader({stepNames, startStepIndex = 0, onStepSelected
const amountOfUnions = stepNames.length - 1;
return (
-
+
{stepNames.map((stepName, index) => {
const isCompletedStep = currentStep > index;
const isLockedStep = currentStep < index;
@@ -104,9 +105,14 @@ function InteractiveStepSubHeader({stepNames, startStepIndex = 0, onStepSelected
]}
disabled={isLockedStep || !onStepSelected}
onPress={moveToStep}
- accessible={false}
- aria-hidden
- role={CONST.ROLE.BUTTON}
+ role={CONST.ROLE.GROUP}
+ aria-current={currentStep === index ? 'step' : undefined}
+ accessibilityState={{selected: currentStep === index}}
+ accessibilityLabel={translate('stepCounter', {
+ step: index + 1,
+ total: stepNames.length,
+ text: currentStep === index ? currentStepAccessibilityDescription : undefined,
+ })}
sentryLabel={CONST.SENTRY_LABEL.INTERACTIVE_STEP_SUB_HEADER.STEP_BUTTON}
>
{isCompletedStep ? (
diff --git a/src/components/InteractiveStepSubPageHeader.tsx b/src/components/InteractiveStepSubPageHeader.tsx
index 66b5762490c6..e46d9818d2ff 100644
--- a/src/components/InteractiveStepSubPageHeader.tsx
+++ b/src/components/InteractiveStepSubPageHeader.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import type {ViewStyle} from 'react-native';
import {View} from 'react-native';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
@@ -17,6 +18,9 @@ type InteractiveStepSubPageHeaderProps = {
/** Current step index (0-based) */
currentStepIndex: number;
+ /** Description of the current step, appended to its accessibility label */
+ currentStepAccessibilityDescription: string;
+
/** Function to call when a step is selected */
onStepSelected?: (stepIndex: number) => void;
};
@@ -24,8 +28,9 @@ type InteractiveStepSubPageHeaderProps = {
const MIN_AMOUNT_FOR_EXPANDING = 3;
const MIN_AMOUNT_OF_STEPS = 2;
-function InteractiveStepSubPageHeader({stepNames, currentStepIndex, onStepSelected}: InteractiveStepSubPageHeaderProps) {
+function InteractiveStepSubPageHeader({stepNames, currentStepIndex, currentStepAccessibilityDescription, onStepSelected}: InteractiveStepSubPageHeaderProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
const containerWidthStyle: ViewStyle = stepNames.length < MIN_AMOUNT_FOR_EXPANDING ? styles.mnw60 : styles.mnw100;
@@ -65,8 +70,14 @@ function InteractiveStepSubPageHeader({stepNames, currentStepIndex, onStepSelect
disabled={isLockedStep || !onStepSelected}
onPress={() => handleStepPress(isLockedStep, index)}
accessible
- accessibilityLabel={stepName}
- role={CONST.ROLE.BUTTON}
+ role={CONST.ROLE.GROUP}
+ aria-current={currentStepIndex === index ? 'step' : undefined}
+ accessibilityState={{selected: currentStepIndex === index}}
+ accessibilityLabel={translate('stepCounter', {
+ step: index + 1,
+ total: stepNames.length,
+ text: currentStepIndex === index ? currentStepAccessibilityDescription : undefined,
+ })}
>
{isCompletedStep ? (
)}
diff --git a/src/components/LHNOptionsList/LHNEmptyState.tsx b/src/components/LHNOptionsList/LHNEmptyState.tsx
index 40fed2097bc6..cb0f4552dbca 100644
--- a/src/components/LHNOptionsList/LHNEmptyState.tsx
+++ b/src/components/LHNOptionsList/LHNEmptyState.tsx
@@ -3,12 +3,16 @@ import {View} from 'react-native';
import type {BlockingViewProps} from '@components/BlockingViews/BlockingView';
import BlockingView from '@components/BlockingViews/BlockingView';
import Icon from '@components/Icon';
+import Text from '@components/Text';
import TextBlock from '@components/TextBlock';
+import TextLink from '@components/TextLink';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
+import {useSidebarOrderedReportsActions, useSidebarOrderedReportsState} from '@hooks/useSidebarOrderedReports';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
+import CONST from '@src/CONST';
import useEmptyLHNIllustration from './useEmptyLHNIllustration';
function LHNEmptyState() {
@@ -16,7 +20,34 @@ function LHNEmptyState() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass', 'Plus']);
- const emptyLHNIllustration = useEmptyLHNIllustration();
+ const emptyLHNIllustration = useEmptyLHNIllustration() as BlockingViewProps;
+ const {activeTab} = useSidebarOrderedReportsState();
+ const {setActiveTab} = useSidebarOrderedReportsActions();
+
+ if (activeTab === CONST.INBOX_TAB.UNREAD || activeTab === CONST.INBOX_TAB.TODO) {
+ const title = activeTab === CONST.INBOX_TAB.UNREAD ? translate('common.emptyLHN.noUnreadChats') : translate('common.emptyLHN.noTodos');
+ const caughtUpSubtitle = (
+
+ {translate('common.emptyLHN.caughtUp')}
+ setActiveTab(CONST.INBOX_TAB.ALL)}
+ style={[styles.textStrong, styles.mt5, styles.ph4, styles.textAlignCenter]}
+ >
+ {translate('common.emptyLHN.seeAllChats')}
+
+
+ );
+
+ return (
+
+ );
+ }
const subtitle = (
@@ -56,7 +87,7 @@ function LHNEmptyState() {
return (
diff --git a/src/components/LHNOptionsList/OptionRowLHN/OptionRow/InfoBadge.tsx b/src/components/LHNOptionsList/OptionRowLHN/OptionRow/InfoBadge.tsx
index 86d7b6ee160e..b50bc3e269a8 100644
--- a/src/components/LHNOptionsList/OptionRowLHN/OptionRow/InfoBadge.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN/OptionRow/InfoBadge.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Icon from '@components/Icon';
+import getActionBadgeText from '@components/utils/getActionBadgeText';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
@@ -15,9 +16,11 @@ type InfoBadgeProps = {
/** Action badge key used to derive the badge label. */
actionBadge: OptionData['actionBadge'];
+ /** Whether to show the "Mark as Done" state for this row. */
+ isMarkAsDone?: boolean;
};
-function InfoBadge({brickRoadIndicator, actionBadge}: InfoBadgeProps) {
+function InfoBadge({brickRoadIndicator, actionBadge, isMarkAsDone}: InfoBadgeProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -27,7 +30,7 @@ function InfoBadge({brickRoadIndicator, actionBadge}: InfoBadgeProps) {
return null;
}
- const actionBadgeText = actionBadge ? translate(`common.actionBadge.${actionBadge}`) : '';
+ const actionBadgeText = getActionBadgeText(actionBadge, translate, isMarkAsDone);
if (actionBadgeText) {
return (
diff --git a/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Pressable.tsx b/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Pressable.tsx
index 73c86874c118..1e0e3d71643b 100644
--- a/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Pressable.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN/OptionRow/Pressable.tsx
@@ -5,6 +5,7 @@ import Hoverable from '@components/Hoverable';
import {useLHNTooltipContext} from '@components/LHNOptionsList/LHNTooltipContext';
import useLHNRowProductTrainingTooltip from '@components/LHNOptionsList/OptionRowLHN/useLHNRowProductTrainingTooltip';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
+import getActionBadgeText from '@components/utils/getActionBadgeText';
import getContextMenuAccessibilityHint from '@components/utils/getContextMenuAccessibilityHint';
import getContextMenuAccessibilityProps from '@components/utils/getContextMenuAccessibilityProps';
import useLocalize from '@hooks/useLocalize';
@@ -41,9 +42,12 @@ type PressableProps = {
/** Row content. */
children: ReactNode;
+
+ /** Whether to show the "Mark as Done" state for this row. */
+ isMarkAsDone?: boolean;
};
-function Pressable({optionItem, isOptionFocused, onSelectRow, onLayout, onHoverIn, onHoverOut, children}: PressableProps) {
+function Pressable({optionItem, isOptionFocused, onSelectRow, onLayout, onHoverIn, onHoverOut, children, isMarkAsDone}: PressableProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -57,7 +61,7 @@ function Pressable({optionItem, isOptionFocused, onSelectRow, onLayout, onHoverI
const reportID = optionItem.reportID;
const brickRoadIndicator = optionItem.brickRoadIndicator;
- const actionBadgeText = optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : '';
+ const actionBadgeText = getActionBadgeText(optionItem.actionBadge, translate, isMarkAsDone);
let accessibilityLabelForBadge = '';
if (brickRoadIndicator) {
diff --git a/src/components/LHNOptionsList/OptionRowLHN/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN/OptionRowLHN.tsx
index 44e8b25fabe2..c787a7e4548e 100644
--- a/src/components/LHNOptionsList/OptionRowLHN/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN/OptionRowLHN.tsx
@@ -5,7 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import OptionRow from './OptionRow';
import useOptionRowChrome from './useOptionRowChrome';
-function OptionRowLHN({isOptionFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', onLayout = () => {}, hasDraftComment, testID}: OptionRowLHNProps) {
+function OptionRowLHN({isOptionFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', onLayout = () => {}, hasDraftComment, testID, isMarkAsDone}: OptionRowLHNProps) {
const styles = useThemeStyles();
const {setHovered, sidebarInnerRowStyle, contentContainerStyles, avatarBackgroundColor} = useOptionRowChrome({
isOptionFocused,
@@ -27,6 +27,7 @@ function OptionRowLHN({isOptionFocused = false, onSelectRow = () => {}, optionIt
onLayout={onLayout}
onHoverIn={() => setHovered(true)}
onHoverOut={() => setHovered(false)}
+ isMarkAsDone={isMarkAsDone}
>
@@ -55,6 +56,7 @@ function OptionRowLHN({isOptionFocused = false, onSelectRow = () => {}, optionIt
@@ -62,6 +64,7 @@ function OptionRowLHN({isOptionFocused = false, onSelectRow = () => {}, optionIt
;
}
+ const shouldUseMarkAsDone =
+ shouldShowMarkAsDone({
+ report: fullReport,
+ isTrackIntentUser,
+ policy,
+ }) && finalOptionItem?.actionBadge === CONST.REPORT.ACTION_BADGE.SUBMIT;
+
return (
);
}
diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts
index 829de863508a..f8aad940fdb2 100644
--- a/src/components/LHNOptionsList/types.ts
+++ b/src/components/LHNOptionsList/types.ts
@@ -95,6 +95,8 @@ type OptionRowLHNProps = {
/** The testID of the row */
testID: number;
+ /** Whether to show "Mark as done" copy instead of "Submit" copy for track-intent users */
+ isMarkAsDone?: boolean;
};
type RenderItemProps = {item: Report; index: number};
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index a8cfa2e174d0..e55dc0ce878a 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1112,7 +1112,7 @@ function MenuItem({
{/* Since subtitle can be of type number, we should allow 0 to be shown */}
{(subtitle === 0 || !!subtitle) && (
- {subtitle}
+ {subtitle}
)}
{(!!rightIconAccountID || !!rightIconReportID) && (
diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx
index c786227ce0b4..98956832a092 100644
--- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx
+++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx
@@ -1,6 +1,6 @@
import {useRoute} from '@react-navigation/native';
import {delegateEmailSelector, isUserValidatedSelector} from '@selectors/Account';
-import {hasSeenTourSelector} from '@selectors/Onboarding';
+import {hasSeenTourSelector, isTrackIntentUserSelector} from '@selectors/Onboarding';
import truncate from 'lodash/truncate';
import React, {useContext} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
@@ -54,6 +54,7 @@ import {
hasViolations as hasViolationsReportUtils,
isInvoiceReport as isInvoiceReportUtil,
isIOUReport as isIOUReportUtil,
+ shouldShowMarkAsDone,
} from '@libs/ReportUtils';
import shouldPopoverUseScrollView from '@libs/shouldPopoverUseScrollView';
import {isTransactionPendingDelete} from '@libs/TransactionUtils';
@@ -385,7 +386,15 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn
});
};
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ policy,
+ report: moneyRequestReport,
+ isTrackIntentUser,
+ });
const allExpensesSelected = selectedTransactionIDs.length > 0 && selectedTransactionIDs.length === nonPendingDeleteTransactions.length;
+ const submitButtonText = shouldUseMarkAsDoneCopy ? translate('common.markAsDone') : translate('common.submit');
+ const approveButtonText = shouldUseMarkAsDoneCopy ? translate('common.markAsDone') : translate('iou.approve');
// Ref writes below are inside onSelected callbacks that only fire on user interaction, never during render.
@@ -393,7 +402,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn
...(hasSubmitAction && !shouldBlockSubmit
? [
{
- text: translate('common.submit'),
+ text: submitButtonText,
icon: expensifyIcons.Send,
value: CONST.REPORT.PRIMARY_ACTIONS.SUBMIT,
onSelected: () => handleSubmitReport(true),
@@ -403,7 +412,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn
...(hasApproveAction && !isBlockSubmitDueToPreventSelfApproval
? [
{
- text: translate('iou.approve'),
+ text: approveButtonText,
icon: expensifyIcons.ThumbsUp,
value: CONST.REPORT.PRIMARY_ACTIONS.APPROVE,
onSelected: () => confirmApproval(true),
diff --git a/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx
index d4f2ea916bee..688368ada57c 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx
+++ b/src/components/MoneyReportHeaderPrimaryAction/ApprovePrimaryAction.tsx
@@ -1,10 +1,11 @@
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
import Button from '@components/Button';
import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
-import {getNextApproverAccountID, isReportOwner} from '@libs/ReportUtils';
+import {getNextApproverAccountID, isReportOwner, shouldShowMarkAsDone} from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import useConfirmApproval from './useConfirmApproval';
@@ -18,6 +19,13 @@ function ApprovePrimaryAction({reportID}: ApprovePrimaryActionProps) {
const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
+
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: moneyRequestReport,
+ policy,
+ });
const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport);
const isSubmitterSameAsNextApprover =
@@ -30,7 +38,7 @@ function ApprovePrimaryAction({reportID}: ApprovePrimaryActionProps) {
);
diff --git a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx
index f788022b2b1e..c63c326a8356 100644
--- a/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx
+++ b/src/components/MoneyReportHeaderPrimaryAction/SubmitPrimaryAction.tsx
@@ -1,4 +1,5 @@
import {delegateEmailSelector} from '@selectors/Account';
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
import AnimatedSubmitButton from '@components/AnimatedSubmitButton';
import {usePaymentAnimationsContext} from '@components/PaymentAnimationsContext';
@@ -17,7 +18,7 @@ import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViol
import {search} from '@libs/actions/Search';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getFilteredReportActionsForReportView} from '@libs/ReportActionsUtils';
-import {hasViolations as hasViolationsReportUtils, shouldBlockSubmitDueToPreventSelfApproval, shouldBlockSubmitDueToStrictPolicyRules} from '@libs/ReportUtils';
+import {hasViolations as hasViolationsReportUtils, shouldBlockSubmitDueToPreventSelfApproval, shouldBlockSubmitDueToStrictPolicyRules, shouldShowMarkAsDone} from '@libs/ReportUtils';
import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasOnlyPendingCardTransactions, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils';
import {submitReport} from '@userActions/IOU/ReportWorkflow';
import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
@@ -44,6 +45,7 @@ function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) {
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
@@ -70,6 +72,11 @@ function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) {
transactions,
);
const shouldBlockSubmit = isBlockSubmitDueToStrictPolicyRules || isBlockSubmitDueToPreventSelfApproval;
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ policy,
+ report: moneyRequestReport,
+ isTrackIntentUser,
+ });
const {currentSearchQueryJSON, currentSearchKey} = useSearchQueryContext();
const {currentSearchResults} = useSearchResultsContext();
@@ -116,7 +123,8 @@ function SubmitPrimaryAction({reportID}: SubmitPrimaryActionProps) {
return (
void;
+ onBackButtonPress: (prioritizeBackTo?: boolean, options?: {afterTransition?: () => void}) => void;
};
function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: MoneyRequestHeaderSecondaryActionsProps) {
@@ -444,6 +441,8 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
}
const backToRoute = route.params?.backTo ?? Navigation.getActiveRoute();
setDeleteTransactionNavigateBackUrl(backToRoute);
+
+ let afterDelete: (() => void) | undefined;
if (isTrackExpenseAction(parentReportAction) && !isExpenseSplit) {
deleteTrackExpense({
chatReportID: report?.parentReportID,
@@ -466,8 +465,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true);
return;
}
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
+ afterDelete = () => {
const deleteResult = deleteTransactions(
[transaction.transactionID],
duplicateTransactions,
@@ -481,14 +479,19 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
}
removeTransaction(transaction.transactionID);
- });
+ };
}
if (isInNarrowPaneModal) {
- Navigation.navigateBackToLastSuperWideRHPScreen();
+ // The super wide RHP close animation was changed, and because of that the report was deleted right after
+ // the animation finished, which caused flickering in the reports list. We want the user to see
+ // the expense in the list for a little bit longer, so we wait for the animation to finish and then
+ // add an additional delay before removing it.
+ // See https://github.com/Expensify/App/issues/92036
+ Navigation.navigateBackToLastSuperWideRHPScreen({afterTransition: () => setTimeout(() => afterDelete?.(), CONST.EXPENSE_REPORT_DELETE_DELAY_MS)});
return;
}
- onBackButtonPress();
+ onBackButtonPress(false, {afterTransition: afterDelete});
});
},
},
@@ -522,15 +525,13 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
const iouType = isExpenseReport(parentReport) ? CONST.IOU.TYPE.SUBMIT : CONST.IOU.TYPE.TRACK;
if (shouldNavigateToUpgradePath && reportID) {
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
- action: CONST.IOU.ACTION.EDIT,
- iouType,
- transactionID: transaction.transactionID,
- reportID,
- upgradePath: CONST.UPGRADE_PATHS.REPORTS,
- }),
- ),
+ ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
+ action: CONST.IOU.ACTION.EDIT,
+ iouType,
+ transactionID: transaction.transactionID,
+ reportID,
+ upgradePath: CONST.UPGRADE_PATHS.REPORTS,
+ }),
);
return;
}
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
index 333261612976..db0495c0b480 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
@@ -1,5 +1,6 @@
/* eslint-disable rulesdir/prefer-early-return */
import {useIsFocused, useRoute} from '@react-navigation/native';
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import isEmpty from 'lodash/isEmpty';
import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {LayoutChangeEvent, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
@@ -42,7 +43,7 @@ import {
isReportActionVisible,
wasMessageReceivedWhileOffline,
} from '@libs/ReportActionsUtils';
-import {canUserPerformWriteAction, chatIncludesChronosWithID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils';
+import {canUserPerformWriteAction, chatIncludesChronosWithID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread, shouldShowMarkAsDone} from '@libs/ReportUtils';
import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import Visibility from '@libs/Visibility';
@@ -148,6 +149,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
const shouldShowHarvestCreatedAction = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID);
const [enableScrollToEnd, setEnableScrollToEnd] = useState(false);
const [lastActionEventId, setLastActionEventId] = useState('');
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
// We are reversing actions because in this View we are starting at the top and don't use Inverted list
const visibleReportActions = useMemo(() => {
@@ -657,6 +659,12 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
return null;
}
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ policy,
+ report,
+ isTrackIntentUser,
+ });
+
return (
{/* Exactly one of these two branches is active at a time:
1. showEmptyState — genuinely empty report
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index 9aa36783c1a3..8b8f8948b3e0 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -20,7 +20,6 @@ import SelectionList from '@components/SelectionList';
import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import Text from '@components/Text';
-import {useWideRHPActions} from '@components/WideRHPContextProvider';
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
@@ -28,6 +27,7 @@ import useHandleSelectionMode from '@hooks/useHandleSelectionMode';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
+import useNavigateToTransactionThread from '@hooks/useNavigateToTransactionThread';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useReportIsArchived from '@hooks/useReportIsArchived';
@@ -38,14 +38,13 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
-import {setOptimisticTransactionThread} from '@libs/actions/Report';
import {getReportLayoutGroupBy, getReportLayoutSelection, setReportLayout} from '@libs/actions/ReportLayout';
import {clearActiveTransactionIDs, setActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
import {resolveTransactionCardFields} from '@libs/CardUtils';
import {hasNonReimbursableTransactions, isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
import {navigationRef} from '@libs/Navigation/Navigation';
import {isPolicyTaxEnabled} from '@libs/PolicyUtils';
-import {getIOUActionForTransactionID, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils';
import {
canAddTransaction,
@@ -68,9 +67,7 @@ import {getTransactionPendingAction, getVisibleTransactionViolations, isTransact
import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear';
import isReportOpenInSuperWideRHP from '@navigation/helpers/isReportOpenInSuperWideRHP';
import Navigation from '@navigation/Navigation';
-import type {ReportsSplitNavigatorParamList} from '@navigation/types';
import variables from '@styles/variables';
-import {createTransactionThreadReport} from '@userActions/Report';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import NAVIGATORS from '@src/NAVIGATORS';
@@ -151,8 +148,6 @@ type TransactionWithOptionalHighlight = OnyxTypes.Transaction & {
shouldBeHighlighted?: boolean;
};
-type ReportScreenNavigationProps = ReportsSplitNavigatorParamList[typeof SCREENS.REPORT];
-
type SortedTransactions = {
sortBy: SortableColumnName;
sortOrder: SortOrder;
@@ -180,7 +175,7 @@ function MoneyRequestReportTransactionList({
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth, isMediumScreenWidth, isInLandscapeMode} = useResponsiveLayout();
const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
- const {markReportIDAsExpense} = useWideRHPActions();
+ const navigateToTransactionThread = useNavigateToTransactionThread();
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedTransactionID, setSelectedTransactionID] = useState('');
const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report);
@@ -206,8 +201,6 @@ function MoneyRequestReportTransactionList({
const [reportLayoutOption] = useOnyx(ONYXKEYS.NVP_REPORT_LAYOUT_OPTION);
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
const [reportDetailsColumns] = useOnyx(ONYXKEYS.NVP_REPORT_DETAILS_COLUMNS);
- const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
- const [betas] = useOnyx(ONYXKEYS.BETAS);
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST);
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});
@@ -554,44 +547,15 @@ function MoneyRequestReportTransactionList({
*/
const navigateToTransaction = useCallback(
(activeTransactionID: string) => {
- const iouAction = getIOUActionForTransactionID(reportActions, activeTransactionID);
- const backTo = Navigation.getActiveRoute();
- let reportIDToNavigate = iouAction?.childReportID;
-
- const routeParams = {
- reportID: reportIDToNavigate,
- backTo,
- } as ReportScreenNavigationProps;
-
- if (!reportIDToNavigate) {
- const transaction = sortedTransactions.find((t) => t.transactionID === activeTransactionID);
- const transactionThreadReport = createTransactionThreadReport({
- introSelected,
- currentUserLogin: currentUserDetails.email ?? '',
- currentUserAccountID: currentUserDetails.accountID,
- betas,
- iouReport: report,
- iouReportAction: iouAction,
- transaction,
- });
- if (transactionThreadReport) {
- reportIDToNavigate = transactionThreadReport.reportID;
- routeParams.reportID = reportIDToNavigate;
- }
- } else {
- setOptimisticTransactionThread(reportIDToNavigate, report?.reportID, iouAction?.reportActionID, report?.policyID);
- }
-
- // Single transaction report will open in RHP, and we need to find every other report ID for the rest of transactions
- // to display prev/next arrows in RHP for navigation - use visual order from grouped transactions
- setActiveTransactionIDs(visualOrderTransactionIDs).then(() => {
- if (reportIDToNavigate) {
- markReportIDAsExpense(reportIDToNavigate);
- }
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
+ navigateToTransactionThread({
+ transactionID: activeTransactionID,
+ reportActions,
+ report,
+ transaction: sortedTransactions.find((t) => t.transactionID === activeTransactionID),
+ siblingTransactionIDs: visualOrderTransactionIDs,
});
},
- [reportActions, visualOrderTransactionIDs, sortedTransactions, report, markReportIDAsExpense, introSelected, betas, currentUserDetails.email, currentUserDetails.accountID],
+ [navigateToTransactionThread, reportActions, sortedTransactions, report, visualOrderTransactionIDs],
);
const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => {
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
index b4c41f0352ed..5c0d06ffb66c 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
@@ -93,7 +93,8 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
};
}, [nextTransactionID, parentReportActions, prevTransactionID, transactionIDsList]);
- const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentTransaction?.reportID}`);
+ const [prevParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${prevTransaction?.reportID}`);
+ const [nextParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nextTransaction?.reportID}`);
const [prevThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${prevParentReportAction?.childReportID}`);
const [nextThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nextParentReportAction?.childReportID}`);
@@ -132,7 +133,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
}
// We know that the next thread report exists, it just wasn't fetched to Onyx yet, so we set it optimistically.
if (!nextThreadReport && nextThreadReportID) {
- setOptimisticTransactionThread(nextThreadReportID, parentReport?.reportID, nextParentReportAction?.reportActionID, parentReport?.policyID);
+ setOptimisticTransactionThread(nextThreadReportID, nextParentReport?.reportID, nextParentReportAction?.reportActionID, nextParentReport?.policyID);
}
// The transaction thread doesn't exist yet, so we should create it
if (!nextThreadReportID) {
@@ -141,7 +142,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
currentUserLogin: currentUserEmail ?? '',
currentUserAccountID,
betas,
- iouReport: parentReport,
+ iouReport: nextParentReport,
iouReportAction: nextParentReportAction,
transaction: nextTransaction,
});
@@ -168,7 +169,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
}
// We know that the previous thread report exists, it just wasn't fetched to Onyx yet, so we set it optimistically.
if (!prevThreadReport && prevThreadReportID) {
- setOptimisticTransactionThread(prevThreadReportID, parentReport?.reportID, prevParentReportAction?.reportActionID, parentReport?.policyID);
+ setOptimisticTransactionThread(prevThreadReportID, prevParentReport?.reportID, prevParentReportAction?.reportActionID, prevParentReport?.policyID);
}
// The transaction thread doesn't exist yet, so we should create it
if (!prevThreadReportID) {
@@ -177,7 +178,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
currentUserLogin: currentUserEmail ?? '',
currentUserAccountID,
betas,
- iouReport: parentReport,
+ iouReport: prevParentReport,
iouReportAction: prevParentReportAction,
transaction: prevTransaction,
});
diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
index 4eb2be1a3425..73d105909a67 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
@@ -15,8 +15,8 @@ import {
getFieldViolationTranslation,
getReportFieldKey,
getReportFieldMaps,
+ isGroupPolicyExpenseReport as isGroupPolicyExpenseReportUtils,
isInvoiceReport as isInvoiceReportUtils,
- isPaidGroupPolicyExpenseReport as isPaidGroupPolicyExpenseReportUtils,
isReportFieldDisabled,
isReportFieldDisabledForUser,
isReportFieldOfTypeTitle,
@@ -119,11 +119,10 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false,
(reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA,
);
const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0));
- const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report);
+ const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report);
const isInvoiceReport = isInvoiceReportUtils(report);
- const areFieldsEnabledForReport = isInvoiceReport ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled;
- const shouldDisplayReportFields = (isPaidGroupPolicyExpenseReport || isInvoiceReport) && !!areFieldsEnabledForReport && (!isOnlyTitleFieldEnabled || !isCombinedReport);
+ const shouldDisplayReportFields = (isGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled && (!isOnlyTitleFieldEnabled || !isCombinedReport);
if (!shouldDisplayReportFields || !sortedPolicyReportFields.length) {
return null;
diff --git a/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx b/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx
new file mode 100644
index 000000000000..7f415b5060ae
--- /dev/null
+++ b/src/components/Navigation/NavigationTabBar/InboxTabButton.tsx
@@ -0,0 +1,201 @@
+import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import {PressableWithFeedback} from '@components/Pressable';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useRootNavigationState from '@hooks/useRootNavigationState';
+import {useSidebarOrderedReportsState} from '@hooks/useSidebarOrderedReports';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import navigationRef from '@libs/Navigation/navigationRef';
+import {isDeletedAction} from '@libs/ReportActionsUtils';
+import {startSpan} from '@libs/telemetry/activeSpans';
+import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import type {Report, ReportActions} from '@src/types/onyx';
+import getLastRoute from './getLastRoute';
+import NAVIGATION_TABS from './NAVIGATION_TABS';
+import TabBarItem from './TabBarItem';
+
+function getStringParam(params: unknown, key: string): string | undefined {
+ if (!params || typeof params !== 'object') {
+ return undefined;
+ }
+ for (const [k, v] of Object.entries(params)) {
+ if (k === key && typeof v === 'string') {
+ return v;
+ }
+ }
+ return undefined;
+}
+
+function startNavigateToInboxTabSpan() {
+ startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, {
+ name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB,
+ op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB,
+ });
+}
+
+type InboxTabButtonProps = {
+ selectedTab: ValueOf;
+ isWideLayout: boolean;
+};
+
+function doesLastReportExistSelector(report: OnyxEntry) {
+ return !!report?.reportID;
+}
+
+function makeDoesLastReportActionExistSelector(actionID: string | undefined) {
+ return (reportActions: OnyxEntry) => {
+ const reportAction = actionID ? reportActions?.[actionID] : undefined;
+ return !!reportAction && !isDeletedAction(reportAction);
+ };
+}
+
+type WideInboxTabButtonProps = {
+ selectedTab: ValueOf;
+ statusIndicatorColor: string | undefined;
+ accessibilityLabel: string;
+};
+
+// The last-viewed report deep link only exists in the wide layout, so the report and report-action
+// Onyx subscriptions live here and are only created when the wide layout is rendered. In the narrow
+// layout tapping Inbox always routes to ROUTES.INBOX, so these subscriptions are never set up.
+function WideInboxTabButton({selectedTab, statusIndicatorColor, accessibilityLabel}: WideInboxTabButtonProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Inbox']);
+
+ const lastReportRouteReportID = useRootNavigationState((rootState) => {
+ if (!rootState) {
+ return undefined;
+ }
+ const route = getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT);
+ return getStringParam(route?.params, 'reportID');
+ });
+
+ const lastReportRouteReportActionID = useRootNavigationState((rootState) => {
+ if (!rootState) {
+ return undefined;
+ }
+ const route = getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT);
+ return getStringParam(route?.params, 'reportActionID');
+ });
+
+ const [doesLastReportExist] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${lastReportRouteReportID}`, {selector: doesLastReportExistSelector}, [lastReportRouteReportID]);
+
+ const doesLastReportActionExistSelector = makeDoesLastReportActionExistSelector(lastReportRouteReportActionID);
+ const [doesLastReportActionExist] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${lastReportRouteReportID}`, {selector: doesLastReportActionExistSelector}, [
+ lastReportRouteReportID,
+ lastReportRouteReportActionID,
+ ]);
+
+ const navigateToChats = () => {
+ if (selectedTab === NAVIGATION_TABS.INBOX) {
+ return;
+ }
+
+ startNavigateToInboxTabSpan();
+
+ if (doesLastReportExist) {
+ // Fetch route params on-demand to avoid storing the full route object in render-time state
+ const rootState = navigationRef.getRootState();
+ const lastRoute = rootState ? getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT) : undefined;
+ if (lastRoute) {
+ const reportID = getStringParam(lastRoute.params, 'reportID');
+ const reportActionID = getStringParam(lastRoute.params, 'reportActionID');
+ const referrer = getStringParam(lastRoute.params, 'referrer');
+ const backTo = getStringParam(lastRoute.params, 'backTo');
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, doesLastReportActionExist ? reportActionID : undefined, referrer, backTo));
+ return;
+ }
+ }
+
+ Navigation.navigate(ROUTES.INBOX);
+ };
+
+ return (
+ [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
+ sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX}
+ >
+ {({hovered}) => (
+
+ )}
+
+ );
+}
+
+function InboxTabButton({selectedTab, isWideLayout}: InboxTabButtonProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const {chatTabBrickRoad} = useSidebarOrderedReportsState();
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Inbox']);
+
+ let statusIndicatorColor: string | undefined;
+ if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
+ statusIndicatorColor = theme.iconSuccessFill;
+ } else if (chatTabBrickRoad) {
+ statusIndicatorColor = theme.danger;
+ }
+
+ const accessibilityLabel = chatTabBrickRoad ? `${translate('common.inbox')}. ${translate('common.yourReviewIsRequired')}` : translate('common.inbox');
+
+ if (isWideLayout) {
+ return (
+
+ );
+ }
+
+ const navigateToChats = () => {
+ if (selectedTab === NAVIGATION_TABS.INBOX) {
+ return;
+ }
+
+ startNavigateToInboxTabSpan();
+ Navigation.navigate(ROUTES.INBOX);
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default InboxTabButton;
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index b6408aa3b02d..12701cdab400 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import FloatingCameraButton from '@components/FloatingCameraButton';
import FloatingGPSButton from '@components/FloatingGPSButton';
@@ -11,26 +10,16 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useRootNavigationState from '@hooks/useRootNavigationState';
-import {useSidebarOrderedReportsState} from '@hooks/useSidebarOrderedReports';
import useStyleUtils from '@hooks/useStyleUtils';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
-import navigationRef from '@libs/Navigation/navigationRef';
-import {isDeletedAction} from '@libs/ReportActionsUtils';
-import {startSpan} from '@libs/telemetry/activeSpans';
-import type {ReportsSplitNavigatorParamList} from '@navigation/types';
import NavigationTabBarAvatar from '@pages/inbox/sidebar/NavigationTabBarAvatar';
import NavigationTabBarFloatingActionButton from '@pages/inbox/sidebar/NavigationTabBarFloatingActionButton';
import CONST from '@src/CONST';
-import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
-import type {Report, ReportActions} from '@src/types/onyx';
-import getLastRoute from './getLastRoute';
+import InboxTabButton from './InboxTabButton';
import NAVIGATION_TABS from './NAVIGATION_TABS';
import SearchTabButton from './SearchTabButton';
import TabBarItem from './TabBarItem';
@@ -41,55 +30,16 @@ type NavigationTabBarProps = {
shouldShowFloatingButtons?: boolean;
};
-function doesLastReportExistSelector(report: OnyxEntry) {
- return !!report?.reportID;
-}
-
function NavigationTabBar({selectedTab, shouldShowFloatingButtons = true}: NavigationTabBarProps) {
- const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {chatTabBrickRoad} = useSidebarOrderedReportsState();
const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED);
- const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Home', 'Inbox']);
-
- const lastReportRouteReportID = useRootNavigationState((rootState) => {
- if (!rootState) {
- return undefined;
- }
- const route = getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT);
- return (route?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT])?.reportID;
- });
-
- const lastReportRouteReportActionID = useRootNavigationState((rootState) => {
- if (!rootState) {
- return undefined;
- }
- const route = getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT);
- return (route?.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT])?.reportActionID;
- });
-
- const [doesLastReportExist] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${lastReportRouteReportID}`, {selector: doesLastReportExistSelector}, [lastReportRouteReportID]);
-
- const doesLastReportActionExistSelector = (reportActions: OnyxEntry) => {
- const reportAction = lastReportRouteReportActionID ? reportActions?.[lastReportRouteReportActionID] : undefined;
- return !!reportAction && !isDeletedAction(reportAction);
- };
- const [doesLastReportActionExist] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${lastReportRouteReportID}`, {selector: doesLastReportActionExistSelector});
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Home']);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const StyleUtils = useStyleUtils();
- let inboxStatusIndicatorColor: string | undefined;
- if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
- inboxStatusIndicatorColor = theme.iconSuccessFill;
- } else if (chatTabBrickRoad) {
- inboxStatusIndicatorColor = theme.danger;
- }
-
- const inboxAccessibilityState = {selected: selectedTab === NAVIGATION_TABS.INBOX};
-
const navigateToNewDotHome = () => {
if (selectedTab === NAVIGATION_TABS.HOME) {
return;
@@ -97,30 +47,6 @@ function NavigationTabBar({selectedTab, shouldShowFloatingButtons = true}: Navig
Navigation.navigate(ROUTES.HOME);
};
- const navigateToChats = () => {
- if (selectedTab === NAVIGATION_TABS.INBOX) {
- return;
- }
-
- startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, {
- name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB,
- op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB,
- });
-
- if (!shouldUseNarrowLayout && doesLastReportExist) {
- // Fetch route params on-demand to avoid storing the full route object in render-time state
- const rootState = navigationRef.getRootState();
- const lastRoute = rootState ? getLastRoute(rootState, NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, SCREENS.REPORT) : undefined;
- if (lastRoute) {
- const {reportID, reportActionID, referrer, backTo} = lastRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT];
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, doesLastReportActionExist ? reportActionID : undefined, referrer, backTo));
- return;
- }
- }
-
- Navigation.navigate(ROUTES.INBOX);
- };
-
const navigateToSettings = () => {
if (selectedTab === NAVIGATION_TABS.SETTINGS) {
return;
@@ -174,24 +100,10 @@ function NavigationTabBar({selectedTab, shouldShowFloatingButtons = true}: Navig
/>
)}
- [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
- sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX}
- >
- {({hovered}) => (
-
- )}
-
+
-
-
-
+
}
{!isClosedExpenseReportWithNoExpenses && (
<>
- {(isPaidGroupPolicyExpenseReport || isInvoiceReport) &&
- areFieldsEnabledForReport &&
+ {(isGroupPolicyExpenseReport || isInvoiceReport) &&
+ !!policy?.areReportFieldsEnabled &&
(!isCombinedReport || !isOnlyTitleFieldEnabled) &&
sortedPolicyReportFields.map((reportField) => {
if (shouldHideSingleReportField(reportField)) {
diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
index 8a635a558816..36129c2e7247 100644
--- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
@@ -51,7 +51,7 @@ import {
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
getCreationReportErrors,
isInvoiceReport,
- isPaidGroupPolicy,
+ isReportInGroupPolicy,
isTrackExpenseReportNew,
} from '@libs/ReportUtils';
import trackExpenseCreationError from '@libs/telemetry/trackExpenseCreationError';
@@ -347,7 +347,7 @@ function MoneyRequestReceiptView({
!isTransactionScanning &&
(hasReceipt || !!receiptRequiredViolation || !!itemizedReceiptRequiredViolation || !!customRulesViolation) &&
!!(receiptViolations.length || didReceiptScanSucceed) &&
- isPaidGroupPolicy(report);
+ isReportInGroupPolicy(report);
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt || hasReceiptUploadError);
const fallbackReceiptError = useMemo(() => {
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/SubmitActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/SubmitActionButton.tsx
index c9d7b8684d70..dc4acd7818b0 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/SubmitActionButton.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/SubmitActionButton.tsx
@@ -1,4 +1,5 @@
import {delegateEmailSelector} from '@selectors/Account';
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
import AnimatedSubmitButton from '@components/AnimatedSubmitButton';
import useConfirmModal from '@hooks/useConfirmModal';
@@ -9,7 +10,7 @@ import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
-import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
+import {hasViolations as hasViolationsReportUtils, shouldShowMarkAsDone} from '@libs/ReportUtils';
import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasOnlyPendingCardTransactions, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils';
import {submitReport} from '@userActions/IOU/ReportWorkflow';
import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
@@ -40,6 +41,7 @@ function SubmitActionButton({iouReportID, isSubmittingAnimationRunning, stopAnim
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`);
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
const {isOffline} = useNetwork();
const reportTransactionsCollection = useReportTransactionsCollection(iouReportID);
@@ -56,11 +58,16 @@ function SubmitActionButton({iouReportID, isSubmittingAnimationRunning, stopAnim
};
const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash);
-
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: iouReport,
+ policy,
+ });
return (
{
if (hasOnlyPendingCardTransactions(transactions)) {
showPendingCardTransactionsBlockModal(showConfirmModal, translate);
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 3f569d589e51..2777962ff331 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -57,7 +57,6 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getRateFromMerchant} from '@libs/MergeTransactionUtils';
import {isBillableEnabledOnPolicy, isSingleTransactionReport} from '@libs/MoneyRequestReportUtils';
-import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import {hasEnabledOptions} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {
@@ -65,8 +64,10 @@ import {
getLengthOfTag,
getPerDiemCustomUnit,
getPolicyByCustomUnitID,
+ getQBOVendorByID,
getTagLists,
hasDependentTags as hasDependentTagsPolicyUtils,
+ hasVendorFeature,
isAttendeeTrackingEnabled,
isGroupPolicyByType,
isPolicyAccessible,
@@ -85,8 +86,8 @@ import {
isExpenseReport,
isInvoiceReport,
isOpenReport,
- isPaidGroupPolicy,
isReportApproved,
+ isReportInGroupPolicy,
isSettled as isSettledReportUtils,
isTrackExpenseReportNew,
shouldEnableNegative,
@@ -131,7 +132,7 @@ import AnimatedEmptyStateBackground from '@pages/inbox/report/AnimatedEmptyState
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -461,6 +462,10 @@ function MoneyRequestView({
});
const shouldShowAttendees = shouldShowAttendeesTransactionUtils(iouType, policy);
+ const transactionVendor = transaction?.comment?.vendor;
+ const transactionVendorName = getQBOVendorByID(policy, transactionVendor?.externalID)?.name ?? '';
+ const shouldShowVendor = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING)) && !(updatedTransaction?.reimbursable ?? !!transactionReimbursable) && !isInvoice;
+
const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID);
const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID;
@@ -488,7 +493,7 @@ function MoneyRequestView({
const tripRoomName = tripRoomInfo?.name;
const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName;
- const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isPaidGroupPolicy(transactionThreadReport));
+ const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isReportInGroupPolicy(transactionThreadReport));
const hasViolations = (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean =>
getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0;
const isMarkAsCash = parentReport && currentUserEmailParam ? isMarkAsCashActionForTransaction(currentUserEmailParam, parentReport, transactionViolations, policy) : false;
@@ -815,15 +820,13 @@ function MoneyRequestView({
if (isTrackExpense) {
if (shouldNavigateToUpgradePath && transactionThreadReport) {
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
- action: CONST.IOU.ACTION.EDIT,
- iouType,
- transactionID: transaction.transactionID,
- reportID: transactionThreadReport?.reportID,
- upgradePath: CONST.UPGRADE_PATHS.DISTANCE_RATES,
- }),
- ),
+ ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
+ action: CONST.IOU.ACTION.EDIT,
+ iouType,
+ transactionID: transaction.transactionID,
+ reportID: transactionThreadReport?.reportID,
+ upgradePath: CONST.UPGRADE_PATHS.DISTANCE_RATES,
+ }),
);
return;
}
@@ -1121,33 +1124,41 @@ function MoneyRequestView({
onPress={() => {
if (shouldNavigateToUpgradePath && transactionThreadReport) {
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
- action: CONST.IOU.ACTION.EDIT,
+ ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
+ action: CONST.IOU.ACTION.EDIT,
+ iouType,
+ transactionID: transaction.transactionID,
+ reportID: transactionThreadReport?.reportID,
+ upgradePath: CONST.UPGRADE_PATHS.CATEGORIES,
+ backTo: ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.EDIT,
iouType,
- transactionID: transaction.transactionID,
- reportID: transactionThreadReport?.reportID,
- upgradePath: CONST.UPGRADE_PATHS.CATEGORIES,
- }),
- ),
+ transaction.transactionID,
+ transactionThreadReport?.reportID,
+ Navigation.getActiveRoute(),
+ ),
+ }),
);
} else if (!policy && shouldSelectPolicy) {
Navigation.navigate(
ROUTES.SET_DEFAULT_WORKSPACE.getRoute(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction.transactionID,
- transactionThreadReport?.reportID,
- ),
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction.transactionID,
+ transactionThreadReport?.reportID,
+ Navigation.getActiveRoute(),
),
),
);
} else {
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID),
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction.transactionID,
+ transactionThreadReport?.reportID,
+ Navigation.getActiveRoute(),
),
);
}
@@ -1159,6 +1170,26 @@ function MoneyRequestView({
/>
)}
+ {shouldShowVendor && (
+
+ {
+ if (!transactionThreadReport?.reportID) {
+ return;
+ }
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_VENDOR.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport.reportID));
+ }}
+ brickRoadIndicator={getErrorForField('vendor') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ errorText={getErrorForField('vendor')}
+ />
+
+ )}
{shouldShowTag && tagList}
{!!shouldShowCard && (
@@ -1189,8 +1220,12 @@ function MoneyRequestView({
}
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID),
+ ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction.transactionID,
+ transactionThreadReport?.reportID,
+ getReportRHPActiveRoute(),
),
);
}}
@@ -1217,8 +1252,12 @@ function MoneyRequestView({
}
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID),
+ ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction.transactionID,
+ transactionThreadReport?.reportID,
+ getReportRHPActiveRoute(),
),
);
}}
@@ -1254,11 +1293,7 @@ function MoneyRequestView({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
- Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID),
- ),
- );
+ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID));
}}
brickRoadIndicator={getErrorForField('attendees') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={getErrorForField('attendees')}
@@ -1339,15 +1374,13 @@ function MoneyRequestView({
}
if (shouldNavigateToUpgradePath) {
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
- iouType,
- action: CONST.IOU.ACTION.EDIT,
- transactionID: transaction?.transactionID,
- reportID: transactionThreadReport?.reportID,
- upgradePath: CONST.UPGRADE_PATHS.REPORTS,
- }),
- ),
+ ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
+ iouType,
+ action: CONST.IOU.ACTION.EDIT,
+ transactionID: transaction?.transactionID,
+ reportID: transactionThreadReport?.reportID,
+ upgradePath: CONST.UPGRADE_PATHS.REPORTS,
+ }),
);
return;
}
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index 4ba34b4a3501..09ebc1a84af4 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -1,6 +1,8 @@
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
@@ -55,6 +57,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`);
const isReportArchived = useReportIsArchived(report?.reportID);
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const isConciergeChat = isConciergeChatReport(report, conciergeReportID);
const isChatRoom = isChatRoomReportUtils(report);
const isSelfDM = isSelfDMReportUtils(report);
@@ -65,6 +68,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
const participantAccountIDs = getParticipantsAccountIDsForDisplay(report, undefined, true, true, reportMetadata);
const moneyRequestOptions = temporary_getMoneyRequestOptions(report, policy, participantAccountIDs, betas, isReportArchived, isRestrictedToPreferredPolicy);
const policyName = getPolicyName({report});
+ const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const filteredOptions = moneyRequestOptions.filter(
(
@@ -125,6 +129,8 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
reportDetailsLink,
shouldShowUsePlusButtonText,
additionalText,
+ isTrackIntentUser: !!isTrackIntentUser,
+ currentUserAccountID,
});
return (
diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx
index f051497606d4..d85691307f2e 100644
--- a/src/components/ScrollOffsetContextProvider.tsx
+++ b/src/components/ScrollOffsetContextProvider.tsx
@@ -61,21 +61,25 @@ function getKey(route: PlatformStackRouteProp | NavigationPartial
function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProps) {
const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
+ const [inboxTab] = useOnyx(ONYXKEYS.NVP_INBOX_TAB);
const scrollOffsetsRef = useRef>({});
const previousPriorityMode = usePrevious(priorityMode);
+ const previousInboxTab = usePrevious(inboxTab);
useEffect(() => {
- if (previousPriorityMode === null || previousPriorityMode === priorityMode) {
+ const priorityModeChanged = previousPriorityMode !== null && previousPriorityMode !== priorityMode;
+ const inboxTabChanged = previousInboxTab !== null && previousInboxTab !== inboxTab;
+ if (!priorityModeChanged && !inboxTabChanged) {
return;
}
- // If the priority mode changes, we need to clear the scroll offsets for the home and search screens because it affects the size of the elements and scroll positions wouldn't be correct.
+ // If the priority mode or inbox tab changes, we need to clear the scroll offsets for the home and search screens because it affects the size of the elements and scroll positions wouldn't be correct.
for (const key of Object.keys(scrollOffsetsRef.current)) {
if (key.includes(SCREENS.INBOX) || key.includes(SCREENS.SEARCH.ROOT)) {
delete scrollOffsetsRef.current[key];
}
}
- }, [priorityMode, previousPriorityMode]);
+ }, [priorityMode, previousPriorityMode, inboxTab, previousInboxTab]);
const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => {
scrollOffsetsRef.current[getKey(route)] = scrollOffset;
diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx
index b2cb1b5f5293..634925cab1f1 100644
--- a/src/components/Search/SearchAutocompleteList.tsx
+++ b/src/components/Search/SearchAutocompleteList.tsx
@@ -68,8 +68,9 @@ type SearchAutocompleteListProps = {
/** Whether to subscribe to KeyboardShortcut arrow keys events */
shouldSubscribeToArrowKeyEvents?: boolean;
- /** Callback to highlight (e.g. scroll to) the first matched item in the list. */
- onHighlightFirstItem?: () => void;
+ /** Whether to highlight the first matched result so Enter selects it. Only the SearchRouter (Cmd+K) uses this;
+ * the search page input keeps focus on the search-query row to match production behavior. */
+ shouldHighlightFirstItem?: boolean;
/** Ref for the external text input */
textInputRef?: RefObject;
@@ -145,7 +146,7 @@ function SearchAutocompleteList({
getAdditionalSections,
onListItemPress,
shouldSubscribeToArrowKeyEvents = true,
- onHighlightFirstItem,
+ shouldHighlightFirstItem = false,
textInputRef,
autocompleteSubstitutions,
ref,
@@ -273,8 +274,8 @@ function SearchAutocompleteList({
// effect can re-fire and correctly focus the first focusable item (skipping section headers).
hasSetInitialFocusRef.current = false;
} else {
- // When query changes to a non-empty value, focus on the search query item (index 0) and scroll to top
- // onHighlightFirstItem will switch focus to the first result when there's a good match
+ // When query changes to a non-empty value, focus on the search query item (index 0) and scroll to top.
+ // The highlight effect below switches focus to the first result when there's a good match.
innerListRef.current?.updateAndScrollToFocusedIndex(0, true);
}
}
@@ -570,8 +571,6 @@ function SearchAutocompleteList({
reports,
]);
- const sectionItemText = sections?.at(1)?.data?.[0]?.text ?? '';
- const normalizedReferenceText = sectionItemText.toLowerCase();
const trimmedAutocompleteQueryValue = autocompleteQueryValue.trim();
const isLoading = !isRecentSearchesDataLoaded;
const suggestionsAnnouncement = suggestionsCount > 0 ? translate('search.suggestionsAvailable', {count: suggestionsCount}, trimmedAutocompleteQueryValue) : '';
@@ -581,29 +580,37 @@ function SearchAutocompleteList({
const shouldAnnounceNoResults = !isLoading && suggestionsCount === 0 && !!trimmedAutocompleteQueryValue;
useDebouncedAccessibilityAnnouncement(noResultsFoundText, shouldAnnounceNoResults, autocompleteQueryValue);
- const firstRecentReportKey = styledRecentReports.at(0)?.keyForList;
+ // Locate the first recent report row in the order it is actually rendered. The two-section switcher sorts the
+ // local "Recent chats" rows by a frozen rank, so the rendered order can differ from styledRecentReports (the
+ // unsorted combined local + server list). Walking sections keeps the focused row, its reference text, and the
+ // initially focused key all pointing at the first row the user actually sees.
+ const recentReportKeys = new Set(styledRecentReports.map((report) => report.keyForList));
+ let firstRecentReportKey: string | undefined;
+ let firstRecentReportText = '';
let firstRecentReportFlatIndex = -1;
- if (firstRecentReportKey) {
- let flatIndex = 0;
- for (const section of sections) {
- const hasData = (section.data?.length ?? 0) > 0;
- const hasHeader = hasData && (section.title !== undefined || ('customHeader' in section && section.customHeader !== undefined));
- if (hasHeader) {
- flatIndex++;
- }
- for (const item of section.data ?? []) {
- if (item.keyForList === firstRecentReportKey) {
- firstRecentReportFlatIndex = flatIndex;
- break;
- }
- flatIndex++;
- }
- if (firstRecentReportFlatIndex !== -1) {
+ let flatIndex = 0;
+ for (const section of sections) {
+ const hasData = (section.data?.length ?? 0) > 0;
+ const hasHeader = hasData && (section.title !== undefined || ('customHeader' in section && section.customHeader !== undefined));
+ if (hasHeader) {
+ flatIndex++;
+ }
+ for (const item of section.data ?? []) {
+ if (item.keyForList && recentReportKeys.has(item.keyForList)) {
+ firstRecentReportKey = item.keyForList;
+ firstRecentReportText = item.text ?? '';
+ firstRecentReportFlatIndex = flatIndex;
break;
}
+ flatIndex++;
+ }
+ if (firstRecentReportFlatIndex !== -1) {
+ break;
}
}
+ const normalizedReferenceText = firstRecentReportText.toLowerCase();
+
// When options initialize after the list is already mounted, initiallyFocusedItemKey has no effect
// because useState(initialFocusedIndex) in useArrowKeyFocusManager only reads the initial value.
// Imperatively focus the first recent report once options become available (desktop only).
@@ -619,10 +626,13 @@ function SearchAutocompleteList({
useEffect(() => {
const targetText = autocompleteQueryValue;
- if (shouldHighlight(normalizedReferenceText, targetText)) {
- onHighlightFirstItem?.();
+ if (!shouldHighlightFirstItem || firstRecentReportFlatIndex === -1 || !shouldHighlight(normalizedReferenceText, targetText)) {
+ return;
}
- }, [autocompleteQueryValue, onHighlightFirstItem, normalizedReferenceText]);
+ // Focus the header-aware flat index of the first result. A fixed index (e.g. searchQueryItems.length)
+ // lands on the "Recent chats" section header row after the two-section switcher was introduced.
+ innerListRef.current?.updateAndScrollToFocusedIndex(firstRecentReportFlatIndex, true);
+ }, [autocompleteQueryValue, firstRecentReportFlatIndex, normalizedReferenceText, shouldHighlightFirstItem]);
if (isLoading) {
return (
diff --git a/src/components/Search/SearchList/BaseSearchList/index.native.tsx b/src/components/Search/SearchList/BaseSearchList/index.native.tsx
index ad5d017c708f..a6955e67be8b 100644
--- a/src/components/Search/SearchList/BaseSearchList/index.native.tsx
+++ b/src/components/Search/SearchList/BaseSearchList/index.native.tsx
@@ -18,6 +18,8 @@ function BaseSearchList({
onViewableItemsChanged,
onLayout,
contentContainerStyle,
+ stickyHeaderIndices,
+ getItemType,
}: BaseSearchListProps) {
const renderItemWithoutKeyboardFocus = useCallback(
({item, index}: {item: SearchListItem; index: number}) => {
@@ -43,6 +45,8 @@ function BaseSearchList({
drawDistance={250}
contentContainerStyle={contentContainerStyle}
maintainVisibleContentPosition={{disabled: true}}
+ stickyHeaderIndices={stickyHeaderIndices}
+ getItemType={getItemType}
/>
);
}
diff --git a/src/components/Search/SearchList/BaseSearchList/index.tsx b/src/components/Search/SearchList/BaseSearchList/index.tsx
index cf25b14b1f8b..88c05b5ea273 100644
--- a/src/components/Search/SearchList/BaseSearchList/index.tsx
+++ b/src/components/Search/SearchList/BaseSearchList/index.tsx
@@ -62,6 +62,8 @@ function BaseSearchList({
selectedTransactions,
isAttendeesEnabledForMovingPolicy,
nonPersonalAndWorkspaceCards,
+ stickyHeaderIndices,
+ getItemType,
}: BaseSearchListProps) {
const hasKeyBeenPressed = useRef(false);
const isFocused = useIsFocused();
@@ -183,6 +185,8 @@ function BaseSearchList({
drawDistance={250}
contentContainerStyle={contentContainerStyle}
maintainVisibleContentPosition={{disabled: true}}
+ stickyHeaderIndices={stickyHeaderIndices}
+ getItemType={getItemType}
/>
);
}
diff --git a/src/components/Search/SearchList/BaseSearchList/types.ts b/src/components/Search/SearchList/BaseSearchList/types.ts
index 63f60b5da280..f923f8cb736b 100644
--- a/src/components/Search/SearchList/BaseSearchList/types.ts
+++ b/src/components/Search/SearchList/BaseSearchList/types.ts
@@ -17,6 +17,7 @@ type BaseSearchListProps = Pick<
| 'keyExtractor'
| 'showsVerticalScrollIndicator'
| 'onLayout'
+ | 'stickyHeaderIndices'
> & {
/** The data to display in the list */
data: SearchListItem[];
@@ -50,6 +51,9 @@ type BaseSearchListProps = Pick<
/** Non-personal and workspace cards for triggering re-render via extraData */
nonPersonalAndWorkspaceCards?: CardList;
+
+ /** Function to determine item type for FlashList recycling */
+ getItemType?: (item: SearchListItem, index: number) => string | number | undefined;
};
export default BaseSearchListProps;
diff --git a/src/components/Search/SearchList/ListItem/ActionCell/DeferredActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/DeferredActionCell.tsx
index a8687c6f60ae..7d7c6f015c9e 100644
--- a/src/components/Search/SearchList/ListItem/ActionCell/DeferredActionCell.tsx
+++ b/src/components/Search/SearchList/ListItem/ActionCell/DeferredActionCell.tsx
@@ -7,7 +7,9 @@ import ActionCell from '.';
import type {ActionCellProps} from '.';
import actionTranslationsMap from './actionTranslationsMap';
-function DeferredActionCell(actionCellProps: ActionCellProps) {
+type DeferredActionCellProps = ActionCellProps & {isMarkAsDone?: boolean};
+
+function DeferredActionCell(actionCellProps: DeferredActionCellProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const shouldRender = useDeferredValue(true, false);
@@ -16,7 +18,12 @@ function DeferredActionCell(actionCellProps: ActionCellProps) {
const action = actionCellProps.action ?? CONST.SEARCH.ACTION_TYPES.VIEW;
const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE;
const isSuccess = !shouldUseViewAction && action !== CONST.SEARCH.ACTION_TYPES.UNDELETE;
- const text = shouldUseViewAction ? translate(actionTranslationsMap[CONST.SEARCH.ACTION_TYPES.VIEW]) : translate(actionTranslationsMap[action]);
+ let text: string;
+ if (shouldUseViewAction) {
+ text = translate(actionTranslationsMap[CONST.SEARCH.ACTION_TYPES.VIEW]);
+ } else {
+ text = actionCellProps.isMarkAsDone ? translate('common.done') : translate(actionTranslationsMap[action]);
+ }
return (
;
};
-function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, shouldDisablePointerEvents}: PayActionCellProps) {
+function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, shouldDisablePointerEvents, chatReport}: PayActionCellProps) {
const styles = useThemeStyles();
const {convertToDisplayString} = useCurrencyListActions();
const {isOffline} = useNetwork();
@@ -37,7 +40,6 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall,
const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID);
const policy = usePolicy(policyID);
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`);
const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined;
const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID);
const {
diff --git a/src/components/Search/SearchList/ListItem/ActionCell/index.tsx b/src/components/Search/SearchList/ListItem/ActionCell/index.tsx
index 97c1fe4ceda5..aeedc8b69b3e 100644
--- a/src/components/Search/SearchList/ListItem/ActionCell/index.tsx
+++ b/src/components/Search/SearchList/ListItem/ActionCell/index.tsx
@@ -1,10 +1,17 @@
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab';
+import {shouldShowMarkAsDone} from '@libs/ReportUtils';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report} from '@src/types/onyx';
import type {SearchTransactionAction} from '@src/types/onyx/SearchResults';
import actionTranslationsMap from './actionTranslationsMap';
import PayActionCell from './PayActionCell';
@@ -21,6 +28,7 @@ type ActionCellProps = {
amount?: number;
extraSmall?: boolean;
shouldDisablePointerEvents?: boolean;
+ chatReport?: OnyxEntry;
};
function ActionCell({
@@ -35,10 +43,14 @@ function ActionCell({
amount,
extraSmall = false,
shouldDisablePointerEvents,
+ chatReport,
}: ActionCellProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
+ const [actionCellPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(policyID)}`);
+ const [actionCellReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`);
const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE;
@@ -75,11 +87,18 @@ function ActionCell({
amount={amount}
extraSmall={extraSmall}
shouldDisablePointerEvents={shouldDisablePointerEvents}
+ chatReport={chatReport}
/>
);
}
- const text = translate(actionTranslationsMap[action]);
+ const shouldUseMarkAsDone =
+ shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: actionCellReport,
+ policy: actionCellPolicy,
+ }) && action === CONST.SEARCH.ACTION_TYPES.SUBMIT;
+ const text = shouldUseMarkAsDone ? translate('common.done') : translate(actionTranslationsMap[action]);
const shouldBeDisabledOffline = action !== CONST.SEARCH.ACTION_TYPES.UNDELETE && isOffline;
const buttonInnerStyles = isSelected && action === CONST.SEARCH.ACTION_TYPES.UNDELETE ? styles.buttonDefaultSelected : {};
diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx
index 9eeb51f6645d..8223449df6b5 100644
--- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx
+++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx
@@ -1,3 +1,4 @@
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
// We need direct access to useOnyx to fetch live policy data at render time
@@ -29,7 +30,7 @@ import {handleActionButtonPress} from '@libs/actions/Search';
import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {isAttendeeTrackingEnabled} from '@libs/PolicyUtils';
-import {getNonHeldAndFullAmount, isInvoiceReport, isOpenExpenseReport, isProcessingReport, isReportPendingDelete} from '@libs/ReportUtils';
+import {getNonHeldAndFullAmount, isInvoiceReport, isOpenExpenseReport, isProcessingReport, isReportPendingDelete, shouldShowMarkAsDone} from '@libs/ReportUtils';
import {isOnHold, isViolationDismissed, shouldShowViolation, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -84,22 +85,33 @@ function ExpenseReportListItem({
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`, {selector: isActionLoadingSelector});
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
const currentUserDetails = useCurrentUserPersonalDetails();
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
// Fetch live policy categories from Onyx to sync violations at render time
const [parentPolicy] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(reportItem.policyID)}`);
const [parentReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportItem.reportID)}`);
- const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportItem.parentReportID)}`);
const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(reportItem.policyID)}`);
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ policy: parentPolicy,
+ report: parentReport,
+ isTrackIntentUser,
+ });
+
const searchData = currentSearchResults?.data;
const snapshotReport = useMemo(() => {
return (searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
}, [searchData, reportItem.reportID]);
+ const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`);
+
const snapshotChatReport = useMemo(() => {
- return searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.parentReportID}`];
- }, [searchData, reportItem.parentReportID]);
+ const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID;
+ return chatReportID ? searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
+ }, [searchData, snapshotReport?.chatReportID, reportItem.parentReportID]);
+
+ const chatReport = parentChatReport ?? snapshotChatReport;
const snapshotPolicy = useMemo(() => {
return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
@@ -171,7 +183,7 @@ function ExpenseReportListItem({
const liveReportTransactions = useMemo(() => Object.values(reportTransactions), [reportTransactions]);
const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({
reportID: reportItem.reportID,
- chatReportPolicyID: parentChatReport?.policyID ?? snapshotChatReport?.policyID,
+ chatReportPolicyID: chatReport?.policyID,
});
const handleOnButtonPress = useCallback(() => {
@@ -192,7 +204,6 @@ function ExpenseReportListItem({
// Search rows render from a snapshot; the report may not exist in the main
// collection yet. Fall back to the snapshot so the modal can submit.
const moneyRequestReport = parentReport ?? snapshotReport;
- const chatReport = parentChatReport ?? snapshotChatReport;
const transactionsForHoldMenu = liveReportTransactions.length > 0 ? liveReportTransactions : holdItem.transactions;
const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, holdItem.canPay ?? false, transactionsForHoldMenu);
const hasNonHeldExpenses = transactionsForHoldMenu.some((t) => !isOnHold(t));
@@ -218,7 +229,7 @@ function ExpenseReportListItem({
betas,
isSelfTourViewed,
activePolicy,
- chatReport: parentChatReport ?? snapshotChatReport,
+ chatReport,
chatReportPolicy,
iouReportCurrentNextStepDeprecated: nextStep,
searchData,
@@ -230,11 +241,10 @@ function ExpenseReportListItem({
onSelectRow,
searchData,
snapshotReport,
- snapshotChatReport,
+ chatReport,
snapshotPolicy,
parentPolicy,
parentReport,
- parentChatReport,
lastPaymentMethod,
userBillingGracePeriodEnds,
personalPolicyID,
@@ -402,12 +412,14 @@ function ExpenseReportListItem({
canSelectMultiple={canSelectMultiple}
onCheckboxPress={handleSelectionButtonPress}
onButtonPress={handleOnButtonPress}
+ chatReport={chatReport}
isSelectAllChecked={isSelected}
isIndeterminate={false}
isDisabledCheckbox={isDisabledCheckbox}
isHovered={hovered}
isFocused={isFocused}
isPendingDelete={isPendingDelete}
+ isMarkAsDone={shouldUseMarkAsDoneCopy}
/>
{getDescription}
diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx
index ab301d389d58..0c311b640d47 100644
--- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx
+++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx
@@ -27,6 +27,7 @@ function ExpenseReportListItemRowWide({
onCheckboxPress = () => {},
onButtonPress = () => {},
isActionLoading,
+ chatReport,
containerStyle,
showTooltip,
canSelectMultiple,
@@ -37,6 +38,7 @@ function ExpenseReportListItemRowWide({
isHovered = false,
isFocused = false,
isPendingDelete = false,
+ isMarkAsDone,
}: ExpenseReportListItemRowWideProps) {
const StyleUtils = useStyleUtils();
const styles = useThemeStyles();
@@ -199,7 +201,9 @@ function ExpenseReportListItemRowWide({
reportID={item.reportID}
hash={item.hash}
amount={item.total}
+ chatReport={chatReport}
shouldDisablePointerEvents={isPendingDelete}
+ isMarkAsDone={isMarkAsDone}
/>
),
diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx
index 30e4854a6291..57c5bdb6bd49 100644
--- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx
+++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx
@@ -17,6 +17,7 @@ function ExpenseReportListItemRow(props: ExpenseReportListItemRowProps) {
isActionLoading={props.isActionLoading}
onButtonPress={props.onButtonPress}
onCheckboxPress={props.onCheckboxPress}
+ chatReport={props.chatReport}
containerStyle={props.containerStyle}
isSelectAllChecked={props.isSelectAllChecked}
isIndeterminate={props.isIndeterminate}
@@ -25,6 +26,7 @@ function ExpenseReportListItemRow(props: ExpenseReportListItemRowProps) {
isFocused={props.isFocused}
isPendingDelete={props.isPendingDelete}
columns={props.columns}
+ isMarkAsDone={props.isMarkAsDone}
/>
);
}
diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts
index 6a405152939a..716e826d7a38 100644
--- a/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts
+++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/types.ts
@@ -1,7 +1,8 @@
import type {StyleProp, ViewStyle} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import type {ExpenseReportListItemType} from '@components/Search/SearchList/ListItem/types';
import type {SearchColumnType} from '@components/Search/types';
-import type {ReportAction} from '@src/types/onyx';
+import type {Report, ReportAction} from '@src/types/onyx';
type ExpenseReportListItemRowNarrowProps = {
item: ExpenseReportListItemType;
@@ -17,11 +18,13 @@ type ExpenseReportListItemRowWideProps = ExpenseReportListItemRowNarrowProps & {
showTooltip: boolean;
isActionLoading?: boolean;
onButtonPress?: () => void;
+ chatReport?: OnyxEntry;
containerStyle?: StyleProp;
isHovered?: boolean;
isFocused?: boolean;
isPendingDelete?: boolean;
columns?: SearchColumnType[];
+ isMarkAsDone: boolean;
};
type ExpenseReportListItemRowProps = ExpenseReportListItemRowWideProps;
diff --git a/src/components/Search/SearchList/ListItem/GroupChildrenContainer.tsx b/src/components/Search/SearchList/ListItem/GroupChildrenContainer.tsx
new file mode 100644
index 000000000000..63fa8911d25a
--- /dev/null
+++ b/src/components/Search/SearchList/ListItem/GroupChildrenContainer.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import {View} from 'react-native';
+import Animated from 'react-native-reanimated';
+import useExpandCollapseAnimation from '@hooks/useExpandCollapseAnimation';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import GroupChildrenContent from './GroupChildrenContent';
+import type {GroupChildrenContentProps} from './types';
+
+type GroupChildrenContainerProps = GroupChildrenContentProps & {
+ isLastItem?: boolean;
+ isSelected?: boolean;
+};
+
+function GroupChildrenContainer({
+ item,
+ isExpanded,
+ groupBy,
+ searchType,
+ columns,
+ canSelectMultiple,
+ onSelectRow,
+ onCheckboxPress,
+ onLongPressRow,
+ nonPersonalAndWorkspaceCards,
+ onUndelete,
+ isLastItem,
+ isSelected,
+ newTransactionID,
+ bankAccountList,
+ cardFeeds,
+ conciergeReportID,
+}: GroupChildrenContainerProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {isRendered, animatedStyle, onLayout} = useExpandCollapseAnimation(isExpanded);
+
+ if (!isExpanded && !isRendered) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default GroupChildrenContainer;
diff --git a/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx b/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx
new file mode 100644
index 000000000000..6f876082cd80
--- /dev/null
+++ b/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx
@@ -0,0 +1,227 @@
+import {useIsFocused} from '@react-navigation/native';
+import React, {useEffect, useMemo, useState} from 'react';
+import {useSearchSelectionContext} from '@components/Search/SearchContext';
+import useActionLoadingReportIDs from '@hooks/useActionLoadingReportIDs';
+import {useCurrencyListActions} from '@hooks/useCurrencyList';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
+import {search} from '@libs/actions/Search';
+import {getSections} from '@libs/SearchUIUtils';
+import {mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
+import TransactionGroupListExpandedItem from './TransactionGroupListExpanded';
+import type {GroupChildrenContentProps, TransactionListItemType} from './types';
+
+function GroupChildrenContent({
+ item,
+ isExpanded,
+ groupBy,
+ searchType,
+ columns,
+ canSelectMultiple,
+ onSelectRow,
+ onCheckboxPress,
+ onLongPressRow,
+ nonPersonalAndWorkspaceCards,
+ onUndelete,
+ newTransactionID,
+ bankAccountList,
+ cardFeeds,
+ conciergeReportID,
+}: GroupChildrenContentProps) {
+ const {translate, formatPhoneNumber} = useLocalize();
+ const {selectedTransactions} = useSearchSelectionContext();
+ const currentUserDetails = useCurrentUserPersonalDetails();
+ const isScreenFocused = useIsFocused();
+ const {convertToDisplayString} = useCurrencyListActions();
+ const {isOffline} = useNetwork();
+
+ const groupItem = item;
+ const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
+
+ const [transactionsSnapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${groupItem.transactionsQueryJSON?.hash}`);
+ const [transactionsVisibleLimit, setTransactionsVisibleLimit] = useState(CONST.TRANSACTION.RESULTS_PAGE_SIZE);
+ const isActionLoadingSet = useActionLoadingReportIDs();
+ const snapshotData = transactionsSnapshot?.data;
+
+ const selectedTransactionIDsSet = useMemo(() => new Set(Object.keys(selectedTransactions)), [selectedTransactions]);
+
+ const transactions: TransactionListItemType[] = useMemo(() => {
+ if (isExpenseReportType) {
+ return groupItem.transactions;
+ }
+ if (!snapshotData) {
+ return [];
+ }
+ const [sectionData] = getSections({
+ type: CONST.SEARCH.DATA_TYPES.EXPENSE,
+ data: snapshotData,
+ currentAccountID: currentUserDetails.accountID,
+ currentUserEmail: currentUserDetails.email ?? '',
+ translate,
+ formatPhoneNumber,
+ bankAccountList,
+ isActionLoadingSet,
+ cardFeeds,
+ conciergeReportID,
+ convertToDisplayString,
+ }) as [TransactionListItemType[], number, boolean];
+ return sectionData.map((transactionItem) => ({
+ ...transactionItem,
+ isSelected: selectedTransactionIDsSet.has(transactionItem.transactionID),
+ }));
+ }, [
+ isExpenseReportType,
+ groupItem.transactions,
+ snapshotData,
+ currentUserDetails.accountID,
+ currentUserDetails.email,
+ translate,
+ formatPhoneNumber,
+ bankAccountList,
+ isActionLoadingSet,
+ cardFeeds,
+ conciergeReportID,
+ convertToDisplayString,
+ selectedTransactionIDsSet,
+ ]);
+
+ const isEmpty = transactions.length === 0;
+ const shouldDisplayEmptyView = isEmpty && isExpenseReportType;
+
+ const refreshTransactions = () => {
+ if (!groupItem.transactionsQueryJSON) {
+ return;
+ }
+ search({
+ queryJSON: groupItem.transactionsQueryJSON,
+ searchKey: undefined,
+ offset: 0,
+ shouldCalculateTotals: false,
+ isLoading: !!transactionsSnapshot?.search?.isLoading,
+ isOffline,
+ });
+ };
+
+ const searchTransactions = (pageSize = 0) => {
+ if (!groupItem.transactionsQueryJSON) {
+ return;
+ }
+ search({
+ queryJSON: groupItem.transactionsQueryJSON,
+ searchKey: undefined,
+ offset: (transactionsSnapshot?.search?.offset ?? 0) + pageSize,
+ shouldCalculateTotals: false,
+ isLoading: !!transactionsSnapshot?.search?.isLoading,
+ isOffline,
+ });
+ };
+
+ useEffect(() => {
+ if (!newTransactionID || !isExpanded) {
+ return;
+ }
+ refreshTransactions();
+ // Only refresh when a new transaction is created in this group — refreshTransactions is excluded to avoid infinite loops
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [newTransactionID, isExpanded]);
+
+ useEffect(() => {
+ if (!isExpanded || isExpenseReportType) {
+ return;
+ }
+ refreshTransactions();
+ // Only fetch on expand — refreshTransactions and isExpenseReportType are excluded to prevent re-fetching on every render
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isExpanded]);
+
+ const wasScreenFocusedRef = React.useRef(isScreenFocused);
+ useEffect(() => {
+ const didReturnToScreen = wasScreenFocusedRef.current === false && isScreenFocused === true;
+ wasScreenFocusedRef.current = isScreenFocused;
+ if (!didReturnToScreen || !isExpanded || isExpenseReportType) {
+ return;
+ }
+ refreshTransactions();
+ // Only refresh when returning to this screen while expanded — other deps excluded to avoid redundant fetches
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isScreenFocused]);
+
+ const filteredViolations: Record = useMemo(() => {
+ if (!snapshotData) {
+ return {};
+ }
+
+ const groupViolations: Record = {};
+ for (const [key, value] of Object.entries(snapshotData)) {
+ if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS)) {
+ groupViolations[key] = value as TransactionViolations;
+ }
+ }
+
+ const result: Record = {};
+ for (const key of Object.keys(snapshotData)) {
+ if (!key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
+ continue;
+ }
+ const snapshotTransaction = snapshotData[key as keyof typeof snapshotData] as Transaction;
+ if (!snapshotTransaction || typeof snapshotTransaction !== 'object' || !('transactionID' in snapshotTransaction) || !('reportID' in snapshotTransaction)) {
+ continue;
+ }
+
+ const report = snapshotData[`${ONYXKEYS.COLLECTION.REPORT}${snapshotTransaction.reportID}`];
+ const policy = snapshotData[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
+
+ if (report && policy) {
+ const transactionViolations = groupViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${snapshotTransaction.transactionID}`];
+ if (transactionViolations) {
+ const merged = mergeProhibitedViolations(
+ transactionViolations.filter((violation) => shouldShowViolation(report, policy, violation.name, currentUserDetails?.email ?? '', true, snapshotTransaction)),
+ );
+ if (merged.length > 0) {
+ result[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${snapshotTransaction.transactionID}`] = merged;
+ }
+ }
+ }
+ }
+ return result;
+ }, [snapshotData, currentUserDetails.email]);
+
+ const onExpandedRowLongPress = (transaction: TransactionListItemType) => {
+ onLongPressRow?.(transaction);
+ };
+
+ return (
+
+ );
+}
+
+export default GroupChildrenContent;
diff --git a/src/components/Search/SearchList/ListItem/GroupHeader.tsx b/src/components/Search/SearchList/ListItem/GroupHeader.tsx
new file mode 100644
index 000000000000..42afe29f4666
--- /dev/null
+++ b/src/components/Search/SearchList/ListItem/GroupHeader.tsx
@@ -0,0 +1,479 @@
+import React, {useMemo, useRef} from 'react';
+import {View} from 'react-native';
+import type {NativeSyntheticEvent} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+// eslint-disable-next-line no-restricted-imports
+import {useOnyx as originalUseOnyx} from 'react-native-onyx';
+import Animated from 'react-native-reanimated';
+import {getButtonRole} from '@components/Button/utils';
+import Icon from '@components/Icon';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import {PressableWithFeedback} from '@components/Pressable';
+import {useSearchSelectionContext} from '@components/Search/SearchContext';
+import SearchTableHeader from '@components/Search/SearchTableHeader';
+import type {SearchColumnType, SearchCustomColumnIds, SearchGroupBy} from '@components/Search/types';
+import type {ExtendedTargetedEvent} from '@components/SelectionList/ListItem/types';
+import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useExpandCollapseAnimation from '@hooks/useExpandCollapseAnimation';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useOnyx from '@hooks/useOnyx';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useSyncFocus from '@hooks/useSyncFocus';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {TransactionPreviewData} from '@libs/actions/Search';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab';
+import {getColumnsToShow} from '@libs/SearchUIUtils';
+import {isDeletedTransaction} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {ReportAction, ReportActions, Transaction} from '@src/types/onyx';
+import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
+import CardListItemHeader from './CardListItemHeader';
+import CategoryListItemHeader from './CategoryListItemHeader';
+import MemberListItemHeader from './MemberListItemHeader';
+import MerchantListItemHeader from './MerchantListItemHeader';
+import MonthListItemHeader from './MonthListItemHeader';
+import QuarterListItemHeader from './QuarterListItemHeader';
+import ReportListItemHeader from './ReportListItemHeader';
+import TagListItemHeader from './TagListItemHeader';
+import type {GroupHeaderItemType, SearchListActionProps, SearchListItem, TransactionListItemType} from './types';
+import WeekListItemHeader from './WeekListItemHeader';
+import WithdrawalIDListItemHeader from './WithdrawalIDListItemHeader';
+import YearListItemHeader from './YearListItemHeader';
+
+type GroupHeaderProps = SearchListActionProps & {
+ item: GroupHeaderItemType;
+ groupBy?: SearchGroupBy;
+ searchType?: SearchDataTypes;
+ columns?: SearchColumnType[];
+ canSelectMultiple: boolean;
+ isExpanded: boolean;
+ onToggle: () => void;
+ onSelectRow: (item: SearchListItem, transactionPreviewData?: TransactionPreviewData, event?: ModifiedMouseEvent) => void;
+ onCheckboxPress: (item: GroupHeaderItemType, itemTransactions?: TransactionListItemType[]) => void;
+ onLongPressRow?: (item: SearchListItem, itemTransactions?: TransactionListItemType[]) => void;
+ onFocus?: (event: NativeSyntheticEvent) => void;
+ shouldSyncFocus?: boolean;
+ isFocused?: boolean;
+ isFirstItem: boolean;
+ isLastItem: boolean;
+ originalKey: string;
+ visibleColumns?: SearchCustomColumnIds[];
+};
+
+function GroupHeader({
+ item,
+ groupBy,
+ searchType,
+ columns,
+ canSelectMultiple,
+ isExpanded,
+ onToggle,
+ onSelectRow,
+ onCheckboxPress,
+ onLongPressRow,
+ onFocus,
+ shouldSyncFocus,
+ isFocused,
+ isFirstItem,
+ isLastItem,
+ originalKey,
+ lastPaymentMethod,
+ personalPolicyID,
+ userBillingGracePeriodEnds,
+ ownerBillingGracePeriodEnd,
+ visibleColumns,
+}: GroupHeaderProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const {isLargeScreenWidth} = useResponsiveLayout();
+ const {selectedTransactions} = useSearchSelectionContext();
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['UpArrow', 'DownArrow']);
+ const currentUserDetails = useCurrentUserPersonalDetails();
+
+ const groupItem = item;
+ const isExpenseReportType = searchType === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
+
+ const oneTransactionItem = groupItem.isOneTransactionReport ? groupItem.transactions.at(0) : undefined;
+ const oneTransactionReportID = getNonEmptyStringOnyxID(oneTransactionItem?.reportID);
+ const oneTransactionID = getNonEmptyStringOnyxID(oneTransactionItem?.transactionID);
+ const oneTransactionChildReportID = oneTransactionItem?.reportAction?.childReportID;
+ const [parentReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${oneTransactionReportID}`);
+ const [oneTransactionThreadReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${oneTransactionChildReportID}`);
+ const [oneTransaction] = originalUseOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${oneTransactionID}`);
+ const parentReportActionSelector = (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${oneTransactionItem?.reportAction?.reportActionID}`];
+ const [parentReportAction] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneTransactionReportID}`, {selector: parentReportActionSelector}, [oneTransactionItem]);
+ const transactionPreviewData: TransactionPreviewData = useMemo(
+ () => ({
+ hasParentReport: !!parentReport,
+ hasTransaction: !!oneTransaction,
+ hasParentReportAction: !!parentReportAction,
+ hasTransactionThreadReport: !!oneTransactionThreadReport,
+ }),
+ [parentReport, oneTransaction, parentReportAction, oneTransactionThreadReport],
+ );
+
+ const [transactionsSnapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${groupItem.transactionsQueryJSON?.hash}`);
+ const snapshotData = transactionsSnapshot?.data;
+ const snapshotSearchType = transactionsSnapshot?.search.type;
+
+ const subHeaderColumns = useMemo(() => {
+ if (isExpenseReportType) {
+ return columns ?? [];
+ }
+ if (!snapshotData) {
+ return [];
+ }
+ return getColumnsToShow({
+ currentAccountID: currentUserDetails.accountID,
+ data: snapshotData,
+ visibleColumns,
+ type: snapshotSearchType,
+ });
+ }, [isExpenseReportType, columns, snapshotData, snapshotSearchType, currentUserDetails.accountID, visibleColumns]);
+
+ const {isSubHeaderAmountColumnWide, isSubHeaderTaxAmountColumnWide, shouldSubHeaderShowYear, isSubHeaderActionColumnWide} = useMemo(() => {
+ let amountWide = false;
+ let taxWide = false;
+ let showYear = false;
+ let actionWide = false;
+ for (const transaction of groupItem.transactions) {
+ if (transaction.isAmountColumnWide) {
+ amountWide = true;
+ }
+ if (transaction.isTaxAmountColumnWide) {
+ taxWide = true;
+ }
+ if (transaction.shouldShowYear) {
+ showYear = true;
+ }
+ if (transaction.isActionColumnWide || isDeletedTransaction(transaction)) {
+ actionWide = true;
+ }
+ if (amountWide && taxWide && showYear && actionWide) {
+ break;
+ }
+ }
+ return {isSubHeaderAmountColumnWide: amountWide, isSubHeaderTaxAmountColumnWide: taxWide, shouldSubHeaderShowYear: showYear, isSubHeaderActionColumnWide: actionWide};
+ }, [groupItem.transactions]);
+
+ const {isRendered: isSubHeaderRendered, animatedStyle: subHeaderAnimatedStyle, onLayout: onSubHeaderLayout} = useExpandCollapseAnimation(isExpanded);
+
+ const hasSnapshotTransactions = !isExpenseReportType && !!snapshotData && Object.keys(snapshotData).some((key) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION));
+ const isEmpty = groupItem.transactions.length === 0 && !hasSnapshotTransactions;
+ const isDisabled = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
+ const isDisabledOrEmpty = isEmpty || isDisabled;
+
+ const effectiveTransactions = useMemo((): TransactionListItemType[] => {
+ if (isExpenseReportType || groupItem.transactions.length > 0) {
+ return groupItem.transactions;
+ }
+ if (!snapshotData) {
+ return [];
+ }
+ const items: TransactionListItemType[] = [];
+ for (const key of Object.keys(snapshotData)) {
+ if (!key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
+ continue;
+ }
+ const transaction = snapshotData[key as keyof typeof snapshotData] as Transaction;
+ if (!transaction?.transactionID) {
+ continue;
+ }
+ const report = snapshotData[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+ items.push({
+ ...transaction,
+ keyForList: transaction.transactionID,
+ report,
+ } as TransactionListItemType);
+ }
+ return items;
+ }, [isExpenseReportType, groupItem.transactions, snapshotData]);
+
+ const {isSelectAllChecked, isIndeterminate} = useMemo(() => {
+ const selectedTransactionIDsSet = new Set(Object.keys(selectedTransactions));
+ const filteredTransactions = effectiveTransactions.filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
+ const selectedCount = filteredTransactions.reduce((acc, transaction) => (selectedTransactionIDsSet.has(transaction.transactionID) ? acc + 1 : acc), 0);
+ const isEmptyReportSelected = isEmpty && originalKey && selectedTransactions[originalKey]?.isSelected;
+ const allChecked = !!isEmptyReportSelected || (selectedCount === filteredTransactions.length && filteredTransactions.length > 0);
+ const indeterminate = selectedCount > 0 && selectedCount !== filteredTransactions.length;
+ return {isSelectAllChecked: allChecked, isIndeterminate: indeterminate};
+ }, [selectedTransactions, effectiveTransactions, isEmpty, originalKey]);
+
+ const isItemSelected = isSelectAllChecked || item?.isSelected;
+
+ const animatedHighlightStyle = useAnimatedHighlightStyle({
+ shouldHighlight: item?.shouldAnimateInHighlight ?? false,
+ highlightColor: theme.messageHighlightBG,
+ backgroundColor: isItemSelected ? theme.activeComponentBG : theme.highlightBG,
+ shouldApplyOtherStyles: false,
+ });
+
+ const handleSelectionButtonPress = () => {
+ onCheckboxPress(item, isExpenseReportType ? undefined : effectiveTransactions);
+ };
+
+ const pendingAction =
+ item.pendingAction ??
+ (groupItem.transactions.length > 0 && groupItem.transactions.every((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
+ ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE
+ : undefined);
+
+ const handleSelectRow = (rowItem: SearchListItem, event?: ModifiedMouseEvent) => {
+ onSelectRow(rowItem, transactionPreviewData, event);
+ };
+
+ const renderHeader = (hovered: boolean) => {
+ if (isExpenseReportType && 'groupedBy' in groupItem && groupItem.groupedBy === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) {
+ return (
+
+ );
+ }
+
+ if (!('groupedBy' in groupItem)) {
+ return null;
+ }
+
+ const commonProps = {
+ onCheckboxPress: handleSelectionButtonPress,
+ isDisabled: isDisabledOrEmpty,
+ columns,
+ canSelectMultiple,
+ isSelectAllChecked,
+ isIndeterminate,
+ onDownArrowClick: onToggle,
+ isExpanded,
+ } as const;
+
+ switch (groupItem.groupedBy) {
+ case CONST.SEARCH.GROUP_BY.FROM:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.CARD:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.CATEGORY:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.MERCHANT:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.TAG:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.MONTH:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.WEEK:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.YEAR:
+ return (
+
+ );
+ case CONST.SEARCH.GROUP_BY.QUARTER:
+ return (
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ const isLastItemCollapsed = isLastItem && !isExpanded && !isSubHeaderRendered;
+ const pressableRef = useRef(null);
+
+ useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus);
+
+ const pressableStyle = [
+ styles.transactionGroupListItemStyle,
+ isLargeScreenWidth && {
+ ...styles.tableRowHeight,
+ borderRadius: 0,
+ ...(isLastItemCollapsed ? styles.tableBottomRadius : {}),
+ },
+ isItemSelected && styles.activeComponentBG,
+ ];
+
+ const shouldDisplayEmptyView = isEmpty && isExpenseReportType;
+
+ const handlePress = (event?: ModifiedMouseEvent) => {
+ if (isExpenseReportType) {
+ onSelectRow(item, transactionPreviewData, event);
+ }
+ if (!isExpenseReportType) {
+ onToggle();
+ }
+ };
+
+ const handleLongPress = () => {
+ onLongPressRow?.(item, isExpenseReportType ? undefined : effectiveTransactions);
+ };
+
+ return (
+
+ e.preventDefault()}
+ id={item.keyForList ?? ''}
+ onFocus={onFocus}
+ style={[
+ pressableStyle,
+ isFocused && StyleUtils.getItemBackgroundColorStyle(!!isItemSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG),
+ ]}
+ wrapperStyle={[
+ styles.mh5,
+ animatedHighlightStyle,
+ styles.userSelectNone,
+ isLargeScreenWidth
+ ? [StyleUtils.getSearchTableGroupRowBorderStyle(isFirstItem, isLastItemCollapsed, isItemSelected), isLastItemCollapsed && styles.overflowHidden]
+ : [
+ isFirstItem && [styles.tableTopRadius, styles.overflowHidden],
+ isLastItemCollapsed && [styles.tableBottomRadius, styles.overflowHidden],
+ !isLastItemCollapsed && !isExpanded && StyleUtils.getSelectedBorderBottomStyle(isItemSelected),
+ ],
+ ]}
+ >
+ {({hovered}) => (
+
+
+ {renderHeader(hovered)}
+ {isLargeScreenWidth && (
+ {
+ if (isEmpty && !shouldDisplayEmptyView) {
+ handlePress();
+ return;
+ }
+ onToggle();
+ }}
+ style={[styles.p3Half, styles.justifyContentCenter, styles.alignItemsCenter, styles.pv2]}
+ accessibilityRole={CONST.ROLE.BUTTON}
+ accessibilityLabel={isExpanded ? CONST.ACCESSIBILITY_LABELS.COLLAPSE : CONST.ACCESSIBILITY_LABELS.EXPAND}
+ sentryLabel={CONST.SENTRY_LABEL.SEARCH.GROUP_EXPAND_TOGGLE}
+ >
+ {({hovered: arrowHovered}) => (
+
+ )}
+
+ )}
+
+ {isLargeScreenWidth && subHeaderColumns.length > 0 && (
+
+ {(isExpanded || isSubHeaderRendered) && (
+
+
+
+
+
+ {}}
+ sortOrder={undefined}
+ sortBy={undefined}
+ shouldShowYear={shouldSubHeaderShowYear}
+ isAmountColumnWide={isSubHeaderAmountColumnWide}
+ isTaxAmountColumnWide={isSubHeaderTaxAmountColumnWide}
+ shouldShowSorting={false}
+ columns={subHeaderColumns}
+ groupBy={groupBy}
+ isExpenseReportView
+ isActionColumnWide={isSubHeaderActionColumnWide}
+ />
+
+
+
+
+
+ )}
+
+ )}
+
+ )}
+
+
+ );
+}
+
+export default GroupHeader;
diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
index 8e97f79ed7b2..5596e7c933bc 100644
--- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
+++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
@@ -1,6 +1,7 @@
import React, {useMemo} from 'react';
import type {ColorValue} from 'react-native';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Checkbox from '@components/Checkbox';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import Icon from '@components/Icon';
@@ -96,6 +97,9 @@ type FirstRowReportHeaderProps = {
/** Whether the down arrow is expanded */
isExpanded?: boolean;
+
+ /** Parent chat report resolved from live Onyx with search snapshot fallback */
+ chatReport?: OnyxEntry;
};
function HeaderFirstRow({
@@ -109,6 +113,7 @@ function HeaderFirstRow({
isIndeterminate,
onDownArrowClick,
isExpanded,
+ chatReport,
}: FirstRowReportHeaderProps) {
const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'UpArrow']);
const styles = useThemeStyles();
@@ -190,6 +195,7 @@ function HeaderFirstRow({
hash={reportItem.hash}
amount={reportItem.total}
extraSmall={!isLargeScreenWidth}
+ chatReport={chatReport}
/>
)}
@@ -228,11 +234,16 @@ function ReportListItemHeader({
const snapshotPolicy = useMemo(() => {
return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
}, [snapshot, reportItem.policyID]);
+ const snapshotChatReport = useMemo(() => {
+ const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID;
+ return chatReportID ? snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
+ }, [snapshot, snapshotReport?.chatReportID, reportItem.parentReportID]);
const [parentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(snapshotReport?.policyID ?? reportItem.policyID)}`);
- const [parentChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID)}`);
+ const [parentChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`);
+ const chatReport = parentChatReport ?? snapshotChatReport;
const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({
reportID: reportItem.reportID,
- chatReportPolicyID: parentChatReport?.policyID,
+ chatReportPolicyID: chatReport?.policyID,
});
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
@@ -265,7 +276,7 @@ function ReportListItemHeader({
betas,
isSelfTourViewed,
activePolicy,
- chatReport: parentChatReport,
+ chatReport,
chatReportPolicy,
iouReportCurrentNextStepDeprecated: nextStep,
searchData: snapshot?.data,
@@ -306,6 +317,7 @@ function ReportListItemHeader({
isIndeterminate={isIndeterminate}
onDownArrowClick={onDownArrowClick}
isExpanded={isExpanded}
+ chatReport={chatReport}
/>
);
diff --git a/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx b/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx
index d9e4d018dcf3..fc8050baf190 100644
--- a/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx
+++ b/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx
@@ -64,6 +64,7 @@ function TransactionGroupListExpanded({
onLongPress,
nonPersonalAndWorkspaceCards,
onUndelete,
+ hideSearchTableHeader,
}: TransactionGroupListExpandedProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -289,7 +290,7 @@ function TransactionGroupListExpanded({
const content = (
- {isLargeScreenWidth && !(isEmpty && shouldDisplayLoadingIndicator) && (
+ {isLargeScreenWidth && !hideSearchTableHeader && !(isEmpty && shouldDisplayLoadingIndicator) && (
<>
({
const isSelectAllChecked = isEmptyReportSelected || (selectedItemsLength === transactionsWithoutPendingDelete.length && transactionsWithoutPendingDelete.length > 0);
const isIndeterminate = selectedItemsLength > 0 && selectedItemsLength !== transactionsWithoutPendingDelete.length;
+ // Currently only the transaction report groups have transactions where the empty view makes sense
const shouldDisplayEmptyView = isEmpty && isExpenseReportType;
+ const isDisabledOrEmpty = isEmpty || isDisabled;
const refreshTransactions = () => {
if (!groupItem.transactionsQueryJSON) {
@@ -290,7 +292,7 @@ function TransactionGroupListItem({
};
const onPress = (event?: ModifiedMouseEvent) => {
- if (isExpenseReportType) {
+ if (isExpenseReportType || transactions.length === 0) {
onSelectRow(item, transactionPreviewData, event);
}
if (!isExpenseReportType) {
@@ -325,7 +327,7 @@ function TransactionGroupListItem({
({
({
({
({
({
({
({
({
({
= Pick<
isInSingleTransactionReport: boolean;
searchTransactions: (pageSize?: number) => void;
onLongPress: (transaction: TransactionListItemType) => void;
+ hideSearchTableHeader?: boolean;
+};
+
+const GROUP_ITEM_TYPES = {
+ GROUP_HEADER: 'group_header',
+ CHILDREN_CONTAINER: 'children_container',
+} as const;
+
+type GroupHeaderListItemType = {listItemType: typeof GROUP_ITEM_TYPES.GROUP_HEADER};
+
+type GroupHeaderItemType =
+ | (TransactionReportGroupListItemType & GroupHeaderListItemType)
+ | (TransactionMemberGroupListItemType & GroupHeaderListItemType)
+ | (TransactionCardGroupListItemType & GroupHeaderListItemType)
+ | (TransactionWithdrawalIDGroupListItemType & GroupHeaderListItemType)
+ | (TransactionCategoryGroupListItemType & GroupHeaderListItemType)
+ | (TransactionMerchantGroupListItemType & GroupHeaderListItemType)
+ | (TransactionTagGroupListItemType & GroupHeaderListItemType)
+ | (TransactionMonthGroupListItemType & GroupHeaderListItemType)
+ | (TransactionWeekGroupListItemType & GroupHeaderListItemType)
+ | (TransactionYearGroupListItemType & GroupHeaderListItemType)
+ | (TransactionQuarterGroupListItemType & GroupHeaderListItemType)
+ | (TransactionGroupListItemType & GroupHeaderListItemType);
+
+type GroupChildrenContainerItemType = TransactionGroupListItemType & {
+ listItemType: typeof GROUP_ITEM_TYPES.CHILDREN_CONTAINER;
+};
+
+function isGroupHeaderItem(item: SearchListItem): item is GroupHeaderItemType {
+ return 'listItemType' in item && item.listItemType === GROUP_ITEM_TYPES.GROUP_HEADER;
+}
+
+function isGroupChildrenContainerItem(item: SearchListItem): item is GroupChildrenContainerItemType {
+ return 'listItemType' in item && item.listItemType === GROUP_ITEM_TYPES.CHILDREN_CONTAINER;
+}
+
+type GroupChildrenContentProps = {
+ item: GroupChildrenContainerItemType;
+ isExpanded: boolean;
+ groupBy?: SearchGroupBy;
+ searchType?: SearchDataTypes;
+ columns?: SearchColumnType[];
+ canSelectMultiple: boolean;
+ onSelectRow: (item: SearchListItem, transactionPreviewData?: TransactionPreviewData, event?: ModifiedMouseEvent) => void;
+ onCheckboxPress: (item: SearchListItem, itemTransactions?: TransactionListItemType[]) => void;
+ onLongPressRow?: (item: SearchListItem, itemTransactions?: TransactionListItemType[]) => void;
+ nonPersonalAndWorkspaceCards?: CardList;
+ onUndelete?: (transaction: Transaction) => void;
+ newTransactionID?: string;
+ bankAccountList?: OnyxEntry;
+ cardFeeds?: OnyxCollection;
+ conciergeReportID?: string;
};
type UnreportedExpenseListItemType = Transaction & {
@@ -512,4 +568,9 @@ export type {
TransactionListItemProps,
ReportActionListItemType,
UnreportedExpenseListItemType,
+ GroupHeaderItemType,
+ GroupChildrenContainerItemType,
+ GroupChildrenContentProps,
};
+
+export {GROUP_ITEM_TYPES, isGroupHeaderItem, isGroupChildrenContainerItem};
diff --git a/src/components/Search/SearchList/index.tsx b/src/components/Search/SearchList/index.tsx
index 836af7e5bdcd..d3dd9cdbb75f 100644
--- a/src/components/Search/SearchList/index.tsx
+++ b/src/components/Search/SearchList/index.tsx
@@ -28,15 +28,18 @@ import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import DateUtils from '@libs/DateUtils';
import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab';
import navigationRef from '@libs/Navigation/navigationRef';
-import {getTableMinWidth} from '@libs/SearchUIUtils';
+import {getTableMinWidth, splitGroupsIntoPairs} from '@libs/SearchUIUtils';
import variables from '@styles/variables';
import type {TransactionPreviewData} from '@userActions/Search';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import {columnsSelector} from '@src/selectors/AdvancedSearchFiltersForm';
import type {CardList, Transaction} from '@src/types/onyx';
import BaseSearchList from './BaseSearchList';
import type ChatListItem from './ListItem/ChatListItem';
import type ExpenseReportListItem from './ListItem/ExpenseReportListItem';
+import GroupChildrenContainer from './ListItem/GroupChildrenContainer';
+import GroupHeader from './ListItem/GroupHeader';
import type TaskListItem from './ListItem/TaskListItem';
import type TransactionGroupListItem from './ListItem/TransactionGroupListItem';
import type TransactionListItem from './ListItem/TransactionListItem';
@@ -53,6 +56,7 @@ import type {
TransactionWeekGroupListItemType,
TransactionYearGroupListItemType,
} from './ListItem/types';
+import {isGroupChildrenContainerItem, isGroupHeaderItem} from './ListItem/types';
import SearchSelectAllMenu from './SearchSelectAllMenu';
const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0);
@@ -232,11 +236,11 @@ function SearchList({
return data;
}, [data, groupBy, type]);
const emptyReports = useMemo(() => {
- if (isTransactionGroupListItemArray(data)) {
- return data.filter((item) => item.transactions.length === 0 && item.keyForList);
+ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
+ return data.filter((item) => item.transactions.length === 0);
}
return [];
- }, [data]);
+ }, [data, type]);
const selectedItemsLength = useMemo(() => {
const selectedTransactionsCount = flattenedItems.reduce((acc, item) => {
@@ -244,7 +248,7 @@ function SearchList({
return acc + (isTransactionSelected ? 1 : 0);
}, 0);
- if (isTransactionGroupListItemArray(data)) {
+ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
const selectedEmptyReports = emptyReports.reduce((acc, item) => {
const isEmptyReportSelected = !!(item.keyForList && selectedTransactions[item.keyForList]?.isSelected);
return acc + (isEmptyReportSelected ? 1 : 0);
@@ -254,10 +258,10 @@ function SearchList({
}
return selectedTransactionsCount;
- }, [flattenedItems, data, emptyReports, selectedTransactions]);
+ }, [flattenedItems, type, data, emptyReports, selectedTransactions]);
const totalItems = useMemo(() => {
- if (isTransactionGroupListItemArray(data)) {
+ if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT && isTransactionGroupListItemArray(data)) {
const selectableEmptyReports = emptyReports.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const selectableTransactions = flattenedItems.filter((item) => {
if ('pendingAction' in item) {
@@ -275,7 +279,7 @@ function SearchList({
return true;
});
return selectableTransactions.length;
- }, [data, flattenedItems, emptyReports]);
+ }, [data, type, flattenedItems, emptyReports]);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -294,10 +298,60 @@ function SearchList({
const hasItemsBeingRemoved = prevDataLength && prevDataLength > data.length;
const {isEditingCell, wasRecentlyEditingCell} = useEditingCellState();
+ const isGroupByActive = !!groupBy || type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
+ const [expandedGroups, setExpandedGroups] = useState>(() => new Set());
+
+ const onToggleGroup = useCallback((key: string) => {
+ setExpandedGroups((prev) => {
+ const next = new Set(prev);
+ if (next.has(key)) {
+ next.delete(key);
+ } else {
+ next.add(key);
+ }
+ return next;
+ });
+ }, []);
+
+ const shouldSplitGroups = isGroupByActive && isLargeScreenWidth;
+
+ const {splitData: listData, stickyHeaderIndices} = useMemo(() => {
+ if (!shouldSplitGroups) {
+ return {splitData: data, stickyHeaderIndices: undefined};
+ }
+ const {splitData, stickyHeaderIndices: allIndices} = splitGroupsIntoPairs(data);
+ const activeIndices = allIndices.filter((idx) => {
+ const item = splitData.at(idx);
+ const originalKey = item?.keyForList?.replace('header_', '') ?? '';
+ return expandedGroups.has(originalKey);
+ });
+ return {splitData, stickyHeaderIndices: activeIndices.length > 0 ? activeIndices : undefined};
+ }, [data, shouldSplitGroups, expandedGroups]);
+
+ const getItemType = useCallback(
+ (item: SearchListItem) => {
+ if (!shouldSplitGroups) {
+ return undefined;
+ }
+ if (isGroupHeaderItem(item)) {
+ return item.listItemType;
+ }
+ if (isGroupChildrenContainerItem(item)) {
+ return item.listItemType;
+ }
+ return 'default';
+ },
+ [shouldSplitGroups],
+ );
+
const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD);
const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID);
const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
+ const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [cardFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER);
+ const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const undeleteTransactions = useUndeleteTransactions();
const handleUndelete = (transaction: Transaction) => undeleteTransactions([transaction]);
@@ -395,16 +449,21 @@ function SearchList({
return;
}
- // Don't scroll while a cell is being edited
- // as it can cause unwanted scrolling when the edit is dismissed
- // See: https://github.com/Expensify/App/pull/83127#issuecomment-4064533155
if (isEditingCell || wasRecentlyEditingCell) {
return;
}
- listRef.current.scrollToIndex({index, animated, viewOffset: -variables.contentHeaderHeight});
+ let targetIndex = index;
+ if (shouldSplitGroups && item.keyForList) {
+ const splitIndex = listData.findIndex((li) => li.keyForList === `header_${item.keyForList}` || li.keyForList === item.keyForList);
+ if (splitIndex !== -1) {
+ targetIndex = splitIndex;
+ }
+ }
+
+ listRef.current.scrollToIndex({index: targetIndex, animated, viewOffset: -variables.contentHeaderHeight});
},
- [data, isEditingCell, wasRecentlyEditingCell],
+ [data, isEditingCell, wasRecentlyEditingCell, shouldSplitGroups, listData],
);
useFocusEffect(
@@ -423,13 +482,81 @@ function SearchList({
useImperativeHandle(ref, () => ({scrollToIndex}), [scrollToIndex]);
const isItemVisible = useCallback((item: SearchListItem) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline, [isOffline]);
- const firstVisibleIndex = useMemo(() => data.findIndex(isItemVisible), [data, isItemVisible]);
- const lastVisibleIndex = useMemo(() => data.findLastIndex(isItemVisible), [data, isItemVisible]);
+ const firstVisibleIndex = useMemo(() => listData.findIndex(isItemVisible), [listData, isItemVisible]);
+ const lastVisibleIndex = useMemo(() => listData.findLastIndex(isItemVisible), [listData, isItemVisible]);
const renderItem = useCallback(
(item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent) => void) => {
+ // Handle group header items (sticky)
+ if (isGroupHeaderItem(item)) {
+ const headerItem = item;
+ const originalKey = (item.keyForList ?? '').replace('header_', '');
+ return (
+ onToggleGroup(originalKey)}
+ onSelectRow={(rowItem, previewData, event) => onSelectRow({...rowItem, keyForList: originalKey}, previewData, event)}
+ onCheckboxPress={(val, itemTransactions) => onCheckboxPress({...val, keyForList: originalKey} as SearchListItem, itemTransactions)}
+ onLongPressRow={(rowItem, itemTransactions) => {
+ const fixedItem = {...rowItem, keyForList: originalKey} as SearchListItem;
+ if (isMobileSelectionModeEnabled) {
+ handleLongPressRowInMobileSelectionMode(fixedItem, itemTransactions);
+ } else {
+ handleLongPressRow(fixedItem, itemTransactions);
+ }
+ }}
+ onFocus={onFocus}
+ isFocused={isItemFocused}
+ isFirstItem={index === firstVisibleIndex}
+ isLastItem={index + 1 >= lastVisibleIndex && !ListFooterComponent}
+ originalKey={originalKey}
+ lastPaymentMethod={lastPaymentMethod}
+ personalPolicyID={personalPolicyID}
+ userBillingGracePeriodEnds={userBillingGracePeriodEnds}
+ ownerBillingGracePeriodEnd={ownerBillingGracePeriodEnd}
+ visibleColumns={visibleColumns}
+ />
+ );
+ }
+
+ // Handle children container items (animated expand/collapse)
+ if (isGroupChildrenContainerItem(item)) {
+ const containerItem = item;
+ const originalKey = (item.keyForList ?? '').replace('children_', '');
+ const containerNewTransactionID = item.keyForList ? newTransactionIDByItemKey.get(originalKey) : undefined;
+ const isContainerSelected =
+ !!containerItem.isSelected || (containerItem.transactions.length > 0 && containerItem.transactions.every((t) => selectedTransactions[t.transactionID]?.isSelected));
+ return (
+
+ );
+ }
+
+ // Default rendering for non-group items
const isDisabled = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
- const shouldApplyAnimation = shouldAnimate && index < data.length - 1;
+ const shouldApplyAnimation = shouldAnimate && index < listData.length - 1;
const newTransactionID = item.keyForList ? newTransactionIDByItemKey.get(item.keyForList) : undefined;
@@ -473,7 +600,7 @@ function SearchList({
groupBy,
newTransactionIDByItemKey,
shouldAnimate,
- data.length,
+ listData.length,
styles.overflowHidden,
hasItemsBeingRemoved,
ListItem,
@@ -494,6 +621,13 @@ function SearchList({
handleUndelete,
firstVisibleIndex,
lastVisibleIndex,
+ expandedGroups,
+ onToggleGroup,
+ bankAccountList,
+ cardFeeds,
+ conciergeReportID,
+ visibleColumns,
+ selectedTransactions,
],
);
@@ -508,6 +642,7 @@ function SearchList({
style={[
styles.searchListHeaderContainerStyle,
isLargeScreenWidth ? [styles.listTableHeaderCompact, styles.searchListHeaderTableStyle, styles.mh5] : styles.listTableHeader,
+ isLargeScreenWidth && shouldSplitGroups && styles.searchListHeaderTableStickyOverlap,
]}
>
{canSelectMultiple && (
@@ -522,10 +657,11 @@ function SearchList({
)}
{SearchTableHeader}
+ {isLargeScreenWidth && shouldSplitGroups && }
)}
{
onRouterClose();
});
- const updateAndScrollToFocusedIndex = useCallback(() => listRef.current?.updateAndScrollToFocusedIndex(searchQueryItems?.length ?? 1, true), [searchQueryItems?.length]);
const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
@@ -424,7 +423,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
searchQueryItems={searchQueryItems}
getAdditionalSections={getAdditionalSections}
onListItemPress={onListItemPress}
- onHighlightFirstItem={updateAndScrollToFocusedIndex}
+ shouldHighlightFirstItem
ref={listRef}
textInputRef={textInputRef}
autocompleteSubstitutions={autocompleteSubstitutions}
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 2068734773bc..b974e2ffb743 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -350,6 +350,7 @@ function Search({
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {selector: columnsSelector});
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST);
+ const [personalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST);
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const reportAttributesDerivedValue = useReportAttributes();
@@ -603,7 +604,8 @@ function Search({
queryJSON,
isActionLoadingSet,
cardFeeds,
- cardList: nonPersonalAndWorkspaceCards,
+ cardList: personalAndWorkspaceCards,
+ nonPersonalAndWorkspaceCardList: nonPersonalAndWorkspaceCards,
isOffline,
allTransactionViolations: violations,
customCardNames,
@@ -638,6 +640,7 @@ function Search({
email,
isActionLoadingSet,
cardFeeds,
+ personalAndWorkspaceCards,
nonPersonalAndWorkspaceCards,
policies,
bankAccountList,
diff --git a/src/components/SelectionList/ListItem/MultiSelectListItem.tsx b/src/components/SelectionList/ListItem/MultiSelectListItem.tsx
index b6d358c58a9b..d8f93bfe6d90 100644
--- a/src/components/SelectionList/ListItem/MultiSelectListItem.tsx
+++ b/src/components/SelectionList/ListItem/MultiSelectListItem.tsx
@@ -28,6 +28,7 @@ function MultiSelectListItem({
wrapperStyle,
titleStyles,
shouldHighlightSelectedItem,
+ titleNumberOfLines,
}: MultiSelectListItemProps) {
const styles = useThemeStyles();
const icon = item.icons?.at(0);
@@ -59,6 +60,7 @@ function MultiSelectListItem({
wrapperStyle={computedWrapperStyle}
titleStyles={titleStyles}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
+ titleNumberOfLines={titleNumberOfLines}
/>
);
}
diff --git a/src/components/SelectionList/ListItem/SingleSelectListItem.tsx b/src/components/SelectionList/ListItem/SingleSelectListItem.tsx
index f47ab05457e3..36fbbe6a2fb3 100644
--- a/src/components/SelectionList/ListItem/SingleSelectListItem.tsx
+++ b/src/components/SelectionList/ListItem/SingleSelectListItem.tsx
@@ -27,6 +27,7 @@ function SingleSelectListItem({
isFocusVisible,
rightHandSideComponent,
selectionButtonPosition,
+ titleNumberOfLines,
}: SingleSelectListItemProps) {
const styles = useThemeStyles();
@@ -51,6 +52,7 @@ function SingleSelectListItem({
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
isFocusVisible={isFocusVisible}
selectionButtonPosition={selectionButtonPosition}
+ titleNumberOfLines={titleNumberOfLines}
/>
);
}
diff --git a/src/components/Skeletons/TabNavigatorSkeleton.tsx b/src/components/Skeletons/TabNavigatorSkeleton.tsx
index a4aa38e96790..efffbbcf250e 100644
--- a/src/components/Skeletons/TabNavigatorSkeleton.tsx
+++ b/src/components/Skeletons/TabNavigatorSkeleton.tsx
@@ -18,32 +18,34 @@ function TabNavigatorSkeleton({reasonAttributes}: TabNavigatorSkeletonProps) {
return (
-
-
+
-
-
-
+
+
+
+
+
-
+ height="35%"
+ backgroundColor={theme.skeletonLHNIn}
+ foregroundColor={theme.skeletonLHNOut}
+ >
+
+
+
);
}
diff --git a/src/components/SpendRules/SpendRulesSection.tsx b/src/components/SpendRules/SpendRulesSection.tsx
index 9e151d269dae..16947ae3762b 100644
--- a/src/components/SpendRules/SpendRulesSection.tsx
+++ b/src/components/SpendRules/SpendRulesSection.tsx
@@ -11,7 +11,6 @@ import Section from '@components/Section';
import Text from '@components/Text';
import useConfirmModal from '@hooks/useConfirmModal';
import useDefaultFundID from '@hooks/useDefaultFundID';
-import useEnvironment from '@hooks/useEnvironment';
import useExpensifyCardRules from '@hooks/useExpensifyCardRulesList';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -45,7 +44,6 @@ function SpendRulesSection({policyID, canWriteRules, showReadOnlyModal}: SpendRu
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Lock', 'Plus']);
const {showConfirmModal} = useConfirmModal();
const illustrations = useMemoizedLazyIllustrations(['ExpensifyCardProtectionIllustration']);
- const {isProduction} = useEnvironment();
const {isOffline} = useNetwork();
const defaultFundID = useDefaultFundID(policyID);
const [expensifyCardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`);
@@ -220,24 +218,22 @@ function SpendRulesSection({policyID, canWriteRules, showReadOnlyModal}: SpendRu
))
)}
- {!isProduction && (
- {
- if (!canWriteRules) {
- showReadOnlyModal();
- return;
- }
- Navigation.navigate(ROUTES.RULES_SPEND_NEW.getRoute(policyID));
- }}
- sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.ADD_SPEND_RULE}
- />
- )}
+ {
+ if (!canWriteRules) {
+ showReadOnlyModal();
+ return;
+ }
+ Navigation.navigate(ROUTES.RULES_SPEND_NEW.getRoute(policyID));
+ }}
+ />
);
}
diff --git a/src/components/SpendRules/configuration/SpendRuleMaxAmountBase.tsx b/src/components/SpendRules/configuration/SpendRuleMaxAmountBase.tsx
index 5f1fe84d2679..7c2459c6678e 100644
--- a/src/components/SpendRules/configuration/SpendRuleMaxAmountBase.tsx
+++ b/src/components/SpendRules/configuration/SpendRuleMaxAmountBase.tsx
@@ -8,6 +8,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useCanWriteCardSpendRules from '@hooks/useCanWriteCardSpendRules';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
@@ -27,6 +28,7 @@ function SpendRuleMaxAmountBase({policyID, maxAmount, currencyCode, onMaxAmountC
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
+ const canWriteCardSpendRules = useCanWriteCardSpendRules(policyID);
const goBack = () => {
Navigation.goBack();
@@ -40,8 +42,9 @@ function SpendRuleMaxAmountBase({policyID, maxAmount, currencyCode, onMaxAmountC
return (
diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx
index 421f643601b1..0b1e4a88a563 100644
--- a/src/components/TabSelector/TabSelectorItem.tsx
+++ b/src/components/TabSelector/TabSelectorItem.tsx
@@ -31,6 +31,8 @@ function TabSelectorItem({
sentryLabel,
equalWidth = false,
badgeText,
+ isBadgeCondensed = false,
+ badgeStyles,
isDisabled = false,
pendingAction,
}: TabSelectorItemProps) {
@@ -90,6 +92,8 @@ function TabSelectorItem({
)}
diff --git a/src/components/TabSelector/types.ts b/src/components/TabSelector/types.ts
index b32c41e37d15..9a80f4ac3aea 100644
--- a/src/components/TabSelector/types.ts
+++ b/src/components/TabSelector/types.ts
@@ -1,6 +1,6 @@
import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs';
// eslint-disable-next-line no-restricted-imports
-import type {Animated} from 'react-native';
+import type {Animated, StyleProp, ViewStyle} from 'react-native';
import type {ThemeColors} from '@styles/theme/types';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -39,6 +39,12 @@ type TabSelectorBaseItem = WithSentryLabel & {
/** Text to display on the badge on the tab. */
badgeText?: string;
+ /** Whether the tab's badge should use the condensed (smaller) style. */
+ isBadgeCondensed?: boolean;
+
+ /** Additional styles for the tab's badge. */
+ badgeStyles?: StyleProp;
+
/** Whether this tab is disabled */
isDisabled?: boolean;
@@ -70,6 +76,9 @@ type TabSelectorBaseProps = {
/** Whether tabs should have equal width. */
equalWidth?: boolean;
+
+ /** Additional styles for the tabs' scroll content container. */
+ contentContainerStyles?: StyleProp;
};
type TabSelectorItemProps = WithSentryLabel & {
@@ -112,6 +121,12 @@ type TabSelectorItemProps = WithSentryLabel & {
/** Text to display on the badge on the tab. */
badgeText?: string;
+ /** Whether the tab's badge should use the condensed (smaller) style. */
+ isBadgeCondensed?: boolean;
+
+ /** Additional styles for the tab's badge. */
+ badgeStyles?: StyleProp;
+
/** Whether this tab is disabled */
isDisabled?: boolean;
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index 89d86d71f8a8..c95436c9b7d7 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -149,6 +149,7 @@ function Table({filters, isItemInFilter});
const filteredData = filterMiddleware(data);
@@ -170,7 +172,16 @@ function Table({isItemInSearch});
const searchedData = searchMiddleware(filteredData);
- const {middleware: sortMiddleware, activeSorting, methods: sortMethods} = useSorting({compareItems, initialSortColumn});
+ const {
+ middleware: sortMiddleware,
+ activeSorting,
+ methods: sortMethods,
+ } = useSorting({
+ compareItems,
+ initialSortColumn,
+ narrowLayoutSortColumn,
+ shouldUseNarrowTableLayout,
+ });
const sortedData = sortMiddleware(searchedData);
const {middleware: selectionMiddleware, methods: selectionMethods, mobileSelectionModalRowKey} = useSelection({data: sortedData, selectedKeys, onRowSelectionChange});
@@ -206,7 +217,6 @@ function Table({data, selected
turnOffMobileSelectionMode();
}, [selectableKeys.length, isSelectionModeEnabled]);
+ const prevSelectionModeEnabledRef = useRef(isSelectionModeEnabled);
+ useEffect(() => {
+ if (prevSelectionModeEnabledRef.current && !isSelectionModeEnabled) {
+ onRowSelectionChange?.([]);
+ }
+ prevSelectionModeEnabledRef.current = isSelectionModeEnabled;
+ }, [isSelectionModeEnabled, onRowSelectionChange]);
+
/**
* Clear all of the currently selected keys
*/
diff --git a/src/components/Table/middlewares/sorting.ts b/src/components/Table/middlewares/sorting.ts
index 1b775fffa91e..a31344ed6fcc 100644
--- a/src/components/Table/middlewares/sorting.ts
+++ b/src/components/Table/middlewares/sorting.ts
@@ -1,4 +1,4 @@
-import {useState} from 'react';
+import {useMemo, useState} from 'react';
import type {SetStateAction} from 'react';
import type {Middleware, MiddlewareHookResult} from './types';
@@ -60,6 +60,8 @@ type SortingMethods = {
type UseSortingProps = {
compareItems?: CompareItemsCallback;
initialSortColumn?: ColumnKey;
+ narrowLayoutSortColumn?: ColumnKey;
+ shouldUseNarrowTableLayout?: boolean;
};
/**
@@ -82,14 +84,24 @@ type UseSortingResult = MiddlewareHookResu
* @param initialSortColumn - The initial column to sort by on mount.
* @returns The result of the sorting middleware.
*/
-function useSorting({compareItems, initialSortColumn}: UseSortingProps): UseSortingResult {
- const [activeSorting, updateSorting] = useState>({
+function useSorting({
+ compareItems,
+ initialSortColumn,
+ narrowLayoutSortColumn,
+ shouldUseNarrowTableLayout,
+}: UseSortingProps): UseSortingResult {
+ const [userSorting, setUserSorting] = useState>({
columnKey: initialSortColumn,
order: 'asc',
});
+ const activeSorting = useMemo(
+ () => (shouldUseNarrowTableLayout && narrowLayoutSortColumn ? ({columnKey: narrowLayoutSortColumn, order: 'asc'} satisfies ActiveSorting) : userSorting),
+ [shouldUseNarrowTableLayout, narrowLayoutSortColumn, userSorting],
+ );
+
const toggleColumnSorting: SortingMethods['toggleColumnSorting'] = (columnKey) => {
- updateSorting((previousSorting) => {
+ setUserSorting((previousSorting) => {
const columnKeyToUse = columnKey ?? previousSorting.columnKey;
const orderToUse = previousSorting.order === 'asc' ? 'desc' : 'asc';
@@ -107,7 +119,7 @@ function useSorting({compareItems, initial
const middleware: Middleware = (data) => sort({data, activeSorting, compareItems});
const methods: SortingMethods = {
- updateSorting,
+ updateSorting: setUserSorting,
toggleColumnSorting,
getActiveSorting,
};
diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts
index 834fad6a2ed9..bb1168abf553 100644
--- a/src/components/Table/types.ts
+++ b/src/components/Table/types.ts
@@ -138,6 +138,9 @@ type TableProps void;
+ dismissError: () => void;
+ onToggleEnabled: (value: boolean) => void;
+};
+
+type WorkspaceDistanceRatesTableRowProps = {
+ item: DistanceRateTableItemData;
+ rowIndex: number;
+ shouldUseNarrowTableLayout: boolean;
+ shouldShowDateColumns: boolean;
+ statusLabels: Record;
+};
+
+function formatDate(dateString: string | null | undefined): string {
+ if (!dateString) {
+ return '';
+ }
+ return format(parseISO(dateString), CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT);
+}
+
+function getRateStatusColors(status: string, theme: ReturnType, isSelected?: boolean) {
+ switch (status) {
+ case CONST.CUSTOM_UNITS.RATE_STATUS.ACTIVE:
+ return theme.reportStatusBadge.paid;
+ case CONST.CUSTOM_UNITS.RATE_STATUS.FUTURE:
+ return theme.reportStatusBadge.draft;
+ case CONST.CUSTOM_UNITS.RATE_STATUS.EXPIRED:
+ return theme.reportStatusBadge.outstanding;
+ case CONST.CUSTOM_UNITS.RATE_STATUS.INACTIVE:
+ default:
+ return {backgroundColor: isSelected ? theme.buttonHoveredBG : theme.badgeDefaultBG, textColor: theme.text};
+ }
+}
+
+function WorkspaceDistanceRatesTableRow({item, rowIndex, shouldUseNarrowTableLayout, shouldShowDateColumns, statusLabels}: WorkspaceDistanceRatesTableRowProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const Expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
+ const {processedData} = useTableContext();
+
+ const {rate, formattedRate, pendingAction, errors} = item;
+ const isDeleting = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
+ const isSelected = processedData.at(rowIndex)?.selected ?? false;
+
+ const status = getRateStatus(rate);
+ const statusColors = getRateStatusColors(status, theme, isSelected);
+ const dateLabelText = DistanceRequestUtils.getRateDateLabel({...rate, unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, translate);
+
+ const accessibilityLabel = [rate.name, statusLabels[status], formattedRate, dateLabelText].filter(Boolean).join(', ');
+
+ const reasonAttributes: SkeletonSpanReasonAttributes = {
+ context: 'WorkspaceDistanceRatesTableItem',
+ isDeleting,
+ };
+
+ return (
+
+ {({hovered}) => (
+ <>
+ {shouldUseNarrowTableLayout && (
+
+
+
+
+
+
+
+ )}
+
+ {!shouldUseNarrowTableLayout && (
+
+
+
+ )}
+
+ {!shouldUseNarrowTableLayout && (
+
+
+
+ )}
+
+ {!shouldUseNarrowTableLayout && (
+
+
+
+ )}
+
+ {!shouldUseNarrowTableLayout && shouldShowDateColumns && (
+
+
+
+ )}
+
+ {!shouldUseNarrowTableLayout && shouldShowDateColumns && (
+
+
+
+ )}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+export default WorkspaceDistanceRatesTableRow;
+export type {DistanceRateTableItemData};
diff --git a/src/components/Tables/WorkspaceDistanceRatesTable/index.tsx b/src/components/Tables/WorkspaceDistanceRatesTable/index.tsx
new file mode 100644
index 000000000000..9f978e980a97
--- /dev/null
+++ b/src/components/Tables/WorkspaceDistanceRatesTable/index.tsx
@@ -0,0 +1,150 @@
+import type {ListRenderItemInfo} from '@shopify/flash-list';
+import React, {useMemo} from 'react';
+import Table from '@components/Table';
+import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getRateStatus} from '@libs/PolicyDistanceRatesUtils';
+import tokenizedSearch from '@libs/tokenizedSearch';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import WorkspaceDistanceRatesTableRow from './WorkspaceDistanceRatesTableRow';
+import type {DistanceRateTableItemData} from './WorkspaceDistanceRatesTableRow';
+
+type DistanceRatesTableColumnKey = 'status' | 'name' | 'rate' | 'startDate' | 'endDate' | 'enabled' | 'actions';
+
+type WorkspaceDistanceRatesTableProps = {
+ ratesData: DistanceRateTableItemData[];
+ selectionEnabled: boolean;
+ selectedKeys: string[];
+ onRowSelectionChange: (selectedRowKeys: string[]) => void;
+};
+
+const STATUS_ORDER: Record = {
+ [CONST.CUSTOM_UNITS.RATE_STATUS.ACTIVE]: 0,
+ [CONST.CUSTOM_UNITS.RATE_STATUS.FUTURE]: 1,
+ [CONST.CUSTOM_UNITS.RATE_STATUS.EXPIRED]: 2,
+ [CONST.CUSTOM_UNITS.RATE_STATUS.INACTIVE]: 3,
+};
+
+function WorkspaceDistanceRatesTable({ratesData, selectionEnabled, selectedKeys, onRowSelectionChange}: WorkspaceDistanceRatesTableProps) {
+ const styles = useThemeStyles();
+ const {translate, localeCompare} = useLocalize();
+ const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
+ const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth;
+
+ const hasAnyDateBound = ratesData.some((item) => !!item.rate.startDate || !!item.rate.endDate);
+
+ const columns: Array> = [
+ {
+ key: 'status',
+ label: translate('workspace.distanceRates.status'),
+ sortable: true,
+ width: variables.tableStatusColumnWidth,
+ styling: {containerStyles: [styles.justifyContentCenter]},
+ },
+ {key: 'name', label: translate('common.name'), sortable: true},
+ {key: 'rate', label: translate('workspace.distanceRates.rate'), sortable: true},
+ ...(hasAnyDateBound
+ ? ([
+ {key: 'startDate', label: translate('workspace.distanceRates.startDate'), sortable: true},
+ {key: 'endDate', label: translate('workspace.distanceRates.endDate'), sortable: true},
+ ] as const)
+ : []),
+ {key: 'enabled', label: translate('common.enabled'), sortable: true, width: variables.tableSwitchColumnWidth, styling: {containerStyles: [styles.justifyContentEnd]}},
+ {key: 'actions', label: '', sortable: false, width: variables.tableCaretColumnWidth},
+ ];
+
+ const compareItems: CompareItemsCallback = (a, b, activeSorting) => {
+ const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1;
+
+ if (activeSorting.columnKey === 'status') {
+ const aStatus = getRateStatus(a.rate);
+ const bStatus = getRateStatus(b.rate);
+ const diff = (STATUS_ORDER[aStatus] ?? 0) - (STATUS_ORDER[bStatus] ?? 0);
+ if (diff !== 0) {
+ return diff * orderMultiplier;
+ }
+ return localeCompare(a.rate.name ?? '', b.rate.name ?? '') * orderMultiplier;
+ }
+
+ if (activeSorting.columnKey === 'name') {
+ return localeCompare(a.rate.name ?? '', b.rate.name ?? '') * orderMultiplier;
+ }
+
+ if (activeSorting.columnKey === 'rate') {
+ return ((a.rate.rate ?? 0) - (b.rate.rate ?? 0)) * orderMultiplier;
+ }
+
+ if (activeSorting.columnKey === 'startDate') {
+ return localeCompare(a.rate.startDate ?? '', b.rate.startDate ?? '') * orderMultiplier;
+ }
+
+ if (activeSorting.columnKey === 'endDate') {
+ return localeCompare(a.rate.endDate ?? '', b.rate.endDate ?? '') * orderMultiplier;
+ }
+
+ if (activeSorting.columnKey === 'enabled') {
+ return ((a.enabled ? 1 : 0) - (b.enabled ? 1 : 0)) * orderMultiplier;
+ }
+
+ return 0;
+ };
+
+ const isItemInSearch: IsItemInSearchCallback = (item, searchString) => {
+ const matchingItems = tokenizedSearch([item], searchString, (i) => [i.rate.name ?? '', i.formattedRate]);
+ return matchingItems.length > 0;
+ };
+
+ const statusLabels = useMemo(
+ () => ({
+ [CONST.CUSTOM_UNITS.RATE_STATUS.ACTIVE]: translate('workspace.distanceRates.statusActive'),
+ [CONST.CUSTOM_UNITS.RATE_STATUS.FUTURE]: translate('workspace.distanceRates.statusFuture'),
+ [CONST.CUSTOM_UNITS.RATE_STATUS.EXPIRED]: translate('workspace.distanceRates.statusExpired'),
+ [CONST.CUSTOM_UNITS.RATE_STATUS.INACTIVE]: translate('workspace.distanceRates.statusInactive'),
+ }),
+ [translate],
+ );
+
+ const renderItem = ({item, index}: ListRenderItemInfo) => (
+
+ );
+
+ const isEmpty = ratesData.length === 0;
+ const shouldShowSearchBar = ratesData.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
+
+ return (
+ item.keyForList}
+ compareItems={compareItems}
+ isItemInSearch={isItemInSearch}
+ initialSortColumn="name"
+ narrowLayoutSortColumn="name"
+ title={translate('workspace.common.distanceRates')}
+ >
+ {!isEmpty && (
+ <>
+ {shouldShowSearchBar && }
+
+
+ >
+ )}
+
+ );
+}
+
+export default WorkspaceDistanceRatesTable;
diff --git a/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx b/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx
index aac049417db3..b7afb850a51b 100644
--- a/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx
+++ b/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import {View} from 'react-native';
-import Avatar from '@components/Avatar';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import type {TableData} from '@components/Table';
@@ -11,7 +10,6 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -22,15 +20,6 @@ type WorkspaceRoomRowData = TableData & {
/** The room display name */
name: string;
- /** Owner accountID for resolving the avatar */
- ownerAccountID?: number;
-
- /** Owner avatar source */
- ownerAvatar?: AvatarSource;
-
- /** Pre-formatted owner display name */
- ownerDisplayName: string;
-
/** Number of members in the room */
memberCount: number;
@@ -59,7 +48,6 @@ function WorkspaceRoomsTableRow({item, rowIndex, shouldUseNarrowTableLayout, sho
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
const memberCountSubtitle = translate('domain.groups.memberCount', {count: item.memberCount});
- const narrowSubtitle = item.ownerDisplayName ? `${translate('common.createdBy')}: ${item.ownerDisplayName} • ${memberCountSubtitle}` : memberCountSubtitle;
return (
- {narrowSubtitle}
+ {memberCountSubtitle}
-
- {!!item.ownerDisplayName && (
- <>
- {!!item.ownerAccountID && (
-
- )}
-
- >
- )}
-
-
{item.memberCount}
diff --git a/src/components/Tables/WorkspaceRoomsTable/index.tsx b/src/components/Tables/WorkspaceRoomsTable/index.tsx
index b8466bc88907..2030d43de85e 100644
--- a/src/components/Tables/WorkspaceRoomsTable/index.tsx
+++ b/src/components/Tables/WorkspaceRoomsTable/index.tsx
@@ -10,7 +10,7 @@ import variables from '@styles/variables';
import WorkspaceRoomsTableRow from './WorkspaceRoomsTableRow';
import type {WorkspaceRoomRowData} from './WorkspaceRoomsTableRow';
-type WorkspaceRoomsTableColumnKey = 'name' | 'createdBy' | 'members' | 'actions';
+type WorkspaceRoomsTableColumnKey = 'name' | 'members' | 'actions';
type WorkspaceRoomsTableProps = {
/** Pre-built row data for each room */
@@ -46,7 +46,6 @@ function WorkspaceRoomsTable({rooms, highlightedReportID}: WorkspaceRoomsTablePr
const columns: Array> = [
{key: 'name', label: translate('common.name'), sortable: true},
- {key: 'createdBy', label: translate('common.createdBy'), sortable: true},
{key: 'members', label: translate('common.members'), width: variables.workspaceRoomsMembersColumnWidth, sortable: true},
{key: 'actions', label: '', width: variables.workspaceRoomsActionsColumnWidth, styling: {containerStyles: [styles.justifyContentEnd, styles.pr3]}, sortable: false},
];
@@ -54,10 +53,6 @@ function WorkspaceRoomsTable({rooms, highlightedReportID}: WorkspaceRoomsTablePr
const compareItems: CompareItemsCallback = (a, b, activeSorting) => {
const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1;
- if (activeSorting.columnKey === 'createdBy') {
- return orderMultiplier * localeCompare(a.ownerDisplayName, b.ownerDisplayName);
- }
-
if (activeSorting.columnKey === 'members') {
return orderMultiplier * (a.memberCount - b.memberCount);
}
diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx
index 8e139d3c8a01..434ae4466905 100644
--- a/src/components/TagPicker/index.tsx
+++ b/src/components/TagPicker/index.tsx
@@ -161,20 +161,20 @@ function TagPicker({
ref: inputCallbackRef as (ref: BaseTextInputRef | null) => void,
};
- const listItemTitleStyles = [styles.breakAll, styles.w100];
-
return (
= CONST.STANDARD_LIST_ITEM_LIMIT}
initiallyFocusedItemKey={selectedOptionKey}
onSelectRow={onSubmit}
+ isRowMultilineSupported
+ titleNumberOfLines={CONST.TRANSACTION_TAG_AND_CATEGORY_PICKER_MAX_TITLE_LINES}
/>
);
}
diff --git a/src/components/TransactionItemRow/DataCells/TotalCell.tsx b/src/components/TransactionItemRow/DataCells/TotalCell.tsx
index 18ec1cd2060c..4f910c6faa3b 100644
--- a/src/components/TransactionItemRow/DataCells/TotalCell.tsx
+++ b/src/components/TransactionItemRow/DataCells/TotalCell.tsx
@@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {convertToBackendAmount, convertToFrontendAmountAsString, getCurrencyDecimals, sanitizeCurrencyCode} from '@libs/CurrencyUtils';
import {formatToParts} from '@libs/NumberFormatUtils';
import {parseFloatAnyLocale, roundToTwoDecimalPlaces} from '@libs/NumberUtils';
-import {isPaidGroupPolicy} from '@libs/PolicyUtils';
+import {isGroupPolicy} from '@libs/PolicyUtils';
import {isExpenseReport, isInvoiceReport, shouldEnableNegative} from '@libs/ReportUtils';
import {getAmount as getTransactionAmount, getCurrency as getTransactionCurrency, isDeletedTransaction, isExpenseUnreported, isScanning} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
@@ -47,7 +47,7 @@ function TotalCell({shouldShowTooltip, transactionItem, canEdit, onSave, report,
const effectiveReport = report ?? transactionItem.report;
const effectivePolicy = policy ?? transactionItem.policy;
const isDeleted = isDeletedTransaction(transactionItem);
- const isFromExpenseReport = (!isEmptyObject(effectiveReport) && isExpenseReport(effectiveReport)) || isPaidGroupPolicy(effectivePolicy);
+ const isFromExpenseReport = (!isEmptyObject(effectiveReport) && isExpenseReport(effectiveReport)) || isGroupPolicy(effectivePolicy);
const amount = getTransactionAmount(transactionItem, isFromExpenseReport, transactionItem.reportID === CONST.REPORT.UNREPORTED_REPORT_ID, isDeleted);
let amountToDisplay = convertToDisplayString(amount, currency);
if (isScanning(transactionItem)) {
diff --git a/src/components/TransactionItemRow/TransactionItemRowWide.tsx b/src/components/TransactionItemRow/TransactionItemRowWide.tsx
index 9c09063f9214..3507989a14b1 100644
--- a/src/components/TransactionItemRow/TransactionItemRowWide.tsx
+++ b/src/components/TransactionItemRow/TransactionItemRowWide.tsx
@@ -113,6 +113,7 @@ function TransactionItemRowWide({
totalPerAttendee,
transactionThreadReportID,
createdAt,
+ isMarkAsDone,
}: TransactionItemRowWideProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -304,6 +305,7 @@ function TransactionItemRowWide({
hash={transactionItem?.hash}
amount={report?.total}
shouldDisablePointerEvents={isDisabled}
+ isMarkAsDone={isMarkAsDone}
/>
)}
diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx
index ae0024e52a13..cebbc85f037d 100644
--- a/src/components/TransactionItemRow/index.tsx
+++ b/src/components/TransactionItemRow/index.tsx
@@ -1,12 +1,14 @@
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import useAttendees from '@hooks/useAttendees';
import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {getCompanyCardDescription} from '@libs/CardUtils';
import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils';
import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
-import {isExpenseReport, isSettled} from '@libs/ReportUtils';
+import {isExpenseReport, isSettled, shouldShowMarkAsDone} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {
getAmount,
@@ -22,6 +24,7 @@ import {
} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
import TransactionItemRowNarrow from './TransactionItemRowNarrow';
import TransactionItemRowWide from './TransactionItemRowWide';
import type {TransactionItemRowProps, TransactionWithOptionalSearchFields} from './types';
@@ -97,6 +100,12 @@ function TransactionItemRow({
canEditAmount,
canEditTag,
}: TransactionItemRowProps) {
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
+ const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({
+ policy,
+ report,
+ isTrackIntentUser,
+ });
const styles = useThemeStyles();
const {translate} = useLocalize();
const createdAt = getTransactionCreated(transactionItem);
@@ -253,6 +262,7 @@ function TransactionItemRow({
totalPerAttendee={!attendeesCount || totalAmount === undefined ? undefined : totalAmount / attendeesCount}
createdAt={createdAt}
transactionThreadReportID={transactionThreadReportID}
+ isMarkAsDone={shouldUseMarkAsDoneCopy}
/>
);
}
diff --git a/src/components/TransactionItemRow/types.ts b/src/components/TransactionItemRow/types.ts
index 747228ef6845..82e5001c6d79 100644
--- a/src/components/TransactionItemRow/types.ts
+++ b/src/components/TransactionItemRow/types.ts
@@ -140,6 +140,7 @@ type TransactionItemRowWideComputedData = Omit {
- if (item.value === CONST.POLICY.ROLE.AUDITOR && !isPolicyControl) {
+ if (item.value === CONST.POLICY.ROLE.AUDITOR && (!isPolicyControl || !canAssignElevatedRoles)) {
+ return false;
+ }
+ if (item.value === CONST.POLICY.ROLE.CARD_ADMIN && (!isPolicyControl || !canAssignElevatedRoles)) {
return false;
}
- if (item.value === CONST.POLICY.ROLE.ADMIN && !canAssignAdminRole) {
+ if (item.value === CONST.POLICY.ROLE.ADMIN && !canAssignElevatedRoles) {
return false;
}
return true;
diff --git a/src/components/utils/getActionBadgeText.ts b/src/components/utils/getActionBadgeText.ts
new file mode 100644
index 000000000000..79d5105c6dfc
--- /dev/null
+++ b/src/components/utils/getActionBadgeText.ts
@@ -0,0 +1,11 @@
+import type {OptionData} from '@libs/ReportUtils';
+import type {TranslationPaths} from '@src/languages/types';
+
+function getActionBadgeText(actionBadge: OptionData['actionBadge'], translate: (key: TranslationPaths) => string, isMarkAsDone?: boolean): string {
+ if (!actionBadge) {
+ return '';
+ }
+ return isMarkAsDone ? translate('common.markAsDone') : translate(`common.actionBadge.${actionBadge}`);
+}
+
+export default getActionBadgeText;
diff --git a/src/hooks/useAddAgentToApprovalWorkflow.ts b/src/hooks/useAddAgentToApprovalWorkflow.ts
deleted file mode 100644
index e34bc1cac936..000000000000
--- a/src/hooks/useAddAgentToApprovalWorkflow.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import Navigation from '@libs/Navigation/Navigation';
-import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils';
-import type {AvatarSource} from '@libs/UserAvatarUtils';
-import {addMembersToWorkspace} from '@userActions/Policy/Member';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import type {Policy} from '@src/types/onyx';
-import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
-import useAllPolicyExpenseChatReportActions from './useAllPolicyExpenseChatReportActions';
-import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
-import useLocalize from './useLocalize';
-import useOnyx from './useOnyx';
-
-type OwnedAgent = {accountID: number; email: string; displayName: string; avatar?: AvatarSource};
-
-/**
- * Returns a handler that seeds an approval workflow with the current user's first available
- * owned agent and navigates to the approver edit screen. When every owned agent is already
- * an approver on the workflow, the handler routes the admin to the create-agent screen so
- * we don't duplicate an existing agent.
- */
-function useAddAgentToApprovalWorkflow(policy: OnyxEntry, policyID: string) {
- const {formatPhoneNumber} = useLocalize();
- const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const [agentPrompts] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT);
- const filteredReportActions = useAllPolicyExpenseChatReportActions();
-
- const ownedAgents: OwnedAgent[] = (() => {
- if (!agentPrompts || !personalDetails) {
- return [];
- }
- return Object.keys(agentPrompts)
- .map((key) => {
- const accountID = Number(key.slice(ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT.length));
- const details = personalDetails[accountID];
- if (!details?.login) {
- return null;
- }
- return {
- accountID,
- email: details.login,
- displayName: details.displayName ?? details.login,
- avatar: details.avatar,
- };
- })
- .filter((agent): agent is OwnedAgent => !!agent);
- })();
-
- return (workflow: ApprovalWorkflow) => {
- const workflowApproverEmail = workflow.approvers.at(0)?.email ?? '';
-
- // Prefer the first owned agent that isn't already an approver on this workflow.
- // If every owned agent is already in the workflow there's nothing to seed, so we
- // route the admin to create a new agent instead of duplicating an existing one.
- const agentToSeed = ownedAgents.find((candidate) => !workflow.approvers.some((approver) => approver.email === candidate.email));
- if (!agentToSeed) {
- Navigation.navigate(
- ROUTES.WORKSPACE_WORKFLOWS_ADD_AGENT.getRoute({
- policyID,
- workflowApproverEmail,
- }),
- );
- return;
- }
-
- // Ensure the agent is a workspace member before they show up as an approver. The
- // server makes this idempotent so it's safe to call even if the agent was added via
- // the API's `policyID` parameter when CREATE_AGENT was first called.
- const isAlreadyMember = !!policy?.employeeList?.[agentToSeed.email];
- if (!isAlreadyMember && policy) {
- const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy.employeeList, false, false));
- addMembersToWorkspace(
- {[agentToSeed.email]: agentToSeed.accountID},
- '',
- policy,
- policyMemberAccountIDs,
- CONST.POLICY.ROLE.USER,
- formatPhoneNumber,
- {
- accountID: currentUserPersonalDetails.accountID,
- displayName: currentUserPersonalDetails.displayName,
- email: currentUserPersonalDetails.login,
- avatar: currentUserPersonalDetails.avatar,
- },
- filteredReportActions,
- );
- }
-
- Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyID, workflowApproverEmail, agentToSeed.email));
- };
-}
-
-export default useAddAgentToApprovalWorkflow;
diff --git a/src/hooks/useCanWriteCardSpendRules.ts b/src/hooks/useCanWriteCardSpendRules.ts
new file mode 100644
index 000000000000..242d171b6ecf
--- /dev/null
+++ b/src/hooks/useCanWriteCardSpendRules.ts
@@ -0,0 +1,13 @@
+import {canMemberWrite} from '@libs/PolicyUtils';
+import CONST from '@src/CONST';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import usePolicy from './usePolicy';
+
+function useCanWriteCardSpendRules(policyID: string | undefined): boolean {
+ const policy = usePolicy(policyID);
+ const {login = ''} = useCurrentUserPersonalDetails();
+
+ return canMemberWrite(policy, login, CONST.POLICY.POLICY_FEATURE.RULES) || canMemberWrite(policy, login, CONST.POLICY.POLICY_FEATURE.EXPENSIFY_CARD);
+}
+
+export default useCanWriteCardSpendRules;
diff --git a/src/hooks/useCreateReport.tsx b/src/hooks/useCreateReport.tsx
index 91e545b6aa5a..0b0225de3bec 100644
--- a/src/hooks/useCreateReport.tsx
+++ b/src/hooks/useCreateReport.tsx
@@ -3,7 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import Navigation from '@libs/Navigation/Navigation';
-import {getDefaultChatEnabledPolicy, isPaidGroupPolicy} from '@libs/PolicyUtils';
+import {getDefaultChatEnabledPolicy, isGroupPolicy} from '@libs/PolicyUtils';
import {generateReportID} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import CONST from '@src/CONST';
@@ -87,15 +87,13 @@ export default function useCreateReport({
const freshReportID = generateReportID();
const freshTransactionID = generateReportID();
Navigation.navigate(
- createDynamicRoute(
- DYNAMIC_ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
- action: CONST.IOU.ACTION.CREATE,
- iouType: CONST.IOU.TYPE.CREATE,
- transactionID: freshTransactionID,
- reportID: freshReportID,
- upgradePath: CONST.UPGRADE_PATHS.REPORTS,
- }),
- ),
+ ROUTES.MONEY_REQUEST_UPGRADE.getRoute({
+ action: CONST.IOU.ACTION.CREATE,
+ iouType: CONST.IOU.TYPE.CREATE,
+ transactionID: freshTransactionID,
+ reportID: freshReportID,
+ upgradePath: CONST.UPGRADE_PATHS.REPORTS,
+ }),
);
return;
}
@@ -106,7 +104,7 @@ export default function useCreateReport({
// at least 2 non-personal workspaces to choose between. Also fall back to the selector if
// the default is billing-restricted and alternatives exist, so the user isn't dead-ended
// on the restricted-action page.
- const isDefaultPersonal = !activePolicy || activePolicy.type === CONST.POLICY.TYPE.PERSONAL || !isPaidGroupPolicy(activePolicy);
+ const isDefaultPersonal = !activePolicy || activePolicy.type === CONST.POLICY.TYPE.PERSONAL || !isGroupPolicy(activePolicy);
const hasMultipleNonPersonalWorkspaces = groupPoliciesWithChatEnabled.length > 1;
const isDefaultBillingRestricted =
!!workspaceIDForReportCreation && shouldRestrictUserBillableActions(defaultChatEnabledPolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID);
diff --git a/src/hooks/useDefaultExpensePolicy.tsx b/src/hooks/useDefaultExpensePolicy.tsx
index 32630fafff88..850004e037d9 100644
--- a/src/hooks/useDefaultExpensePolicy.tsx
+++ b/src/hooks/useDefaultExpensePolicy.tsx
@@ -1,5 +1,5 @@
import type {OnyxCollection} from 'react-native-onyx';
-import {isPaidGroupPolicy, isPolicyAccessible} from '@libs/PolicyUtils';
+import {isGroupPolicy, isPolicyAccessible} from '@libs/PolicyUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
@@ -17,7 +17,7 @@ function getSingleGroupPolicyID(policies: OnyxCollection, login: string)
let singlePolicyID: string | undefined;
for (const policy of Object.values(policies)) {
- if (!policy || !isPaidGroupPolicy(policy) || !isPolicyAccessible(policy, login)) {
+ if (!policy || !isGroupPolicy(policy) || !isPolicyAccessible(policy, login)) {
continue;
}
if (!singlePolicyID) {
@@ -45,11 +45,11 @@ export default function useDefaultExpensePolicy() {
// Per-key lookup for the single group policy (only fires when that specific policy changes)
const [singleGroupPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${singleGroupPolicyID}`);
- if (isRestrictedToPreferredPolicy && isPaidGroupPolicy(preferredPolicy) && isPolicyAccessible(preferredPolicy, login)) {
+ if (isRestrictedToPreferredPolicy && isGroupPolicy(preferredPolicy) && isPolicyAccessible(preferredPolicy, login)) {
return preferredPolicy;
}
- if (isPaidGroupPolicy(activePolicy) && isPolicyAccessible(activePolicy, login)) {
+ if (isGroupPolicy(activePolicy) && isPolicyAccessible(activePolicy, login)) {
return activePolicy;
}
diff --git a/src/hooks/useDeferredAgentWorkflowReconciliation.ts b/src/hooks/useDeferredAgentWorkflowReconciliation.ts
deleted file mode 100644
index dc1fc17da549..000000000000
--- a/src/hooks/useDeferredAgentWorkflowReconciliation.ts
+++ /dev/null
@@ -1,176 +0,0 @@
-import {useEffect, useRef} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {buildDeferredAgentWorkflowSaveKey, clearDeferredAgentWorkflowSave, updateApprovalWorkflow} from '@libs/actions/Workflow';
-import {resolveOptimisticAgent} from '@libs/WorkflowUtils';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy} from '@src/types/onyx';
-import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import useOnyx from './useOnyx';
-
-type ApprovalWorkflowWithRouting = ApprovalWorkflow & {routingFirstApproverEmail: string};
-
-/**
- * Overlays deferred-save approval workflows on top of the server's view and reconciles them
- * once the pending agent's real identity lands.
- *
- * - Overlay: when the admin saves an Edit Approvers page while CREATE_AGENT is still in
- * flight, we stash the workflow in `DEFERRED_AGENT_WORKFLOW_SAVES`. This hook merges that
- * stashed approver chain into the workflows list so the new agent shows up faded in the
- * workflow card. Each workflow also gets a `routingFirstApproverEmail` so the edit route /
- * list key keep using the original (pre-overlay) email even when the overlaid `approvers[0]`
- * is a pending agent with an empty `email` placeholder. A failed `CREATE_AGENT` surfaces
- * `policy.errorFields[ADD_AGENT]` on the pending approver row so the admin sees an RBR
- * right where the optimistic agent is shown.
- * - Reconcile: once the pending agent has a real email (the personal detail at
- * `pendingAgentAccountID` gains a login OR `OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING` echoes a
- * real accountID OR a prompt-match resolves to a known member), we swap that email into
- * the workflow, fire the real `updateApprovalWorkflow`, and clear the entry.
- */
-function useDeferredAgentWorkflowReconciliation(rawApprovalWorkflows: ApprovalWorkflow[], policy: OnyxEntry, policyID: string): ApprovalWorkflowWithRouting[] {
- const [deferredAgentWorkflowSaves] = useOnyx(ONYXKEYS.DEFERRED_AGENT_WORKFLOW_SAVES);
- const [agentPrompts] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT);
- const [optimisticAgentAccountIDMapping] = useOnyx(ONYXKEYS.OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING);
- const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
-
- const addAgentErrors = policy?.errorFields?.[CONST.POLICY.COLLECTION_KEYS.ADD_AGENT];
-
- const approvalWorkflows: ApprovalWorkflowWithRouting[] = (() => {
- if (!deferredAgentWorkflowSaves || isEmptyObject(deferredAgentWorkflowSaves)) {
- return rawApprovalWorkflows.map((workflow) => ({...workflow, routingFirstApproverEmail: workflow.approvers.at(0)?.email ?? ''}));
- }
- return rawApprovalWorkflows.map((workflow) => {
- const firstApproverEmail = workflow.approvers.at(0)?.email ?? '';
- if (!firstApproverEmail) {
- return {...workflow, routingFirstApproverEmail: firstApproverEmail};
- }
- const deferredEntry = deferredAgentWorkflowSaves[buildDeferredAgentWorkflowSaveKey(policyID, firstApproverEmail)];
- if (!deferredEntry) {
- return {...workflow, routingFirstApproverEmail: firstApproverEmail};
- }
- const overlaidApprovers = deferredEntry.approvalWorkflow.approvers
- .filter((approver): approver is NonNullable => !!approver)
- .map((approver) =>
- addAgentErrors && approver.accountID === deferredEntry.pendingAgentAccountID && approver.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD
- ? {...approver, errors: addAgentErrors}
- : approver,
- );
- return {
- ...workflow,
- approvers: overlaidApprovers,
- routingFirstApproverEmail: firstApproverEmail,
- };
- });
- })();
-
- // Tracks deferred-save keys we've already reconciled in this mount. `updateApprovalWorkflow`'s
- // optimistic write to `policy.employeeList` re-renders the page before the matching
- // `clearDeferredAgentWorkflowSave` Onyx merge settles; without this guard the effect would
- // fire `updateApprovalWorkflow` (and the underlying `UPDATE_WORKSPACE_APPROVAL` API call)
- // a second time for the same entry on that intermediate render.
- const reconciledDeferredKeysRef = useRef>(new Set());
-
- useEffect(() => {
- // Drop any tracked keys whose Onyx entries are gone (clear has settled or the entry was
- // removed elsewhere) so the same key can be reconciled again if it ever reappears.
- if (reconciledDeferredKeysRef.current.size > 0) {
- for (const key of [...reconciledDeferredKeysRef.current]) {
- if (!deferredAgentWorkflowSaves?.[key]) {
- reconciledDeferredKeysRef.current.delete(key);
- }
- }
- }
- if (!deferredAgentWorkflowSaves || isEmptyObject(deferredAgentWorkflowSaves) || !policy || !personalDetails) {
- return;
- }
- for (const deferredEntry of Object.values(deferredAgentWorkflowSaves)) {
- if (!deferredEntry || deferredEntry.policyID !== policyID) {
- continue;
- }
- const deferredKey = buildDeferredAgentWorkflowSaveKey(deferredEntry.policyID, deferredEntry.firstApproverEmail);
- if (reconciledDeferredKeysRef.current.has(deferredKey)) {
- continue;
- }
- const pendingApprover = deferredEntry.approvalWorkflow.approvers.find(
- (approver) => approver?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && approver.accountID === deferredEntry.pendingAgentAccountID,
- );
- if (!pendingApprover) {
- // Nothing left to wait on — the save can fire as-is (or was already fired).
- reconciledDeferredKeysRef.current.add(deferredKey);
- clearDeferredAgentWorkflowSave(deferredEntry.policyID, deferredEntry.firstApproverEmail);
- continue;
- }
- const knownEmails = new Set(
- deferredEntry.approvalWorkflow.approvers
- .filter((approver): approver is NonNullable => !!approver?.email && approver.accountID !== deferredEntry.pendingAgentAccountID)
- .map((approver) => approver.email),
- );
- const resolved = resolveOptimisticAgent({
- optimisticAccountID: deferredEntry.pendingAgentAccountID,
- pendingAgentPrompt: deferredEntry.pendingAgentPrompt,
- personalDetails,
- employeeList: policy.employeeList,
- agentPrompts,
- optimisticAccountIDMapping: optimisticAgentAccountIDMapping,
- knownApproverEmails: knownEmails,
- });
- if (!resolved) {
- continue;
- }
- const {accountID: resolvedAccountID, email: resolvedEmail} = resolved;
- // Upgrade the deferred workflow with the real email, then fire the save action.
- // `updateApprovalWorkflow` expects `ApprovalWorkflow` (approvers required, no
- // editor-only fields), so filter out the holes in the Onyx representation and
- // hand off only the fields it cares about.
- const upgradedApprovers = deferredEntry.approvalWorkflow.approvers
- .filter((approver): approver is NonNullable => !!approver)
- .map((approver) =>
- approver === pendingApprover
- ? {
- ...approver,
- email: resolvedEmail,
- accountID: resolvedAccountID,
- pendingAction: undefined,
- }
- : approver,
- );
- // When the saved workflow is the default one, the new agent becomes the new default
- // approver. The deferred snapshot's `members` list was captured before CREATE_AGENT
- // resolved, so the agent isn't in it — and `convertApprovalWorkflowToPolicyEmployees`
- // only rewrites `submitsTo` for emails present in `members`. Without the agent in
- // that list their `submitsTo` (set to the previous default approver by
- // `shareWithEmployees` in CreateAgent.cpp) is never overwritten, leaving them as
- // their own orphan submission group in the workflows list. Injecting them here
- // mirrors the online editor's behaviour (where the agent is already in employeeList
- // when the editor renders and falls naturally into the "Everyone" members list) so
- // the agent ends up submitting to themselves like every other default approver.
- const upgradedMembers =
- deferredEntry.approvalWorkflow.isDefault && !deferredEntry.approvalWorkflow.members.some((member) => member.email === resolvedEmail)
- ? [
- ...deferredEntry.approvalWorkflow.members,
- {
- email: resolvedEmail,
- displayName: pendingApprover.displayName,
- avatar: pendingApprover.avatar,
- },
- ]
- : deferredEntry.approvalWorkflow.members;
- const upgradedWorkflow = {
- members: upgradedMembers,
- approvers: upgradedApprovers,
- isDefault: deferredEntry.approvalWorkflow.isDefault,
- };
- const initialApprovers = deferredEntry.initialApprovalWorkflow.approvers.filter((approver): approver is NonNullable => !!approver);
- const membersToRemove = deferredEntry.initialApprovalWorkflow.members.filter((initialMember) => !upgradedWorkflow.members.some((member) => member.email === initialMember.email));
- const approversToRemove = initialApprovers.filter((initialApprover) => !upgradedWorkflow.approvers.some((approver) => approver.email === initialApprover.email));
- reconciledDeferredKeysRef.current.add(deferredKey);
- updateApprovalWorkflow(upgradedWorkflow, membersToRemove, approversToRemove, policy);
- clearDeferredAgentWorkflowSave(deferredEntry.policyID, deferredEntry.firstApproverEmail);
- }
- }, [deferredAgentWorkflowSaves, policy, personalDetails, policyID, agentPrompts, optimisticAgentAccountIDMapping]);
-
- return approvalWorkflows;
-}
-
-export default useDeferredAgentWorkflowReconciliation;
diff --git a/src/hooks/useExpandCollapseAnimation.ts b/src/hooks/useExpandCollapseAnimation.ts
new file mode 100644
index 000000000000..7fca790c5444
--- /dev/null
+++ b/src/hooks/useExpandCollapseAnimation.ts
@@ -0,0 +1,49 @@
+import {useState} from 'react';
+import type {LayoutChangeEvent} from 'react-native';
+import {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
+import {scheduleOnRN} from 'react-native-worklets';
+import {easing} from '@components/Modal/ReanimatedModal/utils';
+
+const EXPAND_COLLAPSE_DURATION = 300;
+
+function useExpandCollapseAnimation(isExpanded: boolean) {
+ const contentHeight = useSharedValue(0);
+ const hasExpanded = useSharedValue(isExpanded);
+ const [isRendered, setIsRendered] = useState(isExpanded);
+
+ hasExpanded.set(isExpanded);
+ // Matches the pattern in AnimatedCollapsible — mount content synchronously on expand,
+ // unmount asynchronously after collapse animation via scheduleOnRN callback.
+ if (isExpanded && !isRendered) {
+ setIsRendered(true);
+ }
+
+ const animatedHeight = useDerivedValue(() => {
+ if (!contentHeight.get()) {
+ return 0;
+ }
+ const target = hasExpanded.get() ? contentHeight.get() : 0;
+ return withTiming(target, {duration: EXPAND_COLLAPSE_DURATION, easing}, (finished) => {
+ if (!finished || target) {
+ return;
+ }
+ scheduleOnRN(setIsRendered, false);
+ });
+ }, []);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ height: animatedHeight.get(),
+ overflow: 'hidden',
+ }));
+
+ const onLayout = (e: LayoutChangeEvent) => {
+ const height = e.nativeEvent.layout.height;
+ if (height) {
+ contentHeight.set(height);
+ }
+ };
+
+ return {isRendered, animatedStyle, onLayout};
+}
+
+export default useExpandCollapseAnimation;
diff --git a/src/hooks/useExpenseActions.ts b/src/hooks/useExpenseActions.ts
index d91876bcb581..b7fcb7b92195 100644
--- a/src/hooks/useExpenseActions.ts
+++ b/src/hooks/useExpenseActions.ts
@@ -510,24 +510,33 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
);
const deleteNavigateBackUrl = goBackRoute ?? backTo ?? Navigation.getActiveRoute();
setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl);
+ // The wide RHP close animation was changed, and because of that the expense was deleted right after
+ // the animation finished, which caused flickering in the expenses list. We want the user to see
+ // the expense in the list for a little bit longer, so we wait for the animation to finish and then
+ // add an additional delay before removing it.
+ // See https://github.com/Expensify/App/issues/92036
+ const afterTransition = () => {
+ setTimeout(() => {
+ const deleteResult = deleteTransactions(
+ [transaction.transactionID],
+ duplicateTransactions,
+ duplicateTransactionViolations,
+ isReportInSearch ? currentSearchHash : undefined,
+ false,
+ );
+
+ if (deleteResult.action === 'redirected') {
+ return;
+ }
+
+ removeTransaction(transaction.transactionID);
+ }, CONST.EXPENSE_REPORT_DELETE_DELAY_MS);
+ };
if (goBackRoute) {
- navigateOnDeleteExpense(goBackRoute);
+ navigateOnDeleteExpense(goBackRoute, afterTransition);
+ } else {
+ afterTransition();
}
- InteractionManager.runAfterInteractions(() => {
- const deleteResult = deleteTransactions(
- [transaction.transactionID],
- duplicateTransactions,
- duplicateTransactionViolations,
- isReportInSearch ? currentSearchHash : undefined,
- false,
- );
-
- if (deleteResult.action === 'redirected') {
- return;
- }
-
- removeTransaction(transaction.transactionID);
- });
}
return;
}
@@ -546,19 +555,27 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
const deleteNavigateBackUrl = backToRoute ?? Navigation.getActiveRoute();
setDeleteTransactionNavigateBackUrl(deleteNavigateBackUrl);
+ // The wide RHP close animation was changed, and because of that the report was deleted right after
+ // the animation finished, which caused flickering in the reports list. We want the user to see
+ // the report in the list for a little bit longer, so we wait for the animation to finish and then
+ // add an additional delay before removing it.
+ // See https://github.com/Expensify/App/issues/92036
Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.goBack(backToRoute);
- InteractionManager.runAfterInteractions(() => {
- deleteAppReport({
- report: moneyRequestReport,
- selfDMReport,
- currentUserEmailParam: email ?? '',
- currentUserAccountIDParam: accountID,
- reportTransactions,
- allTransactionViolations,
- bankAccountList,
- hash: currentSearchHash,
- });
+ Navigation.goBack(backToRoute, {
+ afterTransition: () => {
+ setTimeout(() => {
+ deleteAppReport({
+ report: moneyRequestReport,
+ selfDMReport,
+ currentUserEmailParam: email ?? '',
+ currentUserAccountIDParam: accountID,
+ reportTransactions,
+ allTransactionViolations,
+ bankAccountList,
+ hash: currentSearchHash,
+ });
+ }, CONST.EXPENSE_REPORT_DELETE_DELAY_MS);
+ },
});
});
},
diff --git a/src/hooks/useHRSyncResultsModal.ts b/src/hooks/useHRSyncResultsModal.ts
index 8a45dff48a74..e7ef15042448 100644
--- a/src/hooks/useHRSyncResultsModal.ts
+++ b/src/hooks/useHRSyncResultsModal.ts
@@ -1,10 +1,13 @@
-import {useEffect} from 'react';
+import {isModalActiveSelector} from '@selectors/Modal';
+import {useEffect, useEffectEvent, useRef} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
-import type {TupleToUnion} from 'type-fest';
import HRSyncResultsModal from '@components/HRSyncResultsModal';
import {useModal} from '@components/Modal/Global/ModalContext';
+import TransitionTracker from '@libs/Navigation/TransitionTracker';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {PolicyConnectionSyncProgress} from '@src/types/onyx/Policy';
+import useOnyx from './useOnyx';
import usePrevious from './usePrevious';
/**
@@ -14,37 +17,55 @@ import usePrevious from './usePrevious';
function useHRSyncResultsModal(policyID: string, connectionSyncProgress: OnyxEntry, isFocused: boolean) {
const modal = useModal();
const previousSyncProgress = usePrevious(connectionSyncProgress);
+ const pendingSyncResultRef = useRef | null>(null);
+ const [isAnyModalActive] = useOnyx(ONYXKEYS.MODAL, {selector: isModalActiveSelector});
const connectionName = connectionSyncProgress?.connectionName;
+ const showSyncResultsModal = useEffectEvent((syncResult: PolicyConnectionSyncProgress['result'], syncConnectionName: PolicyConnectionSyncProgress['connectionName']) => {
+ if (!syncResult || !syncConnectionName) {
+ return;
+ }
+
+ modal.showModal({
+ component: HRSyncResultsModal,
+ props: {result: syncResult, policyID},
+ id: `${syncConnectionName}-sync-results-${policyID}`,
+ });
+ });
useEffect(() => {
const syncResult = connectionSyncProgress?.result;
- const isHRSyncDoneWithResult =
- CONST.POLICY.CONNECTIONS.HR_CONNECTION_NAMES.includes(connectionName as TupleToUnion) &&
- connectionSyncProgress?.stageInProgress === CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE &&
- !!syncResult;
+ const isHRConnectionName = CONST.POLICY.CONNECTIONS.HR_CONNECTION_NAMES.some((hrConnectionName) => hrConnectionName === connectionName);
+ const isHRSyncDoneWithResult = isHRConnectionName && connectionSyncProgress?.stageInProgress === CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE && !!syncResult;
const didTransitionToJobDone = previousSyncProgress?.connectionName === connectionName && previousSyncProgress?.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE;
const didHRSyncComplete = isFocused && isHRSyncDoneWithResult && didTransitionToJobDone;
- if (!didHRSyncComplete || !syncResult || !connectionName) {
+ if (didHRSyncComplete && syncResult && connectionName) {
+ pendingSyncResultRef.current = {connectionName, result: syncResult};
+ }
+
+ const pendingSyncResult = pendingSyncResultRef.current;
+ if (!pendingSyncResult || isAnyModalActive) {
return;
}
- modal.showModal({
- component: HRSyncResultsModal,
- props: {result: syncResult, policyID},
- id: `${connectionName}-sync-results-${policyID}`,
+ const handle = TransitionTracker.runAfterTransitions({
+ callback: () => {
+ showSyncResultsModal(pendingSyncResult.result, pendingSyncResult.connectionName);
+ pendingSyncResultRef.current = null;
+ },
+ waitForUpcomingTransition: true,
});
+ return () => handle.cancel();
}, [
connectionName,
connectionSyncProgress?.result,
connectionSyncProgress?.stageInProgress,
connectionSyncProgress?.timestamp,
+ isAnyModalActive,
isFocused,
- policyID,
previousSyncProgress?.connectionName,
previousSyncProgress?.stageInProgress,
- modal,
]);
}
diff --git a/src/hooks/useInboxTabSpanLifecycle.ts b/src/hooks/useInboxTabSpanLifecycle.ts
new file mode 100644
index 000000000000..9c638f16bbe2
--- /dev/null
+++ b/src/hooks/useInboxTabSpanLifecycle.ts
@@ -0,0 +1,60 @@
+import {useFocusEffect} from '@react-navigation/native';
+import {useCallback, useEffect, useRef} from 'react';
+import {cancelSpan, endSpan, getSpan} from '@libs/telemetry/activeSpans';
+import CONST from '@src/CONST';
+
+/**
+ * Manages the ManualNavigateToInboxTab span lifecycle for the inbox sidebar.
+ *
+ * Three signals are handled:
+ * - onLayout fires on first mount: ends the span (normal path).
+ * - useFocusEffect fires on re-focus when react-freeze has cached the layout: ends the span (warm path).
+ * The blur cleanup cancels any orphaned span when the user navigates away before layout completes.
+ * - useEffect unmount cleanup cancels the span only if layout never completed AND the active span
+ * is the same one that was present when this instance mounted (avoids canceling a span started
+ * by a subsequent tab click).
+ *
+ * Returns `onLayout` to be attached to the sidebar container View.
+ */
+function useInboxTabSpanLifecycle(): () => void {
+ const hasHadFirstLayout = useRef(false);
+ const spanOnMount = useRef(getSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB));
+
+ const onLayout = useCallback(() => {
+ hasHadFirstLayout.current = true;
+ endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
+ spanOnMount.current = undefined;
+ }, []);
+
+ // Focus: ends span on re-visits (react-freeze cached layout, onLayout won't fire again).
+ // Blur cleanup: cancels orphaned span when user navigates away before onLayout fires.
+ useFocusEffect(
+ useCallback(() => {
+ if (hasHadFirstLayout.current) {
+ endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
+ }
+ return () => cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
+ }, []),
+ );
+
+ // Unmount: cancel only if layout never completed AND the active span is
+ // the same one that existed when this instance mounted (avoids canceling
+ // a newer span started by a subsequent tab click).
+ useEffect(
+ () => () => {
+ if (hasHadFirstLayout.current) {
+ return;
+ }
+ const activeSpan = getSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
+ if (activeSpan !== spanOnMount.current) {
+ return;
+ }
+ cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
+ },
+ [],
+ );
+
+ return onLayout;
+}
+
+export default useInboxTabSpanLifecycle;
diff --git a/src/hooks/useIsAllowedToIssueCompanyCard.ts b/src/hooks/useIsAllowedToIssueCompanyCard.ts
index 87db7fae7b69..e9f35646cd93 100644
--- a/src/hooks/useIsAllowedToIssueCompanyCard.ts
+++ b/src/hooks/useIsAllowedToIssueCompanyCard.ts
@@ -1,5 +1,6 @@
import {getCompanyFeeds, getSelectedFeed} from '@libs/CardUtils';
-import {isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils';
+import {canMemberWrite} from '@libs/PolicyUtils';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isAdminSelector} from '@src/selectors/Domain';
import useCardFeeds from './useCardFeeds';
@@ -8,10 +9,10 @@ import useOnyx from './useOnyx';
import usePolicy from './usePolicy';
function useIsAllowedToIssueCompanyCard({policyID}: {policyID?: string}) {
- const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
+ const {accountID: currentUserAccountID, login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const policy = usePolicy(policyID);
- const isPolicyAdmin = isPolicyAdminPolicyUtils(policy);
+ const canWriteCompanyCards = canMemberWrite(policy, currentUserLogin, CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS);
const [cardFeeds] = useCardFeeds(policyID);
const companyCards = getCompanyFeeds(cardFeeds);
@@ -21,7 +22,7 @@ function useIsAllowedToIssueCompanyCard({policyID}: {policyID?: string}) {
const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${selectedFeedData?.domainID}`);
if (selectedFeedData?.domainID === policy?.policyAccountID || (policyID && selectedFeedData?.linkedPolicyIDs?.some((id) => id.toUpperCase() === policyID.toUpperCase()))) {
- return isPolicyAdmin;
+ return canWriteCompanyCards;
}
return isAdminSelector(currentUserAccountID)(domain);
diff --git a/src/hooks/useLifecycleActions.tsx b/src/hooks/useLifecycleActions.tsx
index a25c43c159ee..af868c5c5ff9 100644
--- a/src/hooks/useLifecycleActions.tsx
+++ b/src/hooks/useLifecycleActions.tsx
@@ -1,4 +1,5 @@
import {delegateEmailSelector} from '@selectors/Account';
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import type {ActionHandledType} from '@components/Modal/Global/HoldMenuModalWrapper';
@@ -18,6 +19,7 @@ import {
isExported as isExportedUtils,
isReportOwner,
shouldBlockSubmitDueToStrictPolicyRules,
+ shouldShowMarkAsDone,
} from '@libs/ReportUtils';
import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasOnlyPendingCardTransactions, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils';
import {cancelPayment, markReportPaymentReceived} from '@userActions/IOU/PayMoneyRequest';
@@ -71,6 +73,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation,
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID);
const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions);
@@ -228,7 +231,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation,
const actions: Record = {
[CONST.REPORT.SECONDARY_ACTIONS.SUBMIT]: {
value: CONST.REPORT.SECONDARY_ACTIONS.SUBMIT,
- text: translate('common.submit'),
+ text: shouldShowMarkAsDone({policy, report: moneyRequestReport, isTrackIntentUser}) ? translate('common.markAsDone') : translate('common.submit'),
icon: expensifyIcons.Send,
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.SUBMIT,
onSelected: () => {
diff --git a/src/hooks/useMergeHRInitialSyncingModal.ts b/src/hooks/useMergeHRInitialSyncingModal.ts
index db0bbaabcb6d..d710022f8889 100644
--- a/src/hooks/useMergeHRInitialSyncingModal.ts
+++ b/src/hooks/useMergeHRInitialSyncingModal.ts
@@ -1,6 +1,5 @@
import {useEffect, useEffectEvent, useState} from 'react';
import {setMergeHRInitialSyncModalShown} from '@libs/actions/connections/MergeHR';
-// eslint-disable-next-line no-restricted-imports -- the hook does not use React Navigation hooks internally (isFocused is passed in as a parameter), so there is no navigation instance available to use navigation.addListener for transition detection.
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import Visibility from '@libs/Visibility';
import CONST from '@src/CONST';
diff --git a/src/hooks/useNavigateToTransactionThread.ts b/src/hooks/useNavigateToTransactionThread.ts
new file mode 100644
index 000000000000..914a27ffdc4d
--- /dev/null
+++ b/src/hooks/useNavigateToTransactionThread.ts
@@ -0,0 +1,87 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import {useWideRHPActions} from '@components/WideRHPContextProvider';
+import {createTransactionThreadReport, setOptimisticTransactionThread} from '@libs/actions/Report';
+import {setActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
+import Navigation from '@libs/Navigation/Navigation';
+import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Report, ReportAction, Transaction} from '@src/types/onyx';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useOnyx from './useOnyx';
+
+type NavigateToTransactionThreadParams = {
+ /** The transaction whose thread should be opened */
+ transactionID: string;
+
+ /** Report actions of the parent (IOU/expense) report, used to resolve the IOU action and its thread */
+ reportActions: ReportAction[];
+
+ /** The parent (IOU/expense) report that owns the transaction */
+ report: OnyxEntry;
+
+ /** The transaction being opened, used to build the thread optimistically when it doesn't exist yet */
+ transaction: OnyxEntry;
+
+ /** Ordered list of sibling transaction IDs used to drive the prev/next carousel in the thread RHP */
+ siblingTransactionIDs: string[];
+
+ /** Route to return to when navigating back; defaults to the current active route */
+ backTo?: string;
+};
+
+/**
+ * Shared navigation algorithm for opening a transaction thread (single-expense RHP view with the
+ * prev/next carousel). It resolves the IOU action's `childReportID`, creates the thread optimistically
+ * when it doesn't exist yet, seeds the sibling transaction IDs for the carousel, and navigates to the
+ * SEARCH_REPORT route.
+ *
+ * Callers are responsible for gathering their own `reportActions`, `report`, `transaction`, and
+ * `siblingTransactionIDs` because the data sources differ per screen.
+ */
+function useNavigateToTransactionThread() {
+ const {markReportIDAsExpense} = useWideRHPActions();
+ const currentUserDetails = useCurrentUserPersonalDetails();
+ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+
+ return ({transactionID, reportActions, report, transaction, siblingTransactionIDs, backTo}: NavigateToTransactionThreadParams) => {
+ const iouAction = getIOUActionForTransactionID(reportActions, transactionID);
+ const resolvedBackTo = backTo ?? Navigation.getActiveRoute();
+ let reportIDToNavigate = iouAction?.childReportID;
+
+ const routeParams: {reportID: string | undefined; reportActionID?: string; backTo?: string} = {
+ reportID: reportIDToNavigate,
+ backTo: resolvedBackTo,
+ };
+
+ if (!reportIDToNavigate) {
+ const transactionThreadReport = createTransactionThreadReport({
+ introSelected,
+ currentUserLogin: currentUserDetails.email ?? '',
+ currentUserAccountID: currentUserDetails.accountID,
+ betas,
+ iouReport: report,
+ iouReportAction: iouAction,
+ transaction,
+ });
+ if (transactionThreadReport) {
+ reportIDToNavigate = transactionThreadReport.reportID;
+ routeParams.reportID = reportIDToNavigate;
+ }
+ } else {
+ setOptimisticTransactionThread(reportIDToNavigate, report?.reportID, iouAction?.reportActionID, report?.policyID);
+ }
+
+ // Single transaction report opens in RHP. We seed every sibling transaction ID so the RHP can
+ // display prev/next arrows for navigation between expenses.
+ setActiveTransactionIDs(siblingTransactionIDs).then(() => {
+ if (reportIDToNavigate) {
+ markReportIDAsExpense(reportIDToNavigate);
+ }
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
+ });
+ };
+}
+
+export default useNavigateToTransactionThread;
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 4845a30cdd7d..82f17e6c3552 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -4,7 +4,6 @@ import {emailSelector} from '@selectors/Session';
import {useEffect} from 'react';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import Navigation from '@libs/Navigation/Navigation';
-// eslint-disable-next-line no-restricted-imports
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import {isLoggingInAsNewUser} from '@libs/SessionUtils';
import {startOnboardingFlow} from '@userActions/Welcome/OnboardingFlow';
diff --git a/src/hooks/useParticipantSubmission.ts b/src/hooks/useParticipantSubmission.ts
index 751802e0cb01..4ffc3ebba138 100644
--- a/src/hooks/useParticipantSubmission.ts
+++ b/src/hooks/useParticipantSubmission.ts
@@ -4,9 +4,8 @@ import {setTransactionReport} from '@libs/actions/Transaction';
import {READ_COMMANDS} from '@libs/API/types';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import HttpUtils from '@libs/HttpUtils';
-import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import Navigation from '@libs/Navigation/Navigation';
-import {isPaidGroupPolicy} from '@libs/PolicyUtils';
+import {isGroupPolicy} from '@libs/PolicyUtils';
import {findSelfDMReportID, generateReportID, isInvoiceRoomWithID} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {isDistanceRequest} from '@libs/TransactionUtils';
@@ -23,7 +22,7 @@ import {createDraftWorkspace, generateDefaultWorkspaceName} from '@userActions/P
import CONST from '@src/CONST';
import type {IOUAction, IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
import {lastWorkspaceNumberSelector} from '@src/selectors/Policy';
import type {Policy, Transaction} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
@@ -97,7 +96,7 @@ function useParticipantSubmission({
const isActivePolicyRequest =
iouType === CONST.IOU.TYPE.CREATE &&
- isPaidGroupPolicy(activePolicy) &&
+ isGroupPolicy(activePolicy) &&
activePolicy?.isPolicyExpenseChatEnabled &&
!shouldRestrictUserBillableActions(activePolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, currentUserPersonalDetails.accountID);
@@ -354,10 +353,7 @@ function useParticipantSubmission({
}
Navigation.setNavigationActionToMicrotaskQueue(() => {
if (isCategorizing) {
- const confirmationRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.SUBMIT, initialTransactionID, expenseChatReportID, undefined, true);
- Navigation.navigate(
- createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, initialTransactionID, expenseChatReportID), confirmationRoute),
- );
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, initialTransactionID, expenseChatReportID));
} else {
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.SUBMIT, initialTransactionID, expenseChatReportID, undefined, true));
}
@@ -376,7 +372,7 @@ function useParticipantSubmission({
);
const route = isCategorizing
- ? createDynamicRoute(DYNAMIC_ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID), iouConfirmationPageRoute)
+ ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID, iouConfirmationPageRoute)
: iouConfirmationPageRoute;
KeyboardUtils.dismissKeyboardAndExecute(() => {
diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts
index b3f88e88a2ac..74f3e8384158 100644
--- a/src/hooks/usePolicyForMovingExpenses.ts
+++ b/src/hooks/usePolicyForMovingExpenses.ts
@@ -1,7 +1,7 @@
import {activePolicySelector} from '@selectors/Policy';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {useSession} from '@components/OnyxListItemProvider';
-import {canSubmitPerDiemExpenseFromWorkspace, isPaidGroupPolicy, isPolicyMemberWithoutPendingDelete, isTimeTrackingEnabled} from '@libs/PolicyUtils';
+import {canSubmitPerDiemExpenseFromWorkspace, isGroupPolicy, isPolicyMemberWithoutPendingDelete, isTimeTrackingEnabled} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
@@ -24,7 +24,7 @@ function isPolicyValidForMovingExpenses(policy: OnyxEntry, login: string
return (
checkForUserPendingDelete(login, policy) &&
isPolicyMemberByRole(policy) &&
- isPaidGroupPolicy(policy) &&
+ isGroupPolicy(policy) &&
policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
(!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(policy)) &&
(!isTimeRequest || isTimeTrackingEnabled(policy))
diff --git a/src/hooks/useReceiptScanDrop.tsx b/src/hooks/useReceiptScanDrop.tsx
index 0ea8a8ac55d4..6e519cc74611 100644
--- a/src/hooks/useReceiptScanDrop.tsx
+++ b/src/hooks/useReceiptScanDrop.tsx
@@ -3,7 +3,7 @@ import React, {useMemo} from 'react';
import {setTransactionReport} from '@libs/actions/Transaction';
import {navigateToParticipantPage} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {hasOnlyPersonalPolicies as hasOnlyPersonalPoliciesUtil, isPaidGroupPolicy} from '@libs/PolicyUtils';
+import {hasOnlyPersonalPolicies as hasOnlyPersonalPoliciesUtil, isGroupPolicy} from '@libs/PolicyUtils';
import {generateReportID, getPolicyExpenseChat, isSelfDM} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
@@ -80,7 +80,7 @@ function useReceiptScanDrop() {
}
if (
- isPaidGroupPolicy(activePolicy) &&
+ isGroupPolicy(activePolicy) &&
activePolicy?.isPolicyExpenseChatEnabled &&
!shouldRestrictUserBillableActions(activePolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, currentUserPersonalDetails.accountID)
) {
diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts
index eb1594d833d2..87b9dd722ee8 100644
--- a/src/hooks/useSearchBulkActions.ts
+++ b/src/hooks/useSearchBulkActions.ts
@@ -1,4 +1,5 @@
/* eslint-disable react-hooks/refs -- Refs in this hook are used inside callbacks that capture stable references; the lint rule flags false positives for these patterns */
+import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {InteractionManager} from 'react-native';
@@ -55,6 +56,7 @@ import {
isInvoiceReport,
isIOUReport as isIOUReportUtil,
isSelfDM,
+ shouldShowMarkAsDone,
} from '@libs/ReportUtils';
import {buildSearchQueryJSON, buildSearchQueryString, serializeQueryJSONForBackend} from '@libs/SearchQueryUtils';
import {getSelectedGroupFilterEntry, navigateToSearchRHP, shouldShowDeleteOption} from '@libs/SearchUIUtils';
@@ -381,6 +383,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION);
const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION);
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const isExpenseReportType = queryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
const expensifyIcons = useMemoizedLazyExpensifyIcons([
@@ -1243,6 +1246,56 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
});
}, [isExpenseReportType, defaultExpensePolicy, selectedReports, accountID]);
+ const allReportsShouldMarkAsDone = useMemo(() => {
+ if (selectedReports.length > 0) {
+ return selectedReports.every((report) => {
+ const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`];
+
+ return shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: fullReport,
+ policy: policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`],
+ });
+ });
+ }
+
+ return Object.values(selectedTransactions).every((transaction) => {
+ const reportID = transaction.reportID;
+ const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+
+ return shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: fullReport,
+ policy: policies?.[`${ONYXKEYS.COLLECTION.POLICY}${transaction.policyID}`],
+ });
+ });
+ }, [selectedReports, currentSearchResults?.data, isTrackIntentUser, policies, selectedTransactions]);
+
+ const noReportsShouldMarkAsDone = useMemo(() => {
+ if (selectedReports.length > 0) {
+ return selectedReports.every((report) => {
+ const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`];
+
+ return !shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: fullReport,
+ policy: policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`],
+ });
+ });
+ }
+
+ return Object.values(selectedTransactions).every((transaction) => {
+ const reportID = transaction.reportID;
+ const fullReport = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+
+ return !shouldShowMarkAsDone({
+ isTrackIntentUser,
+ report: fullReport,
+ policy: policies?.[`${ONYXKEYS.COLLECTION.POLICY}${transaction.policyID}`],
+ });
+ });
+ }, [selectedReports, currentSearchResults?.data, isTrackIntentUser, policies, selectedTransactions]);
+
const headerButtonsOptions = useMemo(() => {
if (selectedTransactionsKeys.length === 0 || status == null || !hash) {
return CONST.EMPTY_ARRAY as unknown as Array>;
@@ -1577,13 +1630,17 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
!isOffline &&
areSelectedTransactionsIncludedInReports &&
(selectedReports.length
- ? selectedReports.every((report) => report.canSubmit)
- : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.SUBMIT));
+ ? selectedReports.every((report) => report.canSubmit) &&
+ // Disable for mixed selections: all must be the same submit type
+ (isTrackIntentUser ? allReportsShouldMarkAsDone || noReportsShouldMarkAsDone : true)
+ : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.SUBMIT) &&
+ // Disable for mixed selections: all must be the same submit type
+ (isTrackIntentUser ? allReportsShouldMarkAsDone || noReportsShouldMarkAsDone : true));
if (shouldShowSubmitOption) {
options.push({
icon: expensifyIcons.Send,
- text: translate('common.submit'),
+ text: allReportsShouldMarkAsDone ? translate('common.markAsDone') : translate('common.submit'),
value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT,
shouldCloseModalOnSelect: true,
onSelected: () => {
@@ -1944,6 +2001,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
userBillingGracePeriodEnds,
ownerBillingGracePeriodEnd,
currentSearchKey,
+ isTrackIntentUser,
getCurrencyDecimals,
amountOwed,
allTransactions,
diff --git a/src/hooks/useSearchSections.ts b/src/hooks/useSearchSections.ts
index 89e6c3e3a8d2..c2c7881f8c92 100644
--- a/src/hooks/useSearchSections.ts
+++ b/src/hooks/useSearchSections.ts
@@ -26,6 +26,7 @@ function useSearchSections(): UseSearchSectionsResult {
const {convertToDisplayString} = useCurrencyListActions();
const [cardFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER);
+ const [personalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.PERSONAL_AND_WORKSPACE_CARD_LIST);
const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST);
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
@@ -55,7 +56,8 @@ function useSearchSections(): UseSearchSectionsResult {
archivedReportsIDList: archivedReportsIDSet,
isActionLoadingSet,
cardFeeds,
- cardList: nonPersonalAndWorkspaceCards,
+ cardList: personalAndWorkspaceCards,
+ nonPersonalAndWorkspaceCardList: nonPersonalAndWorkspaceCards,
conciergeReportID,
convertToDisplayString,
reportAttributesDerivedValue,
diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts
index b30b4d0bd8d8..28920ad130ff 100644
--- a/src/hooks/useSearchTypeMenuSections.ts
+++ b/src/hooks/useSearchTypeMenuSections.ts
@@ -2,11 +2,11 @@ import {defaultExpensifyCardSelector} from '@selectors/Card';
import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft';
import {useCallback, useEffect, useMemo, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
-import isTrackOnboardingChoice from '@libs/OnboardingUtils';
import {createTypeMenuSections, doesSearchItemMatchSort} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {IntroSelected, Policy, Session} from '@src/types/onyx';
+import {isTrackIntentUserSelector} from '@src/selectors/Onboarding';
+import type {Policy, Session} from '@src/types/onyx';
import useCardFeedsForDisplay from './useCardFeedsForDisplay';
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
import useMappedPolicies from './useMappedPolicies';
@@ -44,8 +44,6 @@ const currentUserLoginAndAccountIDSelector = (session: OnyxEntry) => ({
accountID: session?.accountID,
});
-const isTrackIntentUserSelector = (introSelected: OnyxEntry) => isTrackOnboardingChoice(introSelected?.choice);
-
type UseSearchTypeMenuSectionsParams = {
hash?: number;
similarSearchHash?: number;
diff --git a/src/hooks/useSelectionModeReportActions.ts b/src/hooks/useSelectionModeReportActions.ts
index bb4dea802e5b..2f0a90af1d63 100644
--- a/src/hooks/useSelectionModeReportActions.ts
+++ b/src/hooks/useSelectionModeReportActions.ts
@@ -1,5 +1,5 @@
import {delegateEmailSelector, isUserValidatedSelector} from '@selectors/Account';
-import {hasSeenTourSelector} from '@selectors/Onboarding';
+import {hasSeenTourSelector, isTrackIntentUserSelector} from '@selectors/Onboarding';
import truncate from 'lodash/truncate';
import {useContext, useEffect, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
@@ -38,6 +38,7 @@ import {
isIOUReport as isIOUReportUtil,
isReportOwner,
shouldBlockSubmitDueToStrictPolicyRules,
+ shouldShowMarkAsDone,
} from '@libs/ReportUtils';
import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasOnlyPendingCardTransactions, showPendingCardTransactionsBlockModal} from '@libs/TransactionUtils';
import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
@@ -126,6 +127,8 @@ function useSelectionModeReportActions({
const [invoiceReceiverPolicy] = useOnyx(
`${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`,
);
+ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
+
const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID);
const activeAdminPolicies = useActiveAdminPolicies();
const lastWorkspaceNumber = useLastWorkspaceNumber();
@@ -511,7 +514,7 @@ function useSelectionModeReportActions({
let idx = 0;
if (hasSubmitAction && !shouldBlockSubmit) {
actions[idx++] = {
- text: translate('common.submit'),
+ text: shouldShowMarkAsDone({policy, report, isTrackIntentUser}) ? translate('common.markAsDone') : translate('common.submit'),
icon: expensifyIcons.Send,
value: CONST.REPORT.PRIMARY_ACTIONS.SUBMIT,
onSelected: handleSubmitReport,
diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx
index 2e4e31f13a0d..c15e52fe29ae 100644
--- a/src/hooks/useSidebarOrderedReports.tsx
+++ b/src/hooks/useSidebarOrderedReports.tsx
@@ -1,5 +1,7 @@
import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import {setInboxTab} from '@libs/actions/User';
import Log from '@libs/Log';
import SidebarUtils from '@libs/SidebarUtils';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
@@ -27,27 +29,40 @@ type SidebarOrderedReportsContextProviderProps = {
};
type SidebarOrderedReportsStateContextValue = {
- orderedReports: OnyxTypes.Report[];
+ /** The reports rendered in the LHN for the active Inbox tab (a filtered subset of orderedReportIDs). */
+ filteredReports: OnyxTypes.Report[];
+ /** All ordered LHN report IDs, unfiltered by the active Inbox tab. Used for total counts (e.g. focus-mode switch) and brick road. */
orderedReportIDs: string[];
currentReportID: string | undefined;
chatTabBrickRoad: BrickRoad;
+ activeTab: ValueOf;
+ inboxTabCounts: Record;
};
type SidebarOrderedReportsActionsContextValue = {
clearLHNCache: () => void;
+ setActiveTab: (tab: ValueOf) => void;
+ setStickyReportID: (reportID: string) => void;
};
-type ReportsToDisplayInLHN = Record;
+type ReportsToDisplayInLHN = Record;
const SidebarOrderedReportsStateContext = createContext({
- orderedReports: [],
+ filteredReports: [],
orderedReportIDs: [],
currentReportID: '',
chatTabBrickRoad: undefined,
+ activeTab: CONST.INBOX_TAB.ALL,
+ inboxTabCounts: {
+ [CONST.INBOX_TAB.TODO]: 0,
+ [CONST.INBOX_TAB.UNREAD]: 0,
+ },
});
const SidebarOrderedReportsActionsContext = createContext({
clearLHNCache: () => {},
+ setActiveTab: () => {},
+ setStickyReportID: () => {},
});
const policyMapper = (policy: OnyxEntry): PartialPolicyForSidebar =>
@@ -72,6 +87,8 @@ function SidebarOrderedReportsContextProvider({
}: SidebarOrderedReportsContextProviderProps) {
const {localeCompare} = useLocalize();
const [priorityMode = CONST.PRIORITY_MODE.DEFAULT] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
+ const [inboxTab = CONST.INBOX_TAB.ALL] = useOnyx(ONYXKEYS.NVP_INBOX_TAB);
+ const activeTab = inboxTab ?? CONST.INBOX_TAB.ALL;
const [chatReports, {sourceValue: reportUpdates}] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [, {sourceValue: policiesUpdates}] = useMappedPolicies(policyMapper);
const [transactions, {sourceValue: transactionsUpdates}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
@@ -278,7 +295,32 @@ function SidebarOrderedReportsContextProvider({
const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]);
- // Get the actual reports based on the ordered IDs
+ // When a report is opened from the To-do/Unread tab (see setStickyReportID), we remember it so it
+ // stays visible after viewing it removes it from the tab (e.g. it gets read). It's only set on a
+ // non-All tab, so opening a chat from the All tab never makes it appear under Unread/To-do.
+ const [stickyReport, setStickyReport] = useState<{reportID: string; tab: ValueOf} | undefined>(undefined);
+
+ // The reports for the active tab, plus the sticky report opened from it (kept visible even after it's read).
+ const stickyReportID = stickyReport?.reportID;
+ const stickyReportTab = stickyReport?.tab;
+ const filteredReportIDs = useMemo(() => {
+ const baseFilteredReportIDs = SidebarUtils.filterReportsForInboxTab(orderedReportIDs, reportsToDisplayInLHN, activeTab);
+ if (activeTab === CONST.INBOX_TAB.ALL || !stickyReportID || stickyReportTab !== activeTab || baseFilteredReportIDs.includes(stickyReportID)) {
+ return baseFilteredReportIDs;
+ }
+ if (!orderedReportIDs.includes(stickyReportID)) {
+ // While opening the report, reading it can briefly drop it from the LHN set entirely (before
+ // navigation marks it as the focused report). Keep it at the top so the list doesn't flash empty.
+ return [stickyReportID, ...baseFilteredReportIDs];
+ }
+ const baseSet = new Set(baseFilteredReportIDs);
+ return orderedReportIDs.filter((reportID) => baseSet.has(reportID) || reportID === stickyReportID);
+ }, [orderedReportIDs, reportsToDisplayInLHN, activeTab, stickyReportTab, stickyReportID]);
+
+ // The count shown in each tab's badge, derived from the full "All" set (not the currently filtered view).
+ const inboxTabCounts = useMemo(() => SidebarUtils.getInboxTabCounts(orderedReportIDs, reportsToDisplayInLHN), [orderedReportIDs, reportsToDisplayInLHN]);
+
+ // Get the actual reports based on the filtered IDs
const getOrderedReports = useCallback(
(reportIDs: string[]): OnyxTypes.Report[] => {
if (!chatReports) {
@@ -289,7 +331,7 @@ function SidebarOrderedReportsContextProvider({
[chatReports],
);
- const orderedReports = useMemo(() => getOrderedReports(orderedReportIDs), [getOrderedReports, orderedReportIDs]);
+ const filteredReports = useMemo(() => getOrderedReports(filteredReportIDs), [getOrderedReports, filteredReportIDs]);
const clearLHNCache = useCallback(() => {
Log.info('[useSidebarOrderedReports] Clearing sidebar cache manually via debug modal');
@@ -297,6 +339,25 @@ function SidebarOrderedReportsContextProvider({
setClearCacheDummyCounter((current) => current + 1);
}, []);
+ const setActiveTab = useCallback((tab: ValueOf) => {
+ setInboxTab(tab);
+
+ // The sticky report is scoped to the tab it was opened from, so reset it when switching tabs.
+ setStickyReport(undefined);
+ }, []);
+
+ // Called when a report is opened from the LHN. On the To-do/Unread tabs we remember it so it stays
+ // visible after viewing it removes it from the tab. On the All tab we keep nothing sticky.
+ const setStickyReportID = useCallback(
+ (reportID: string) => {
+ if (activeTab === CONST.INBOX_TAB.ALL) {
+ return;
+ }
+ setStickyReport({reportID, tab: activeTab});
+ },
+ [activeTab],
+ );
+
const stateValue: SidebarOrderedReportsStateContextValue = useMemo(() => {
// We need to make sure the current report is in the list of reports, but we do not want
// to have to re-generate the list every time the currentReportID changes. To do that
@@ -308,31 +369,52 @@ function SidebarOrderedReportsContextProvider({
// requirement for web. Consider a case, where we have report with expenses and we click on
// any expense, a new LHN item is added in the list and is visible on web. But on mobile, we
// just navigate to the screen with expense details, so there seems no point to execute this logic on mobile.
+ // Only the "All" tab force-regenerates to surface the current report. On the To-do/Unread tabs the
+ // sticky-aware filteredReportIDs already keeps the opened report visible, and re-filtering here
+ // (without the sticky report) would briefly empty the list while opening it.
if (
- (!shouldUseNarrowLayout || orderedReportIDs.length === 0) &&
+ activeTab === CONST.INBOX_TAB.ALL &&
+ (!shouldUseNarrowLayout || filteredReportIDs.length === 0) &&
derivedCurrentReportID &&
derivedCurrentReportID !== '-1' &&
- orderedReportIDs.indexOf(derivedCurrentReportID) === -1
+ filteredReportIDs.indexOf(derivedCurrentReportID) === -1
) {
const updatedReportIDs = getOrderedReportIDs();
- const updatedReports = getOrderedReports(updatedReportIDs);
+ const updatedFilteredIDs = SidebarUtils.filterReportsForInboxTab(updatedReportIDs, reportsToDisplayInLHN, activeTab);
+ const updatedReports = getOrderedReports(updatedFilteredIDs);
return {
- orderedReports: updatedReports,
+ filteredReports: updatedReports,
orderedReportIDs: updatedReportIDs,
currentReportID: derivedCurrentReportID,
chatTabBrickRoad: getChatTabBrickRoad(updatedReportIDs, reportAttributes),
+ activeTab,
+ inboxTabCounts,
};
}
return {
- orderedReports,
+ filteredReports,
orderedReportIDs,
currentReportID: derivedCurrentReportID,
chatTabBrickRoad: getChatTabBrickRoad(orderedReportIDs, reportAttributes),
+ activeTab,
+ inboxTabCounts,
};
- }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID, shouldUseNarrowLayout, getOrderedReports, orderedReports, reportAttributes]);
+ }, [
+ getOrderedReportIDs,
+ orderedReportIDs,
+ filteredReportIDs,
+ derivedCurrentReportID,
+ shouldUseNarrowLayout,
+ getOrderedReports,
+ filteredReports,
+ reportAttributes,
+ activeTab,
+ inboxTabCounts,
+ reportsToDisplayInLHN,
+ ]);
- const actionsValue: SidebarOrderedReportsActionsContextValue = useMemo(() => ({clearLHNCache}), [clearLHNCache]);
+ const actionsValue: SidebarOrderedReportsActionsContextValue = useMemo(() => ({clearLHNCache, setActiveTab, setStickyReportID}), [clearLHNCache, setActiveTab, setStickyReportID]);
useEffect(() => {
const hookExecutionDuration = performance.now() - hookStartTime.current;
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 884e41cab1b3..8fd0f0768da0 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: 'Mehrfachauswahl',
saveChanges: 'Änderungen speichern',
submit: 'Senden',
+ markAsDone: 'Als erledigt markieren',
submitted: 'Übermittelt',
+ markedAsDoneStatus: 'Als erledigt markiert',
rotate: 'Drehen',
zoom: 'Zoom',
password: 'Passwort',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: 'Beschreibung',
title: 'Titel',
assignee: 'Zuständige Person',
- createdBy: 'Erstellt von',
with: 'mit',
shareCode: 'Code teilen',
share: 'Teilen',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: 'Händler',
change: 'Ändern',
category: 'Kategorie',
+ vendor: 'Anbieter',
report: 'Bericht',
billable: 'Abrechenbar',
nonBillable: 'Nicht abrechenbar',
@@ -356,6 +359,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Finde einen Chat über die',
subtitleText2: 'Schaltfläche oben oder erstellen Sie etwas mit der',
subtitleText3: 'Schaltfläche unten.',
+ noUnreadChats: 'Keine ungelesenen Chats',
+ noTodos: 'Keine To-dos',
+ caughtUp: 'Sie sind auf dem neuesten Stand. Gut gemacht!',
+ seeAllChats: 'Alle Chats anzeigen',
},
businessName: 'Firmenname',
clear: 'Löschen',
@@ -853,6 +860,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `Dieser Chat ist mit ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`Hier reicht ${submitterDisplayName} Auslagen bei ${workspaceName} ein. Nutze einfach die +-Taste.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'Hier können Sie Ausgaben nachverfolgen',
beginningOfChatHistorySelfDM: 'Dies ist dein persönlicher Bereich. Nutze ihn für Notizen, Aufgaben, Entwürfe und Erinnerungen.',
beginningOfChatHistorySystemDM: 'Willkommen! Lassen Sie uns Ihre Einrichtung vornehmen.',
chatWithAccountManager: 'Chatte hier mit deiner/deinem Account Manager',
@@ -958,7 +966,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Verbindung der persönlichen Karte ${cardName} reparieren` : 'Verbindung der persönlichen Karte reparieren'),
subtitle: 'Wallet',
},
- validateAccount: {title: 'Bestätigen Sie Ihr Konto, um Expensify weiter zu verwenden', subtitle: 'Konto', cta: 'Bestätigen'},
+ validateAccount: {title: 'Bestätigen Sie Ihr Konto', subtitle: 'Konto', cta: 'Bestätigen'},
fixFailedBilling: {title: 'Wir konnten Ihre hinterlegte Karte nicht belasten', subtitle: 'Abonnement'},
unlockBankAccount: {
workspaceTitle: 'Ihr Geschäftskonto wurde gesperrt',
@@ -1013,6 +1021,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Alles erledigt',
f1FlagsDescription: 'Sie haben alle offenen Aufgaben abgeschlossen.',
},
+ reviewExpenses: ({count}: {count: number}) => `Überprüfen Sie ${count} ${count === 1 ? 'Ausgabe' : 'Spesen'}`,
},
upcomingTravel: 'Bevorstehende Reisen',
upcomingTravelSection: {
@@ -1349,6 +1358,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `${amount}-Rechnung senden`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `für ${comment}` : ''}`,
submitted: (memo?: string) => `eingereicht${memo ? `, mit dem Vermerk ${memo}` : ''}`,
+ markedAsDone: (memo) => `als erledigt markiert${memo ? `, mit dem Vermerk ${memo}` : ''}`,
automaticallySubmitted: `eingereicht über Einreichungen verzögern `,
queuedToSubmitViaDEW: 'zur Einreichung über benutzerdefinierten Genehmigungsworkflow eingereiht',
queuedToApproveViaDEW: 'Zur Genehmigung über benutzerdefinierten Genehmigungsworkflow eingereiht',
@@ -1565,6 +1575,9 @@ const translations: TranslationDeepObject = {
removed: 'entfernt',
transactionPending: 'Transaktion ausstehend.',
chooseARate: 'Wähle einen Rückerstattungssatz pro Meile oder Kilometer für den Workspace aus',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} bis ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Gültig ab ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Gültig bis ${endDate}`,
unapprove: 'Genehmigung aufheben',
unapproveReport: 'Bericht ablehnen',
headsUp: 'Achtung!',
@@ -1665,6 +1678,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'Beheben Sie den Kursfehler und versuchen Sie es erneut.',
AskToExplain: `. Erklären `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge hat diese Ausgabe ${vendorName} zugeordnet`,
duplicateNonDefaultWorkspacePerDiemError:
'Sie können Per-Diem-Ausgaben nicht über mehrere Workspaces hinweg duplizieren, da sich die Sätze zwischen den Workspaces unterscheiden können.',
rulesModifiedFields: {
@@ -1794,6 +1808,21 @@ const translations: TranslationDeepObject = {
return `Warten darauf, dass ein Admin Spesen einreicht.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `Wartet darauf, dass Sie dies als erledigt markieren.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `Warten darauf, dass ${actor} dies als erledigt markiert.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `Warten darauf, dass ein Admin dies als erledigt markiert.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2227,10 +2256,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Verdächtige Aktivität melden',
lockAccount: 'Konto sperren',
+ lockMyAccount: 'Sperren Sie mein Konto',
unlockAccount: 'Konto entsperren',
- compromisedDescription:
- 'Ist Ihnen etwas Ungewöhnliches mit Ihrem Konto aufgefallen? Wenn Sie es melden, wird Ihr Konto sofort gesperrt, neue Expensify Karte-Transaktionen werden blockiert und alle Kontenänderungen verhindert.',
- domainAdminsDescription: 'Für Domain-Admins: Dadurch werden auch alle Aktivitäten der Expensify Karte und alle Administratoraktionen in Ihrer/Ihren Domain(s) pausiert.',
+ findYourSituation: 'Die meisten Probleme erfordern keine Sperrung Ihres Kontos! Finden Sie Ihre Situation unten:',
+ lostCardOrCharges:
+ 'Karte verloren oder unbekannte Abbuchungen : Sperren Sie Ihre Karte und kontaktieren Sie Concierge, um unbekannte Transaktionen anzufechten.',
+ unauthorizedAccess:
+ 'Unbefugter Kontozugriff : Sperren Sie Ihr Konto unten. Dadurch werden neue Expensify Karte-Transaktionen, Kartenbestellungen und Kontenänderungen blockiert. Wenn Sie ein Domain-Admin sind, wird dadurch außerdem alle domänenweite Kartenaktivität und Administratoraktionen angehalten.',
+ securityTeamFollowUp: 'Unser Sicherheitsteam wird sich nach der Sperrung von risk@expensify.com bei Ihnen melden.',
areYouSure: 'Möchtest du dein Expensify-Konto wirklich sperren?',
onceLocked: 'Sobald Ihr Konto gesperrt ist, wird es eingeschränkt, bis eine Entsperrungsanfrage gestellt und eine Sicherheitsprüfung durchgeführt wurde',
unlockTitle: 'Wir haben Ihre Anfrage erhalten',
@@ -2599,13 +2632,10 @@ ${amount} für ${merchant} – ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `Ausgaben von ${members} und die genehmigende Person ist ${approvers}`,
addApprovalButton: 'Genehmigungsablauf hinzufügen',
editWorkflowAction: 'Bearbeiten',
- addAgentAction: 'Agent hinzufügen',
findWorkflow: 'Workflow suchen',
addApprovalTip: 'Dieser Standard-Workflow gilt für alle Mitglieder, sofern kein spezifischerer Workflow vorhanden ist.',
approver: 'Genehmiger',
addApprovalsDescription: 'Zusätzliche Genehmigung einholen, bevor eine Zahlung autorisiert wird.',
- automateApprovalsWithAgentsTitle: 'Genehmigungen mit Agenten automatisieren',
- automateApprovalsWithAgentsSubtitle: 'Fügen Sie die untenstehende Person zum Workflow hinzu, um Genehmigungen zu automatisieren.',
makeOrTrackPaymentsTitle: 'Zahlungen',
makeOrTrackPaymentsDescription:
'Fügen Sie eine bevollmächtigte zahlende Person für in Expensify getätigte Zahlungen hinzu oder verfolgen Sie Zahlungen, die andernorts getätigt wurden.',
@@ -2901,6 +2931,12 @@ ${amount} für ${merchant} – ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: 'Willkommen im #Focus-Modus!',
+ prompt: (priorityModePageUrl: string) =>
+ `Behalten Sie den Überblick, indem Sie nur ungelesene Chats oder Chats sehen, die Ihre Aufmerksamkeit erfordern. Keine Sorge, Sie können dies jederzeit in den Einstellungen ändern.`,
+ },
+ inboxTabs: {all: 'Alle', todo: 'Aufgaben', unread: 'Ungelesen'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'PDF erstellen',
@@ -3468,11 +3504,6 @@ ${amount} für ${merchant} – ${date}`,
year: 'Jahr',
selectYear: 'Bitte ein Jahr auswählen',
},
- focusModeUpdateModal: {
- title: 'Willkommen im #Fokusmodus!',
- prompt: (priorityModePageUrl: string) =>
- `Behalte den Überblick, indem du nur ungelesene Chats oder Chats siehst, die deine Aufmerksamkeit benötigen. Keine Sorge, du kannst das jederzeit in den Einstellungen ändern.`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'Der Chat, den du suchst, kann nicht gefunden werden.',
getMeOutOfHere: 'Hol mich hier raus',
@@ -4136,28 +4167,31 @@ ${amount} für ${merchant} – ${date}`,
verificationFailed: 'Die Verifizierung ist fehlgeschlagen, daher benötigen wir zusätzliche Dokumente, um dich und dein Unternehmen zu überprüfen',
taxIDVerification: 'Steuer-ID-Verifizierung',
taxIDVerificationDescription: dedent(`
- Bitte lade eine der folgenden Dateien hoch:
- • IRS TIN/EIN-Zuweisungsschreiben
- • IRS TIN/EIN-Antragsbestätigung (enthält normalerweise „Congratulations! The EIN has been successfully assigned“)
- • IRS-Steuerbefreiungsschreiben mit Firmenname und EIN`),
+ Bitte lade eine der folgenden Dateien hoch:
+ • IRS TIN/EIN-Zuweisungsschreiben
+ • IRS TIN/EIN-Antragsbestätigung (enthält normalerweise „Congratulations! The EIN has been successfully assigned“)
+ • IRS-Steuerbefreiungsschreiben mit Firmenname und EIN
+ `),
nameChangeDocument: 'Dokument zur Namensänderung',
nameChangeDocumentDescription:
'Wenn sich der Name deines Unternehmens seit der Beantragung der TIN/EIN geändert hat, benötigen wir dieses Dokument zur Verifizierung der angegebenen Steuer-ID',
companyAddressVerification: 'Verifizierung der Unternehmensadresse',
companyAddressVerificationDescription: dedent(`
- Bitte lade eine der folgenden Dateien hoch:
- • Aktuelle Strom-, Wasser- oder Gasrechnung mit Firmenname und Adresse
- • Kontoauszug mit Firmenname und Adresse
- • Aktueller Miet- oder Leasingvertrag inkl. Unterschriftsseite mit Firmenname und aktueller Adresse
- • Versicherungsnachweis mit Firmenname und Adresse
- • TIN-Zuweisungsdokument mit Firmenname und Adresse`),
+ Bitte lade eine der folgenden Dateien hoch:
+ • Aktuelle Strom-, Wasser- oder Gasrechnung mit Firmenname und Adresse
+ • Kontoauszug mit Firmenname und Adresse
+ • Aktueller Miet- oder Leasingvertrag inkl. Unterschriftsseite mit Firmenname und aktueller Adresse
+ • Versicherungsnachweis mit Firmenname und Adresse
+ • TIN-Zuweisungsdokument mit Firmenname und Adresse
+ `),
userAddressVerification: 'Adressverifizierung',
userAddressVerificationDescription: dedent(`
- Bitte lade eine der folgenden Dateien hoch:
- • Wählerregistrierungskarte
- • Führerschein
- • Kontoauszug
- • Versorgungsrechnung`),
+ Bitte lade eine der folgenden Dateien hoch:
+ • Wählerregistrierungskarte
+ • Führerschein
+ • Kontoauszug
+ • Versorgungsrechnung
+ `),
userDOBVerification: 'Geburtsdatumsverifizierung',
userDOBVerificationDescription: 'Bitte lade einen in den USA ausgestellten Ausweis hoch',
finishViaChat: 'Über Chat abschließen',
@@ -4340,7 +4374,6 @@ ${amount} für ${merchant} – ${date}`,
customFieldHint: 'Füge benutzerdefinierte Codierung hinzu, die für alle Ausgaben dieses Mitglieds gilt.',
reports: 'Berichte',
reportFields: 'Berichtsfelder',
- invoiceFields: 'Rechnungsfelder',
reportTitle: 'Berichtstitel',
reportField: 'Berichtsfeld',
taxes: 'Steuern',
@@ -4424,13 +4457,13 @@ ${amount} für ${merchant} – ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Admin';
+ return 'Workspace-Administrator';
case CONST.POLICY.ROLE.AUDITOR:
return 'Prüfer';
case CONST.POLICY.ROLE.EDITOR:
return 'Editor';
case CONST.POLICY.ROLE.CARD_ADMIN:
- return 'Kartenverwaltung';
+ return 'Kartenadministrator';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
return 'Personenverwaltung';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
@@ -6025,29 +6058,6 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
reportFieldInitialValueRequiredError: 'Bitte wähle einen Anfangswert für ein Berichtsfeld aus',
genericFailureMessage: 'Beim Aktualisieren des Berichtfelds ist ein Fehler aufgetreten. Bitte versuche es erneut.',
},
- invoiceFields: {
- subtitle: 'Rechnungsfelder können hilfreich sein, wenn du zusätzliche Informationen einfügen möchtest.',
- importedFromAccountingSoftware: 'Die folgenden Rechnungsfelder werden importiert aus Ihrem',
- disableInvoiceFields: 'Rechnungsfelder deaktivieren',
- disableInvoiceFieldsConfirmation: 'Sind Sie sicher? Rechnungsfelder werden auf Rechnungen deaktiviert.',
- delete: 'Rechnungsfeld löschen',
- deleteConfirmation: 'Sind Sie sicher, dass Sie dieses Rechnungsfeld löschen möchten?',
- findInvoiceField: 'Rechnungsfeld suchen',
- nameInputSubtitle: 'Wähle einen Namen für das Rechnungsfeld.',
- typeInputSubtitle: 'Wähle aus, welcher Rechnungsfeldtyp verwendet werden soll.',
- initialValueInputSubtitle: 'Gib einen Startwert ein, der im Rechnungsfeld angezeigt werden soll.',
- listValuesInputSubtitle: 'Diese Werte werden im Dropdown-Menü des Rechnungsfelds angezeigt. Aktivierte Werte können von Mitgliedern ausgewählt werden.',
- listInputSubtitle: 'Diese Werte werden in der Rechnungsfeldliste angezeigt. Aktivierte Werte können von Mitgliedern ausgewählt werden.',
- emptyInvoiceFieldsValues: {
- title: 'Noch keine Listenwerte',
- subtitle: 'Füge benutzerdefinierte Werte hinzu, die auf Rechnungen angezeigt werden.',
- },
- existingInvoiceFieldNameError: 'Ein Rechnungsfeld mit diesem Namen existiert bereits',
- invoiceFieldNameRequiredError: 'Bitte gib einen Namen für das Rechnungsfeld ein',
- invoiceFieldTypeRequiredError: 'Bitte wähle einen Rechnungsfeldtyp aus',
- invoiceFieldInitialValueRequiredError: 'Bitte wähle einen Anfangswert für ein Rechnungsfeld aus',
- addField: 'Feld hinzufügen',
- },
tags: {
tagName: 'Tag-Name',
requiresTag: 'Mitglieder müssen alle Ausgaben taggen',
@@ -6271,8 +6281,8 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
other: 'Mitglieder erstellen',
}),
makeAdmin: () => ({
- one: 'Als Admin festlegen',
- other: 'Admins ernennen',
+ one: 'Als Workspace-Admin festlegen',
+ other: 'Workspace-Admins ernennen',
}),
makeAuditor: () => ({
one: 'Zum Prüfer machen',
@@ -6301,12 +6311,14 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} hat einen ausstehenden Bericht in Bearbeitung, zu dem eine Aktion erforderlich ist. Bitte bitten Sie diese Person, die erforderliche Aktion abzuschließen, bevor Sie sie aus dem Workspace entfernen.`,
allMembers: 'Alle Mitglieder',
- admins: 'Admins',
+ admins: 'Workspace-Administratoren',
approvers: 'Genehmigende',
auditors: 'Prüfer',
emptyRoleFilter: {title: 'Keine Mitglieder entsprechen diesem Filter', subtitle: 'Laden Sie ein Mitglied ein oder ändern Sie den Filter oben.'},
configureHRSync: (providerName: string) => `Synchronisierung mit ${providerName} einrichten.`,
syncWithHR: (providerName: string) => `Mit ${providerName} synchronisieren`,
+ makeCardAdmin: () => ({one: 'Zum Karten-Admin machen', other: 'Karten-Admins festlegen'}),
+ cardAdmins: 'Karten-Admins',
},
card: {
getStartedIssuing: 'Beginne, indem du deine erste virtuelle oder physische Karte ausstellst.',
@@ -6724,6 +6736,10 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
}),
enableRate: 'Kurs aktivieren',
status: 'Status',
+ statusActive: 'Aktiv',
+ statusFuture: 'Zukünftig',
+ statusExpired: 'Abgelaufen',
+ statusInactive: 'Inaktiv',
unit: 'Einheit',
taxFeatureNotEnabledMessage:
'Steuern müssen im Workspace aktiviert sein, um diese Funktion zu verwenden. Gehe zu Weitere Funktionen , um das zu ändern. ',
@@ -6852,12 +6868,6 @@ Möchten Sie sie wirklich noch einmal exportieren?`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Berichtsfelder sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`} `,
},
- invoiceFields: {
- title: 'Rechnungsfelder',
- description: `Mit Rechnungsfeldern kannst du zusätzliche Details auf Rechnungsebene in Rechnungen aufnehmen.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Rechnungsfelder sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Profitiere von automatischer Synchronisierung und reduziere manuelle Eingaben mit der Expensify + NetSuite-Integration. Gewinne detaillierte Finanzanalysen in Echtzeit mit nativer und benutzerdefinierter Segmentunterstützung, einschließlich Projekt- und Kundenzuordnung.`,
@@ -6959,12 +6969,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Entfernungsraten sind im Collect-Tarif verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`} `,
},
- auditor: {
- title: 'Prüfer',
- description: 'Prüfer erhalten schreibgeschützten Zugriff auf alle Berichte für vollständige Transparenz und Compliance-Überwachung.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Auditor:innen sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Mehrere Genehmigungsstufen',
description:
@@ -7064,6 +7068,12 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Die Rechnungsstellung ist in den Collect- und Control-Tarifen verfügbar, beginnend ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`} `,
},
+ controlPolicyRoles: {
+ title: 'Rollen für Richtliniensteuerung',
+ description: 'Verwenden Sie spezialisierte Rollen wie Auditor und Karten-Admin, um Mitgliedern nur den Zugriff zu gewähren, den sie benötigen.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Spezialisierte Arbeitsbereichsrollen sind nur im Control-Tarif verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7390,6 +7400,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
deleteRuleConfirmation: 'Sind Sie sicher, dass Sie diese Regel löschen möchten?',
describeRuleTitle: 'Beschreiben Sie Ihre Regel',
describeRuleSubtitle: 'Beschreiben Sie Ihre Regel und Concierge erstellt sie',
+ disclaimer: 'KI-Agenten können Fehler machen.',
},
},
planTypePage: {
@@ -8070,21 +8081,19 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} von ${cards}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `hat den Standardsteuersatz der Kategorie „${categoryName}“ auf „${newTax}“ geändert (zuvor „${oldTax}“)`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `„${rateName}“-Satz von ${rateValue} hinzugefügt`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) => `„${rateName}“-Satz von ${rateValue} hinzugefügt, gültig ab ${startDate}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `„${rateName}“-Satz von ${rateValue} hinzugefügt, gültig bis ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`hat den Tarif „${rateName}“ mit ${rateValue} hinzugefügt, gültig vom ${startDate} bis ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `Startdatum des Tarifs „${rateName}“ auf ${newDate} aktualisiert (zuvor ${oldDate})` : `Startdatum des Tarifs „${rateName}“ auf ${newDate} festlegen`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `Enddatum des Tarifs „${rateName}“ auf ${newDate} aktualisiert (zuvor ${oldDate})` : `Enddatum des Tarifs „${rateName}“ auf ${newDate} setzen`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `Start- und Enddatum des Tarifs „${rateName}“ auf ${newStartDate} - ${newEndDate} aktualisiert (zuvor ${oldStartDate} - ${oldEndDate})`
- : `Start- und Enddatum des Tarifs „${rateName}“ auf ${newStartDate} – ${newEndDate} festlegen`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `Startdatum aus Tarif „${rateName}“ entfernt (zuvor ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `Enddatum aus dem Tarif „${rateName}“ entfernt (zuvor ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `hat den Entfernungstarif „${rateName}“ aktualisiert, sodass er für ${newDateRange} gilt (zuvor ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `ab dem ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `bis ${date}`,
+ customUnitRateDateRangeAllDates: () => `für alle Daten`,
},
roomMembersPage: {
memberNotFound: 'Mitglied nicht gefunden.',
@@ -8526,8 +8535,11 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
`Die Verbindung für ${feedName} ist unterbrochen. Um Kartenimporte wiederherzustellen, melden Sie sich bei Ihrer Bank an .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`Die Plaid-Verbindung zu Ihrem Geschäftskonto ist unterbrochen. Bitte verbinden Sie Ihr Bankkonto ${maskedAccountNumber} erneut , damit Sie Ihre Expensify Karten weiterhin verwenden können.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} ist über den Arbeitsbereichs-Einladungslink beigetreten` : `${email} als ${role === 'member' ? 'a' : 'an'} ${role} hinzugefügt`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'an' : 'a';
+ return didJoinPolicy ? `${email} ist über den Arbeitsbereichs-Einladungslink beigetreten` : `${email} wurde als ${article} ${translatedRole} hinzugefügt`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `hat die Rolle von ${email} in ${newRole} geändert (zuvor ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9425,6 +9437,11 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`Als Copilot von ${accountOwnerEmail} hast du keine Berechtigung, diese Aktion auszuführen. Entschuldigung!`,
+ removeCopilotAccess: 'Meinen Copilot-Zugriff entfernen',
+ removeCopilotAccessTitle: 'Copilot-Zugriff entfernen?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Sind Sie sicher, dass Sie Ihren Copilot-Zugriff auf das Expensify-Konto von ${delegatorName} entfernen möchten? Diese Aktion kann nicht rückgängig gemacht werden.`,
+ removeCopilotAccessConfirm: 'Zugriff entfernen',
copilotAccess: 'Copilot-Zugriff',
},
debug: {
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 6afe4e0574a8..cdf27de7f1a7 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -28,6 +28,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -109,8 +110,11 @@ const translations = {
selectMultiple: 'Select multiple',
saveChanges: 'Save changes',
submit: 'Submit',
+ markAsDone: 'Mark as done',
// @context Status label meaning an item has already been sent or submitted (e.g., a form or report). Not the action “to submit.”
submitted: 'Submitted',
+ // @context Status label meaning an item has been marked as done (track-intent users). Not the action “to mark as done.”
+ markedAsDoneStatus: 'Marked as done',
rotate: 'Rotate',
zoom: 'Zoom',
password: 'Password',
@@ -307,7 +311,6 @@ const translations = {
description: 'Description',
title: 'Title',
assignee: 'Assignee',
- createdBy: 'Created by',
with: 'with',
shareCode: 'Share code',
share: 'Share',
@@ -329,6 +332,7 @@ const translations = {
merchant: 'Merchant',
change: 'Change',
category: 'Category',
+ vendor: 'Vendor',
report: 'Report',
billable: 'Billable',
nonBillable: 'Non-billable',
@@ -372,6 +376,10 @@ const translations = {
subtitleText1: 'Find a chat using the',
subtitleText2: 'button above, or create something using the',
subtitleText3: 'button below.',
+ noUnreadChats: 'No unread chats',
+ noTodos: 'No to-dos',
+ caughtUp: "You're all caught up. Well done!",
+ seeAllChats: 'See all chats',
},
businessName: 'Business name',
clear: 'Clear',
@@ -886,6 +894,7 @@ const translations = {
beginningOfChatHistory: (users: string) => `This chat is with ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`This is where ${submitterDisplayName} will submit expenses to ${workspaceName} . Just use the + button.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: "This is where you'll track expenses.",
beginningOfChatHistorySelfDM: 'This is your personal space. Use it for notes, tasks, drafts, and reminders.',
beginningOfChatHistorySystemDM: "Welcome! Let's get you set up.",
chatWithAccountManager: 'Chat with your account manager here',
@@ -1004,7 +1013,7 @@ const translations = {
cta: 'Review',
},
validateAccount: {
- title: 'Validate your account to continue using Expensify',
+ title: 'Validate your account',
subtitle: 'Account',
cta: 'Validate',
},
@@ -1051,6 +1060,7 @@ const translations = {
menuItemDescription: 'See what Expensify can do in 2 min',
},
forYouSection: {
+ reviewExpenses: ({count}: {count: number}) => `Review ${count} ${count === 1 ? 'expense' : 'expenses'}`,
submit: ({count}: {count: number}) => `Submit ${count} ${count === 1 ? 'report' : 'reports'}`,
approve: ({count}: {count: number}) => `Approve ${count} ${count === 1 ? 'report' : 'reports'}`,
pay: ({count}: {count: number}) => `Pay ${count} ${count === 1 ? 'report' : 'reports'}`,
@@ -1409,6 +1419,7 @@ const translations = {
sendInvoice: (amount: string) => `Send ${amount} invoice`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`,
submitted: (memo?: string) => `submitted${memo ? `, saying ${memo}` : ''}`,
+ markedAsDone: (memo?: string) => `marked as done${memo ? `, saying ${memo}` : ''}`,
automaticallySubmitted: `submitted via delay submissions `,
queuedToSubmitViaDEW: 'queued to submit via custom approval workflow',
failedToAutoSubmitViaDEW: (reason: string) => `failed to submit the report via delay submissions . ${reason}`,
@@ -1625,6 +1636,9 @@ const translations = {
removed: 'removed',
transactionPending: 'Transaction pending.',
chooseARate: 'Select a workspace reimbursement rate per mile or kilometer',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} to ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Valid from ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Valid until ${endDate}`,
unapprove: 'Unapprove',
unapproveReport: 'Unapprove report',
headsUp: 'Heads up!',
@@ -1725,6 +1739,7 @@ const translations = {
},
correctRateError: 'Fix the rate error and try again.',
AskToExplain: `. Explain `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge matched this expense to ${vendorName} `,
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'),
billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'),
@@ -1853,6 +1868,22 @@ const translations = {
return `Waiting for an admin to submit expenses.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ // eslint-disable-next-line default-case
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `Waiting for you to mark this as done.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `Waiting for ${actor} to mark this as done.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `Waiting for an admin to mark this as done.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2309,12 +2340,16 @@ const translations = {
lockAccountPage: {
reportSuspiciousActivity: 'Report suspicious activity',
lockAccount: 'Lock account',
+ lockMyAccount: 'Lock my account',
unlockAccount: 'Unlock account',
unlockTitle: 'We’ve received your request',
unlockDescription: 'We’ll review the account to verify it’s safe to unlock and reach out via Concierge with any questions.',
- compromisedDescription:
- 'Notice something off with your account? Reporting it will immediately lock your account, block new Expensify Card transactions, and prevent any account changes.',
- domainAdminsDescription: 'For domain admins: This also pauses all Expensify Card activity and admin actions across your domain(s).',
+ findYourSituation: 'Most issues don’t require locking your account! Find your situation below:',
+ lostCardOrCharges:
+ 'Lost card or unfamiliar charges : Cancel your card and contact Concierge to dispute unknown transactions.',
+ unauthorizedAccess:
+ 'Unauthorized account access : Lock your account below. This blocks new Expensify Card transactions, card orders, and account changes. If you’re a domain admin, this also pauses all domain-wide card activity and admin actions.',
+ securityTeamFollowUp: 'Our security team will follow up from risk@expensify.com after locking.',
areYouSure: 'Are you sure you want to lock your Expensify account?',
onceLocked: 'Once locked, your account will be restricted pending an unlock request and a security review',
},
@@ -2664,13 +2699,10 @@ const translations = {
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `expenses from ${members}, and the approver is ${approvers}`,
addApprovalButton: 'Add approval workflow',
editWorkflowAction: 'Edit',
- addAgentAction: 'Add agent',
findWorkflow: 'Find workflow',
addApprovalTip: 'This default workflow applies to all members, unless a more specific workflow exists.',
approver: 'Approver',
addApprovalsDescription: 'Require additional approval before authorizing a payment.',
- automateApprovalsWithAgentsTitle: 'Automate approvals with agents',
- automateApprovalsWithAgentsSubtitle: 'Add agent below to workflow to automate approvals.',
configureViaHR: ({provider}: {provider: string}) => `Configure via ${provider}.`,
hrApprovalWorkflowLockedPrompt: ({provider}: {provider: string}) =>
`Approvals are managed by your ${provider} integration. To update your approval workflow, head to your ${provider} connection settings.`,
@@ -2981,6 +3013,16 @@ const translations = {
},
},
},
+ focusModeUpdateModal: {
+ title: 'Welcome to #focus mode!',
+ prompt: (priorityModePageUrl: string) =>
+ `Stay on top of things by only seeing unread chats or chats that need your attention. Don’t worry, you can change this at any point in settings .`,
+ },
+ inboxTabs: {
+ all: 'All',
+ todo: 'To-dos',
+ unread: 'Unread',
+ },
reportDetailsPage: {
goToRoom: 'Go to room',
inWorkspace: (policyName: string) => `in ${policyName}`,
@@ -3557,11 +3599,6 @@ const translations = {
month: 'Month',
selectMonth: 'Please select a month',
},
- focusModeUpdateModal: {
- title: 'Welcome to #focus mode!',
- prompt: (priorityModePageUrl: string) =>
- `Stay on top of things by only seeing unread chats or chats that need your attention. Don’t worry, you can change this at any point in settings .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'The chat you are looking for cannot be found.',
getMeOutOfHere: 'Get me out of here',
@@ -4429,7 +4466,6 @@ const translations = {
customFieldHint: 'Add custom coding that applies to all spend from this member.',
reports: 'Reports',
reportFields: 'Report fields',
- invoiceFields: 'Invoice fields',
reportTitle: 'Report title',
reportField: 'Report field',
taxes: 'Taxes',
@@ -4525,7 +4561,7 @@ const translations = {
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Admin';
+ return 'Workspace Admin';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditor';
case CONST.POLICY.ROLE.EDITOR:
@@ -6072,29 +6108,6 @@ const translations = {
reportFieldInitialValueRequiredError: 'Please choose a report field initial value',
genericFailureMessage: 'An error occurred while updating the report field. Please try again.',
},
- invoiceFields: {
- subtitle: "Invoice fields can be helpful when you'd like to include extra information.",
- importedFromAccountingSoftware: 'The invoice fields below are imported from your',
- disableInvoiceFields: 'Disable invoice fields',
- disableInvoiceFieldsConfirmation: 'Are you sure? Invoice fields will be disabled on invoices.',
- delete: 'Delete invoice field',
- deleteConfirmation: 'Are you sure you want to delete this invoice field?',
- findInvoiceField: 'Find invoice field',
- nameInputSubtitle: 'Choose a name for the invoice field.',
- typeInputSubtitle: 'Choose what type of invoice field to use.',
- initialValueInputSubtitle: 'Enter a starting value to show in the invoice field.',
- listValuesInputSubtitle: 'These values will appear in your invoice field dropdown. Enabled values can be selected by members.',
- listInputSubtitle: 'These values will appear in your invoice field list. Enabled values can be selected by members.',
- emptyInvoiceFieldsValues: {
- title: 'No list values yet',
- subtitle: 'Add custom values to appear on invoices.',
- },
- existingInvoiceFieldNameError: 'An invoice field with this name already exists',
- invoiceFieldNameRequiredError: 'Please enter an invoice field name',
- invoiceFieldTypeRequiredError: 'Please choose an invoice field type',
- invoiceFieldInitialValueRequiredError: 'Please choose an invoice field initial value',
- addField: 'Add field',
- },
tags: {
tagName: 'Tag name',
requiresTag: 'Members must tag all expenses',
@@ -6321,13 +6334,17 @@ const translations = {
other: 'Make members',
}),
makeAdmin: () => ({
- one: 'Make admin',
- other: 'Make admins',
+ one: 'Make workspace admin',
+ other: 'Make workspace admins',
}),
makeAuditor: () => ({
one: 'Make auditor',
other: 'Make auditors',
}),
+ makeCardAdmin: () => ({
+ one: 'Make card admin',
+ other: 'Make card admins',
+ }),
selectAll: 'Select all',
error: {
genericAdd: 'There was a problem adding this workspace member',
@@ -6340,7 +6357,8 @@ const translations = {
configureHRSync: (providerName: string) => `Configure ${providerName} sync.`,
syncWithHR: (providerName: string) => `Sync with ${providerName}`,
allMembers: 'All members',
- admins: 'Admins',
+ admins: 'Workspace Admins',
+ cardAdmins: 'Card Admins',
approvers: 'Approvers',
auditors: 'Auditors',
emptyRoleFilter: {
@@ -6863,6 +6881,10 @@ const translations = {
}),
enableRate: 'Enable rate',
status: 'Status',
+ statusActive: 'Active',
+ statusFuture: 'Future',
+ statusExpired: 'Expired',
+ statusInactive: 'Inactive',
unit: 'Unit',
taxFeatureNotEnabledMessage:
'Taxes must be enabled on the workspace to use this feature. Head over to More features to make that change. ',
@@ -6975,12 +6997,6 @@ const translations = {
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Report fields are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} `,
},
- invoiceFields: {
- title: 'Invoice fields',
- description: `Invoice fields let you include extra invoice-level details on invoices.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Invoice fields are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Enjoy automated syncing and reduce manual entries with the Expensify + NetSuite integration. Gain in-depth, realtime financial insights with native and custom segment support, including project and customer mapping.`,
@@ -7104,11 +7120,11 @@ const translations = {
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Distance rates are available on the Collect plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} `,
},
- auditor: {
- title: 'Auditor',
- description: 'Auditors get read-only access to all reports for full visibility and compliance monitoring.',
+ controlPolicyRoles: {
+ title: 'Control policy roles',
+ description: 'Use specialized roles like Auditor and Card Admin to grant members access only to what they need.',
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Auditors are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} `,
+ `Specialized workspace roles are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`} `,
},
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Multiple approval levels',
@@ -7497,6 +7513,7 @@ const translations = {
deleteRuleConfirmation: 'Are you sure you want to delete this rule?',
describeRuleTitle: 'Describe your rule',
describeRuleSubtitle: 'Describe your rule and Concierge will build it',
+ disclaimer: 'AI agents can make mistakes.',
},
},
planTypePage: {
@@ -7736,16 +7753,12 @@ const translations = {
updatedCustomUnitRateEnabled: (customUnitName: string, customUnitRateName: string, newValue: boolean) => {
return `${newValue ? 'enabled' : 'disabled'} the ${customUnitName} rate "${customUnitRateName}"`;
},
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `updated start date of "${rateName}" rate to ${newDate} (previously ${oldDate})` : `set start date of "${rateName}" rate to ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `updated end date of "${rateName}" rate to ${newDate} (previously ${oldDate})` : `set end date of "${rateName}" rate to ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `updated start and end date of "${rateName}" rate to ${newStartDate} - ${newEndDate} (previously ${oldStartDate} - ${oldEndDate})`
- : `set start and end date of "${rateName}" rate to ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `removed start date from "${rateName}" rate (previously ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `removed end date from "${rateName}" rate (previously ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `updated the distance rate "${rateName}" to apply ${newDateRange} (previously ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `from ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `until ${date}`,
+ customUnitRateDateRangeAllDates: () => `for all dates`,
updateReportFieldDefaultValue: (defaultValue?: string, fieldName?: string) => `set the default value of report field "${fieldName}" to "${defaultValue}"`,
addedReportFieldOption: (fieldName: string, optionName: string) => `added the option "${optionName}" to the report field "${fieldName}"`,
removedReportFieldOption: (fieldName: string, optionName: string) => `removed the option "${optionName}" from the report field "${fieldName}"`,
@@ -8029,6 +8042,8 @@ const translations = {
updatedAutoPayApprovedReportsLimit: ({oldLimit, newLimit}: {oldLimit: string; newLimit: string}) =>
`changed the auto-pay approved reports threshold to "${newLimit}" (previously "${oldLimit}")`,
removedAutoPayApprovedReportsLimit: 'removed the auto-pay approved reports threshold',
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `changed the "${categoryName}" category default tax rate to "${newTax}" (previously "${oldTax}")`,
updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) =>
`changed the default spend category for "${mccGroupName}" to "${newCategory}" (previously "${oldCategory}")`,
changedDefaultApprover: ({newApprover, previousApprover}: {newApprover: string; previousApprover?: string}) =>
@@ -8572,8 +8587,11 @@ const translations = {
`The ${feedName} connection is broken. To restore card imports, log into your bank .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`the Plaid connection to your business bank account is broken. Please reconnect your bank account ${maskedAccountNumber} so you can continue to use your Expensify Cards.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} joined via the workspace invite link` : `added ${email} as ${role === 'member' ? 'a' : 'an'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'an' : 'a';
+ return didJoinPolicy ? `${email} joined via the workspace invite link` : `added ${email} as ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `updated the role of ${email} to ${newRole} (previously ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9463,6 +9481,11 @@ const translations = {
},
removeCopilot: 'Remove copilot',
removeCopilotConfirmation: 'Are you sure you want to remove this copilot?',
+ removeCopilotAccess: 'Remove my copilot access',
+ removeCopilotAccessTitle: 'Remove copilot access?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Are you sure you want to remove your copilot access to ${delegatorName}'s Expensify account? This action cannot be undone.`,
+ removeCopilotAccessConfirm: 'Remove access',
changeAccessLevel: 'Change access level',
makeSureItIsYou: "Let's make sure it's you",
enterMagicCode: (contactMethod: string) => `Please enter the magic code sent to ${contactMethod} to add a copilot. It should arrive within a minute or two.`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index dcdb87c44509..be753ba8c002 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -14,7 +14,7 @@ import dedent from '@libs/StringUtils/dedent';
import CONST from '@src/CONST';
import type {OriginalMessageSettlementAccountLocked, PersonalRulesModifiedFields, PolicyRulesModifiedFields} from '@src/types/onyx/OriginalMessage';
import type en from './en';
-import type {ConciergeBrokenCardConnectionParams, PaidElsewhereParams, UnsupportedFormulaValueErrorParams} from './params';
+import type {ConciergeBrokenCardConnectionParams, PaidElsewhereParams, RemoveCopilotAccessConfirmationParams, UnsupportedFormulaValueErrorParams} from './params';
import type {TranslationDeepObject} from './types';
const translations: TranslationDeepObject = {
@@ -63,7 +63,9 @@ const translations: TranslationDeepObject = {
save: 'Guardar',
saveChanges: 'Guardar cambios',
submit: 'Enviar',
+ markAsDone: 'Marcar como listo',
submitted: 'Enviado',
+ markedAsDoneStatus: 'Marcado como listo',
rotate: 'Rotar',
zoom: 'Zoom',
password: 'Contraseña',
@@ -249,7 +251,6 @@ const translations: TranslationDeepObject = {
description: 'Descripción',
title: 'Título',
assignee: 'Asignado a',
- createdBy: 'Creado por',
with: 'con',
shareCode: 'Compartir código',
share: 'Compartir',
@@ -270,6 +271,7 @@ const translations: TranslationDeepObject = {
merchant: 'Comerciante',
change: 'Cambio',
category: 'Categoría',
+ vendor: 'Proveedor',
report: 'Informe',
billable: 'Facturable',
nonBillable: 'No facturable',
@@ -312,6 +314,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Encuentra un chat usando el botón',
subtitleText2: 'o crea algo usando el botón',
subtitleText3: '.',
+ noUnreadChats: 'No hay chats sin leer',
+ noTodos: 'No hay tareas pendientes',
+ caughtUp: 'Te has puesto al día. ¡Bien hecho!',
+ seeAllChats: 'Ver todos los chats',
},
businessName: 'Nombre de la empresa',
clear: 'Borrar',
@@ -801,6 +807,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users) => `Este chat es con ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName, submitterDisplayName) =>
`Aquí es donde ${submitterDisplayName} enviará los gastos al espacio de trabajo ${workspaceName} . Solo usa el botón +.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'Aquí es donde harás seguimiento de los gastos',
beginningOfChatHistorySelfDM: 'Este es tu espacio personal. Úsalo para notas, tareas, borradores y recordatorios.',
beginningOfChatHistorySystemDM: '¡Bienvenido! Vamos a configurar tu cuenta.',
chatWithAccountManager: 'Chatea con tu gestor de cuenta aquí',
@@ -916,7 +923,7 @@ const translations: TranslationDeepObject = {
cta: 'Revisar',
},
validateAccount: {
- title: 'Valida tu cuenta para continuar usando Expensify',
+ title: 'Valida tu cuenta',
subtitle: 'Cuenta',
cta: 'Validar',
},
@@ -996,6 +1003,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Todo al día',
f1FlagsDescription: 'Has completado todas las tareas pendientes.',
},
+ reviewExpenses: ({count}: {count: number}) => `Revisa ${count} ${count === 1 ? 'gasto' : 'gastos'}`,
},
gettingStartedSection: {
title: 'Primeros pasos',
@@ -1312,6 +1320,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount) => `Enviar factura de ${amount}`,
expenseAmount: (formattedAmount, comment) => `${formattedAmount}${comment ? ` para ${comment}` : ''}`,
submitted: (memo) => `enviado${memo ? `, dijo ${memo}` : ''}`,
+ markedAsDone: (memo) => `marcado como listo${memo ? `, dijo ${memo}` : ''}`,
automaticallySubmitted: `envió mediante retrasar envíos `,
queuedToSubmitViaDEW: 'en cola para enviar a través del flujo de aprobación personalizado',
failedToAutoSubmitViaDEW: (reason: string) => `no ha podido enviar este informe mediante retrasar envíos . ${reason}`,
@@ -1531,6 +1540,9 @@ const translations: TranslationDeepObject = {
removed: 'eliminó',
transactionPending: 'Transacción pendiente.',
chooseARate: 'Selecciona una tasa de reembolso por milla o kilómetro para el espacio de trabajo',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} a ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Válido desde ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Válido hasta ${endDate}`,
unapprove: 'Desaprobar',
unapproveReport: 'Anular la aprobación del informe',
headsUp: 'Atención!',
@@ -1631,6 +1643,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'Corrige el error de la tasa y vuelve a intentarlo.',
AskToExplain: `. Explicar `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge asoció este gasto con ${vendorName} `,
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marcó el gasto como "reembolsable"' : 'marcó el gasto como "no reembolsable"'),
billable: (value: boolean) => (value ? 'marcó el gasto como "facturable"' : 'marcó el gasto como "no facturable"'),
@@ -1739,6 +1752,17 @@ const translations: TranslationDeepObject = {
return `Esperando a que un administrador envíe los gastos.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (actor, actorType, _eta, _etaType) => {
+ // eslint-disable-next-line default-case
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `Esperando a que tú lo marques como listo.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `Esperando a que ${actor} lo marque como listo.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `Esperando a que un administrador lo marque como listo.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (_actor, _actorType, _eta, _etaType) => `¡No se requiere ninguna acción adicional!`,
[CONST.NEXT_STEP.MESSAGE_KEY.WAITING_FOR_SUBMITTER_ACCOUNT]: (actor, actorType, _eta, _etaType) => {
switch (actorType) {
@@ -2116,12 +2140,16 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Informar de actividad sospechosa',
lockAccount: 'Bloquear cuenta',
+ lockMyAccount: 'Bloquear mi cuenta',
unlockAccount: 'Desbloquear cuenta',
unlockTitle: 'Hemos recibido tu solicitud',
unlockDescription: 'Revisaremos la cuenta para verificar que sea seguro desbloquearla y nos comunicaremos a través de Concierge si tenemos alguna pregunta.',
- compromisedDescription:
- '¿Notas algo extraño en tu cuenta? Informarlo bloqueará tu cuenta de inmediato, detendrá nuevas transacciones con la Tarjeta Expensify y evitará cualquier cambio en la cuenta.',
- domainAdminsDescription: 'Para administradores de dominio: Esto también detiene toda la actividad de la Tarjeta Expensify y las acciones administrativas en tus dominios.',
+ findYourSituation: '¡La mayoría de los problemas no requieren bloquear tu cuenta! Busca tu situación a continuación:',
+ lostCardOrCharges:
+ 'Tarjeta perdida o cargos desconocidos : Cancela tu tarjeta y contacta con Concierge para disputar transacciones desconocidas.',
+ unauthorizedAccess:
+ 'Acceso no autorizado a la cuenta : Bloquea tu cuenta abajo. Esto bloquea nuevas transacciones con la Tarjeta Expensify, pedidos de tarjetas y cambios en la cuenta. Si eres administrador de dominio, esto también pausa toda la actividad de tarjetas y las acciones de administrador en todo el dominio.',
+ securityTeamFollowUp: 'Nuestro equipo de seguridad hará un seguimiento desde risk@expensify.com después del bloqueo.',
areYouSure: '¿Estás seguro de que deseas bloquear tu cuenta de Expensify?',
onceLocked: 'Una vez bloqueada, tu cuenta estará restringida hasta que se solicite el desbloqueo y se realice una revisión de seguridad.',
},
@@ -2474,13 +2502,10 @@ ${amount} para ${merchant} - ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `gastos de ${members}, y el aprobador es ${approvers}`,
addApprovalButton: 'Añadir flujo de aprobación',
editWorkflowAction: 'Editar',
- addAgentAction: 'Añadir agente',
findWorkflow: 'Buscar flujo de trabajo',
addApprovalTip: 'Este flujo de trabajo por defecto se aplica a todos los miembros, a menos que exista un flujo de trabajo más específico.',
approver: 'Aprobador',
addApprovalsDescription: 'Requiere una aprobación adicional antes de autorizar un pago.',
- automateApprovalsWithAgentsTitle: 'Automatiza las aprobaciones con agentes',
- automateApprovalsWithAgentsSubtitle: 'Añade el agente de abajo al flujo de trabajo para automatizar las aprobaciones.',
configureViaHR: ({provider}: {provider: string}) => `Configurar mediante ${provider}.`,
hrApprovalWorkflowLockedPrompt: ({provider}: {provider: string}) =>
`Las aprobaciones se gestionan mediante tu integración de ${provider}. Para actualizar tu flujo de aprobación, ve a la configuración de conexión de ${provider}.`,
@@ -2779,6 +2804,12 @@ ${amount} para ${merchant} - ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: '¡Bienvenido al modo #focus!',
+ prompt: (priorityModePageUrl: string) =>
+ `Mantente al tanto de todo viendo solo los chats no leídos o los chats que necesitan tu atención. No te preocupes, puedes cambiarlo en cualquier momento en los ajustes .`,
+ },
+ inboxTabs: {all: 'Todo', todo: 'Tareas pendientes', unread: 'No leído'},
reportDetailsPage: {
goToRoom: 'Ir a la sala',
inWorkspace: (policyName) => `en ${policyName}`,
@@ -3344,11 +3375,6 @@ ${amount} para ${merchant} - ${date}`,
month: 'Mes',
selectMonth: 'Por favor, selecciona un mes',
},
- focusModeUpdateModal: {
- title: '¡Bienvenido al modo #concentración!',
- prompt: (priorityModePageUrl) =>
- `Mantente al tanto de todo viendo sólo los chats no leídos o los que necesitan tu atención. No te preocupes, puedes cambiar el ajuste en cualquier momento desde la configuración .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'El chat que estás buscando no se pudo encontrar.',
getMeOutOfHere: 'Sácame de aquí',
@@ -4019,27 +4045,30 @@ ${amount} para ${merchant} - ${date}`,
verificationFailed: 'La verificación falló, por lo que necesitaremos documentos adicionales para verificarte a ti y a tu empresa',
taxIDVerification: 'Verificación del ID fiscal',
taxIDVerificationDescription: dedent(`
- Por favor, sube uno de los siguientes archivos:
- • Carta de asignación de TIN/EIN del IRS
- • Confirmación de solicitud de TIN/EIN del IRS (normalmente indica "Congratulations! The EIN has been successfully assigned")
- • Carta de exención fiscal del IRS que incluya el nombre de la empresa y el EIN`),
+ Por favor, sube uno de los siguientes archivos:
+ • Carta de asignación de TIN/EIN del IRS
+ • Confirmación de solicitud de TIN/EIN del IRS (normalmente indica "Congratulations! The EIN has been successfully assigned")
+ • Carta de exención fiscal del IRS que incluya el nombre de la empresa y el EIN
+ `),
nameChangeDocument: 'Documento de cambio de nombre',
nameChangeDocumentDescription: 'Si el nombre de tu empresa cambió desde que solicitaste el TIN/EIN, necesitamos este documento para verificar el número de ID fiscal proporcionado',
companyAddressVerification: 'Verificación de la dirección de la empresa',
companyAddressVerificationDescription: dedent(`
- Por favor, sube uno de los siguientes archivos:
- • Factura reciente de servicios públicos con nombre y dirección de la empresa
- • Estado de cuenta bancario con nombre y dirección de la empresa
- • Contrato de arrendamiento vigente con página de firmas que muestre el nombre y la dirección actual de la empresa
- • Estado de seguro con nombre y dirección de la empresa
- • Documento de asignación de TIN con nombre y dirección de la empresa`),
+ Por favor, sube uno de los siguientes archivos:
+ • Factura reciente de servicios públicos con nombre y dirección de la empresa
+ • Estado de cuenta bancario con nombre y dirección de la empresa
+ • Contrato de arrendamiento vigente con página de firmas que muestre el nombre y la dirección actual de la empresa
+ • Estado de seguro con nombre y dirección de la empresa
+ • Documento de asignación de TIN con nombre y dirección de la empresa
+ `),
userAddressVerification: 'Verificación de dirección',
userAddressVerificationDescription: dedent(`
- Por favor, sube uno de los siguientes archivos:
- • Tarjeta de registro de votante
- • Licencia de conducir
- • Estado de cuenta bancario
- • Factura de servicios públicos`),
+ Por favor, sube uno de los siguientes archivos:
+ • Tarjeta de registro de votante
+ • Licencia de conducir
+ • Estado de cuenta bancario
+ • Factura de servicios públicos
+ `),
userDOBVerification: 'Verificación de fecha de nacimiento',
userDOBVerificationDescription: 'Por favor, sube una identificación emitida en EE. UU.',
finishViaChat: 'Finalizar por chat',
@@ -4222,7 +4251,6 @@ ${amount} para ${merchant} - ${date}`,
customFieldHint: 'Añade una codificación personalizada que se aplique a todos los gastos de este miembro.',
reports: 'Informes',
reportFields: 'Campos de informe',
- invoiceFields: 'Campos de factura',
reportTitle: 'El título del informe.',
taxes: 'Impuestos',
bills: 'Pagar facturas',
@@ -4315,13 +4343,13 @@ ${amount} para ${merchant} - ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Administrador';
+ return 'Administrador del espacio de trabajo';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditor';
case CONST.POLICY.ROLE.EDITOR:
return 'Editor';
case CONST.POLICY.ROLE.CARD_ADMIN:
- return 'Administrador de tarjeta';
+ return 'Administrador de tarjetas';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
return 'Administrador de personas';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
@@ -5865,29 +5893,6 @@ ${amount} para ${merchant} - ${date}`,
reportFieldInitialValueRequiredError: 'Elige un valor inicial de campo de informe',
genericFailureMessage: 'Se ha producido un error al actualizar el campo de informe. Por favor, inténtalo de nuevo.',
},
- invoiceFields: {
- subtitle: 'Los campos de factura pueden ayudarte cuando quieras incluir información adicional.',
- importedFromAccountingSoftware: 'Campos de factura importados desde',
- disableInvoiceFields: 'Desactivar campos de factura',
- disableInvoiceFieldsConfirmation: '¿Estás seguro? Los campos de factura se desactivarán en las facturas.',
- delete: 'Eliminar campo de factura',
- deleteConfirmation: '¿Seguro que deseas eliminar este campo de factura?',
- findInvoiceField: 'Buscar campo de factura',
- nameInputSubtitle: 'Elige un nombre para el campo de factura.',
- typeInputSubtitle: 'Elige qué tipo de campo de factura usar.',
- initialValueInputSubtitle: 'Ingresa un valor inicial para mostrar en el campo de factura.',
- listValuesInputSubtitle: 'Estos valores aparecerán en el menú desplegable del campo de factura. Los miembros pueden seleccionar los valores activados.',
- listInputSubtitle: 'Estos valores aparecerán en la lista del campo de factura. Los miembros pueden seleccionar los valores activados.',
- emptyInvoiceFieldsValues: {
- title: 'Aún no hay valores de lista',
- subtitle: 'Agrega valores personalizados para que aparezcan en las facturas.',
- },
- existingInvoiceFieldNameError: 'Ya existe un campo de factura con este nombre',
- invoiceFieldNameRequiredError: 'Ingresa un nombre para el campo de factura',
- invoiceFieldTypeRequiredError: 'Elige un tipo de campo de factura',
- invoiceFieldInitialValueRequiredError: 'Elige un valor inicial para el campo de factura',
- addField: 'Añadir campo',
- },
tags: {
tagName: 'Nombre de etiqueta',
requiresTag: 'Los miembros deben etiquetar todos los gastos',
@@ -6110,8 +6115,8 @@ ${amount} para ${merchant} - ${date}`,
other: 'Convertir en miembros',
}),
makeAdmin: () => ({
- one: 'Hacer administrador',
- other: 'Convertir en administradores',
+ one: 'Hacer administrador del espacio de trabajo',
+ other: 'Convertir en administradores del espacio de trabajo',
}),
makeAuditor: () => ({
one: 'Convertir en auditor',
@@ -6127,7 +6132,7 @@ ${amount} para ${merchant} - ${date}`,
invitedBySecondaryLogin: (secondaryLogin) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
workspaceMembersCount: (count) => `Total de miembros del espacio de trabajo: ${count}`,
allMembers: 'Todos los miembros',
- admins: 'Administradores',
+ admins: 'Administradores del espacio de trabajo',
approvers: 'Aprobadores',
auditors: 'Auditores',
emptyRoleFilter: {
@@ -6149,6 +6154,8 @@ ${amount} para ${merchant} - ${date}`,
`${memberName} tiene un informe en proceso pendiente de acción. Pídele que complete la acción requerida antes de eliminarlo del espacio de trabajo.`,
configureHRSync: (providerName: string) => `Configura la sincronización de ${providerName}.`,
syncWithHR: (providerName: string) => `Sincronizar con ${providerName}`,
+ makeCardAdmin: () => ({one: 'Hacer administrador de tarjetas', other: 'Hacer administradores de tarjetas'}),
+ cardAdmins: 'Administradores de tarjetas',
},
accounting: {
settings: 'configuración',
@@ -6648,6 +6655,10 @@ ${amount} para ${merchant} - ${date}`,
}),
enableRate: 'Activar tasa',
status: 'Estado',
+ statusActive: 'Activo',
+ statusFuture: 'Futuro',
+ statusExpired: 'Expirado',
+ statusInactive: 'Inactivo',
unit: 'Unidad',
taxFeatureNotEnabledMessage:
'Los impuestos deben estar activados en el área de trabajo para poder utilizar esta función. Dirígete a Más funcionalidades para hacer ese cambio. ',
@@ -6784,12 +6795,6 @@ ${amount} para ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) =>
`Los campos de informe sólo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} `,
},
- invoiceFields: {
- title: 'Campos de factura',
- description: `Los campos de factura te permiten incluir detalles adicionales a nivel de factura.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) =>
- `Los campos de factura sólo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Disfruta de la sincronización automática y reduce las entradas manuales con la integración Expensify + NetSuite. Obtén información financiera en profundidad y en tiempo real con la compatibilidad nativa y personalizada con segmentos, incluida la asignación de proyectos y clientes.`,
@@ -6914,12 +6919,6 @@ ${amount} para ${merchant} - ${date}`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) =>
`Las tasas de distancia están disponibles en el plan Recopilar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} `,
},
- auditor: {
- title: 'Auditor',
- description: 'Los auditores tienen acceso de lectura a todos los informes para una visibilidad completa y la supervisión del cumplimiento.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Los auditores solo están disponibles con el plan Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Múltiples niveles de aprobación',
description:
@@ -6993,6 +6992,12 @@ ${amount} para ${merchant} - ${date}`,
upgradeWorkspaceWarningForRestrictedPolicyCreationPrompt:
'Su empresa ha restringido la creación de espacios de trabajo. Por favor, contacte a un administrador para obtener ayuda.',
},
+ controlPolicyRoles: {
+ title: 'Controlar roles de la política',
+ description: 'Usa roles especializados como Auditor y Administrador de tarjetas para dar a los miembros acceso solo a lo que necesitan.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Los roles especializados del espacio de trabajo solo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7291,6 +7296,7 @@ ${amount} para ${merchant} - ${date}`,
editRuleTitle: 'Editar regla',
deleteRule: 'Eliminar regla',
deleteRuleConfirmation: '¿Seguro que quieres eliminar esta regla?',
+ disclaimer: 'Los agentes de IA pueden cometer errores.',
},
},
emptyDomain: {
@@ -7779,6 +7785,8 @@ ${amount} para ${merchant} - ${date}`,
updatedAutoPayApprovedReportsLimit: ({oldLimit, newLimit}: {oldLimit: string; newLimit: string}) =>
`cambió el umbral de autopago de informes aprobados a "${newLimit}" (previamente "${oldLimit}")`,
removedAutoPayApprovedReportsLimit: 'eliminó el umbral de autopago de informes aprobados',
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `cambió la tasa de impuesto predeterminada de la categoría "${categoryName}" a "${newTax}" (previamente "${oldTax}")`,
updatedMccGroupCategory: ({mccGroupName, oldCategory, newCategory}: {mccGroupName: string; oldCategory: string; newCategory: string}) =>
`cambió la categoría de gasto predeterminada para "${mccGroupName}" a "${newCategory}" (previamente "${oldCategory}")`,
changedDefaultApprover: ({newApprover, previousApprover}: {newApprover: string; previousApprover?: string}) =>
@@ -7873,18 +7881,12 @@ ${amount} para ${merchant} - ${date}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `añadió la tasa "${rateName}" de ${rateValue}, válida hasta ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`añadió la tasa "${rateName}" de ${rateValue}, válida del ${startDate} al ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `actualizó la fecha de inicio de la tasa "${rateName}" a ${newDate} (previamente ${oldDate})` : `establecer la fecha de inicio de la tasa "${rateName}" a ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate
- ? `actualizó la fecha de finalización de la tasa "${rateName}" a ${newDate} (previamente ${oldDate})`
- : `establecer la fecha de finalización de la tasa "${rateName}" en ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `actualizó la fecha de inicio y fin de la tasa "${rateName}" a ${newStartDate} - ${newEndDate} (previamente ${oldStartDate} - ${oldEndDate})`
- : `establece la fecha de inicio y fin de la tasa "${rateName}" a ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `se eliminó la fecha de inicio de la tasa "${rateName}" (previamente ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `eliminó la fecha de finalización de la tasa «${rateName}» (previamente ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `actualizó la tasa de distancia «${rateName}» para aplicar ${newDateRange} (previamente ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `desde ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `hasta ${date}`,
+ customUnitRateDateRangeAllDates: () => `para todas las fechas`,
},
roomMembersPage: {
memberNotFound: 'Miembro no encontrado.',
@@ -8322,8 +8324,11 @@ ${amount} para ${merchant} - ${date}`,
`La conexión ${feedName} está rota. Para restaurar las importaciones de tarjetas, inicia sesión en tu banco .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`la conexión Plaid con tu cuenta bancaria de empresa está rota. Por favor, reconecta tu cuenta bancaria ${maskedAccountNumber} para poder seguir usando tus Tarjetas Expensify.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} se unió mediante el enlace de invitación del espacio de trabajo` : `se añadió ${email} como ${role === 'member' ? 'a' : 'a un'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'un' : 'a';
+ return didJoinPolicy ? `${email} se unió mediante el enlace de invitación del espacio de trabajo` : `añadió ${email} como ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}) => `actualizó el rol ${email} a ${newRole} (previamente ${currentRole})`,
updatedCustomField1: (email, newValue, previousValue) => {
if (!newValue) {
@@ -9582,6 +9587,11 @@ ${amount} para ${merchant} - ${date}`,
},
removeCopilot: 'Eliminar copiloto',
removeCopilotConfirmation: '¿Estás seguro de que quieres eliminar este copiloto?',
+ removeCopilotAccess: 'Eliminar mi acceso de copiloto',
+ removeCopilotAccessTitle: '¿Eliminar acceso de copiloto?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `¿Estás seguro de que quieres eliminar tu acceso de copiloto a la cuenta de Expensify de ${delegatorName}? Esta acción no se puede deshacer.`,
+ removeCopilotAccessConfirm: 'Eliminar acceso',
changeAccessLevel: 'Cambiar nivel de acceso',
makeSureItIsYou: 'Vamos a asegurarnos de que eres tú',
enterMagicCode: (contactMethod) => `Por favor, introduce el código mágico enviado a ${contactMethod} para agregar un copiloto. Debería llegar en un par de minutos.`,
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index cae4d17519d5..bfef9ba65a21 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1,7 +1,6 @@
/**
* _____ __ __
* / ___/__ ___ ___ _______ _/ /____ ___/ /
- * / (_ / -_) _ \/ -_) __/ _ \`/ __/ -_) _ /
* \___/\__/_//_/\__/_/ \_,_/\__/\__/\_,_/
*
* This file was automatically generated. Please consider these alternatives before manually editing it:
@@ -40,6 +39,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +108,9 @@ const translations: TranslationDeepObject = {
selectMultiple: 'Sélection multiple',
saveChanges: 'Enregistrer les modifications',
submit: 'Soumettre',
+ markAsDone: 'Marquer comme terminé',
submitted: 'Soumis',
+ markedAsDoneStatus: 'Marqué comme terminé',
rotate: 'Pivoter',
zoom: 'Zoom',
password: 'Mot de passe',
@@ -293,7 +295,6 @@ const translations: TranslationDeepObject = {
description: 'Description',
title: 'Titre',
assignee: 'Attribué',
- createdBy: 'Créé par',
with: 'avec',
shareCode: 'Partager le code',
share: 'Partager',
@@ -314,6 +315,7 @@ const translations: TranslationDeepObject = {
merchant: 'Commerçant',
change: 'Modifier',
category: 'Catégorie',
+ vendor: 'Fournisseur',
report: 'Note de frais',
billable: 'Facturable',
nonBillable: 'Non refacturable',
@@ -355,6 +357,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Recherchez une discussion à l’aide de la',
subtitleText2: 'bouton ci-dessus, ou créez quelque chose en utilisant le',
subtitleText3: 'bouton ci-dessous.',
+ noUnreadChats: 'Aucune discussion non lue',
+ noTodos: 'Aucune tâche à faire',
+ caughtUp: 'Vous êtes à jour. Bravo !',
+ seeAllChats: 'Voir toutes les discussions',
},
businessName: 'Nom de l’entreprise',
clear: 'Effacer',
@@ -856,6 +862,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `Cette discussion est avec ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`C’est ici que ${submitterDisplayName} soumettra des dépenses à ${workspaceName} . Utilisez simplement le bouton +.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'C\u2019est ici que vous suivrez vos dépenses',
beginningOfChatHistorySelfDM: 'Ceci est votre espace personnel. Utilisez-le pour vos notes, tâches, brouillons et rappels.',
beginningOfChatHistorySystemDM: 'Bienvenue ! Procédons à la configuration.',
chatWithAccountManager: 'Discutez avec votre gestionnaire de compte ici',
@@ -961,7 +968,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Réparer la connexion de la carte personnelle ${cardName}` : 'Corriger la connexion de la carte personnelle'),
subtitle: 'Portefeuille',
},
- validateAccount: {title: 'Validez votre compte pour continuer à utiliser Expensify', subtitle: 'Compte', cta: 'Valider'},
+ validateAccount: {title: 'Validez votre compte', subtitle: 'Compte', cta: 'Valider'},
fixFailedBilling: {title: 'Nous n’avons pas pu débiter votre carte enregistrée', subtitle: 'Abonnement'},
unlockBankAccount: {
workspaceTitle: 'Votre compte bancaire professionnel a été verrouillé',
@@ -1016,6 +1023,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Tout est à jour',
f1FlagsDescription: 'Vous avez terminé toutes les tâches en cours.',
},
+ reviewExpenses: ({count}: {count: number}) => `Examiner ${count} ${count === 1 ? 'dépense' : 'dépenses'}`,
},
upcomingTravel: 'Voyages à venir',
upcomingTravelSection: {
@@ -1354,6 +1362,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `Envoyer la facture de ${amount}`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `pour ${comment}` : ''}`,
submitted: (memo?: string) => `soumis${memo ? `, indiquant « ${memo} »` : ''}`,
+ markedAsDone: (memo?: string) => `marqué comme terminé${memo ? `, en indiquant « ${memo} »` : ''}`,
automaticallySubmitted: `soumis via soumissions différées `,
queuedToSubmitViaDEW: 'en file d’attente pour être soumis via le circuit d’approbation personnalisé',
queuedToApproveViaDEW: 'mis en file d’attente pour approbation via un processus d’approbation personnalisé',
@@ -1570,6 +1579,9 @@ const translations: TranslationDeepObject = {
removed: 'supprimé',
transactionPending: 'Transaction en attente.',
chooseARate: 'Sélectionnez un taux de remboursement par mile ou kilomètre pour l’espace de travail',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} au ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Valide à partir du ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Valide jusqu’au ${endDate}`,
unapprove: 'Annuler l’approbation',
unapproveReport: 'Retirer l’approbation de la note de frais',
headsUp: 'Attention !',
@@ -1671,6 +1683,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'Corrigez l’erreur de taux et réessayez.',
AskToExplain: `. Expliquer `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge a associé cette dépense au fournisseur ${vendorName} `,
duplicateNonDefaultWorkspacePerDiemError:
'Vous ne pouvez pas dupliquer les indemnités journalières entre plusieurs espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.',
rulesModifiedFields: {
@@ -1800,6 +1813,21 @@ const translations: TranslationDeepObject = {
return `En attente qu’un administrateur soumette des dépenses.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `En attente que vous marquiez ceci comme terminé.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `En attente que ${actor} marque ceci comme terminé.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `En attente qu'un administrateur marque ceci comme terminé.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2233,11 +2261,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Signaler une activité suspecte',
lockAccount: 'Verrouiller le compte',
+ lockMyAccount: 'Verrouiller mon compte',
unlockAccount: 'Déverrouiller le compte',
- compromisedDescription:
- 'Vous remarquez quelque chose d’inhabituel sur votre compte ? Le signaler bloquera immédiatement votre compte, empêchera les nouvelles transactions avec la Carte Expensify et interdira toute modification du compte.',
- domainAdminsDescription:
- "Pour les administrateurs de domaine : cela met également en pause toute l'activité de Carte Expensify et toutes les actions d'administration sur l’ensemble de votre (vos) domaine(s).",
+ findYourSituation: 'La plupart des problèmes ne nécessitent pas de verrouiller votre compte ! Trouvez votre situation ci-dessous :',
+ lostCardOrCharges:
+ 'Carte perdue ou frais inconnus : Annulez votre carte et contactez Concierge pour contester les transactions inconnues.',
+ unauthorizedAccess:
+ 'Accès non autorisé au compte : Verrouillez votre compte ci-dessous. Cela bloque les nouvelles transactions Carte Expensify, les commandes de carte et les modifications de compte. Si vous êtes administrateur de domaine, cela met également en pause toute l’activité de carte au niveau du domaine et les actions des administrateurs.',
+ securityTeamFollowUp: 'Notre équipe de sécurité effectuera un suivi depuis risk@expensify.com après le verrouillage.',
areYouSure: 'Voulez-vous vraiment verrouiller votre compte Expensify ?',
onceLocked: 'Une fois verrouillé, votre compte sera restreint en attendant une demande de déverrouillage et un contrôle de sécurité',
unlockTitle: 'Nous avons bien reçu votre demande',
@@ -2607,13 +2638,10 @@ ${amount} pour ${merchant} - ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `dépenses de ${members}, et l'approbateur est ${approvers}`,
addApprovalButton: 'Ajouter un workflow d’approbation',
editWorkflowAction: 'Modifier',
- addAgentAction: 'Ajouter un agent',
findWorkflow: 'Rechercher un flux de travail',
addApprovalTip: 'Ce workflow par défaut s’applique à tous les membres, sauf si un workflow plus spécifique existe.',
approver: 'Approbateur',
addApprovalsDescription: 'Exiger une approbation supplémentaire avant d’autoriser un paiement.',
- automateApprovalsWithAgentsTitle: 'Automatisez les approbations avec des agents',
- automateApprovalsWithAgentsSubtitle: 'Ajoutez l’agent ci-dessous au flux de travail pour automatiser les approbations.',
makeOrTrackPaymentsTitle: 'Paiements',
makeOrTrackPaymentsDescription: 'Ajoutez un payeur autorisé pour les paiements effectués dans Expensify ou suivez les paiements effectués ailleurs.',
customApprovalWorkflowEnabled:
@@ -2909,6 +2937,12 @@ ${amount} pour ${merchant} - ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: 'Bienvenue dans le mode #focus !',
+ prompt: (priorityModePageUrl: string) =>
+ `Gardez le contrôle en n’affichant que les discussions non lues ou celles qui nécessitent votre attention. Ne vous inquiétez pas, vous pouvez modifier ce réglage à tout moment dans les paramètres .`,
+ },
+ inboxTabs: {all: 'Tout', todo: 'Tâches', unread: 'Non lu'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `dans ${policyName}`,
generatingPDF: 'Générer le PDF',
@@ -3480,11 +3514,6 @@ ${amount} pour ${merchant} - ${date}`,
year: 'Année',
selectYear: 'Veuillez sélectionner une année',
},
- focusModeUpdateModal: {
- title: 'Bienvenue en mode #focus !',
- prompt: (priorityModePageUrl: string) =>
- `Gardez le contrôle en affichant uniquement les discussions non lues ou celles qui nécessitent votre attention. Ne vous inquiétez pas, vous pouvez modifier ce paramètre à tout moment dans les paramètres .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'La discussion que vous recherchez est introuvable.',
getMeOutOfHere: 'Faites-moi sortir d’ici',
@@ -4149,28 +4178,31 @@ ${amount} pour ${merchant} - ${date}`,
verificationFailed: 'La vérification a échoué, nous aurons donc besoin de documents supplémentaires pour te vérifier ainsi que ton entreprise',
taxIDVerification: 'Vérification de l’identifiant fiscal',
taxIDVerificationDescription: dedent(`
- Veuillez téléverser l’un des fichiers suivants :
- • Lettre d’attribution TIN/EIN de l’IRS
- • Confirmation de demande TIN/EIN de l’IRS (indique généralement « Congratulations! The EIN has been successfully assigned »)
- • Lettre d’exonération fiscale de l’IRS indiquant le nom de l’entreprise et l’EIN`),
+ Veuillez téléverser l’un des fichiers suivants :
+ • Lettre d’attribution TIN/EIN de l’IRS
+ • Confirmation de demande TIN/EIN de l’IRS (indique généralement « Congratulations! The EIN has been successfully assigned »)
+ • Lettre d’exonération fiscale de l’IRS indiquant le nom de l’entreprise et l’EIN
+ `),
nameChangeDocument: 'Document de changement de nom',
nameChangeDocumentDescription:
'Si le nom de ton entreprise a changé depuis la demande du TIN/EIN, ce document est nécessaire pour vérifier le numéro d’identification fiscale fourni',
companyAddressVerification: 'Vérification de l’adresse de l’entreprise',
companyAddressVerificationDescription: dedent(`
- Veuillez téléverser l’un des fichiers suivants :
- • Facture récente de services publics indiquant le nom et l’adresse de l’entreprise
- • Relevé bancaire indiquant le nom et l’adresse de l’entreprise
- • Contrat de location en cours incluant la page de signature avec le nom et l’adresse actuelle de l’entreprise
- • Attestation d’assurance indiquant le nom et l’adresse de l’entreprise
- • Document d’attribution TIN indiquant le nom et l’adresse de l’entreprise`),
+ Veuillez téléverser l’un des fichiers suivants :
+ • Facture récente de services publics indiquant le nom et l’adresse de l’entreprise
+ • Relevé bancaire indiquant le nom et l’adresse de l’entreprise
+ • Contrat de location en cours incluant la page de signature avec le nom et l’adresse actuelle de l’entreprise
+ • Attestation d’assurance indiquant le nom et l’adresse de l’entreprise
+ • Document d’attribution TIN indiquant le nom et l’adresse de l’entreprise
+ `),
userAddressVerification: 'Vérification de l’adresse',
userAddressVerificationDescription: dedent(`
- Veuillez téléverser l’un des fichiers suivants :
- • Carte d’inscription électorale
- • Permis de conduire
- • Relevé bancaire
- • Facture de services publics`),
+ Veuillez téléverser l’un des fichiers suivants :
+ • Carte d’inscription électorale
+ • Permis de conduire
+ • Relevé bancaire
+ • Facture de services publics
+ `),
userDOBVerification: 'Vérification de la date de naissance',
userDOBVerificationDescription: 'Veuillez téléverser une pièce d’identité délivrée aux États-Unis',
finishViaChat: 'Finaliser via le chat',
@@ -4352,7 +4384,6 @@ ${amount} pour ${merchant} - ${date}`,
customFieldHint: 'Ajoutez un codage personnalisé qui s’applique à toutes les dépenses de ce membre.',
reports: 'Notes de frais',
reportFields: 'Champs de note de frais',
- invoiceFields: 'Champs de facture',
reportTitle: 'Titre de la note de frais',
reportField: 'Champ de note de frais',
taxes: 'Taxes',
@@ -4436,7 +4467,7 @@ ${amount} pour ${merchant} - ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Administrateur';
+ return 'Administrateur d’espace de travail';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditeur';
case CONST.POLICY.ROLE.EDITOR:
@@ -4444,7 +4475,7 @@ ${amount} pour ${merchant} - ${date}`,
case CONST.POLICY.ROLE.CARD_ADMIN:
return 'Administrateur de carte';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
- return 'Administration des personnes';
+ return 'Admin personnes';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
return 'Administrateur des paiements';
case CONST.POLICY.ROLE.USER:
@@ -6051,29 +6082,6 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
reportFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale pour le champ de note de frais',
genericFailureMessage: 'Une erreur s’est produite lors de la mise à jour du champ de note de frais. Veuillez réessayer.',
},
- invoiceFields: {
- subtitle: 'Les champs de facture peuvent être utiles lorsque vous souhaitez inclure des informations supplémentaires.',
- importedFromAccountingSoftware: 'Les champs de facture ci-dessous sont importés depuis votre',
- disableInvoiceFields: 'Désactiver les champs de facture',
- disableInvoiceFieldsConfirmation: 'Êtes-vous sûr ? Les champs de facture seront désactivés sur les factures.',
- delete: 'Supprimer le champ de facture',
- deleteConfirmation: 'Voulez-vous vraiment supprimer ce champ de facture ?',
- findInvoiceField: 'Trouver un champ de facture',
- nameInputSubtitle: 'Choisissez un nom pour le champ de facture.',
- typeInputSubtitle: 'Choisissez le type de champ de facture à utiliser.',
- initialValueInputSubtitle: 'Saisissez une valeur de départ à afficher dans le champ de facture.',
- listValuesInputSubtitle: 'Ces valeurs apparaîtront dans la liste déroulante du champ de facture. Les valeurs activées peuvent être sélectionnées par les membres.',
- listInputSubtitle: 'Ces valeurs apparaîtront dans la liste du champ de facture. Les valeurs activées peuvent être sélectionnées par les membres.',
- emptyInvoiceFieldsValues: {
- title: 'Aucune valeur de liste pour le moment',
- subtitle: 'Ajoutez des valeurs personnalisées à afficher sur les factures.',
- },
- existingInvoiceFieldNameError: 'Un champ de facture portant ce nom existe déjà',
- invoiceFieldNameRequiredError: 'Veuillez saisir un nom de champ de facture',
- invoiceFieldTypeRequiredError: 'Veuillez choisir un type de champ de facture',
- invoiceFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale de champ de facture',
- addField: 'Ajouter un champ',
- },
tags: {
tagName: 'Nom du tag',
requiresTag: 'Les membres doivent taguer toutes les dépenses',
@@ -6296,8 +6304,8 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
other: 'Créer des membres',
}),
makeAdmin: () => ({
- one: 'Nommer administrateur',
- other: 'Nommer des administrateurs',
+ one: 'Nommer administrateur de l’espace de travail',
+ other: 'Nommer des administrateurs de l’espace de travail',
}),
makeAuditor: () => ({
one: 'Nommer auditeur',
@@ -6326,12 +6334,14 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} a une note de frais en cours de traitement sur laquelle il doit agir. Veuillez lui demander d’effectuer l’action requise avant de le retirer de l’espace de travail.`,
allMembers: 'Tous les membres',
- admins: 'Administrateurs',
+ admins: 'Administrateurs de l’espace de travail',
approvers: 'Approbateurs',
auditors: 'Auditeurs',
emptyRoleFilter: {title: 'Aucun membre ne correspond à ce filtre', subtitle: 'Invitez un membre ou modifiez le filtre ci-dessus.'},
configureHRSync: (providerName: string) => `Configurer la synchronisation ${providerName}.`,
syncWithHR: (providerName: string) => `Synchroniser avec ${providerName}`,
+ makeCardAdmin: () => ({one: 'Nommer administrateur de carte', other: 'Nommer des administrateurs de carte'}),
+ cardAdmins: 'Administrateurs de cartes',
},
card: {
getStartedIssuing: 'Commencez par émettre votre première carte virtuelle ou physique.',
@@ -6751,6 +6761,10 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
}),
enableRate: 'Activer le taux',
status: 'Statut',
+ statusActive: 'Actif',
+ statusFuture: 'Futur',
+ statusExpired: 'Expiré',
+ statusInactive: 'Inactif',
unit: 'Unité',
taxFeatureNotEnabledMessage:
'Les taxes doivent être activées sur l’espace de travail pour utiliser cette fonctionnalité. Rendez-vous dans Plus de fonctionnalités pour effectuer cette modification. ',
@@ -6879,12 +6893,6 @@ Voulez-vous vraiment les exporter à nouveau ?`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Les champs de note de frais sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} `,
},
- invoiceFields: {
- title: 'Champs de facture',
- description: `Les champs de facture vous permettent d'inclure des détails supplémentaires au niveau de la facture.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Les champs de facture sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Profitez de la synchronisation automatisée et réduisez les saisies manuelles grâce à l’intégration Expensify + NetSuite. Obtenez des informations financières détaillées et en temps réel avec la prise en charge des segments natifs et personnalisés, y compris la correspondance des projets et des clients.`,
@@ -6986,12 +6994,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Les taux de distance sont disponibles avec l’abonnement Collect, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} `,
},
- auditor: {
- title: 'Auditeur',
- description: 'Les auditeurs obtiennent un accès en lecture seule à toutes les notes de frais pour une visibilité complète et le suivi de la conformité.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Les auditeurs ne sont disponibles que dans l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Niveaux d’approbation multiples',
description:
@@ -7092,6 +7094,12 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`La facturation est disponible avec les offres Collect et Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} `,
},
+ controlPolicyRoles: {
+ title: 'Contrôler les rôles de politique',
+ description: 'Utilisez des rôles spécialisés comme Auditeur et Administrateur de cartes pour donner aux membres accès uniquement à ce dont ils ont besoin.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Les rôles spécialisés dans l’espace de travail sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7418,6 +7426,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
deleteRuleConfirmation: 'Voulez-vous vraiment supprimer cette règle ?',
describeRuleTitle: 'Décrivez votre règle',
describeRuleSubtitle: 'Décrivez votre règle et Concierge la créera',
+ disclaimer: 'Les agents IA peuvent faire des erreurs.',
},
},
planTypePage: {
@@ -8105,22 +8114,20 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `a modifié le taux de taxe par défaut de la catégorie « ${categoryName} » en « ${newTax} » (auparavant « ${oldTax} »)`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `a ajouté le taux « ${rateName} » de ${rateValue}`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) =>
`a ajouté le taux « ${rateName} » de ${rateValue}, valable à partir du ${startDate}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `a ajouté le taux « ${rateName} » de ${rateValue}, valable jusqu’au ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`a ajouté le taux « ${rateName} » de ${rateValue}, valable du ${startDate} au ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `a mis à jour la date de début du taux « ${rateName} » à ${newDate} (auparavant ${oldDate})` : `définir la date de début du taux « ${rateName} » sur ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `a mis à jour la date de fin du taux « ${rateName} » à ${newDate} (auparavant ${oldDate})` : `définir la date de fin du taux « ${rateName} » sur ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `a mis à jour les dates de début et de fin du taux « ${rateName} » en ${newStartDate} - ${newEndDate} (précédemment ${oldStartDate} - ${oldEndDate})`
- : `définir les dates de début et de fin du taux « ${rateName} » sur ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `a supprimé la date de début du taux « ${rateName} » (précédemment ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `a supprimé la date de fin du taux « ${rateName} » (auparavant ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `a mis à jour le taux kilométrique « ${rateName} » pour s’appliquer à ${newDateRange} (précédemment ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `à partir du ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `jusqu’au ${date}`,
+ customUnitRateDateRangeAllDates: () => `pour toutes les dates`,
},
roomMembersPage: {
memberNotFound: 'Membre introuvable.',
@@ -8561,8 +8568,11 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
`La connexion ${feedName} est interrompue. Pour rétablir l’importation des cartes, connectez-vous à votre banque .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`la connexion Plaid à votre compte bancaire professionnel est rompue. Veuillez reconnecter votre compte bancaire ${maskedAccountNumber} afin de pouvoir continuer à utiliser vos Cartes Expensify.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} a rejoint via le lien d’invitation de l’espace de travail` : `a ajouté ${email} en tant que ${role === 'member' ? 'a' : 'un'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'un' : 'a';
+ return didJoinPolicy ? `${email} a rejoint via le lien d’invitation de l’espace de travail` : `a ajouté ${email} en tant que ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `a mis à jour le rôle de ${email} en ${newRole} (précédemment ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9458,6 +9468,11 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`En tant que copilote pour ${accountOwnerEmail}, vous n’avez pas l’autorisation d’effectuer cette action. Désolé !`,
+ removeCopilotAccess: 'Supprimer mon accès copilote',
+ removeCopilotAccessTitle: "Supprimer l'accès copilote ?",
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Êtes-vous sûr de vouloir supprimer votre accès copilote au compte Expensify de ${delegatorName} ? Cette action est irréversible.`,
+ removeCopilotAccessConfirm: "Supprimer l'accès",
copilotAccess: 'Accès Copilot',
},
debug: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 0fcfffdb73b7..c3c0525786ec 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: 'Selezione multipla',
saveChanges: 'Salva modifiche',
submit: 'Invia',
+ markAsDone: 'Segna come completata',
submitted: 'Inviato',
+ markedAsDoneStatus: 'Contrassegnato come completato',
rotate: 'Ruota',
zoom: 'Zoom',
password: 'Password',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: 'Descrizione',
title: 'Titolo',
assignee: 'Assegnatario',
- createdBy: 'Creato da',
with: 'con',
shareCode: 'Condividi codice',
share: 'Condividi',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: 'Esercente',
change: 'Modifica',
category: 'Categoria',
+ vendor: 'Fornitore',
report: 'Report',
billable: 'Fatturabile',
nonBillable: 'Non fatturabile',
@@ -356,6 +359,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Trova una chat usando la',
subtitleText2: 'pulsante sopra oppure crea qualcosa utilizzando il',
subtitleText3: 'pulsante qui sotto.',
+ noUnreadChats: 'Nessuna chat non letta',
+ noTodos: 'Nessuna attività da fare',
+ caughtUp: 'Hai gestito tutto. Ben fatto!',
+ seeAllChats: 'Vedi tutte le chat',
},
businessName: 'Nome azienda',
clear: 'Pulisci',
@@ -854,6 +861,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `Questa chat è con ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`Qui è dove ${submitterDisplayName} invierà le spese a ${workspaceName} . Usa semplicemente il pulsante +.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'Qui è dove terrai traccia delle spese',
beginningOfChatHistorySelfDM: 'Questo è il tuo spazio personale. Usalo per note, attività, bozze e promemoria.',
beginningOfChatHistorySystemDM: 'Benvenuto/a! Procediamo con la configurazione.',
chatWithAccountManager: 'Chatta qui con il tuo account manager',
@@ -959,7 +967,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Correggi la connessione della carta personale ${cardName}` : 'Correggi connessione carta personale'),
subtitle: 'Portafoglio',
},
- validateAccount: {title: 'Conferma il tuo account per continuare a usare Expensify', subtitle: 'Account', cta: 'Conferma'},
+ validateAccount: {title: 'Conferma il tuo account', subtitle: 'Account', cta: 'Conferma'},
fixFailedBilling: {title: 'Non abbiamo potuto addebitare la carta salvata nel profilo', subtitle: 'Abbonamento'},
unlockBankAccount: {
workspaceTitle: 'Il conto bancario della tua azienda è stato bloccato',
@@ -1014,6 +1022,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Tutto a posto',
f1FlagsDescription: 'Hai completato tutte le attività in sospeso.',
},
+ reviewExpenses: ({count}: {count: number}) => `Esamina ${count} ${count === 1 ? 'spesa' : 'spese'}`,
},
upcomingTravel: 'Prossimi viaggi',
upcomingTravelSection: {
@@ -1349,6 +1358,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `Invia fattura da ${amount}`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `per ${comment}` : ''}`,
submitted: (memo?: string) => `inviato${memo ? `, con nota: ${memo}` : ''}`,
+ markedAsDone: (memo) => `contrassegnata come completata${memo ? `, dicendo ${memo}` : ''}`,
automaticallySubmitted: `inviato tramite invio posticipato `,
queuedToSubmitViaDEW: 'in coda per l’invio tramite flusso di approvazione personalizzato',
queuedToApproveViaDEW: 'in coda per l’approvazione tramite flusso di approvazione personalizzato',
@@ -1564,6 +1574,9 @@ const translations: TranslationDeepObject = {
removed: 'rimosso',
transactionPending: 'Transazione in sospeso.',
chooseARate: 'Seleziona una tariffa di rimborso per miglio o chilometro per lo spazio di lavoro',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} al ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Valido dal ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Valido fino al ${endDate}`,
unapprove: 'Revoca approvazione',
unapproveReport: 'Annulla approvazione rapporto',
headsUp: 'Attenzione!',
@@ -1664,6 +1677,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: "Correggi l'errore di tariffa e riprova.",
AskToExplain: `. Spiega `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge ha associato questa spesa al fornitore ${vendorName} `,
duplicateNonDefaultWorkspacePerDiemError: 'Non puoi duplicare le spese di diaria tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse tra gli spazi di lavoro.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'ha contrassegnato la spesa come "rimborsabile"' : 'ha contrassegnato la spesa come "non rimborsabile"'),
@@ -1792,6 +1806,21 @@ const translations: TranslationDeepObject = {
return `In attesa che un amministratore invii le spese.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `In attesa che tu lo segni come completato.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `In attesa che ${actor} lo segni come completato.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `In attesa che un amministratore lo segni come completato.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2225,11 +2254,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Segnala attività sospetta',
lockAccount: 'Blocca account',
+ lockMyAccount: 'Blocca il mio account',
unlockAccount: 'Sblocca conto',
- compromisedDescription:
- 'Hai notato qualcosa di strano nel tuo account? Segnalarlo bloccherà immediatamente il tuo account, fermerà le nuove transazioni con la Carta Expensify e impedirà qualsiasi modifica all’account.',
- domainAdminsDescription:
- 'Per gli amministratori di dominio: questo mette in pausa anche tutta l’attività della Carta Expensify e le azioni di amministratore in tutti i tuoi domini.',
+ findYourSituation: 'La maggior parte dei problemi non richiede il blocco del tuo account! Trova la tua situazione qui sotto:',
+ lostCardOrCharges:
+ 'Carta smarrita o addebiti non riconosciuti : Annulla la tua carta e contatta Concierge per contestare le transazioni sconosciute.',
+ unauthorizedAccess:
+ 'Accesso non autorizzato all’account : Blocca il tuo account qui sotto. Questo blocca le nuove transazioni con la Carta Expensify, gli ordini di carte e le modifiche all’account. Se sei un amministratore di dominio, sospende anche tutta l’attività delle carte a livello di dominio e le azioni amministrative.',
+ securityTeamFollowUp: 'Il nostro team di sicurezza ti ricontatterà da risk@expensify.com dopo il blocco.',
areYouSure: 'Sei sicuro di voler bloccare il tuo account Expensify?',
onceLocked: 'Una volta bloccato, il tuo account sarà limitato in attesa di una richiesta di sblocco e di una revisione di sicurezza',
unlockTitle: 'Abbiamo ricevuto la tua richiesta',
@@ -2595,13 +2627,10 @@ ${amount} per ${merchant} - ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `spese di ${members} e l'approvatore è ${approvers}`,
addApprovalButton: 'Aggiungi flusso di approvazione',
editWorkflowAction: 'Modifica',
- addAgentAction: 'Aggiungi agente',
findWorkflow: 'Cerca flusso di lavoro',
addApprovalTip: 'Questo flusso di lavoro predefinito si applica a tutti i membri, a meno che non esista un flusso di lavoro più specifico.',
approver: 'Approvante',
addApprovalsDescription: 'Richiedi un’approvazione aggiuntiva prima di autorizzare un pagamento.',
- automateApprovalsWithAgentsTitle: 'Automatizza le approvazioni con gli agenti',
- automateApprovalsWithAgentsSubtitle: 'Aggiungi l’agente qui sotto al workflow per automatizzare le approvazioni.',
makeOrTrackPaymentsTitle: 'Pagamenti',
makeOrTrackPaymentsDescription: 'Aggiungi un pagatore autorizzato per i pagamenti effettuati in Expensify o tieni traccia dei pagamenti effettuati altrove.',
customApprovalWorkflowEnabled:
@@ -2897,6 +2926,12 @@ ${amount} per ${merchant} - ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: 'Benvenuto nella modalità #focus!',
+ prompt: (priorityModePageUrl: string) =>
+ `Tieniti al passo vedendo solo le chat non lette o quelle che richiedono la tua attenzione. Non preoccuparti, puoi cambiare questa impostazione in qualsiasi momento nelle impostazioni .`,
+ },
+ inboxTabs: {all: 'Tutti', todo: 'Attività da fare', unread: 'Non letti'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'Genera PDF',
@@ -3460,11 +3495,6 @@ ${amount} per ${merchant} - ${date}`,
year: 'Anno',
selectYear: 'Seleziona un anno',
},
- focusModeUpdateModal: {
- title: 'Benvenuto/a nella modalità #focus!',
- prompt: (priorityModePageUrl: string) =>
- `Resta sempre aggiornato vedendo solo le chat non lette o quelle che richiedono la tua attenzione. Non preoccuparti, puoi modificare questa impostazione in qualsiasi momento nelle impostazioni .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'La chat che stai cercando non può essere trovata.',
getMeOutOfHere: 'Fammi uscire di qui',
@@ -4124,28 +4154,31 @@ ${amount} per ${merchant} - ${date}`,
verificationFailed: 'La verifica non è riuscita, quindi avremo bisogno di documenti aggiuntivi per verificare te e la tua azienda',
taxIDVerification: 'Verifica dell’ID fiscale',
taxIDVerificationDescription: dedent(`
- Carica uno dei seguenti file:
- • Lettera di assegnazione TIN/EIN dell’IRS
- • Conferma della richiesta TIN/EIN dell’IRS (di solito indica "Congratulations! The EIN has been successfully assigned")
- • Lettera di esenzione fiscale dell’IRS con nome dell’azienda ed EIN`),
+ Carica uno dei seguenti file:
+ • Lettera di assegnazione TIN/EIN dell’IRS
+ • Conferma della richiesta TIN/EIN dell’IRS (di solito indica "Congratulations! The EIN has been successfully assigned")
+ • Lettera di esenzione fiscale dell’IRS con nome dell’azienda ed EIN
+ `),
nameChangeDocument: 'Documento di cambio nome',
nameChangeDocumentDescription:
'Se il nome della tua azienda è cambiato dopo la richiesta del TIN/EIN, abbiamo bisogno di questo documento per verificare il numero di ID fiscale fornito',
companyAddressVerification: 'Verifica dell’indirizzo aziendale',
companyAddressVerificationDescription: dedent(`
- Carica uno dei seguenti file:
- • Bolletta recente con nome e indirizzo dell’azienda
- • Estratto conto bancario con nome e indirizzo dell’azienda
- • Contratto di locazione attuale con pagina firme che mostri nome e indirizzo attuale dell’azienda
- • Documento assicurativo con nome e indirizzo dell’azienda
- • Documento di assegnazione TIN con nome e indirizzo dell’azienda`),
+ Carica uno dei seguenti file:
+ • Bolletta recente con nome e indirizzo dell’azienda
+ • Estratto conto bancario con nome e indirizzo dell’azienda
+ • Contratto di locazione attuale con pagina firme che mostri nome e indirizzo attuale dell’azienda
+ • Documento assicurativo con nome e indirizzo dell’azienda
+ • Documento di assegnazione TIN con nome e indirizzo dell’azienda
+ `),
userAddressVerification: 'Verifica dell’indirizzo',
userAddressVerificationDescription: dedent(`
- Carica uno dei seguenti file:
- • Tessera elettorale
- • Patente di guida
- • Estratto conto bancario
- • Bolletta`),
+ Carica uno dei seguenti file:
+ • Tessera elettorale
+ • Patente di guida
+ • Estratto conto bancario
+ • Bolletta
+ `),
userDOBVerification: 'Verifica della data di nascita',
userDOBVerificationDescription: 'Carica un documento di identità rilasciato negli Stati Uniti',
finishViaChat: 'Completa via chat',
@@ -4326,7 +4359,6 @@ ${amount} per ${merchant} - ${date}`,
customFieldHint: 'Aggiungi una codifica personalizzata che si applichi a tutte le spese di questo membro.',
reports: 'Report',
reportFields: 'Campi del report',
- invoiceFields: 'Campi fattura',
reportTitle: 'Titolo del report',
reportField: 'Campo report',
taxes: 'Tasse',
@@ -4410,7 +4442,7 @@ ${amount} per ${merchant} - ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Amministratore';
+ return 'Amministratore spazio di lavoro';
case CONST.POLICY.ROLE.AUDITOR:
return 'Revisore';
case CONST.POLICY.ROLE.EDITOR:
@@ -4420,7 +4452,7 @@ ${amount} per ${merchant} - ${date}`,
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
return 'Amministrazione persone';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
- return 'Amministrazione pagamenti';
+ return 'Amministratore pagamenti';
case CONST.POLICY.ROLE.USER:
return 'Membro';
default:
@@ -6015,29 +6047,6 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
reportFieldInitialValueRequiredError: 'Scegli un valore iniziale per il campo del resoconto',
genericFailureMessage: 'Si è verificato un errore durante l’aggiornamento del campo del report. Riprova.',
},
- invoiceFields: {
- subtitle: 'I campi della fattura possono essere utili quando vuoi includere informazioni aggiuntive.',
- importedFromAccountingSoftware: 'I campi della fattura riportati di seguito sono importati dal tuo',
- disableInvoiceFields: 'Disattiva i campi della fattura',
- disableInvoiceFieldsConfirmation: 'Sei sicuro? I campi della fattura verranno disattivati nelle fatture.',
- delete: 'Elimina campo della fattura',
- deleteConfirmation: 'Sei sicuro di voler eliminare questo campo della fattura?',
- findInvoiceField: 'Trova campo della fattura',
- nameInputSubtitle: 'Scegli un nome per il campo della fattura.',
- typeInputSubtitle: 'Scegli il tipo di campo della fattura da utilizzare.',
- initialValueInputSubtitle: 'Inserisci un valore iniziale da mostrare nel campo della fattura.',
- listValuesInputSubtitle: 'Questi valori appariranno nel menu a discesa del campo della fattura. I valori abilitati possono essere selezionati dai membri.',
- listInputSubtitle: 'Questi valori appariranno nell’elenco del campo della fattura. I valori abilitati possono essere selezionati dai membri.',
- emptyInvoiceFieldsValues: {
- title: 'Nessun valore elenco ancora',
- subtitle: 'Aggiungi valori personalizzati da mostrare sulle fatture.',
- },
- existingInvoiceFieldNameError: 'Esiste già un campo della fattura con questo nome',
- invoiceFieldNameRequiredError: 'Inserisci un nome per il campo della fattura',
- invoiceFieldTypeRequiredError: 'Scegli un tipo di campo della fattura',
- invoiceFieldInitialValueRequiredError: 'Scegli un valore iniziale per il campo della fattura',
- addField: 'Aggiungi campo',
- },
tags: {
tagName: 'Nome tag',
requiresTag: 'I membri devono etichettare tutte le spese',
@@ -6259,8 +6268,8 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
other: 'Rendi membri',
}),
makeAdmin: () => ({
- one: 'Rendi amministratore',
- other: 'Rendi amministratori',
+ one: 'Rendi amministratore dello spazio di lavoro',
+ other: 'Rendi amministratori dello spazio di lavoro',
}),
makeAuditor: () => ({
one: 'Rendi revisore',
@@ -6289,12 +6298,14 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} ha un rapporto in sospeso su cui deve intervenire. Chiedi loro di completare l’azione richiesta prima di rimuoverlə dallo spazio di lavoro.`,
allMembers: 'Tutti i membri',
- admins: 'Amministratori',
+ admins: 'Amministratori dello spazio di lavoro',
approvers: 'Approvatori',
auditors: 'Revisori',
emptyRoleFilter: {title: 'Nessun membro corrisponde a questo filtro', subtitle: 'Invita un membro o modifica il filtro qui sopra.'},
configureHRSync: (providerName: string) => `Configura la sincronizzazione di ${providerName}.`,
syncWithHR: (providerName: string) => `Sincronizza con ${providerName}`,
+ makeCardAdmin: () => ({one: 'Rendi amministratore carta', other: 'Rendi amministratori carta'}),
+ cardAdmins: 'Amministratori carta',
},
card: {
getStartedIssuing: 'Inizia emettendo la tua prima carta virtuale o fisica.',
@@ -6710,6 +6721,10 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
}),
enableRate: 'Abilita tariffa',
status: 'Stato',
+ statusActive: 'Attivo',
+ statusFuture: 'Futuro',
+ statusExpired: 'Scaduto',
+ statusInactive: 'Inattivo',
unit: 'Unit',
taxFeatureNotEnabledMessage:
'Per usare questa funzione devi abilitare le imposte nello spazio di lavoro. Vai su Altre funzionalità per effettuare questa modifica. ',
@@ -6836,12 +6851,6 @@ Vuoi davvero esportarli di nuovo?`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`I campi del report sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} `,
},
- invoiceFields: {
- title: 'Campi fattura',
- description: `I campi fattura ti consentono di includere dettagli aggiuntivi a livello di fattura.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `I campi fattura sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Approfitta della sincronizzazione automatica e riduci le registrazioni manuali con l’integrazione Expensify + NetSuite. Ottieni approfondimenti finanziari dettagliati e in tempo reale grazie al supporto di segmenti nativi e personalizzati, inclusa la mappatura di progetti e clienti.`,
@@ -6943,12 +6952,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Le tariffe chilometriche sono disponibili con il piano Collect, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} `,
},
- auditor: {
- title: 'Revisore',
- description: 'I revisori ottengono accesso in sola lettura a tutti i report per garantire completa visibilità e monitoraggio della conformità.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Gli auditor sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Livelli di approvazione multipli',
description:
@@ -7048,6 +7051,12 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`La fatturazione è disponibile nei piani Collect e Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`} `,
},
+ controlPolicyRoles: {
+ title: 'Ruoli dei criteri di controllo',
+ description: 'Usa ruoli specializzati come Revisore e Amministratore carte per concedere ai membri l’accesso solo a ciò di cui hanno bisogno.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `I ruoli specializzati dello spazio di lavoro sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per membro al mese.` : `per membro attivo al mese.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7375,6 +7384,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
deleteRuleConfirmation: 'Sei sicuro di voler eliminare questa regola?',
describeRuleTitle: 'Descrivi la tua regola',
describeRuleSubtitle: 'Descrivi la tua regola e Concierge la creerà',
+ disclaimer: 'Gli agenti IA possono commettere errori.',
},
},
planTypePage: {
@@ -8060,25 +8070,19 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} da ${cards}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `ha cambiato l’aliquota fiscale predefinita della categoria "${categoryName}" in "${newTax}" (in precedenza "${oldTax}")`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `ha aggiunto la tariffa "${rateName}" di ${rateValue}`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) => `ha aggiunto l’aliquota "${rateName}" di ${rateValue}, valida dal ${startDate}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `ha aggiunto la tariffa "${rateName}" di ${rateValue}, valida fino al ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`ha aggiunto la tariffa "${rateName}" di ${rateValue}, valida dal ${startDate} al ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate
- ? `aggiornata la data di inizio della tariffa "${rateName}" a ${newDate} (in precedenza ${oldDate})`
- : `imposta la data di inizio della tariffa "${rateName}" su ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate
- ? `ha aggiornato la data di fine della tariffa "${rateName}" a ${newDate} (in precedenza ${oldDate})`
- : `imposta la data di fine della tariffa "${rateName}" al ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `ha aggiornato la data di inizio e fine della tariffa "${rateName}" in ${newStartDate} - ${newEndDate} (in precedenza ${oldStartDate} - ${oldEndDate})`
- : `imposta data di inizio e fine della tariffa "${rateName}" su ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `rimossa la data di inizio dalla tariffa "${rateName}" (in precedenza ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `data di fine rimossa dalla tariffa "${rateName}" (precedentemente ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `ha aggiornato la tariffa chilometrica "${rateName}" per applicarla a ${newDateRange} (in precedenza ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `dal ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `fino al ${date}`,
+ customUnitRateDateRangeAllDates: () => `per tutte le date`,
},
roomMembersPage: {
memberNotFound: 'Membro non trovato.',
@@ -8519,8 +8523,11 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
`La connessione ${feedName} è interrotta. Per ripristinare le importazioni della carta, accedi alla tua banca .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`la connessione Plaid al conto bancario della tua azienda non funziona. Per favore, ricollega il conto bancario ${maskedAccountNumber} così puoi continuare a usare le tue Carte Expensify.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} si è unito tramite il link di invito allo spazio di lavoro` : `ha aggiunto ${email} come ${role === 'member' ? 'a' : 'un'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'un' : 'a';
+ return didJoinPolicy ? `${email} si è unito tramite il link di invito allo spazio di lavoro` : `ha aggiunto ${email} come ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `ha aggiornato il ruolo di ${email} a ${newRole} (in precedenza ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9417,6 +9424,11 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`Come copilota per ${accountOwnerEmail}, non hai l'autorizzazione per eseguire questa azione. Spiacenti!`,
+ removeCopilotAccess: 'Rimuovi il mio accesso copilota',
+ removeCopilotAccessTitle: "Rimuovere l'accesso copilota?",
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Sei sicuro di voler rimuovere il tuo accesso copilota all'account Expensify di ${delegatorName}? Questa azione non può essere annullata.`,
+ removeCopilotAccessConfirm: 'Rimuovi accesso',
copilotAccess: 'Accesso a Copilot',
},
debug: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 98aae61a7580..22b0a46b97c3 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: '複数選択',
saveChanges: '変更を保存',
submit: '送信',
+ markAsDone: '完了にする',
submitted: '送信済み',
+ markedAsDoneStatus: '完了済み',
rotate: '回転',
zoom: 'ズーム',
password: 'パスワード',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: '説明',
title: 'タイトル',
assignee: '担当者',
- createdBy: '作成者',
with: '〜で',
shareCode: 'コードを共有',
share: '共有',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: '加盟店',
change: '変更',
category: 'カテゴリ',
+ vendor: 'ベンダー',
report: 'レポート',
billable: '請求可能',
nonBillable: '請求不可',
@@ -355,6 +358,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'チャットを検索するには',
subtitleText2: '上のボタン、または次を使って何かを作成する',
subtitleText3: '下のボタンを押してください。',
+ noUnreadChats: '未読のチャットはありません',
+ noTodos: 'To-do はありません',
+ caughtUp: 'すべて確認済みです。お疲れさまでした!',
+ seeAllChats: 'すべてのチャットを表示',
},
businessName: '会社名',
clear: 'クリア',
@@ -843,6 +850,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `このチャットの相手は${users}です。`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`ここは${submitterDisplayName} さんが${workspaceName} に経費精算を提出する場所です。+ボタンを押すだけでOKです。`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'ここは経費を管理する場所です',
beginningOfChatHistorySelfDM: 'ここはあなたの個人スペースです。メモ、タスク、下書き、リマインダーに活用してください。',
beginningOfChatHistorySystemDM: 'ようこそ!設定を始めましょう。',
chatWithAccountManager: 'ここでアカウントマネージャーとチャットする',
@@ -945,7 +953,7 @@ const translations: TranslationDeepObject = {
subtitle: ({policyName}: {policyName: string}) => `${policyName} > 会計`,
},
fixPersonalCardConnection: {title: ({cardName}: {cardName?: string}) => (cardName ? `${cardName}個人カードの接続を修正` : '個人カードの連携を修正'), subtitle: 'ウォレット'},
- validateAccount: {title: 'Expensify を引き続きご利用いただくには、アカウントを認証してください', subtitle: 'アカウント', cta: '検証する'},
+ validateAccount: {title: 'アカウントを認証してください', subtitle: 'アカウント', cta: '検証する'},
fixFailedBilling: {title: '登録されているカードから請求できませんでした', subtitle: 'サブスクリプション'},
unlockBankAccount: {
workspaceTitle: 'ビジネス用銀行口座がロックされました',
@@ -1000,6 +1008,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'すべて確認済みです',
f1FlagsDescription: 'すべての未処理の To-do が完了しました。',
},
+ reviewExpenses: ({count}: {count: number}) => `${count} 件の${count === 1 ? '経費' : '経費'}を確認`,
},
upcomingTravel: '今後の出張',
upcomingTravelSection: {
@@ -1335,6 +1344,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `${amount} の請求書を送信`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `${comment}用` : ''}`,
submitted: (memo?: string) => `送信済み${memo ? `、メモ: ${memo}` : ''}`,
+ markedAsDone: (memo) => `完了としてマークしました${memo ? `(メモ:${memo})` : ''}`,
automaticallySubmitted: `提出の延期 経由で提出されました`,
queuedToSubmitViaDEW: 'カスタム承認ワークフローで提出待ち',
queuedToApproveViaDEW: 'カスタム承認ワークフローで承認待ちに設定されました',
@@ -1548,6 +1558,9 @@ const translations: TranslationDeepObject = {
removed: '削除済み',
transactionPending: '取引は保留中です。',
chooseARate: 'ワークスペースの払い戻しレートをマイルまたはキロメートルごとに選択してください',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate}から${endDate}まで有効`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `${startDate}から有効`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `${endDate}まで有効`,
unapprove: '承認を取り消す',
unapproveReport: 'レポートの承認を取り消す',
headsUp: 'ご注意ください!',
@@ -1648,6 +1661,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'レートのエラーを修正して、もう一度お試しください。',
AskToExplain: `・説明 `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge がこの経費を ${vendorName} に一致させました`,
duplicateNonDefaultWorkspacePerDiemError: 'ワークスペースごとに日当レートが異なる場合があるため、日当経費をワークスペース間で複製することはできません。',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? '経費を「精算対象」に指定しました' : '経費を「精算対象外」にマークしました'),
@@ -1775,6 +1789,21 @@ const translations: TranslationDeepObject = {
return `管理者が経費を送信するのを待っています。`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `あなた が完了にするのを待っています。`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `${actor} が完了にするのを待っています。`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `管理者が完了にするのを待っています。`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2207,10 +2236,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: '不審な行為を報告',
lockAccount: 'アカウントをロック',
+ lockMyAccount: 'アカウントをロックする',
unlockAccount: 'アカウントのロック解除',
- compromisedDescription:
- 'アカウントにおかしな点がありますか?報告すると、すぐにアカウントがロックされ、新しい Expensify カードの取引がブロックされ、アカウントの変更もできなくなります。',
- domainAdminsDescription: 'ドメイン管理者向け:これにより、ドメイン全体のすべての Expensify カードの利用と管理者による操作も一時停止されます。',
+ findYourSituation: 'ほとんどの問題ではアカウントをロックする必要はありません。以下からご自身の状況をお探しください。',
+ lostCardOrCharges:
+ 'カードの紛失または身に覚えのない請求 :カードを解約し、不明な取引について異議申し立てをするために Concierge へご連絡ください。',
+ unauthorizedAccess:
+ '不正なアカウントアクセス :以下からアカウントをロックしてください。これにより、新しい Expensify カードの利用、カードの発行依頼、およびアカウントの変更ができなくなります。ドメイン管理者の場合、ドメイン全体のカードアクティビティと管理者アクションも一時停止されます。',
+ securityTeamFollowUp: 'ロック後、セキュリティチームがrisk@expensify.com からご連絡します。',
areYouSure: 'Expensify アカウントをロックしてもよろしいですか?',
onceLocked: 'ロックされると、解除リクエストとセキュリティ審査が完了するまでアカウントは制限されます',
unlockTitle: 'リクエストを受け付けました',
@@ -2572,13 +2605,10 @@ ${date} の ${merchant} への ${amount}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `${members} の経費で、承認者は ${approvers} です`,
addApprovalButton: '承認ワークフローを追加',
editWorkflowAction: '編集',
- addAgentAction: 'エージェントを追加',
findWorkflow: 'ワークフローを検索',
addApprovalTip: 'より詳細なワークフローが存在する場合を除き、このデフォルトのワークフローがすべてのメンバーに適用されます。',
approver: '承認者',
addApprovalsDescription: '支払いを承認する前に、追加の承認を必須にする。',
- automateApprovalsWithAgentsTitle: '代理を使って承認を自動化する',
- automateApprovalsWithAgentsSubtitle: '承認を自動化するには、以下のエージェントをワークフローに追加してください。',
makeOrTrackPaymentsTitle: '支払',
makeOrTrackPaymentsDescription: 'Expensifyでの支払いに対する承認済み支払者を追加するか、他の場所で行われた支払いを記録します。',
customApprovalWorkflowEnabled:
@@ -2872,6 +2902,12 @@ ${date} の ${merchant} への ${amount}`,
},
},
},
+ focusModeUpdateModal: {
+ title: '#focus モードへようこそ!',
+ prompt: (priorityModePageUrl: string) =>
+ `未読のチャットや対応が必要なチャットだけを表示して、状況を常に把握できるようにしましょう。いつでも設定 で変更できます。`,
+ },
+ inboxTabs: {all: 'すべて', todo: 'To-do リスト', unread: '未読'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `${policyName} 内`,
generatingPDF: 'PDFを生成',
@@ -3434,11 +3470,6 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの'
year: '年',
selectYear: '年を選択してください',
},
- focusModeUpdateModal: {
- title: '#focusモードへようこそ!',
- prompt: (priorityModePageUrl: string) =>
- `未読のチャットや対応が必要なチャットだけを表示して、常に状況を把握しましょう。いつでも設定 から変更できます。`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'お探しのチャットが見つかりません。',
getMeOutOfHere: 'ここから出して',
@@ -4096,27 +4127,30 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの'
verificationFailed: '確認に失敗したため、追加の書類で本人および事業の確認が必要です',
taxIDVerification: '納税者番号の確認',
taxIDVerificationDescription: dedent(`
- 以下のいずれかの書類をアップロードしてください:
- • IRS TIN/EIN 割当通知書
- • IRS TIN/EIN 申請確認書(通常「Congratulations! The EIN has been successfully assigned」と記載)
- • 会社名と EIN が記載された IRS の免税通知書`),
+ 以下のいずれかの書類をアップロードしてください:
+ • IRS TIN/EIN 割当通知書
+ • IRS TIN/EIN 申請確認書(通常「Congratulations! The EIN has been successfully assigned」と記載)
+ • 会社名と EIN が記載された IRS の免税通知書
+ `),
nameChangeDocument: '名称変更書類',
nameChangeDocumentDescription: 'TIN/EIN 申請後に会社名が変更された場合、提供された納税者番号を確認するためにこの書類が必要です',
companyAddressVerification: '会社住所の確認',
companyAddressVerificationDescription: dedent(`
- 以下のいずれかの書類をアップロードしてください:
- • 会社名と住所が記載された最近の公共料金請求書
- • 会社名と住所が記載された銀行取引明細書
- • 署名ページを含む現行の賃貸契約書(会社名と現住所が記載されたもの)
- • 会社名と住所が記載された保険証書
- • 会社名と住所が記載された TIN 割当書類`),
+ 以下のいずれかの書類をアップロードしてください:
+ • 会社名と住所が記載された最近の公共料金請求書
+ • 会社名と住所が記載された銀行取引明細書
+ • 署名ページを含む現行の賃貸契約書(会社名と現住所が記載されたもの)
+ • 会社名と住所が記載された保険証書
+ • 会社名と住所が記載された TIN 割当書類
+ `),
userAddressVerification: '住所確認',
userAddressVerificationDescription: dedent(`
- 以下のいずれかの書類をアップロードしてください:
- • 有権者登録カード
- • 運転免許証
- • 銀行取引明細書
- • 公共料金請求書`),
+ 以下のいずれかの書類をアップロードしてください:
+ • 有権者登録カード
+ • 運転免許証
+ • 銀行取引明細書
+ • 公共料金請求書
+ `),
userDOBVerification: '生年月日の確認',
userDOBVerificationDescription: '米国発行の身分証明書をアップロードしてください',
finishViaChat: 'チャットで完了',
@@ -4297,7 +4331,6 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの'
customFieldHint: 'このメンバーのすべての支出に適用されるカスタムコードを追加します。',
reports: 'レポート',
reportFields: 'レポート項目',
- invoiceFields: '請求書項目',
reportTitle: 'レポートタイトル',
reportField: 'レポート項目',
taxes: '税金',
@@ -4380,18 +4413,17 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの'
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return '管理者';
+ return 'ワークスペース管理者';
case CONST.POLICY.ROLE.AUDITOR:
return '監査人';
case CONST.POLICY.ROLE.EDITOR:
return '編集者';
- return '監査担当者';
case CONST.POLICY.ROLE.CARD_ADMIN:
return 'カード管理者';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
return 'メンバー管理';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
- return '支払管理者';
+ return '支払い管理者';
case CONST.POLICY.ROLE.USER:
return 'メンバー';
default:
@@ -5946,29 +5978,6 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
reportFieldInitialValueRequiredError: 'レポート項目の初期値を選択してください',
genericFailureMessage: 'レポートフィールドの更新中にエラーが発生しました。もう一度お試しください。',
},
- invoiceFields: {
- subtitle: '追加情報を含めたい場合、請求書フィールドが役立ちます。',
- importedFromAccountingSoftware: '以下の請求書フィールドは、次からインポートされます',
- disableInvoiceFields: '請求書フィールドを無効にする',
- disableInvoiceFieldsConfirmation: 'よろしいですか?請求書フィールドは請求書で無効になります。',
- delete: '請求書フィールドを削除',
- deleteConfirmation: 'この請求書フィールドを削除してもよろしいですか?',
- findInvoiceField: '請求書フィールドを検索',
- nameInputSubtitle: '請求書フィールドの名前を選択してください。',
- typeInputSubtitle: '使用する請求書フィールドの種類を選択してください。',
- initialValueInputSubtitle: '請求書フィールドに表示する開始値を入力してください。',
- listValuesInputSubtitle: 'これらの値は請求書フィールドのドロップダウンに表示されます。有効な値はメンバーが選択できます。',
- listInputSubtitle: 'これらの値は請求書フィールドのリストに表示されます。有効な値はメンバーが選択できます。',
- emptyInvoiceFieldsValues: {
- title: 'リスト値はまだありません',
- subtitle: '請求書に表示するカスタム値を追加します。',
- },
- existingInvoiceFieldNameError: 'この名前の請求書フィールドは既に存在します',
- invoiceFieldNameRequiredError: '請求書フィールド名を入力してください',
- invoiceFieldTypeRequiredError: '請求書フィールドの種類を選択してください',
- invoiceFieldInitialValueRequiredError: '請求書フィールドの初期値を選択してください',
- addField: 'フィールドを追加',
- },
tags: {
tagName: 'タグ名',
requiresTag: 'メンバーはすべての経費にタグを付ける必要があります',
@@ -6189,8 +6198,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
other: 'メンバーにする',
}),
makeAdmin: () => ({
- one: '管理者にする',
- other: '管理者にする',
+ one: 'ワークスペース管理者にする',
+ other: 'ワークスペース管理者にする',
}),
makeAuditor: () => ({
one: '監査担当者に設定',
@@ -6219,12 +6228,14 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} は、対応が必要な未処理のレポートがあります。ワークスペースから削除する前に、必要な対応を完了するよう依頼してください。`,
allMembers: 'すべてのメンバー',
- admins: '管理者',
+ admins: 'ワークスペース管理者',
approvers: '承認者',
auditors: '監査担当者',
emptyRoleFilter: {title: 'このフィルターに一致するメンバーはいません', subtitle: 'メンバーを招待するか、上のフィルターを変更してください。'},
configureHRSync: (providerName: string) => `${providerName} の同期を設定します。`,
syncWithHR: (providerName: string) => `${providerName}と同期`,
+ makeCardAdmin: () => ({one: 'カード管理者にする', other: 'カード管理者に設定'}),
+ cardAdmins: 'カード管理者',
},
card: {
getStartedIssuing: 'まずは最初のバーチャルカードまたは物理カードを発行しましょう。',
@@ -6639,6 +6650,10 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
}),
enableRate: 'レートを有効にする',
status: 'ステータス',
+ statusActive: 'アクティブ',
+ statusFuture: '将来',
+ statusExpired: '期限切れ',
+ statusInactive: '無効',
unit: '単位',
taxFeatureNotEnabledMessage:
'この機能を利用するには、ワークスペースで税金設定を有効にする必要があります。設定を変更するには、その他の機能 に移動してください。 ',
@@ -6764,12 +6779,6 @@ ${reportName}
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`レポートフィールドは、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からのControlプランでのみ利用できます `,
},
- invoiceFields: {
- title: '請求書項目',
- description: `請求書フィールドを使うと、請求書レベルの追加情報を請求書に含めることができます。`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `請求書フィールドは、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からのControlプランでのみ利用できます `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Expensify と NetSuite の連携により、自動同期を活用して手入力を減らしましょう。プロジェクトや顧客のマッピングを含むネイティブおよびカスタムセグメントのサポートで、詳細かつリアルタイムな財務インサイトを得られます。`,
@@ -6870,12 +6879,6 @@ ${reportName}
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`距離レートはCollectプランで利用できます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用になれます `,
},
- auditor: {
- title: '監査人',
- description: '監査担当者は、可視性とコンプライアンス監視を徹底するため、すべてのレポートを閲覧専用で確認できます。',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `監査人は、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からの Control プランでのみ利用できます `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: '複数の承認レベル',
description: '複数承認レベルは、精算前にレポートを複数人で承認する必要がある会社向けのワークフローツールです。',
@@ -6970,6 +6973,12 @@ ${reportName}
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`請求書発行機能は、Collect プランと Control プランでご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からご利用可能です。 `,
},
+ controlPolicyRoles: {
+ title: 'コントロールポリシーのロール',
+ description: '監査人やカード管理者などの専用ロールを使って、メンバーが必要なものにだけアクセスできるようにします。',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `特別なワークスペースロールは Control プランでのみご利用いただけます(${formattedPrice} から、${hasTeam2025Pricing ? `メンバー1人あたり月額。` : `アクティブメンバー1人あたり/月`})。 `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7293,6 +7302,7 @@ ${reportName}
deleteRuleConfirmation: 'このルールを削除してもよろしいですか?',
describeRuleTitle: 'ルールの内容を記入してください',
describeRuleSubtitle: 'ルールの内容を入力すると、Concierge が自動作成します',
+ disclaimer: 'AI エージェントは間違える場合があります。',
},
},
planTypePage: {
@@ -7964,21 +7974,19 @@ ${reportName}
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${cards} からの ${content}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `「${categoryName}」カテゴリのデフォルト税率を「${newTax}」に変更しました(以前は「${oldTax}」)`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `「${rateName}」レート(${rateValue})を追加しました`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) => `${startDate}から有効な「${rateName}」レート(${rateValue})を追加しました`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `「${rateName}」レート(${rateValue})を${endDate}まで有効として追加しました`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`「${rateName}」レート(${rateValue})を追加しました。有効期間:${startDate}〜${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `「${rateName}」レートの開始日を${newDate}に更新しました(以前は${oldDate})` : `「${rateName}」レートの開始日を${newDate}に設定する`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `「${rateName}」レートの終了日を${newDate}(以前は${oldDate})に更新しました` : `「${rateName}」レートの終了日を${newDate}に設定します`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `「${rateName}」レートの開始日と終了日を${newStartDate}〜${newEndDate}に更新しました(以前は${oldStartDate}〜${oldEndDate})`
- : `「${rateName}」レートの開始日と終了日を${newStartDate}〜${newEndDate}に設定しました`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `「${rateName}」レートの開始日を削除しました(以前の開始日:${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `「${rateName}」レートの終了日を削除しました(以前は ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `距離レート「${rateName}」を更新し、${newDateRange} に適用しました(以前は ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `${date} から`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `${date}まで`,
+ customUnitRateDateRangeAllDates: () => `すべての日付に対して`,
},
roomMembersPage: {
memberNotFound: 'メンバーが見つかりません。',
@@ -8408,8 +8416,11 @@ ${reportName}
`${feedName} との接続が切断されています。カードの取引明細の取込を再開するには、銀行にログイン してください。`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`Plaid によるビジネス銀行口座との接続が切断されています。Expensify カードを引き続きご利用いただくために、銀行口座 ${maskedAccountNumber} を再接続 してください。`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} さんがワークスペースの招待リンクから参加しました` : `${role === 'member' ? 'a' : '1つの'} ${role} として ${email} を追加しました`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? '1つの' : 'a';
+ return didJoinPolicy ? `${email} さんがワークスペースの招待リンクから参加しました` : `${email} を ${article} ${translatedRole} として追加しました`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `${email} のロールを ${currentRole} から ${newRole} に更新しました`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9293,6 +9304,11 @@ ${reportName}
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`${accountOwnerEmail} のコパイロット として、この操作を行う権限がありません。申し訳ありません。`,
+ removeCopilotAccess: '自分のコパイロットアクセスを削除',
+ removeCopilotAccessTitle: 'コパイロットアクセスを削除しますか?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `${delegatorName}のExpensifyアカウントへのコパイロットアクセスを削除してもよろしいですか?この操作は元に戻せません。`,
+ removeCopilotAccessConfirm: 'アクセスを削除',
copilotAccess: 'Copilot へのアクセス',
},
debug: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ba3d4b5b121c..b6b836726f01 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: 'Meerdere selecteren',
saveChanges: 'Wijzigingen opslaan',
submit: 'Verzenden',
+ markAsDone: 'Markeren als voltooid',
submitted: 'Ingediend',
+ markedAsDoneStatus: 'Als voltooid gemarkeerd',
rotate: 'Draaien',
zoom: 'Zoom',
password: 'Wachtwoord',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: 'Beschrijving',
title: 'Titel',
assignee: 'Toegewezene',
- createdBy: 'Gemaakt door',
with: 'met',
shareCode: 'Code delen',
share: 'Delen',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: 'Handelaar',
change: 'Wijzigen',
category: 'Categorie',
+ vendor: 'Leverancier',
report: 'Rapport',
billable: 'Factureerbaar',
nonBillable: 'Niet-factureerbaar',
@@ -355,6 +358,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Zoek een chat met de',
subtitleText2: 'knop hierboven, of maak iets met de',
subtitleText3: 'knop hieronder.',
+ noUnreadChats: 'Geen ongelezen chats',
+ noTodos: 'Geen taken',
+ caughtUp: 'Je bent helemaal bij. Goed gedaan!',
+ seeAllChats: 'Alle chats bekijken',
},
businessName: 'Bedrijfsnaam',
clear: 'Wissen',
@@ -852,6 +859,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `Deze chat is met ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`Dit is waar ${submitterDisplayName} declaraties indient bij ${workspaceName} . Gebruik gewoon de +-knop.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'Hier houd je je uitgaven bij',
beginningOfChatHistorySelfDM: 'Dit is je persoonlijke ruimte. Gebruik het voor notities, taken, concepten en herinneringen.',
beginningOfChatHistorySystemDM: 'Welkom! Laten we je instellen.',
chatWithAccountManager: 'Chat hier met je accountmanager',
@@ -957,7 +965,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Verbinding van persoonlijke kaart ${cardName} herstellen` : 'Verbinding persoonlijke kaart herstellen'),
subtitle: 'Portemonnee',
},
- validateAccount: {title: 'Valideer je account om Expensify te blijven gebruiken', subtitle: 'Account', cta: 'Valideren'},
+ validateAccount: {title: 'Valideer je account', subtitle: 'Account', cta: 'Valideren'},
fixFailedBilling: {title: 'We konden je kaart in ons bestand niet belasten', subtitle: 'Abonnement'},
unlockBankAccount: {
workspaceTitle: 'Je zakelijke bankrekening is geblokkeerd',
@@ -1012,6 +1020,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Helemaal bij',
f1FlagsDescription: "Je hebt alle openstaande to-do's afgerond.",
},
+ reviewExpenses: ({count}: {count: number}) => `Beoordeel ${count} ${count === 1 ? 'uitgave' : 'uitgaven'}`,
},
upcomingTravel: 'Aankomende reizen',
upcomingTravelSection: {
@@ -1347,6 +1356,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `Verzend factuur van ${amount}`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `voor ${comment}` : ''}`,
submitted: (memo?: string) => `ingediend${memo ? `, met de omschrijving ${memo}` : ''}`,
+ markedAsDone: (memo) => `gemarkeerd als voltooid${memo ? `, met als omschrijving ${memo}` : ''}`,
automaticallySubmitted: `ingediend via uitgestelde indieningen `,
queuedToSubmitViaDEW: 'in wachtrij om via aangepast goedkeuringsproces in te dienen',
queuedToApproveViaDEW: 'in de wachtrij gezet voor goedkeuring via aangepaste goedkeuringsworkflow',
@@ -1560,6 +1570,9 @@ const translations: TranslationDeepObject = {
removed: 'verwijderd',
transactionPending: 'Transactie in behandeling.',
chooseARate: 'Selecteer een workspace-vergoeding per mijl of kilometer',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} tot ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Geldig vanaf ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Geldig tot ${endDate}`,
unapprove: 'Intrekken goedkeuring',
unapproveReport: 'Goedkeuring rapport intrekken',
headsUp: 'Let op!',
@@ -1660,6 +1673,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'Los de tarieffout op en probeer het opnieuw.',
AskToExplain: `. Uitleggen `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge heeft deze uitgave gekoppeld aan ${vendorName} `,
duplicateNonDefaultWorkspacePerDiemError: 'Je kunt dagvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'markeerde de uitgave als „terugbetaalbaar”' : 'heeft de uitgave als ‘niet-vergoedbaar’ gemarkeerd'),
@@ -1787,6 +1801,21 @@ const translations: TranslationDeepObject = {
return `Wachten tot een beheerder de onkosten indient.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `Er wordt gewacht tot jij dit als voltooid markeert.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `Wachten tot ${actor} dit als voltooid markeert.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `Wachten tot een beheerder dit als voltooid markeert.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2221,10 +2250,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Verdachte activiteit melden',
lockAccount: 'Account vergrendelen',
+ lockMyAccount: 'Account vergrendelen',
unlockAccount: 'Account ontgrendelen',
- compromisedDescription:
- 'Val je iets op aan je account? Als je dit meldt, wordt je account onmiddellijk vergrendeld, worden nieuwe Expensify Kaart-transacties geblokkeerd en worden wijzigingen aan je account voorkomen.',
- domainAdminsDescription: 'Voor domeinbeheerders: dit pauzeert ook alle Expensify Kaart-activiteit en beheerdersacties in je domein(en).',
+ findYourSituation: 'Voor de meeste problemen hoef je je account niet te blokkeren. Zoek hieronder jouw situatie:',
+ lostCardOrCharges:
+ 'Kaart kwijt of onbekende kosten : Annuleer je kaart en neem contact op met Concierge om onbekende transacties te betwisten.',
+ unauthorizedAccess:
+ 'Onbevoegde accounttoegang : Vergrendel hieronder je account. Dit blokkeert nieuwe Expensify Kaart-transacties, kaartbestellingen en accountwijzigingen. Als je domeinbeheerder bent, pauzeert dit ook alle domeinbrede kaartactiviteiten en beheerdersacties.',
+ securityTeamFollowUp: 'Ons beveiligingsteam neemt na het blokkeren contact met je op vanaf risk@expensify.com .',
areYouSure: 'Weet je zeker dat je je Expensify-account wilt vergrendelen?',
onceLocked: 'Zodra deze wordt vergrendeld, wordt je account beperkt in afwachting van een deblokkeringsverzoek en een beveiligingscontrole',
unlockTitle: 'We hebben je verzoek ontvangen',
@@ -2592,13 +2625,10 @@ ${amount} voor ${merchant} - ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `de uitgaven van ${members}, en de goedkeurder is ${approvers}`,
addApprovalButton: 'Goedkeuringsworkflow toevoegen',
editWorkflowAction: 'Bewerken',
- addAgentAction: 'Agent toevoegen',
findWorkflow: 'Workflow zoeken',
addApprovalTip: 'Deze standaardworkflow is van toepassing op alle leden, tenzij er een specifiekere workflow bestaat.',
approver: 'Fiatteur',
addApprovalsDescription: 'Extra goedkeuring vereisen voordat je een betaling autoriseert.',
- automateApprovalsWithAgentsTitle: 'Automatiseer goedkeuringen met agents',
- automateApprovalsWithAgentsSubtitle: 'Voeg hieronder een agent toe aan de workflow om goedkeuringen te automatiseren.',
makeOrTrackPaymentsTitle: 'Betalingen',
makeOrTrackPaymentsDescription: 'Voeg een gemachtigde betaler toe voor betalingen die in Expensify worden gedaan of volg betalingen die elders zijn gedaan.',
customApprovalWorkflowEnabled:
@@ -2894,6 +2924,12 @@ ${amount} voor ${merchant} - ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: 'Welkom in #focus-modus!',
+ prompt: (priorityModePageUrl: string) =>
+ `Blijf op de hoogte door alleen ongelezen chats of chats te zien die je aandacht nodig hebben. Geen zorgen, je kunt dit op elk moment wijzigen in de instellingen .`,
+ },
+ inboxTabs: {all: 'Alles', todo: 'Te doen', unread: 'Ongelezen'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'PDF genereren',
@@ -3456,11 +3492,6 @@ ${amount} voor ${merchant} - ${date}`,
year: 'Jaar',
selectYear: 'Selecteer een jaar',
},
- focusModeUpdateModal: {
- title: 'Welkom bij de #focus-modus!',
- prompt: (priorityModePageUrl: string) =>
- `Houd het overzicht door alleen ongelezen chats of chats die je aandacht nodig hebben te zien. Geen zorgen, je kunt dit op elk moment wijzigen in de instellingen .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'De chat die je zoekt, kan niet worden gevonden.',
getMeOutOfHere: 'Haal me hier weg',
@@ -4120,27 +4151,30 @@ ${amount} voor ${merchant} - ${date}`,
verificationFailed: 'De verificatie is mislukt, daarom hebben we extra documenten nodig om jou en je bedrijf te verifiëren',
taxIDVerification: 'Belastingnummerverificatie',
taxIDVerificationDescription: dedent(`
- Upload een van de volgende bestanden:
- • IRS TIN/EIN-toewijzingsbrief
- • IRS TIN/EIN-aanvraagbevestiging (bevat meestal "Congratulations! The EIN has been successfully assigned")
- • IRS-belastingvrijstellingsbrief met bedrijfsnaam en EIN`),
+ Upload een van de volgende bestanden:
+ • IRS TIN/EIN-toewijzingsbrief
+ • IRS TIN/EIN-aanvraagbevestiging (bevat meestal "Congratulations! The EIN has been successfully assigned")
+ • IRS-belastingvrijstellingsbrief met bedrijfsnaam en EIN
+ `),
nameChangeDocument: 'Document naamswijziging',
nameChangeDocumentDescription: 'Als de naam van je bedrijf is gewijzigd sinds de TIN/EIN-aanvraag, hebben we dit document nodig om het opgegeven belastingnummer te verifiëren',
companyAddressVerification: 'Verificatie van bedrijfsadres',
companyAddressVerificationDescription: dedent(`
- Upload een van de volgende bestanden:
- • Recente energierekening met bedrijfsnaam en adres
- • Bankafschrift met bedrijfsnaam en adres
- • Huidige huur- of leaseovereenkomst inclusief ondertekeningspagina met bedrijfsnaam en huidig adres
- • Verzekeringsverklaring met bedrijfsnaam en adres
- • TIN-toewijzingsdocument met bedrijfsnaam en adres`),
+ Upload een van de volgende bestanden:
+ • Recente energierekening met bedrijfsnaam en adres
+ • Bankafschrift met bedrijfsnaam en adres
+ • Huidige huur- of leaseovereenkomst inclusief ondertekeningspagina met bedrijfsnaam en huidig adres
+ • Verzekeringsverklaring met bedrijfsnaam en adres
+ • TIN-toewijzingsdocument met bedrijfsnaam en adres
+ `),
userAddressVerification: 'Adresverificatie',
userAddressVerificationDescription: dedent(`
- Upload een van de volgende bestanden:
- • Kiezersregistratiekaart
- • Rijbewijs
- • Bankafschrift
- • Energierekening`),
+ Upload een van de volgende bestanden:
+ • Kiezersregistratiekaart
+ • Rijbewijs
+ • Bankafschrift
+ • Energierekening
+ `),
userDOBVerification: 'Verificatie van geboortedatum',
userDOBVerificationDescription: 'Upload een in de VS uitgegeven identiteitsbewijs',
finishViaChat: 'Afronden via chat',
@@ -4321,7 +4355,6 @@ ${amount} voor ${merchant} - ${date}`,
customFieldHint: 'Voeg aangepaste codering toe die van toepassing is op alle uitgaven van dit lid.',
reports: 'Rapporten',
reportFields: 'Rapportvelden',
- invoiceFields: 'Factuurvelden',
reportTitle: 'Rapporttitel',
reportField: 'Rapportveld',
taxes: 'Belastingen',
@@ -4405,7 +4438,7 @@ ${amount} voor ${merchant} - ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Beheer';
+ return 'Werkruimtebeheerder';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditor';
case CONST.POLICY.ROLE.EDITOR:
@@ -4413,7 +4446,7 @@ ${amount} voor ${merchant} - ${date}`,
case CONST.POLICY.ROLE.CARD_ADMIN:
return 'Kaartbeheer';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
- return 'Mensenbeheer';
+ return 'Personenbeheer';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
return 'Beheerder betalingen';
case CONST.POLICY.ROLE.USER:
@@ -5993,29 +6026,6 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
reportFieldInitialValueRequiredError: 'Kies een beginwaarde voor een rapportveld',
genericFailureMessage: 'Er is een fout opgetreden bij het bijwerken van het rapportveld. Probeer het opnieuw.',
},
- invoiceFields: {
- subtitle: 'Factuurvelden kunnen handig zijn als je extra informatie wilt toevoegen.',
- importedFromAccountingSoftware: 'De onderstaande factuurvelden zijn geïmporteerd uit je',
- disableInvoiceFields: 'Factuurvelden uitschakelen',
- disableInvoiceFieldsConfirmation: 'Weet je het zeker? Factuurvelden worden uitgeschakeld op facturen.',
- delete: 'Factuurveld verwijderen',
- deleteConfirmation: 'Weet je zeker dat je dit factuurveld wilt verwijderen?',
- findInvoiceField: 'Factuurveld zoeken',
- nameInputSubtitle: 'Kies een naam voor het factuurveld.',
- typeInputSubtitle: 'Kies welk type factuurveld je wilt gebruiken.',
- initialValueInputSubtitle: 'Voer een beginwaarde in om in het factuurveld weer te geven.',
- listValuesInputSubtitle: 'Deze waarden verschijnen in de vervolgkeuzelijst van je factuurveld. Ingeschakelde waarden kunnen door leden worden geselecteerd.',
- listInputSubtitle: 'Deze waarden verschijnen in je factuurveldenlijst. Ingeschakelde waarden kunnen door leden worden geselecteerd.',
- emptyInvoiceFieldsValues: {
- title: 'Nog geen lijstwaarden',
- subtitle: 'Voeg aangepaste waarden toe om op facturen te tonen.',
- },
- existingInvoiceFieldNameError: 'Er bestaat al een factuurveld met deze naam',
- invoiceFieldNameRequiredError: 'Voer een naam voor een factuurveld in',
- invoiceFieldTypeRequiredError: 'Kies een veldtype voor de factuur',
- invoiceFieldInitialValueRequiredError: 'Kies een beginwaarde voor een factuurveld',
- addField: 'Veld toevoegen',
- },
tags: {
tagName: 'Tagnaam',
requiresTag: 'Leden moeten alle uitgaven taggen',
@@ -6237,8 +6247,8 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
other: 'Leden maken',
}),
makeAdmin: () => ({
- one: 'Admin maken',
- other: 'Beheerders maken',
+ one: 'Workspace-beheerder maken',
+ other: 'Workspace-beheerders maken',
}),
makeAuditor: () => ({
one: 'Auditeur maken',
@@ -6267,12 +6277,14 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} heeft een openstaand rapport in verwerking waarop actie moet worden ondernomen. Vraag hen dit vereiste actiepunt af te ronden voordat je hen uit de workspace verwijdert.`,
allMembers: 'Alle leden',
- admins: 'Beheerders',
+ admins: 'Workspace-beheerders',
approvers: 'Fiatteurs',
auditors: 'Accountants',
emptyRoleFilter: {title: 'Geen leden komen overeen met dit filter', subtitle: 'Nodig een lid uit of wijzig het filter hierboven.'},
configureHRSync: (providerName: string) => `Stel ${providerName}-synchronisatie in.`,
syncWithHR: (providerName: string) => `Synchroniseren met ${providerName}`,
+ makeCardAdmin: () => ({one: 'Kaartbeheerder maken', other: 'Kaartbeheerders maken'}),
+ cardAdmins: 'Kaartbeheerders',
},
card: {
getStartedIssuing: 'Begin met het uitgeven van je eerste virtuele of fysieke kaart.',
@@ -6689,6 +6701,10 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
}),
enableRate: 'Tarief inschakelen',
status: 'Status',
+ statusActive: 'Actief',
+ statusFuture: 'Toekomstig',
+ statusExpired: 'Verlopen',
+ statusInactive: 'Inactief',
unit: 'Eenheid',
taxFeatureNotEnabledMessage:
'Belastingen moeten in de workspace zijn ingeschakeld om deze functie te gebruiken. Ga naar Meer functies om dat te wijzigen. ',
@@ -6815,12 +6831,6 @@ Weet je zeker dat je ze opnieuw wilt exporteren?`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Rapportvelden zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} `,
},
- invoiceFields: {
- title: 'Factuurvelden',
- description: `Met factuurvelden kun je extra gegevens op factuurniveau toevoegen aan facturen.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Factuurvelden zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Profiteer van automatische synchronisatie en verminder handmatige invoer met de Expensify + NetSuite-integratie. Krijg diepgaande realtime financiële inzichten met ondersteuning voor native en aangepaste segmenten, inclusief project- en klanttoewijzing.`,
@@ -6921,12 +6931,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Afstandstarieven zijn beschikbaar in het Collect-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} `,
},
- auditor: {
- title: 'Auditor',
- description: 'Auditors krijgen alleen-lezen-toegang tot alle rapporten voor volledige transparantie en nalevingsbewaking.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Auditors zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Meerdere goedkeuringsniveaus',
description:
@@ -7024,6 +7028,12 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Facturatie is beschikbaar in de Collect- en Control-abonnementen, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`} `,
},
+ controlPolicyRoles: {
+ title: 'Beleidsrollen controleren',
+ description: 'Gebruik gespecialiseerde rollen zoals Auditor en Kaartbeheerder om leden alleen toegang te geven tot wat ze nodig hebben.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Gespecialiseerde werkruimterollen zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actief lid per maand.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7349,6 +7359,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
deleteRuleConfirmation: 'Weet je zeker dat je deze regel wilt verwijderen?',
describeRuleTitle: 'Beschrijf je regel',
describeRuleSubtitle: 'Beschrijf je regel en Concierge maakt hem voor je',
+ disclaimer: 'AI-agents kunnen fouten maken.',
},
},
planTypePage: {
@@ -8027,21 +8038,19 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} van ${cards}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `heeft het standaardbelastingtarief van de categorie "${categoryName}" gewijzigd naar "${newTax}" (voorheen "${oldTax}")`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `heeft tarief „${rateName}” van ${rateValue} toegevoegd`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) => `tarief "${rateName}" van ${rateValue} toegevoegd, geldig vanaf ${startDate}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `heeft tarief „${rateName}” van ${rateValue} toegevoegd, geldig tot ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`heeft tarief "${rateName}" van ${rateValue} toegevoegd, geldig van ${startDate} - ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `startdatum van tarief "${rateName}" bijgewerkt naar ${newDate} (voorheen ${oldDate})` : `startdatum van tarief "${rateName}" instellen op ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `einddatum van tarief "${rateName}" bijgewerkt naar ${newDate} (voorheen ${oldDate})` : `einddatum van tarief "${rateName}" instellen op ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `start- en einddatum van tarief "${rateName}" bijgewerkt naar ${newStartDate} - ${newEndDate} (voorheen ${oldStartDate} - ${oldEndDate})`
- : `start- en einddatum van tarief "${rateName}" ingesteld op ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `startdatum verwijderd van tarief "${rateName}" (voorheen ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `einddatum verwijderd van tarief "${rateName}" (voorheen ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `heeft het kilometertarief "${rateName}" bijgewerkt zodat het geldt van ${newDateRange} (voorheen ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `vanaf ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `tot ${date}`,
+ customUnitRateDateRangeAllDates: () => `voor alle data`,
},
roomMembersPage: {
memberNotFound: 'Lid niet gevonden.',
@@ -8482,8 +8491,11 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
`De verbinding met ${feedName} is verbroken. Log in bij je bank om kaartimports te herstellen.`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`de Plaid-verbinding met je zakelijke bankrekening is verbroken. Verbind je bankrekening ${maskedAccountNumber} opnieuw zodat je je Expensify Kaarten kunt blijven gebruiken.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} is via de werkruimte-uitnodigingslink lid geworden` : `heeft ${email} toegevoegd als ${role === 'member' ? 'een' : 'een'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'een' : 'een';
+ return didJoinPolicy ? `${email} is lid geworden via de uitnodigingslink voor de workspace` : `${email} toegevoegd als ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `heeft de rol van ${email} bijgewerkt naar ${newRole} (voorheen ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9379,6 +9391,11 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`Als copiloot voor ${accountOwnerEmail} heb je geen toestemming om deze actie uit te voeren. Sorry!`,
+ removeCopilotAccess: 'Mijn copilot-toegang verwijderen',
+ removeCopilotAccessTitle: 'Copilot-toegang verwijderen?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Weet je zeker dat je je copilot-toegang tot het Expensify-account van ${delegatorName} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.`,
+ removeCopilotAccessConfirm: 'Toegang verwijderen',
copilotAccess: 'Copilot-toegang',
},
debug: {
diff --git a/src/languages/params.ts b/src/languages/params.ts
index 41610783b027..b9c0884faab4 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -102,6 +102,8 @@ type SyncStageNameConnectionsParams = {stage: PolicyConnectionSyncStage};
type DelegateRoleParams = {role: DelegateRole};
+type RemoveCopilotAccessConfirmationParams = {delegatorName: string};
+
type RemovedFromApprovalWorkflowParams = {
submittersNames: string[];
};
@@ -135,6 +137,7 @@ export type {
InvalidValueParams,
RemovedFromApprovalWorkflowParams,
DelegateRoleParams,
+ RemoveCopilotAccessConfirmationParams,
SyncStageNameConnectionsParams,
IntacctMappingTitleParams,
ExportIntegrationSelectedParams,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 29f9591d54bf..9a6d8b988c7b 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: 'Wielokrotny wybór',
saveChanges: 'Zapisz zmiany',
submit: 'Wyślij',
+ markAsDone: 'Oznacz jako wykonane',
submitted: 'Przesłano',
+ markedAsDoneStatus: 'Oznaczone jako ukończone',
rotate: 'Obróć',
zoom: 'Powiększenie',
password: 'Hasło',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: 'Opis',
title: 'Tytuł',
assignee: 'Osoba przypisana',
- createdBy: 'Utworzone przez',
with: 'z',
shareCode: 'Udostępnij kod',
share: 'Udostępnij',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: 'Sprzedawca',
change: 'Zmień',
category: 'Kategoria',
+ vendor: 'Dostawca',
report: 'Raport',
billable: 'Fakturowalne',
nonBillable: 'Nierozliczalne',
@@ -355,6 +358,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Znajdź czat za pomocą',
subtitleText2: 'przycisk powyżej lub utwórz coś za pomocą',
subtitleText3: 'przycisk poniżej.',
+ noUnreadChats: 'Brak nieprzeczytanych czatów',
+ noTodos: 'Brak zadań',
+ caughtUp: 'Ze wszystkim już się uporałeś. Dobra robota!',
+ seeAllChats: 'Zobacz wszystkie czaty',
},
businessName: 'Nazwa firmy',
clear: 'Wyczyść',
@@ -853,6 +860,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `Ten czat jest z ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`Tutaj ${submitterDisplayName} będzie przesyłać wydatki do ${workspaceName} . Po prostu użyj przycisku +.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'Tutaj będziesz śledzić wydatki',
beginningOfChatHistorySelfDM: 'To Twoja osobista przestrzeń. Używaj jej na notatki, zadania, szkice i przypomnienia.',
beginningOfChatHistorySystemDM: 'Witamy! Zacznijmy konfigurację.',
chatWithAccountManager: 'Porozmawiaj tutaj z opiekunem konta',
@@ -959,7 +967,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Napraw połączenie z prywatną kartą ${cardName}` : 'Napraw połączenie karty prywatnej'),
subtitle: 'Portfel',
},
- validateAccount: {title: 'Zweryfikuj swoje konto, aby dalej korzystać z Expensify', subtitle: 'Konto', cta: 'Zatwierdź'},
+ validateAccount: {title: 'Zweryfikuj swoje konto', subtitle: 'Konto', cta: 'Zatwierdź'},
fixFailedBilling: {title: 'Nie mogliśmy obciążyć zapisanej karty', subtitle: 'Subskrypcja'},
unlockBankAccount: {
workspaceTitle: 'Twoje firmowe konto bankowe zostało zablokowane',
@@ -1014,6 +1022,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Wszystko nadrobione',
f1FlagsDescription: 'Ukończyłeś wszystkie zaległe zadania.',
},
+ reviewExpenses: ({count}: {count: number}) => `Przejrzyj ${count} ${count === 1 ? 'wydatek' : 'wydatki'}`,
},
upcomingTravel: 'Nadchodząca podróż',
upcomingTravelSection: {
@@ -1347,6 +1356,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `Wyślij fakturę na ${amount}`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `za ${comment}` : ''}`,
submitted: (memo?: string) => `przesłano${memo ? `, wpisując ${memo}` : ''}`,
+ markedAsDone: (memo) => `oznaczono jako zakończone${memo ? `, z adnotacją: ${memo}` : ''}`,
automaticallySubmitted: `przesłano przez opóźnianie wysyłania `,
queuedToSubmitViaDEW: 'w kolejce do przesłania przez niestandardowy proces zatwierdzania',
queuedToApproveViaDEW: 'oczekuje na zatwierdzenie w niestandardowym procesie akceptacji',
@@ -1559,6 +1569,9 @@ const translations: TranslationDeepObject = {
removed: 'usunięto',
transactionPending: 'Transakcja w toku.',
chooseARate: 'Wybierz stawkę zwrotu kosztów za milę lub kilometr dla przestrzeni roboczej',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} do ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Ważne od ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Ważne do ${endDate}`,
unapprove: 'Cofnij zatwierdzenie',
unapproveReport: 'Cofnij zatwierdzenie raportu',
headsUp: 'Uwaga!',
@@ -1659,6 +1672,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'Napraw błąd stawki i spróbuj ponownie.',
AskToExplain: `. Wyjaśnij `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge dopasował ten wydatek do ${vendorName} `,
duplicateNonDefaultWorkspacePerDiemError:
'Nie możesz duplikować wydatków z tytułu diet między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.',
rulesModifiedFields: {
@@ -1788,6 +1802,21 @@ const translations: TranslationDeepObject = {
return `Oczekiwanie, aż administrator złoży wydatki.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `Czekamy, aż Ty oznaczysz to jako wykonane.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `Oczekiwanie, aż ${actor} oznaczy to jako wykonane.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `Oczekiwanie, aż administrator oznaczy to jako wykonane.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2221,10 +2250,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Zgłoś podejrzaną aktywność',
lockAccount: 'Zablokuj konto',
+ lockMyAccount: 'Zablokuj moje konto',
unlockAccount: 'Odblokuj konto',
- compromisedDescription:
- 'Zauważyłeś coś niepokojącego na swoim koncie? Zgłoszenie tego natychmiast zablokuje twoje konto, wstrzyma nowe transakcje Kartą Expensify i uniemożliwi wprowadzanie jakichkolwiek zmian na koncie.',
- domainAdminsDescription: 'Dla administratorów domen: to również wstrzymuje całą aktywność Kart Expensify i działania administracyjne w twoich domenach.',
+ findYourSituation: 'Większość problemów nie wymaga blokowania konta! Znajdź swoją sytuację poniżej:',
+ lostCardOrCharges:
+ 'Zgubiona karta lub nieznane obciążenia : Anuluj swoją kartę i skontaktuj się z Concierge, żeby zakwestionować nieznane transakcje.',
+ unauthorizedAccess:
+ 'Nieautoryzowany dostęp do konta : Zablokuj swoje konto poniżej. To zablokuje nowe transakcje Kartą Expensify, zamówienia kart i zmiany na koncie. Jeśli jesteś administratorem domeny, to wstrzyma też całą aktywność kart i działania administratorów w obrębie domeny.',
+ securityTeamFollowUp: 'Nasz zespół ds. bezpieczeństwa skontaktuje się z tobą z adresu risk@expensify.com po zablokowaniu.',
areYouSure: 'Czy na pewno chcesz zablokować swoje konto Expensify?',
onceLocked: 'Po zablokowaniu Twoje konto będzie ograniczone do czasu złożenia prośby o odblokowanie i przeprowadzenia kontroli bezpieczeństwa',
unlockTitle: 'Otrzymaliśmy Twoją prośbę',
@@ -2588,13 +2621,10 @@ ${amount} dla ${merchant} - ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `wydatki od ${members}, a zatwierdzającym jest ${approvers}`,
addApprovalButton: 'Dodaj proces akceptacji',
editWorkflowAction: 'Edytuj',
- addAgentAction: 'Dodaj agenta',
findWorkflow: 'Znajdź przepływ pracy',
addApprovalTip: 'Domyślny proces pracy ma zastosowanie do wszystkich członków, chyba że istnieje bardziej szczegółowy proces pracy.',
approver: 'Osoba zatwierdzająca',
addApprovalsDescription: 'Wymagaj dodatkowej akceptacji przed autoryzacją płatności.',
- automateApprovalsWithAgentsTitle: 'Automatyzuj zatwierdzanie z agentami',
- automateApprovalsWithAgentsSubtitle: 'Dodaj agenta poniżej do przepływu pracy, aby zautomatyzować zatwierdzanie.',
makeOrTrackPaymentsTitle: 'Płatności',
makeOrTrackPaymentsDescription: 'Dodaj upoważnionego płatnika dla płatności dokonywanych w Expensify lub śledź płatności wykonywane gdzie indziej.',
customApprovalWorkflowEnabled:
@@ -2888,6 +2918,12 @@ ${amount} dla ${merchant} - ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: 'Witaj w trybie #focus!',
+ prompt: (priorityModePageUrl: string) =>
+ `Bądź na bieżąco, widząc tylko nieprzeczytane czaty lub czaty wymagające twojej uwagi. Spokojnie, możesz to zmienić w dowolnym momencie w ustawieniach .`,
+ },
+ inboxTabs: {all: 'Wszystko', todo: 'Zadania do wykonania', unread: 'Nieprzeczytane'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `w ${policyName}`,
generatingPDF: 'Wygeneruj PDF',
@@ -3447,11 +3483,6 @@ ${amount} dla ${merchant} - ${date}`,
year: 'Rok',
selectYear: 'Wybierz rok',
},
- focusModeUpdateModal: {
- title: 'Witamy w trybie #focus!',
- prompt: (priorityModePageUrl: string) =>
- `Miej wszystko pod kontrolą, wyświetlając tylko nieprzeczytane czaty lub czaty wymagające Twojej uwagi. Nie martw się, możesz to zmienić w dowolnym momencie w ustawieniach .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'Nie można znaleźć czatu, którego szukasz.',
getMeOutOfHere: 'Wyprowadź mnie stąd',
@@ -4112,27 +4143,30 @@ ${amount} dla ${merchant} - ${date}`,
verificationFailed: 'Weryfikacja nie powiodła się, dlatego potrzebujemy dodatkowych dokumentów do potwierdzenia Twojej tożsamości i firmy',
taxIDVerification: 'Weryfikacja numeru podatkowego',
taxIDVerificationDescription: dedent(`
- Prześlij jeden z poniższych plików:
- • List przydziału TIN/EIN z IRS
- • Potwierdzenie wniosku TIN/EIN z IRS (zwykle zawiera „Congratulations! The EIN has been successfully assigned”)
- • Pismo o zwolnieniu podatkowym z IRS zawierające nazwę firmy i EIN`),
+ Prześlij jeden z poniższych plików:
+ • List przydziału TIN/EIN z IRS
+ • Potwierdzenie wniosku TIN/EIN z IRS (zwykle zawiera „Congratulations! The EIN has been successfully assigned”)
+ • Pismo o zwolnieniu podatkowym z IRS zawierające nazwę firmy i EIN
+ `),
nameChangeDocument: 'Dokument zmiany nazwy',
nameChangeDocumentDescription: 'Jeśli nazwa firmy zmieniła się od momentu złożenia wniosku o TIN/EIN, dokument ten jest wymagany do weryfikacji podanego numeru podatkowego',
companyAddressVerification: 'Weryfikacja adresu firmy',
companyAddressVerificationDescription: dedent(`
- Prześlij jeden z poniższych plików:
- • Aktualny rachunek za media z nazwą i adresem firmy
- • Wyciąg bankowy z nazwą i adresem firmy
- • Aktualna umowa najmu z podpisaną stroną zawierającą nazwę i adres firmy
- • Dokument ubezpieczeniowy z nazwą i adresem firmy
- • Dokument przydziału TIN z nazwą i adresem firmy`),
+ Prześlij jeden z poniższych plików:
+ • Aktualny rachunek za media z nazwą i adresem firmy
+ • Wyciąg bankowy z nazwą i adresem firmy
+ • Aktualna umowa najmu z podpisaną stroną zawierającą nazwę i adres firmy
+ • Dokument ubezpieczeniowy z nazwą i adresem firmy
+ • Dokument przydziału TIN z nazwą i adresem firmy
+ `),
userAddressVerification: 'Weryfikacja adresu',
userAddressVerificationDescription: dedent(`
- Prześlij jeden z poniższych plików:
- • Karta rejestracji wyborcy
- • Prawo jazdy
- • Wyciąg bankowy
- • Rachunek za media`),
+ Prześlij jeden z poniższych plików:
+ • Karta rejestracji wyborcy
+ • Prawo jazdy
+ • Wyciąg bankowy
+ • Rachunek za media
+ `),
userDOBVerification: 'Weryfikacja daty urodzenia',
userDOBVerificationDescription: 'Prześlij dokument tożsamości wydany w USA',
finishViaChat: 'Zakończ przez czat',
@@ -4313,7 +4347,6 @@ ${amount} dla ${merchant} - ${date}`,
customFieldHint: 'Dodaj niestandardowe kodowanie, które będzie stosowane do wszystkich wydatków tego członka.',
reports: 'Raporty',
reportFields: 'Pola raportu',
- invoiceFields: 'Pola faktury',
reportTitle: 'Tytuł raportu',
reportField: 'Pole raportu',
taxes: 'Podatki',
@@ -4397,7 +4430,7 @@ ${amount} dla ${merchant} - ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Administrator';
+ return 'Administrator przestrzeni roboczej';
case CONST.POLICY.ROLE.AUDITOR:
return 'Audytor';
case CONST.POLICY.ROLE.EDITOR:
@@ -5987,29 +6020,6 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
reportFieldInitialValueRequiredError: 'Wybierz początkową wartość pola raportu',
genericFailureMessage: 'Wystąpił błąd podczas aktualizowania pola raportu. Spróbuj ponownie.',
},
- invoiceFields: {
- subtitle: 'Pola faktury mogą być pomocne, gdy chcesz dodać dodatkowe informacje.',
- importedFromAccountingSoftware: 'Poniższe pola faktury są importowane z Twojego',
- disableInvoiceFields: 'Wyłącz pola faktury',
- disableInvoiceFieldsConfirmation: 'Czy na pewno? Pola faktury zostaną wyłączone na fakturach.',
- delete: 'Usuń pole faktury',
- deleteConfirmation: 'Czy na pewno chcesz usunąć to pole faktury?',
- findInvoiceField: 'Znajdź pole faktury',
- nameInputSubtitle: 'Wybierz nazwę pola faktury.',
- typeInputSubtitle: 'Wybierz typ pola faktury, którego chcesz użyć.',
- initialValueInputSubtitle: 'Wprowadź wartość początkową, która ma być wyświetlana w polu faktury.',
- listValuesInputSubtitle: 'Te wartości pojawią się na liście rozwijanej pola faktury. Członkowie mogą wybierać włączone wartości.',
- listInputSubtitle: 'Te wartości pojawią się na liście pola faktury. Członkowie mogą wybierać włączone wartości.',
- emptyInvoiceFieldsValues: {
- title: 'Brak wartości listy',
- subtitle: 'Dodaj niestandardowe wartości, które mają pojawiać się na fakturach.',
- },
- existingInvoiceFieldNameError: 'Pole faktury o tej nazwie już istnieje',
- invoiceFieldNameRequiredError: 'Wprowadź nazwę pola faktury',
- invoiceFieldTypeRequiredError: 'Wybierz typ pola faktury',
- invoiceFieldInitialValueRequiredError: 'Wybierz początkową wartość pola faktury',
- addField: 'Dodaj pole',
- },
tags: {
tagName: 'Nazwa tagu',
requiresTag: 'Członkowie muszą otagować wszystkie wydatki',
@@ -6232,8 +6242,8 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
other: 'Utwórz członków',
}),
makeAdmin: () => ({
- one: 'Ustaw jako administratora',
- other: 'Ustaw jako administratorów',
+ one: 'Ustaw jako administratora przestrzeni roboczej',
+ other: 'Ustaw jako administratorów przestrzeni roboczej',
}),
makeAuditor: () => ({
one: 'Ustaw jako audytora',
@@ -6262,12 +6272,14 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} ma nierozliczony raport w trakcie przetwarzania, który wymaga działania. Poproś tę osobę o wykonanie wymaganej czynności przed jej usunięciem z przestrzeni roboczej.`,
allMembers: 'Wszyscy członkowie',
- admins: 'Administratorzy',
+ admins: 'Administratorzy przestrzeni roboczej',
approvers: 'Osoby zatwierdzające',
auditors: 'Audytorzy',
emptyRoleFilter: {title: 'Żadni członkowie nie pasują do tego filtra', subtitle: 'Zaproś członka lub zmień filtr powyżej.'},
configureHRSync: (providerName: string) => `Skonfiguruj synchronizację ${providerName}.`,
syncWithHR: (providerName: string) => `Synchronizuj z ${providerName}`,
+ makeCardAdmin: () => ({one: 'Ustaw jako administratora karty', other: 'Ustaw administratorów kart'}),
+ cardAdmins: 'Administratorzy kart',
},
card: {
getStartedIssuing: 'Zacznij od wydania swojej pierwszej wirtualnej lub fizycznej karty.',
@@ -6682,6 +6694,10 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
}),
enableRate: 'Włącz stawkę',
status: 'Status',
+ statusActive: 'Aktywna',
+ statusFuture: 'Przyszła',
+ statusExpired: 'Wygasła',
+ statusInactive: 'Nieaktywna',
unit: 'Jednostka',
taxFeatureNotEnabledMessage:
'Aby korzystać z tej funkcji, włącz podatki w przestrzeni roboczej. Przejdź do sekcji Więcej funkcji , aby to zmienić. ',
@@ -6808,12 +6824,6 @@ Czy na pewno chcesz wyeksportować je ponownie?`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Pola raportu są dostępne tylko w planie Control, od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} `,
},
- invoiceFields: {
- title: 'Pola faktury',
- description: `Pola faktury pozwalają dodać dodatkowe szczegóły na poziomie faktury.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Pola faktury są dostępne tylko w planie Control, od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Korzystaj z automatycznej synchronizacji i ograniczaj ręczne wprowadzanie danych dzięki integracji Expensify + NetSuite. Uzyskaj dogłębny, aktualny w czasie rzeczywistym wgląd w finanse dzięki obsłudze natywnych i niestandardowych segmentów, w tym mapowaniu projektów i klientów.`,
@@ -6915,12 +6925,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Stawki za przejazdy są dostępne w planie Collect, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} `,
},
- auditor: {
- title: 'Audytor',
- description: 'Biegli rewidentci otrzymują dostęp tylko do odczytu do wszystkich raportów, aby zapewnić pełną widoczność i monitorowanie zgodności.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Audytorzy są dostępni tylko w planie Control, zaczynającym się od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Wiele poziomów zatwierdzania',
description:
@@ -7020,6 +7024,12 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Fakturowanie jest dostępne w planach Collect i Control, zaczynając od ${formattedPrice} ${hasTeam2025Pricing ? `za członka za miesiąc.` : `za aktywnego członka miesięcznie.`} `,
},
+ controlPolicyRoles: {
+ title: 'Role zasad kontroli',
+ description: 'Użyj wyspecjalizowanych ról, takich jak Audytor i Administrator kart, żeby dać członkom dostęp tylko do tego, czego potrzebują.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Specjalistyczne role w przestrzeni roboczej są dostępne tylko w planie Control, od ${formattedPrice} ${hasTeam2025Pricing ? `za członka miesięcznie.` : `za aktywnego członka miesięcznie.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7342,6 +7352,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
deleteRuleConfirmation: 'Na pewno chcesz usunąć tę regułę?',
describeRuleTitle: 'Opisz swoją regułę',
describeRuleSubtitle: 'Opisz swoją regułę, a Concierge ją utworzy',
+ disclaimer: 'Agenci AI mogą popełniać błędy.',
},
},
planTypePage: {
@@ -8019,22 +8030,20 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} z ${cards}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `zmienił(a) domyślną stawkę podatku dla kategorii „${categoryName}” na „${newTax}” (wcześniej „${oldTax}”)`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `dodano stawkę „${rateName}” w wysokości ${rateValue}`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) =>
`dodano stawkę „${rateName}” w wysokości ${rateValue}, obowiązującą od ${startDate}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `dodano stawkę „${rateName}” w wysokości ${rateValue}, obowiązującą do ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`dodano stawkę „${rateName}” w wysokości ${rateValue}, obowiązującą od ${startDate} do ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `zaktualizowano datę rozpoczęcia stawki „${rateName}” na ${newDate} (wcześniej ${oldDate})` : `ustaw datę rozpoczęcia stawki „${rateName}” na ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `zaktualizowano datę końcową stawki „${rateName}” na ${newDate} (wcześniej ${oldDate})` : `ustaw datę zakończenia stawki „${rateName}” na ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `zaktualizowano datę początkową i końcową stawki „${rateName}” na ${newStartDate} – ${newEndDate} (wcześniej ${oldStartDate} – ${oldEndDate})`
- : `ustaw początkową i końcową datę stawki „${rateName}” na ${newStartDate} – ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `usunięto datę początkową ze stawki „${rateName}” (wcześniej ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `usunięto datę zakończenia ze stawki „${rateName}” (wcześniej ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `zaktualizowano stawkę za dystans „${rateName}”, aby obowiązywała ${newDateRange} (wcześniej ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `od ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `do ${date}`,
+ customUnitRateDateRangeAllDates: () => `dla wszystkich dat`,
},
roomMembersPage: {
memberNotFound: 'Nie znaleziono członka.',
@@ -8474,8 +8483,11 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
`Połączenie ${feedName} jest przerwane. Aby przywrócić importy kart, zaloguj się do swojego banku .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`połączenie Plaid z twoim firmowym kontem bankowym jest przerwane. Proszę, połącz ponownie swoje konto bankowe ${maskedAccountNumber} , aby móc dalej używać Kart Expensify.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} dołączył(a) przez link z zaproszeniem do przestrzeni roboczej` : `dodano ${email} jako ${role === 'member' ? 'a' : 'jeden'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'an' : 'a';
+ return didJoinPolicy ? `${email} dołączył za pomocą linku z zaproszeniem do przestrzeni roboczej` : `dodano ${email} jako ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `zaktualizowano rolę użytkownika ${email} na ${newRole} (wcześniej ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9364,6 +9376,11 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`Jako kopilot dla ${accountOwnerEmail} nie masz uprawnień do wykonania tej akcji. Przepraszamy!`,
+ removeCopilotAccess: 'Usuń mój dostęp kopilota',
+ removeCopilotAccessTitle: 'Usunąć dostęp kopilota?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Czy na pewno chcesz usunąć swój dostęp kopilota do konta Expensify użytkownika ${delegatorName}? Tej czynności nie można cofnąć.`,
+ removeCopilotAccessConfirm: 'Usuń dostęp',
copilotAccess: 'Dostęp do Copilota',
},
debug: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index aa19d8aceaf8..0e1b4717bb76 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: 'Seleção múltipla',
saveChanges: 'Salvar alterações',
submit: 'Enviar',
+ markAsDone: 'Marcar como concluído',
submitted: 'Enviado',
+ markedAsDoneStatus: 'Marcado como concluído',
rotate: 'Girar',
zoom: 'Zoom',
password: 'Senha',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: 'Descrição',
title: 'Título',
assignee: 'Responsável',
- createdBy: 'Criado por',
with: 'com',
shareCode: 'Compartilhar código',
share: 'Compartilhar',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: 'Estabelecimento',
change: 'Alterar',
category: 'Categoria',
+ vendor: 'Fornecedor',
report: 'Relatório',
billable: 'Faturável',
nonBillable: 'Não faturável',
@@ -355,6 +358,10 @@ const translations: TranslationDeepObject = {
subtitleText1: 'Encontre um chat usando o',
subtitleText2: 'botão acima ou crie algo usando o',
subtitleText3: 'botão abaixo.',
+ noUnreadChats: 'Nenhum chat não lido',
+ noTodos: 'Nenhuma tarefa pendente',
+ caughtUp: 'Você está em dia. Muito bem!',
+ seeAllChats: 'Ver todas as conversas',
},
businessName: 'Nome da empresa',
clear: 'Limpar',
@@ -852,6 +859,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `Este chat é com ${users}.`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`É aqui que ${submitterDisplayName} enviará despesas para ${workspaceName} . Basta usar o botão +.`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: 'É aqui que você vai acompanhar as despesas',
beginningOfChatHistorySelfDM: 'Este é o seu espaço pessoal. Use-o para anotações, tarefas, rascunhos e lembretes.',
beginningOfChatHistorySystemDM: 'Bem-vindo(a)! Vamos fazer a sua configuração.',
chatWithAccountManager: 'Converse com seu gerente de conta aqui',
@@ -957,7 +965,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Corrigir conexão do cartão pessoal ${cardName}` : 'Corrigir conexão do cartão pessoal'),
subtitle: 'Carteira',
},
- validateAccount: {title: 'Valide sua conta para continuar usando o Expensify', subtitle: 'Conta', cta: 'Validar'},
+ validateAccount: {title: 'Valide sua conta', subtitle: 'Conta', cta: 'Validar'},
fixFailedBilling: {title: 'Não foi possível cobrar o cartão cadastrado', subtitle: 'Assinatura'},
unlockBankAccount: {
workspaceTitle: 'Sua conta bancária comercial foi bloqueada',
@@ -1012,6 +1020,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: 'Tudo em dia',
f1FlagsDescription: 'Você concluiu todas as tarefas pendentes.',
},
+ reviewExpenses: ({count}: {count: number}) => `Revisar ${count} ${count === 1 ? 'despesa' : 'despesas'}`,
},
upcomingTravel: 'Próximas viagens',
upcomingTravelSection: {
@@ -1347,6 +1356,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `Enviar fatura de ${amount}`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `para ${comment}` : ''}`,
submitted: (memo?: string) => `enviado${memo ? `, dizendo ${memo}` : ''}`,
+ markedAsDone: (memo) => `marcado como concluído${memo ? `, dizendo ${memo}` : ''}`,
automaticallySubmitted: `enviado via atrasar envios `,
queuedToSubmitViaDEW: 'na fila para enviar via fluxo de aprovação personalizado',
queuedToApproveViaDEW: 'na fila para aprovar via fluxo de aprovação personalizado',
@@ -1558,6 +1568,9 @@ const translations: TranslationDeepObject = {
removed: 'removido',
transactionPending: 'Transação pendente.',
chooseARate: 'Selecione uma taxa de reembolso do espaço de trabalho por milha ou quilômetro',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} a ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `Válido a partir de ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `Válido até ${endDate}`,
unapprove: 'Reprovar',
unapproveReport: 'Rejeitar relatório',
headsUp: 'Atenção!',
@@ -1658,6 +1671,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: 'Corrija o erro de taxa e tente novamente.',
AskToExplain: `. Explicar `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge associou esta despesa ao fornecedor ${vendorName} `,
duplicateNonDefaultWorkspacePerDiemError: 'Você não pode duplicar despesas de diárias entre espaços de trabalho porque as tarifas podem variar entre eles.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marcou a despesa como "reembolsável"' : 'marcou a despesa como “não reembolsável”'),
@@ -1785,6 +1799,21 @@ const translations: TranslationDeepObject = {
return `Aguardando um administrador enviar as despesas.`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `Aguardando você marcar como concluído.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `Aguardando ${actor} marcar como concluído.`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `Aguardando um administrador marcar como concluído.`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2218,10 +2247,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: 'Denunciar atividade suspeita',
lockAccount: 'Bloquear conta',
+ lockMyAccount: 'Bloquear minha conta',
unlockAccount: 'Desbloquear conta',
- compromisedDescription:
- 'Percebeu algo estranho na sua conta? Ao denunciar, sua conta será imediatamente bloqueada, novas transações com o Cartão Expensify serão impedidas e qualquer alteração na conta será bloqueada.',
- domainAdminsDescription: 'Para admins de domínio: isso também pausa todas as atividades do Cartão Expensify e ações de administração em todo(s) o(s) seu(s) domínio(s).',
+ findYourSituation: 'A maioria dos problemas não exige bloquear sua conta! Encontre sua situação abaixo:',
+ lostCardOrCharges:
+ 'Cartão perdido ou cobranças desconhecidas : Cancele seu cartão e contate o Concierge para contestar transações desconhecidas.',
+ unauthorizedAccess:
+ 'Acesso não autorizado à conta : Bloqueie sua conta abaixo. Isso bloqueia novas transações com o Cartão Expensify, pedidos de cartão e alterações na conta. Se você for admin de domínio, isso também pausa toda a atividade de cartões em todo o domínio e as ações de admin.',
+ securityTeamFollowUp: 'Nossa equipe de segurança vai entrar em contato a partir de risk@expensify.com após o bloqueio.',
areYouSure: 'Tem certeza de que deseja bloquear sua conta Expensify?',
onceLocked: 'Depois de bloqueada, sua conta ficará restrita até que seja feita uma solicitação de desbloqueio e uma revisão de segurança',
unlockTitle: 'Recebemos sua solicitação',
@@ -2586,13 +2619,10 @@ ${amount} para ${merchant} - ${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `despesas de ${members}, e o aprovador é ${approvers}`,
addApprovalButton: 'Adicionar fluxo de aprovação',
editWorkflowAction: 'Editar',
- addAgentAction: 'Adicionar agente',
findWorkflow: 'Buscar fluxo de trabalho',
addApprovalTip: 'Este fluxo de trabalho padrão se aplica a todos os membros, a menos que exista um fluxo de trabalho mais específico.',
approver: 'Aprovador',
addApprovalsDescription: 'Exigir aprovação adicional antes de autorizar um pagamento.',
- automateApprovalsWithAgentsTitle: 'Automatize aprovações com agentes',
- automateApprovalsWithAgentsSubtitle: 'Adicione o agente abaixo ao fluxo de trabalho para automatizar as aprovações.',
makeOrTrackPaymentsTitle: 'Pagamentos',
makeOrTrackPaymentsDescription: 'Adicione um pagador autorizado para pagamentos feitos no Expensify ou acompanhe pagamentos feitos em outros lugares.',
customApprovalWorkflowEnabled:
@@ -2888,6 +2918,12 @@ ${amount} para ${merchant} - ${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: 'Bem-vindo ao modo #focus!',
+ prompt: (priorityModePageUrl: string) =>
+ `Fique no controle vendo apenas chats não lidos ou que precisam da sua atenção. Não se preocupe, você pode mudar isso a qualquer momento em configurações .`,
+ },
+ inboxTabs: {all: 'Todos', todo: 'Pendências', unread: 'Não lidas'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `em ${policyName}`,
generatingPDF: 'Gerar PDF',
@@ -3448,11 +3484,6 @@ ${amount} para ${merchant} - ${date}`,
year: 'Ano',
selectYear: 'Selecione um ano',
},
- focusModeUpdateModal: {
- title: 'Bem-vindo ao modo #focus!',
- prompt: (priorityModePageUrl: string) =>
- `Mantenha tudo sob controle vendo apenas os chats não lidos ou que precisam da sua atenção. Não se preocupe, você pode alterar isso a qualquer momento em configurações .`,
- },
notFound: {
chatYouLookingForCannotBeFound: 'O chat que você está procurando não foi encontrado.',
getMeOutOfHere: 'Me tire daqui',
@@ -4112,27 +4143,30 @@ ${amount} para ${merchant} - ${date}`,
verificationFailed: 'A verificação falhou, então precisaremos de documentos adicionais para verificar você e sua empresa',
taxIDVerification: 'Verificação de ID fiscal',
taxIDVerificationDescription: dedent(`
- Envie um dos seguintes arquivos:
- • Carta de atribuição de TIN/EIN do IRS
- • Confirmação de solicitação de TIN/EIN do IRS (normalmente contém "Congratulations! The EIN has been successfully assigned")
- • Carta de isenção fiscal do IRS com o nome da empresa e o EIN`),
+ Envie um dos seguintes arquivos:
+ • Carta de atribuição de TIN/EIN do IRS
+ • Confirmação de solicitação de TIN/EIN do IRS (normalmente contém "Congratulations! The EIN has been successfully assigned")
+ • Carta de isenção fiscal do IRS com o nome da empresa e o EIN
+ `),
nameChangeDocument: 'Documento de alteração de nome',
nameChangeDocumentDescription: 'Se o nome da sua empresa mudou desde a solicitação do TIN/EIN, precisamos deste documento para verificar o número de identificação fiscal informado',
companyAddressVerification: 'Verificação de endereço da empresa',
companyAddressVerificationDescription: dedent(`
- Envie um dos seguintes arquivos:
- • Conta recente de serviços públicos com nome e endereço da empresa
- • Extrato bancário com nome e endereço da empresa
- • Contrato de locação atual incluindo a página de assinatura com nome e endereço atual da empresa
- • Apólice ou declaração de seguro com nome e endereço da empresa
- • Documento de atribuição de TIN com nome e endereço da empresa`),
+ Envie um dos seguintes arquivos:
+ • Conta recente de serviços públicos com nome e endereço da empresa
+ • Extrato bancário com nome e endereço da empresa
+ • Contrato de locação atual incluindo a página de assinatura com nome e endereço atual da empresa
+ • Apólice ou declaração de seguro com nome e endereço da empresa
+ • Documento de atribuição de TIN com nome e endereço da empresa
+ `),
userAddressVerification: 'Verificação de endereço',
userAddressVerificationDescription: dedent(`
- Envie um dos seguintes arquivos:
- • Título de eleitor
- • Carteira de motorista
- • Extrato bancário
- • Conta de serviços públicos`),
+ Envie um dos seguintes arquivos:
+ • Título de eleitor
+ • Carteira de motorista
+ • Extrato bancário
+ • Conta de serviços públicos
+ `),
userDOBVerification: 'Verificação de data de nascimento',
userDOBVerificationDescription: 'Envie um documento de identidade emitido nos EUA',
finishViaChat: 'Finalizar pelo chat',
@@ -4316,7 +4350,6 @@ ${amount} para ${merchant} - ${date}`,
reportFields: 'Campos do relatório',
reportTitle: 'Título do relatório',
reportField: 'Campo de relatório',
- invoiceFields: 'Campos de fatura',
taxes: 'Impostos',
bills: 'Contas',
invoices: 'Faturas',
@@ -4398,17 +4431,17 @@ ${amount} para ${merchant} - ${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return 'Admin';
+ return 'Admin. do workspace';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditor';
case CONST.POLICY.ROLE.EDITOR:
return 'Editor';
case CONST.POLICY.ROLE.CARD_ADMIN:
- return 'Admin. do Cartão';
+ return 'Admin. de Cartão';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
- return 'Administração de pessoas';
+ return 'Administração de Pessoas';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
- return 'Admin de pagamentos';
+ return 'Admin de Pagamentos';
case CONST.POLICY.ROLE.USER:
return 'Membro';
default:
@@ -5992,29 +6025,6 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
reportFieldInitialValueRequiredError: 'Escolha um valor inicial para o campo de relatório',
genericFailureMessage: 'Ocorreu um erro ao atualizar o campo do relatório. Tente novamente.',
},
- invoiceFields: {
- subtitle: 'Os campos da fatura podem ser úteis quando você quiser incluir informações extras.',
- importedFromAccountingSoftware: 'Os campos da fatura abaixo são importados do seu',
- disableInvoiceFields: 'Desativar campos da fatura',
- disableInvoiceFieldsConfirmation: 'Tem certeza? Os campos da fatura serão desativados nas faturas.',
- delete: 'Excluir campo da fatura',
- deleteConfirmation: 'Tem certeza de que deseja excluir este campo da fatura?',
- findInvoiceField: 'Encontrar campo da fatura',
- nameInputSubtitle: 'Escolha um nome para o campo da fatura.',
- typeInputSubtitle: 'Escolha o tipo de campo da fatura que deseja usar.',
- initialValueInputSubtitle: 'Insira um valor inicial para mostrar no campo da fatura.',
- listValuesInputSubtitle: 'Esses valores aparecerão no menu suspenso do campo da fatura. Valores ativados podem ser selecionados pelos membros.',
- listInputSubtitle: 'Esses valores aparecerão na lista do campo da fatura. Valores ativados podem ser selecionados pelos membros.',
- emptyInvoiceFieldsValues: {
- title: 'Ainda não há valores de lista',
- subtitle: 'Adicione valores personalizados para aparecerem nas faturas.',
- },
- existingInvoiceFieldNameError: 'Já existe um campo da fatura com este nome',
- invoiceFieldNameRequiredError: 'Insira um nome para o campo da fatura',
- invoiceFieldTypeRequiredError: 'Escolha um tipo de campo da fatura',
- invoiceFieldInitialValueRequiredError: 'Escolha um valor inicial para o campo da fatura',
- addField: 'Adicionar campo',
- },
tags: {
tagName: 'Nome da tag',
requiresTag: 'Membros devem marcar todas as despesas',
@@ -6236,8 +6246,8 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
other: 'Tornar membros',
}),
makeAdmin: () => ({
- one: 'Tornar admin',
- other: 'Tornar admins',
+ one: 'Tornar admin do workspace',
+ other: 'Tornar admins do workspace',
}),
makeAuditor: () => ({
one: 'Tornar auditor',
@@ -6266,12 +6276,14 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) =>
`${memberName} tem um relatório pendente de processamento que requer ação. Peça para que concluam a ação necessária antes de removê-los do workspace.`,
allMembers: 'Todos os membros',
- admins: 'Administradores',
+ admins: 'Admins do workspace',
approvers: 'Aprovadores',
auditors: 'Auditores',
emptyRoleFilter: {title: 'Nenhum membro corresponde a este filtro', subtitle: 'Convide um membro ou altere o filtro acima.'},
configureHRSync: (providerName: string) => `Configurar a sincronização do ${providerName}.`,
syncWithHR: (providerName: string) => `Sincronizar com ${providerName}`,
+ makeCardAdmin: () => ({one: 'Tornar admin do cartão', other: 'Tornar administradores do cartão'}),
+ cardAdmins: 'Administradores de cartões',
},
card: {
getStartedIssuing: 'Comece emitindo seu primeiro cartão virtual ou físico.',
@@ -6687,6 +6699,10 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
}),
enableRate: 'Ativar tarifa',
status: 'Status',
+ statusActive: 'Ativo',
+ statusFuture: 'Futuro',
+ statusExpired: 'Expirado',
+ statusInactive: 'Inativo',
unit: 'Unidade',
taxFeatureNotEnabledMessage:
'Os impostos precisam estar ativados no workspace para usar este recurso. Vá até Mais recursos para fazer essa alteração. ',
@@ -6813,12 +6829,6 @@ Tem certeza de que deseja exportá-los novamente?`,
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Os campos de relatório estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} `,
},
- invoiceFields: {
- title: 'Campos de fatura',
- description: `Os campos da fatura permitem incluir detalhes extras no nível da fatura.`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Os campos da fatura estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `Aproveite a sincronização automática e reduza lançamentos manuais com a integração Expensify + NetSuite. Obtenha insights financeiros profundos e em tempo real com suporte a segmentos nativos e personalizados, incluindo mapeamento de projetos e clientes.`,
@@ -6919,12 +6929,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`Tarifas por distância estão disponíveis no plano Collect, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} `,
},
- auditor: {
- title: 'Auditor',
- description: 'Auditores têm acesso somente leitura a todos os relatórios para total visibilidade e monitoramento de conformidade.',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `Auditores estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: 'Múltiplos níveis de aprovação',
description:
@@ -7022,6 +7026,12 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`O faturamento está disponível nos planos Collect e Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} `,
},
+ controlPolicyRoles: {
+ title: 'Funções da política de controle',
+ description: 'Use funções especializadas como Auditor e Administrador de Cartão para conceder aos membros acesso apenas ao que precisam.',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `Funções especializadas no espaço de trabalho estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7348,6 +7358,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
deleteRuleConfirmation: 'Tem certeza de que quer excluir esta regra?',
describeRuleTitle: 'Descreva sua regra',
describeRuleSubtitle: 'Descreva sua regra e a Concierge vai criá-la',
+ disclaimer: 'Os agentes de IA podem cometer erros.',
},
},
planTypePage: {
@@ -8021,22 +8032,20 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
composeFromCards: ({content, cards}: {content: string; cards: string}) => `${content} de ${cards}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `alterou a alíquota de imposto padrão da categoria "${categoryName}" para "${newTax}" (antes "${oldTax}")`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `adicionou a taxa "${rateName}" de ${rateValue}`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) =>
`adicionou a taxa "${rateName}" de ${rateValue}, válida a partir de ${startDate}`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `adicionou a taxa "${rateName}" de ${rateValue}, válida até ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`adicionou a tarifa "${rateName}" de ${rateValue}, válida de ${startDate} a ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `atualizou a data de início da tarifa "${rateName}" para ${newDate} (antes ${oldDate})` : `definir data de início da tarifa "${rateName}" para ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `atualizou a data de término da tarifa "${rateName}" para ${newDate} (antes ${oldDate})` : `definir data de término da tarifa "${rateName}" para ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `atualizou a data de início e término da tarifa "${rateName}" para ${newStartDate} - ${newEndDate} (antes ${oldStartDate} - ${oldEndDate})`
- : `define a data de início e de término da tarifa "${rateName}" para ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `removeu a data de início da tarifa "${rateName}" (antes ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `removeu a data de término da tarifa "${rateName}" (anteriormente ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) =>
+ `atualizou a taxa de distância "${rateName}" para se aplicar a ${newDateRange} (anteriormente ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `de ${date}`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `até ${date}`,
+ customUnitRateDateRangeAllDates: () => `para todas as datas`,
},
roomMembersPage: {
memberNotFound: 'Membro não encontrado.',
@@ -8477,8 +8486,11 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
`A conexão de ${feedName} está interrompida. Para restaurar as importações do cartão, faça login no seu banco .`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`a conexão Plaid com a sua conta bancária empresarial foi interrompida. Por favor, reconecte sua conta bancária ${maskedAccountNumber} para continuar usando seus Cartões Expensify.`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} entrou pelo link de convite do workspace` : `adicionou ${email} como ${role === 'member' ? 'a' : 'um'} ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? 'um' : 'um';
+ return didJoinPolicy ? `${email} entrou pelo link de convite do workspace` : `adicionou ${email} como ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `atualizou a função de ${email} para ${newRole} (anteriormente ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9372,6 +9384,11 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
`),
notAllowedMessage: (accountOwnerEmail: string) =>
`Como copiloto de ${accountOwnerEmail}, você não tem permissão para realizar esta ação. Desculpe!`,
+ removeCopilotAccess: 'Remover meu acesso de copiloto',
+ removeCopilotAccessTitle: 'Remover acesso de copiloto?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) =>
+ `Tem certeza de que deseja remover seu acesso de copiloto à conta Expensify de ${delegatorName}? Esta ação não pode ser desfeita.`,
+ removeCopilotAccessConfirm: 'Remover acesso',
copilotAccess: 'Acesso ao Copilot',
},
debug: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 9bb7121b54b8..5d9c083b3282 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -40,6 +40,7 @@ import type {
OptionalParam,
PaidElsewhereParams,
ParentNavigationSummaryParams,
+ RemoveCopilotAccessConfirmationParams,
RemovedFromApprovalWorkflowParams,
ReportArchiveReasonsClosedParams,
ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams,
@@ -108,7 +109,9 @@ const translations: TranslationDeepObject = {
selectMultiple: '多选',
saveChanges: '保存更改',
submit: '提交',
+ markAsDone: '标记为完成',
submitted: '已提交',
+ markedAsDoneStatus: '已标记为完成',
rotate: '旋转',
zoom: '缩放',
password: '密码',
@@ -293,7 +296,6 @@ const translations: TranslationDeepObject = {
description: '描述',
title: '标题',
assignee: '受托人',
- createdBy: '创建者',
with: '与',
shareCode: '共享代码',
share: '分享',
@@ -314,6 +316,7 @@ const translations: TranslationDeepObject = {
merchant: '商户',
change: '更改',
category: '类别',
+ vendor: '供应商',
report: '报表',
billable: '可计费',
nonBillable: '不可计费',
@@ -355,6 +358,10 @@ const translations: TranslationDeepObject = {
subtitleText1: '使用以下方式查找聊天',
subtitleText2: '上方的按钮,或使用以下内容创建',
subtitleText3: '下方按钮。',
+ noUnreadChats: '没有未读聊天',
+ noTodos: '没有待办事项',
+ caughtUp: '你已经全部处理完了。干得好!',
+ seeAllChats: '查看所有聊天',
},
businessName: '公司名称',
clear: '清除',
@@ -831,6 +838,7 @@ const translations: TranslationDeepObject = {
beginningOfChatHistory: (users: string) => `此聊天对象为 ${users}。`,
beginningOfChatHistoryPolicyExpenseChat: (workspaceName: string, submitterDisplayName: string) =>
`这是 ${submitterDisplayName} 向 ${workspaceName} 提交报销的地方。只需使用“+”按钮即可。`,
+ beginningOfChatHistoryPolicyExpenseChatTrack: '在这里跟踪你的报销费用',
beginningOfChatHistorySelfDM: '这是你的个人空间。可在此记录笔记、任务、草稿和提醒事项。',
beginningOfChatHistorySystemDM: '欢迎!让我们帮你完成设置。',
chatWithAccountManager: '在这里与您的客户经理聊天',
@@ -928,7 +936,7 @@ const translations: TranslationDeepObject = {
defaultSubtitle: '工作区',
subtitle: ({policyName}: {policyName: string}) => `${policyName} > 会计`,
},
- validateAccount: {title: '验证您的账户以继续使用 Expensify', subtitle: '账户', cta: '验证'},
+ validateAccount: {title: '验证您的账户', subtitle: '账户', cta: '验证'},
fixFailedBilling: {title: '我们无法向您档案中的银行卡收费', subtitle: '订阅'},
unlockBankAccount: {
workspaceTitle: '您的企业银行账户已被锁定',
@@ -983,6 +991,7 @@ const translations: TranslationDeepObject = {
f1FlagsTitle: '全部完成',
f1FlagsDescription: '你已完成所有未完成的待办事项。',
},
+ reviewExpenses: ({count}: {count: number}) => `审核 ${count} ${count === 1 ? '费用' : '费用'}`,
},
upcomingTravel: '即将出行',
upcomingTravelSection: {
@@ -1306,6 +1315,7 @@ const translations: TranslationDeepObject = {
sendInvoice: (amount: string) => `发送 ${amount} 发票`,
expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `用于 ${comment}` : ''}`,
submitted: (memo?: string) => `已提交${memo ? `,备注为 ${memo}` : ''}`,
+ markedAsDone: (memo) => `标记为已完成${memo ? `,说明:${memo}` : ''}`,
automaticallySubmitted: `通过延迟提交 提交`,
queuedToSubmitViaDEW: '已排队,待通过自定义审批流程提交',
queuedToApproveViaDEW: '已排队,等待通过自定义审批流程批准',
@@ -1515,6 +1525,9 @@ const translations: TranslationDeepObject = {
removed: '已移除',
transactionPending: '交易处理中。',
chooseARate: '选择工作区每英里或每公里的报销费率',
+ rateValidDateRange: ({startDate, endDate}: {startDate: string; endDate: string}) => `${startDate} 至 ${endDate}`,
+ rateValidFrom: ({startDate}: {startDate: string}) => `有效期自 ${startDate}`,
+ rateValidUntil: ({endDate}: {endDate: string}) => `有效期至 ${endDate}`,
unapprove: '取消批准',
unapproveReport: '取消批准报销单',
headsUp: '注意!',
@@ -1613,6 +1626,7 @@ const translations: TranslationDeepObject = {
},
correctRateError: '修复费率错误后请重试。',
AskToExplain: `。说明 `,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge 已将此支出匹配到${vendorName} `,
duplicateNonDefaultWorkspacePerDiemError: '您无法在不同工作区之间复制每日津贴报销,因为各工作区的补贴标准可能不同。',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? '将该报销单标记为“可报销”' : '将该报销单标记为“不可报销”'),
@@ -1736,6 +1750,21 @@ const translations: TranslationDeepObject = {
return `正在等待管理员提交报销。`;
}
},
+ [CONST.NEXT_STEP.MESSAGE_KEY.WAITING_TO_MARK_AS_DONE]: (
+ actor: string,
+ actorType: ValueOf,
+ _eta?: string,
+ _etaType?: ValueOf,
+ ) => {
+ switch (actorType) {
+ case CONST.NEXT_STEP.ACTOR_TYPE.CURRENT_USER:
+ return `正在等待你 标记为完成。`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.OTHER_USER:
+ return `正在等待${actor} 标记为完成。`;
+ case CONST.NEXT_STEP.ACTOR_TYPE.UNSPECIFIED_ADMIN:
+ return `正在等待管理员标记为完成。`;
+ }
+ },
[CONST.NEXT_STEP.MESSAGE_KEY.NO_FURTHER_ACTION]: (
_actor: string,
_actorType: ValueOf,
@@ -2166,9 +2195,14 @@ const translations: TranslationDeepObject = {
lockAccountPage: {
reportSuspiciousActivity: '举报可疑活动',
lockAccount: '锁定账户',
+ lockMyAccount: '锁定我的账户',
unlockAccount: '解锁账户',
- compromisedDescription: '发现账户有异常?提交报告后将立即锁定账户、阻止新的 Expensify 卡交易,并禁止任何账户更改。',
- domainAdminsDescription: '针对域管理员:这也会暂停您所有域中的所有 Expensify 卡活动和管理员操作。',
+ findYourSituation: '大多数问题都不需要锁定你的账户!请在以下列表中找到与你相符的情况:',
+ lostCardOrCharges:
+ '卡片遗失或出现不明扣款 :请挂失您的卡片,并联系 Concierge 申诉未知交易。',
+ unauthorizedAccess:
+ '未经授权的账号访问 :请在下方锁定你的账号。这将阻止新的 Expensify 卡交易、卡片申请和账号更改。如果你是域管理员,这也会暂停整个域的所有卡片活动和管理员操作。',
+ securityTeamFollowUp: '锁定后,我们的安全团队将通过 risk@expensify.com 与您后续联系。',
areYouSure: '你确定要锁定你的 Expensify 账户吗?',
onceLocked: '一旦被锁定,您的账户将受到限制,直至提交解锁请求并完成安全审查',
unlockTitle: '我们已收到您的请求',
@@ -2521,13 +2555,10 @@ ${amount},商户:${merchant} - 日期:${date}`,
accessibilityLabel: ({members, approvers}: {members: string; approvers: string}) => `来自${members}的报销,审批人是${approvers}`,
addApprovalButton: '添加审批工作流',
editWorkflowAction: '编辑',
- addAgentAction: '添加代理',
findWorkflow: '查找工作流',
addApprovalTip: '除非存在更具体的工作流程,否则此默认工作流程适用于所有成员。',
approver: '审批人',
addApprovalsDescription: '在付款前需要额外审批。',
- automateApprovalsWithAgentsTitle: '使用代理自动化审批',
- automateApprovalsWithAgentsSubtitle: '在下方添加代理到工作流程中以自动化审批。',
makeOrTrackPaymentsTitle: '付款',
makeOrTrackPaymentsDescription: '为在 Expensify 中进行的付款添加授权付款人,或跟踪在其他地方进行的付款。',
customApprovalWorkflowEnabled:
@@ -2815,6 +2846,11 @@ ${amount},商户:${merchant} - 日期:${date}`,
},
},
},
+ focusModeUpdateModal: {
+ title: '欢迎使用 #focus 模式!',
+ prompt: (priorityModePageUrl: string) => `通过只查看未读聊天或需要你关注的聊天,随时掌握最新进展。别担心,你可以随时在设置 中更改此项。`,
+ },
+ inboxTabs: {all: '全部', todo: '待办事项', unread: '未读'},
reportDetailsPage: {
inWorkspace: (policyName: string) => `在 ${policyName} 中`,
generatingPDF: '生成 PDF',
@@ -3372,10 +3408,6 @@ ${amount},商户:${merchant} - 日期:${date}`,
year: '年份',
selectYear: '请选择年份',
},
- focusModeUpdateModal: {
- title: '欢迎进入 #focus 模式!',
- prompt: (priorityModePageUrl: string) => `通过仅查看未读聊天或需要你关注的聊天来随时掌握进展。别担心,你可以随时在设置 中更改此项。`,
- },
notFound: {
chatYouLookingForCannotBeFound: '找不到您要查找的聊天。',
getMeOutOfHere: '带我离开这里',
@@ -4024,27 +4056,30 @@ ${amount},商户:${merchant} - 日期:${date}`,
verificationFailed: '验证失败,因此我们需要额外的文件来验证你及你的企业',
taxIDVerification: '税务识别号验证',
taxIDVerificationDescription: dedent(`
- 请上传以下任一文件:
- • IRS TIN/EIN 分配函
- • IRS TIN/EIN 申请确认函(通常包含“Congratulations! The EIN has been successfully assigned”)
- • 显示公司名称和 EIN 的 IRS 免税函`),
+ 请上传以下任一文件:
+ • IRS TIN/EIN 分配函
+ • IRS TIN/EIN 申请确认函(通常包含“Congratulations! The EIN has been successfully assigned”)
+ • 显示公司名称和 EIN 的 IRS 免税函
+ `),
nameChangeDocument: '名称变更文件',
nameChangeDocumentDescription: '如果你的公司名称在申请 TIN/EIN 后发生更改,我们需要此文件来验证你提供的税务识别号',
companyAddressVerification: '公司地址验证',
companyAddressVerificationDescription: dedent(`
- 请上传以下任一文件:
- • 显示公司名称和地址的近期水电账单
- • 显示公司名称和地址的银行对账单
- • 包含签字页的有效租赁协议,显示公司名称和当前地址
- • 显示公司名称和地址的保险声明
- • 显示公司名称和地址的 TIN 分配文件`),
+ 请上传以下任一文件:
+ • 显示公司名称和地址的近期水电账单
+ • 显示公司名称和地址的银行对账单
+ • 包含签字页的有效租赁协议,显示公司名称和当前地址
+ • 显示公司名称和地址的保险声明
+ • 显示公司名称和地址的 TIN 分配文件
+ `),
userAddressVerification: '地址验证',
userAddressVerificationDescription: dedent(`
- 请上传以下任一文件:
- • 选民登记卡
- • 驾驶证
- • 银行对账单
- • 水电账单`),
+ 请上传以下任一文件:
+ • 选民登记卡
+ • 驾驶证
+ • 银行对账单
+ • 水电账单
+ `),
userDOBVerification: '出生日期验证',
userDOBVerificationDescription: '请上传美国签发的身份证件',
finishViaChat: '通过聊天完成',
@@ -4221,7 +4256,6 @@ ${amount},商户:${merchant} - 日期:${date}`,
customFieldHint: '为该成员的所有支出添加适用的自定义编码。',
reports: '报表',
reportFields: '报表字段',
- invoiceFields: '发票字段',
reportTitle: '报表标题',
reportField: '报表字段',
taxes: '税费',
@@ -4304,15 +4338,15 @@ ${amount},商户:${merchant} - 日期:${date}`,
roleName: (role?: string) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
- return '管理员';
+ return '工作区管理员';
case CONST.POLICY.ROLE.AUDITOR:
return '审计员';
case CONST.POLICY.ROLE.EDITOR:
- return '编辑';
+ return '编辑器';
case CONST.POLICY.ROLE.CARD_ADMIN:
return '卡片管理员';
case CONST.POLICY.ROLE.PEOPLE_ADMIN:
- return '人员管理';
+ return '人员管理员';
case CONST.POLICY.ROLE.PAYMENTS_ADMIN:
return '付款管理员';
case CONST.POLICY.ROLE.USER:
@@ -5837,29 +5871,6 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
reportFieldInitialValueRequiredError: '请选择报表字段的初始值',
genericFailureMessage: '更新报表字段时出错。请重试。',
},
- invoiceFields: {
- subtitle: '当你想添加更多信息时,发票字段会很有帮助。',
- importedFromAccountingSoftware: '以下发票字段是从你的',
- disableInvoiceFields: '禁用发票字段',
- disableInvoiceFieldsConfirmation: '确定吗?发票字段将在发票中被禁用。',
- delete: '删除发票字段',
- deleteConfirmation: '确定要删除此发票字段吗?',
- findInvoiceField: '查找发票字段',
- nameInputSubtitle: '为发票字段选择一个名称。',
- typeInputSubtitle: '选择要使用的发票字段类型。',
- initialValueInputSubtitle: '输入要在发票字段中显示的起始值。',
- listValuesInputSubtitle: '这些值将显示在你的发票字段下拉菜单中。成员可以选择已启用的值。',
- listInputSubtitle: '这些值将显示在你的发票字段列表中。成员可以选择已启用的值。',
- emptyInvoiceFieldsValues: {
- title: '还没有列表值',
- subtitle: '添加要显示在发票上的自定义值。',
- },
- existingInvoiceFieldNameError: '已存在同名发票字段',
- invoiceFieldNameRequiredError: '请输入发票字段名称',
- invoiceFieldTypeRequiredError: '请选择发票字段类型',
- invoiceFieldInitialValueRequiredError: '请选择发票字段的初始值',
- addField: '添加字段',
- },
tags: {
tagName: '标签名称',
requiresTag: '成员必须为所有报销添加标签',
@@ -6080,8 +6091,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
other: '设为成员',
}),
makeAdmin: () => ({
- one: '设为管理员',
- other: '设为管理员',
+ one: '设为工作区管理员',
+ other: '设为工作区管理员',
}),
makeAuditor: () => ({
one: '设为审计员',
@@ -6106,12 +6117,14 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
`如果您将 ${memberName} 从此工作区中移除,我们会将其技术联系人替换为工作区所有者 ${workspaceOwner}。`,
cannotRemoveUserDueToReport: ({memberName}: {memberName: string}) => `${memberName} 还有一份待处理报告需要处理。请在将其从工作区中移除之前,先让 TA 完成所需操作。`,
allMembers: '所有成员',
- admins: '管理员',
+ admins: '工作区管理员',
approvers: '审批人',
auditors: '审计员',
emptyRoleFilter: {title: '没有成员符合此筛选条件', subtitle: '邀请成员或更改上方的筛选条件。'},
configureHRSync: (providerName: string) => `配置 ${providerName} 同步。`,
syncWithHR: (providerName: string) => `与 ${providerName} 同步`,
+ makeCardAdmin: () => ({one: '设为卡片管理员', other: '设为卡管理员'}),
+ cardAdmins: '卡片管理员',
},
card: {
getStartedIssuing: '从发放您的第一张虚拟卡或实体卡开始使用。',
@@ -6522,6 +6535,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
}),
enableRate: '启用费率',
status: '状态',
+ statusActive: '活跃',
+ statusFuture: '未来',
+ statusExpired: '已过期',
+ statusInactive: '未启用',
unit: '单位',
taxFeatureNotEnabledMessage: '必须在工作区中启用税费才能使用此功能。前往更多功能 进行更改。 ',
deleteDistanceRate: '删除距离费率',
@@ -6640,12 +6657,6 @@ ${reportName}
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`报表字段仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`} `,
},
- invoiceFields: {
- title: '发票字段',
- description: `发票字段可用于在发票上包含额外的发票级别详情。`,
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `发票字段仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`} `,
- },
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {
title: 'NetSuite',
description: `通过 Expensify + NetSuite 集成实现自动同步,减少手动录入。借助对原生和自定义维度的支持(包括项目和客户映射),获取深入的实时财务洞察。`,
@@ -6745,12 +6756,6 @@ ${reportName}
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`里程费率适用于 Collect 方案,起始价格为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`} `,
},
- auditor: {
- title: '审计员',
- description: '审计员将获得所有报表的只读访问权限,以实现全面可见性和合规监控。',
- onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
- `审核员仅适用于 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`} `,
- },
[CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
title: '多级审批',
description: '多级审批是一个工作流程工具,适用于在报销前需要多个人批准报销单的公司。',
@@ -6844,6 +6849,12 @@ ${reportName}
onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
`开票功能适用于 Collect 和 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`} `,
},
+ controlPolicyRoles: {
+ title: '控制策略角色',
+ description: '使用审计员、卡片管理员等专门角色,只授予成员完成工作所需的访问权限。',
+ onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) =>
+ `专用工作区角色仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`} `,
+ },
},
downgrade: {
commonFeatures: {
@@ -7159,6 +7170,7 @@ ${reportName}
deleteRuleConfirmation: '确定要删除此规则吗?',
describeRuleTitle: '描述你的规则',
describeRuleSubtitle: '描述你的规则,我们会由 Concierge 为你创建',
+ disclaimer: 'AI 智能体可能会犯错。',
},
},
planTypePage: {
@@ -7812,21 +7824,18 @@ ${reportName}
composeFromCards: ({content, cards}: {content: string; cards: string}) => `来自 ${cards} 的 ${content}`,
},
},
+ updatedCategoryTaxRate: ({categoryName, oldTax, newTax}: {categoryName: string; oldTax: string; newTax: string}) =>
+ `将“${categoryName}”类别的默认税率更改为“${newTax}”(之前为“${oldTax}”)`,
addCustomUnitRateWithAmount: (rateName: string, rateValue: string) => `已添加“${rateName}”汇率,数值为 ${rateValue}`,
addCustomUnitRateWithAmountAndStartDate: (rateName: string, rateValue: string, startDate: string) => `已添加“${rateName}”费率 ${rateValue},自 ${startDate} 起生效`,
addCustomUnitRateWithAmountAndEndDate: (rateName: string, rateValue: string, endDate: string) => `已添加“${rateName}”费率 ${rateValue},有效期至 ${endDate}`,
addCustomUnitRateWithAmountAndDates: (rateName: string, rateValue: string, startDate: string, endDate: string) =>
`已添加“${rateName}”费率 ${rateValue},有效期为 ${startDate} - ${endDate}`,
- updatedCustomUnitRateStartDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `将“${rateName}”费率的开始日期更新为 ${newDate}(之前为 ${oldDate})` : `将“${rateName}”费率的开始日期设置为 ${newDate}`,
- updatedCustomUnitRateEndDate: (rateName: string, newDate: string, oldDate?: string) =>
- oldDate ? `已将“${rateName}”费率的结束日期更新为 ${newDate}(之前为 ${oldDate})` : `将“${rateName}”费率的结束日期设置为 ${newDate}`,
- updatedCustomUnitRateStartAndEndDate: (rateName: string, newStartDate: string, newEndDate: string, oldStartDate?: string, oldEndDate?: string) =>
- oldStartDate && oldEndDate
- ? `已将“${rateName}”费率的起止日期更新为 ${newStartDate} - ${newEndDate}(此前为 ${oldStartDate} - ${oldEndDate})`
- : `将“${rateName}”费率的开始和结束日期设为 ${newStartDate} - ${newEndDate}`,
- removedCustomUnitRateStartDate: (rateName: string, oldDate: string) => `已从“${rateName}”费率中移除开始日期(原为 ${oldDate})`,
- removedCustomUnitRateEndDate: (rateName: string, oldDate: string) => `已从“${rateName}”费率中移除结束日期(之前为 ${oldDate})`,
+ updatedCustomUnitRateDateRange: (rateName: string, newDateRange: string, oldDateRange: string) => `已将距离费率“${rateName}”更新为适用于 ${newDateRange}(此前为 ${oldDateRange})`,
+ customUnitRateDateRangeStartToEnd: (startDate: string, endDate: string) => `${startDate} - ${endDate}`,
+ customUnitRateDateRangeFrom: (date: string) => `自 ${date} 起`,
+ customUnitRateDateRangeUntilEnd: (date: string) => `直到 ${date}`,
+ customUnitRateDateRangeAllDates: () => `适用于所有日期`,
},
roomMembersPage: {
memberNotFound: '未找到成员。',
@@ -8247,8 +8256,11 @@ ${reportName}
`${feedName} 连接已中断。要恢复银行卡导入,请登录您的银行账户 。`,
plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
`您与企业银行账户的 Plaid 连接已中断。请重新连接您的银行账户 ${maskedAccountNumber} ,以便继续使用 Expensify 卡。`,
- addEmployee: (email: string, role: string, didJoinPolicy?: boolean) =>
- didJoinPolicy ? `${email} 通过工作区邀请链接加入` : `已将 ${email} 添加为 ${role === 'member' ? '一个' : '一个'} 的 ${role}`,
+ addEmployee: (email: string, role: string, didJoinPolicy?: boolean) => {
+ const translatedRole = String(translations.workspace.common.roleName(role)).toLowerCase();
+ const article = role === CONST.POLICY.ROLE.AUDITOR ? '一个' : 'a';
+ return didJoinPolicy ? `${email} 通过工作区邀请链接加入` : `已将 ${email} 添加为 ${article} ${translatedRole}`;
+ },
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `已将 ${email} 的角色更新为 ${newRole}(先前为 ${currentRole})`,
updatedCustomField1: (email: string, newValue: string, previousValue: string) => {
if (!newValue) {
@@ -9121,6 +9133,10 @@ ${reportName}
作为副驾驶,你无权访问此页面。抱歉!
`),
notAllowedMessage: (accountOwnerEmail: string) => `作为${accountOwnerEmail}的副驾驶 ,你没有权限执行此操作。抱歉!`,
+ removeCopilotAccess: '移除我的副驾驶访问权限',
+ removeCopilotAccessTitle: '移除副驾驶访问权限?',
+ removeCopilotAccessConfirmation: ({delegatorName}: RemoveCopilotAccessConfirmationParams) => `您确定要移除对${delegatorName}的 Expensify 账户的副驾驶访问权限吗?此操作无法撤销。`,
+ removeCopilotAccessConfirm: '移除访问权限',
copilotAccess: 'Copilot 访问',
},
debug: {
diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts
deleted file mode 100644
index fa22805f4e71..000000000000
--- a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type CreateWorkspaceInvoiceFieldListValueParams = {
- policyID: string;
- /**
- * Stringified JSON object with type of following structure:
- * Array
- */
- invoiceFields: string;
-};
-
-export default CreateWorkspaceInvoiceFieldListValueParams;
diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts
deleted file mode 100644
index 5cce12e49273..000000000000
--- a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type CreateWorkspaceInvoiceFieldParams = {
- policyID: string;
- /**
- * Stringified JSON object with type of following structure:
- * Array
- */
- invoiceFields: string;
-};
-
-export default CreateWorkspaceInvoiceFieldParams;
diff --git a/src/libs/API/parameters/DeletePolicyInvoiceField.ts b/src/libs/API/parameters/DeletePolicyInvoiceField.ts
deleted file mode 100644
index 5b3c37fb1e07..000000000000
--- a/src/libs/API/parameters/DeletePolicyInvoiceField.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type DeletePolicyInvoiceField = {
- policyID: string;
- /**
- * Stringified JSON object with type of following structure:
- * Array
- */
- invoiceFields: string;
-};
-
-export default DeletePolicyInvoiceField;
diff --git a/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts b/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts
deleted file mode 100644
index 5afc5c912d90..000000000000
--- a/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-type EnablePolicyInvoiceFieldsParams = {
- policyID: string;
- enabled: boolean;
-};
-
-export default EnablePolicyInvoiceFieldsParams;
diff --git a/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts
deleted file mode 100644
index a52405efad54..000000000000
--- a/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type EnableWorkspaceInvoiceFieldListValueParams = {
- policyID: string;
- /**
- * Stringified JSON object with type of following structure:
- * Array
- */
- invoiceFields: string;
-};
-
-export default EnableWorkspaceInvoiceFieldListValueParams;
diff --git a/src/libs/API/parameters/RemoveDelegatorParams.ts b/src/libs/API/parameters/RemoveDelegatorParams.ts
new file mode 100644
index 000000000000..81e6f0441dd3
--- /dev/null
+++ b/src/libs/API/parameters/RemoveDelegatorParams.ts
@@ -0,0 +1,5 @@
+type RemoveDelegatorParams = {
+ delegatorEmail: string;
+};
+
+export default RemoveDelegatorParams;
diff --git a/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts
deleted file mode 100644
index c793b4bdf10a..000000000000
--- a/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type RemoveWorkspaceInvoiceFieldListValueParams = {
- policyID: string;
- /**
- * Stringified JSON object with type of following structure:
- * Array
- */
- invoiceFields: string;
-};
-
-export default RemoveWorkspaceInvoiceFieldListValueParams;
diff --git a/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts b/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts
deleted file mode 100644
index eeb3cd5106aa..000000000000
--- a/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-type UpdateWorkspaceInvoiceFieldInitialValueParams = {
- policyID: string;
- /**
- * Stringified JSON object with type of following structure:
- * Array
- */
- invoiceFields: string;
-};
-
-export default UpdateWorkspaceInvoiceFieldInitialValueParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index ea8c05a42cde..d56b78063826 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -259,7 +259,6 @@ export type {default as SetPolicyTravelSettingsParams} from './SetPolicyTravelSe
export type {default as EnablePolicyTagsParams} from './EnablePolicyTagsParams';
export type {default as SetPolicyTagsEnabled} from './SetPolicyTagsEnabled';
export type {default as EnablePolicyWorkflowsParams} from './EnablePolicyWorkflowsParams';
-export type {default as EnablePolicyInvoiceFieldsParams} from './EnablePolicyInvoiceFieldsParams';
export type {default as EnablePolicyReportFieldsParams} from './EnablePolicyReportFieldsParams';
export type {default as EnablePolicyExpensifyCardsParams} from './EnablePolicyExpensifyCardsParams';
export type {default as UpdateGustoApprovalModeParams} from './UpdateGustoApprovalModeParams';
@@ -338,21 +337,15 @@ export type {default as DowngradeToTeamParams} from './DowngradeToTeamParams';
export type {default as RejectMoneyRequestInBulkParams} from './RejectMoneyRequestInBulkParams';
export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyRequestOnSearchParams';
export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams';
-export type {default as DeletePolicyInvoiceField} from './DeletePolicyInvoiceField';
export type {default as DeletePolicyReportField} from './DeletePolicyReportField';
export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams';
export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams';
-export type {default as CreateWorkspaceInvoiceFieldParams} from './CreateWorkspaceInvoiceFieldParams';
export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams';
-export type {default as UpdateWorkspaceInvoiceFieldInitialValueParams} from './UpdateWorkspaceInvoiceFieldInitialValueParams';
export type {default as EnableWorkspaceReportFieldListValueParams} from './EnableWorkspaceReportFieldListValueParams';
-export type {default as EnableWorkspaceInvoiceFieldListValueParams} from './EnableWorkspaceInvoiceFieldListValueParams';
export type {default as EnablePolicyInvoicingParams} from './EnablePolicyInvoicingParams';
export type {default as EnablePolicyTimeTrackingParams} from './EnablePolicyTimeTrackingParams';
export type {default as CreateWorkspaceReportFieldListValueParams} from './CreateWorkspaceReportFieldListValueParams';
-export type {default as CreateWorkspaceInvoiceFieldListValueParams} from './CreateWorkspaceInvoiceFieldListValueParams';
export type {default as RemoveWorkspaceReportFieldListValueParams} from './RemoveWorkspaceReportFieldListValueParams';
-export type {default as RemoveWorkspaceInvoiceFieldListValueParams} from './RemoveWorkspaceInvoiceFieldListValueParams';
export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams';
export type {default as OpenPolicyTravelPageParams} from './OpenPolicyTravelPageParams';
export type {default as OpenPolicyEditCardLimitTypePageParams} from './OpenPolicyEditCardLimitTypePageParams';
@@ -411,6 +404,7 @@ export type {default as SetCardReconciliationBankAccountParams} from './SetCardR
export type {default as CardDeactivateParams} from './CardDeactivateParams';
export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams';
export type {default as RemoveDelegateParams} from './RemoveDelegateParams';
+export type {default as RemoveDelegatorParams} from './RemoveDelegatorParams';
export type {default as SetPolicyTagApproverParams} from './SetPolicyTagApproverParams';
export type {default as SaveSearchParams} from './SaveSearch';
export type {default as DeleteSavedSearchParams} from './DeleteSavedSearch';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 0bdcd9ff6228..973435cea37a 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -193,7 +193,6 @@ const WRITE_COMMANDS = {
UPDATE_POLICY_CATEGORY_GL_CODE: 'UpdatePolicyCategoryGLCode',
DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories',
DELETE_POLICY_REPORT_FIELD: 'DeletePolicyReportField',
- DELETE_POLICY_INVOICE_FIELD: 'DeletePolicyInvoiceField',
SET_POLICY_TAGS_REQUIRED: 'SetPolicyTagsRequired',
SET_POLICY_TAG_LISTS_REQUIRED: 'SetPolicyTagListsRequired',
SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag',
@@ -271,7 +270,6 @@ const WRITE_COMMANDS = {
ENABLE_POLICY_TAXES: 'EnablePolicyTaxes',
ENABLE_POLICY_WORKFLOWS: 'EnablePolicyWorkflows',
ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields',
- ENABLE_POLICY_INVOICE_FIELDS: 'EnablePolicyInvoiceFields',
ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards',
TOGGLE_POLICY_PER_DIEM: 'TogglePolicyPerDiem',
ENABLE_POLICY_COMPANY_CARDS: 'EnablePolicyCompanyCards',
@@ -411,14 +409,9 @@ const WRITE_COMMANDS = {
OPEN_SIDE_PANEL: 'OpenSidePanel',
CLOSE_SIDE_PANEL: 'CloseSidePanel',
UPDATE_NETSUITE_SUBSIDIARY: 'UpdateNetSuiteSubsidiary',
- CREATE_WORKSPACE_INVOICE_FIELD: 'CreatePolicyInvoiceField',
- CREATE_WORKSPACE_INVOICE_FIELD_LIST_VALUE: 'CreatePolicyInvoiceFieldOption',
CREATE_WORKSPACE_REPORT_FIELD: 'CreatePolicyReportField',
- UPDATE_WORKSPACE_INVOICE_FIELD_INITIAL_VALUE: 'SetPolicyInvoiceFieldDefault',
UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE: 'SetPolicyReportFieldDefault',
- ENABLE_WORKSPACE_INVOICE_FIELD_LIST_VALUE: 'EnablePolicyInvoiceFieldOption',
ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'EnablePolicyReportFieldOption',
- REMOVE_WORKSPACE_INVOICE_FIELD_LIST_VALUE: 'RemovePolicyInvoiceFieldOption',
CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'CreatePolicyReportFieldOption',
REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'RemovePolicyReportFieldOption',
UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION: 'UpdateNetSuiteSyncTaxConfiguration',
@@ -527,6 +520,7 @@ const WRITE_COMMANDS = {
QUEUE_EXPENSIFY_CARD_FOR_BILLING: 'Domain_QueueExpensifyCardForBilling',
ADD_DELEGATE: 'AddDelegate',
REMOVE_DELEGATE: 'RemoveDelegate',
+ REMOVE_DELEGATOR: 'RemoveDelegator',
UPDATE_DELEGATE_ROLE: 'UpdateDelegateRole',
TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation',
TOGGLE_CARD_CASHBACK_TO_BILL: 'ToggleCardCashbackToBill',
@@ -821,7 +815,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE]: Parameters.UpdatePolicyCategoryPayrollCodeParams;
[WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE]: Parameters.UpdatePolicyCategoryGLCodeParams;
[WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField;
- [WRITE_COMMANDS.DELETE_POLICY_INVOICE_FIELD]: Parameters.DeletePolicyInvoiceField;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
[WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired;
[WRITE_COMMANDS.SET_POLICY_TAG_LISTS_REQUIRED]: Parameters.SetPolicyTagListsRequired;
@@ -918,7 +911,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ENABLE_POLICY_TAXES]: Parameters.EnablePolicyTaxesParams;
[WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS]: Parameters.EnablePolicyWorkflowsParams;
[WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams;
- [WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS]: Parameters.EnablePolicyInvoiceFieldsParams;
[WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams;
[WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM]: Parameters.TogglePolicyPerDiemParams;
[WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS]: Parameters.EnablePolicyCompanyCardsParams;
@@ -1119,6 +1111,11 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_REPORT_EXPORT_STATUS]: Parameters.UpdateFinancialForceGenericTypeParams<'exportStatus', FinancialForceReportExportStatus>;
[WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_AUTO_SYNC]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_SYNC_REIMBURSED_REPORTS]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_PARENT_TAG_MAPPING]: Parameters.UpdateFinancialForceGenericTypeParams<'parentTagMapping', ValueOf>;
+ [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_SYNC_MILESTONES]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_TAX_NON_BILLABLE]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_EXPORT_FOREIGN_CURRENCY]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
+ [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_COMPANY]: Parameters.UpdateFinancialForceGenericTypeParams<'companyID', string>;
[WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_TAX_NON_BILLABLE]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_EXPORT_FOREIGN_CURRENCY]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>;
@@ -1131,15 +1128,10 @@ type WriteCommandParameters = {
// Workspace report field parameters
[WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD]: Parameters.CreateWorkspaceReportFieldParams;
- [WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD]: Parameters.CreateWorkspaceInvoiceFieldParams;
[WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE]: Parameters.UpdateWorkspaceReportFieldInitialValueParams;
- [WRITE_COMMANDS.UPDATE_WORKSPACE_INVOICE_FIELD_INITIAL_VALUE]: Parameters.UpdateWorkspaceInvoiceFieldInitialValueParams;
[WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.EnableWorkspaceReportFieldListValueParams;
- [WRITE_COMMANDS.ENABLE_WORKSPACE_INVOICE_FIELD_LIST_VALUE]: Parameters.EnableWorkspaceInvoiceFieldListValueParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.CreateWorkspaceReportFieldListValueParams;
- [WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD_LIST_VALUE]: Parameters.CreateWorkspaceInvoiceFieldListValueParams;
[WRITE_COMMANDS.REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.RemoveWorkspaceReportFieldListValueParams;
- [WRITE_COMMANDS.REMOVE_WORKSPACE_INVOICE_FIELD_LIST_VALUE]: Parameters.RemoveWorkspaceInvoiceFieldListValueParams;
[WRITE_COMMANDS.UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_TAX_SOLUTION_ID]: Parameters.UpdateNetSuiteGenericTypeParams<'taxSolutionID', string>;
@@ -1213,6 +1205,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.ADD_DELEGATE]: Parameters.AddDelegateParams;
[WRITE_COMMANDS.UPDATE_DELEGATE_ROLE]: Parameters.UpdateDelegateRoleParams;
[WRITE_COMMANDS.REMOVE_DELEGATE]: Parameters.RemoveDelegateParams;
+ [WRITE_COMMANDS.REMOVE_DELEGATOR]: Parameters.RemoveDelegatorParams;
[WRITE_COMMANDS.TOGGLE_CARD_CONTINUOUS_RECONCILIATION]: Parameters.ToggleCardContinuousReconciliationParams;
[WRITE_COMMANDS.TOGGLE_CARD_CASHBACK_TO_BILL]: Parameters.ToggleCardCashbackToBillParams;
[WRITE_COMMANDS.SET_CARD_RECONCILIATION_BANK_ACCOUNT]: Parameters.SetCardReconciliationBankAccountParams;
@@ -1369,7 +1362,6 @@ const READ_COMMANDS = {
OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage',
OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage',
OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage',
- OPEN_POLICY_INVOICES_PAGE: 'OpenPolicyInvoicesPage',
OPEN_POLICY_RULES_PAGE: 'OpenPolicyRulesPage',
OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage',
OPEN_POLICY_TRAVEL_PAGE: 'OpenPolicyTravelPage',
@@ -1474,7 +1466,6 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams;
[READ_COMMANDS.OPEN_POLICY_TAXES_PAGE]: Parameters.OpenPolicyTaxesPageParams;
[READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams;
- [READ_COMMANDS.OPEN_POLICY_INVOICES_PAGE]: Parameters.OpenPolicyReportFieldsPageParams;
[READ_COMMANDS.OPEN_POLICY_RULES_PAGE]: Parameters.OpenPolicyRulesPageParams;
[READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams;
[READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams;
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index fb1d4e173eb0..a0288d2847c6 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -1,3 +1,4 @@
+import {format, parseISO} from 'date-fns';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {CurrencyListActionsContextType} from '@components/CurrencyListContextProvider';
@@ -635,6 +636,29 @@ function prepareTextForDisplay(text: string): string {
return text.replaceAll(/[^0-9., ]/g, '').replace(/^0+(?=\d)/, '');
}
+function getRateDateLabel(rate: MileageRate, translate: LocaleContextProps['translate']): string {
+ const dateFormat = CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT;
+
+ try {
+ if (rate.startDate && rate.endDate) {
+ return translate('iou.rateValidDateRange', {
+ startDate: format(parseISO(rate.startDate), dateFormat),
+ endDate: format(parseISO(rate.endDate), dateFormat),
+ });
+ }
+ if (rate.startDate) {
+ return translate('iou.rateValidFrom', {startDate: format(parseISO(rate.startDate), dateFormat)});
+ }
+ if (rate.endDate) {
+ return translate('iou.rateValidUntil', {endDate: format(parseISO(rate.endDate), dateFormat)});
+ }
+ } catch {
+ return '';
+ }
+
+ return '';
+}
+
export default {
getDefaultMileageRate,
getDistanceMerchant,
@@ -659,6 +683,7 @@ export default {
prepareTextForDisplay,
isRateEligibleForDate,
getBestEligibleRate,
+ getRateDateLabel,
};
export type {MileageRate};
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 61d0b20ebf17..ee7ac77b3720 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -15,7 +15,7 @@ import {formatList} from './Localize';
import Log from './Log';
import Parser from './Parser';
import {getPersonalDetailByEmail} from './PersonalDetailsUtils';
-import {getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, getSortedTagKeys, isPolicyAdmin} from './PolicyUtils';
+import {getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, getQBOVendorByID, getSortedTagKeys, isPolicyAdmin} from './PolicyUtils';
import {getOriginalMessage, isModifiedExpenseAction} from './ReportActionsUtils';
// This cycle import is safe because ReportNameUtils was extracted from ReportUtils to separate report name computation logic.
// The functions imported here are pure utility functions that don't create initialization-time dependencies.
@@ -448,6 +448,36 @@ function getForReportAction({
buildMessageFragmentForValue(translate, newReimbursable, oldReimbursable, translate('iou.expense'), true, setFragments, removalFragments, changeFragments);
}
+ // Onyx applies updates with `shouldRemoveNestedNulls: true`, so when we build an optimistic
+ // MODIFIED_EXPENSE for a vendor "set" (no prior, `oldVendor: null`) or "remove" (no new,
+ // `vendor: null`), the null-valued key is stripped on merge and only one of the two keys
+ // survives in storage. Treat either key's presence as a vendor modification — requiring both
+ // would let "add" and "remove" cases fall through to the generic "changed the expense"
+ // fallback.
+ const hasModifiedVendor = isReportActionOriginalMessageAnObject && ('oldVendor' in reportActionOriginalMessage || 'vendor' in reportActionOriginalMessage);
+ if (hasModifiedVendor) {
+ // Vendor is stored on the action as `{externalID, isManuallySet}` (or absent/null). Resolve
+ // the display name from the policy's QBO vendor list; if the vendor has since been removed
+ // from QBO the name is unrecoverable, so fall back to the externalID so the fragment still
+ // identifies which vendor was set rather than rendering `set vendor ""`.
+ const resolveVendorName = (entry: typeof reportActionOriginalMessage.vendor): string => {
+ if (!entry?.externalID) {
+ return '';
+ }
+ return getQBOVendorByID(policy, entry.externalID)?.name ?? entry.externalID;
+ };
+ buildMessageFragmentForValue(
+ translate,
+ resolveVendorName(reportActionOriginalMessage?.vendor),
+ resolveVendorName(reportActionOriginalMessage?.oldVendor),
+ translate('common.vendor'),
+ true,
+ setFragments,
+ removalFragments,
+ changeFragments,
+ );
+ }
+
const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'newAttendees' in reportActionOriginalMessage;
if (hasModifiedAttendees) {
const [oldAttendees, attendees] = getFormattedAttendees(reportActionOriginalMessage.newAttendees, reportActionOriginalMessage.oldAttendees);
diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
index f953488083a9..7eb752147fe0 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
@@ -19,7 +19,6 @@ import {getReportIDFromLink} from '@libs/ReportUtils';
import * as SessionUtils from '@libs/SessionUtils';
import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans';
import {getSearchParamFromUrl} from '@libs/Url';
-import {openAgentsPage} from '@userActions/Agent';
import * as App from '@userActions/App';
import * as Download from '@userActions/Download';
import {clearStaleExportDownloads} from '@userActions/Export';
@@ -132,11 +131,6 @@ function AuthScreensInitHandler() {
App.reconnectApp(initialLastUpdateIDAppliedToClient);
}
- // Hydrate the user's custom-agent prompts so AgentZeroStatusProvider can recognize
- // custom-agent chats opened directly from a deep link or right after sign-in, without
- // requiring a prior visit to Settings > Agents.
- openAgentsPage();
-
App.setUpPoliciesAndNavigate(
session,
introSelected,
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 6a86b048a5dc..668a25069717 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -174,10 +174,11 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepConfirmation').default,
[SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: () => require('../../../../pages/iou/request/step/MoneyRequestStepConfirmationVerifyAccountPage').default,
[SCREENS.MONEY_REQUEST.STEP_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepAmount').default,
- [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TAX_AMOUNT]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepTaxAmountPage').default,
- [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_TAX_RATE]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepTaxRatePage').default,
- [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_CATEGORY]: () => require('../../../../pages/iou/request/step/DynamicIOURequestStepCategoryPage').default,
+ [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxAmountPage').default,
+ [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: () => require('../../../../pages/iou/request/step/IOURequestStepTaxRatePage').default,
+ [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: () => require('../../../../pages/iou/request/step/IOURequestStepCategory').default,
[SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE]: () => require('../../../../pages/iou/request/step/IOURequestStepCategoryCreate').default,
+ [SCREENS.MONEY_REQUEST.STEP_VENDOR]: () => require('../../../../pages/iou/request/step/IOURequestStepVendor').default,
[SCREENS.MONEY_REQUEST.STEP_DATE]: () => require('../../../../pages/iou/request/step/IOURequestStepDate').default,
[SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../../pages/iou/request/step/IOURequestStepDescription').default,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../../pages/iou/request/step/IOURequestStepDistance').default,
@@ -200,9 +201,9 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/AddDebitCardPage').default,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/Pay/EnablePaymentsPage').default,
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
- [SCREENS.MONEY_REQUEST.DYNAMIC_STEP_ATTENDEES]: () => require