From eca73c4f1df816b9c56f6c287a32c7c60d652765 Mon Sep 17 00:00:00 2001 From: daledah Date: Thu, 19 Mar 2026 00:27:20 +0700 Subject: [PATCH 01/12] feature: add a new category within the category list --- .../request/step/IOURequestStepCategory.tsx | 29 +++++++++++++++++-- .../iou/request/step/StepScreenWrapper.tsx | 12 ++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index bfb123323f82..38fa9fefee43 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -10,7 +10,7 @@ import {useSearchStateContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionListWithSections/types'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -27,7 +27,7 @@ import {isCategoryMissing} from '@libs/CategoryUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; import {hasEnabledOptions} from '@libs/OptionsListUtils'; -import {isPolicyAdmin} from '@libs/PolicyUtils'; +import {getValidConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils'; import {getReportOrDraftReport, getTransactionDetails, isGroupPolicy, isReportInGroupPolicy} from '@libs/ReportUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {getRequestType} from '@libs/TransactionUtils'; @@ -55,6 +55,7 @@ function IOURequestStepCategory({ const styles = useThemeStyles(); const {translate} = useLocalize(); const illustrations = useMemoizedLazyIllustrations(['EmptyStateExpenses']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Plus']); const requestType = getRequestType(transaction); const isPerDiemRequest = requestType === CONST.IOU.REQUEST_TYPE.PER_DIEM; const transactionReport = getReportOrDraftReport(transaction?.reportID); @@ -88,6 +89,28 @@ function IOURequestStepCategory({ const categoryForDisplay = isCategoryMissing(transactionCategory) ? '' : transactionCategory; + const canCreateCategoryInSitu = isPolicyAdmin(policy) && !getValidConnectedIntegration(policy); + + const createCategoryMenuItems = canCreateCategoryInSitu + ? [ + { + icon: expensifyIcons.Plus, + text: translate('workspace.categories.addCategory'), + onSelected: () => { + if (!policyID || !report?.reportID) { + return; + } + Navigation.navigate( + ROUTES.SETTINGS_CATEGORY_CREATE.getRoute( + policyID, + ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, report.reportID, backTo, reportActionID), + ), + ); + }, + }, + ] + : undefined; + const shouldShowCategory = (isReportInGroupPolicy(report) || isGroupPolicy(policy?.type ?? '')) && // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor @@ -178,6 +201,8 @@ function IOURequestStepCategory({ shouldShowOfflineIndicator={policyCategories !== undefined} testID="IOURequestStepCategory" shouldEnableKeyboardAvoidingView={false} + threeDotsMenuItems={createCategoryMenuItems} + shouldMinimizeMenuButton > {isLoading && ( { // If props.children is a function, call it to provide the insets to the children From 623faa60ae68ce365143b725d547a5f9466371da Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 30 Mar 2026 16:04:52 +0700 Subject: [PATCH 02/12] fix auto select created category --- src/ONYXKEYS.ts | 4 ++++ src/libs/actions/Policy/Category.ts | 18 +++++++++++++++ .../request/step/IOURequestStepCategory.tsx | 22 +++++++++++++++++-- .../categories/CreateCategoryPage.tsx | 3 ++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6f1a861e3cc9..12e62c9393a9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -117,6 +117,9 @@ const ONYXKEYS = { /** Object containing contact method that's going to be added */ PENDING_CONTACT_ACTION: 'pendingContactAction', + /** Stores the category name created in-situ from IOURequestStepCategory so it can be auto-applied to the transaction */ + PENDING_CATEGORY_SELECTION: 'pendingCategorySelection', + /** Store the information of magic code */ VALIDATE_ACTION_CODE: 'validateActionCode', @@ -1309,6 +1312,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction; + [ONYXKEYS.PENDING_CATEGORY_SELECTION]: {transactionID: string; categoryName?: string}; [ONYXKEYS.VALIDATE_ACTION_CODE]: OnyxTypes.ValidateMagicCodeAction; [ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE]: OnyxTypes.ValidateDomainTwoFactorCode; [ONYXKEYS.JOINABLE_POLICIES]: OnyxTypes.JoinablePolicies; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index a0fe3aeda0dc..f8a5d04ebfcb 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1853,6 +1853,21 @@ function setPolicyCategoryAttendeesRequired(policyID: string, categoryName: stri API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_ATTENDEES_REQUIRED, parameters, onyxData); } +/** Marks a transaction as waiting for an in-situ category creation to complete. */ +function setPendingCategorySelection(transactionID: string) { + Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, {transactionID}); +} + +/** Records the newly created category name so IOURequestStepCategory can auto-apply it. */ +function completePendingCategorySelection(categoryName: string) { + Onyx.merge(ONYXKEYS.PENDING_CATEGORY_SELECTION, {categoryName}); +} + +/** Clears the pending category selection after it has been applied (or cancelled). */ +function clearPendingCategorySelection() { + Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, null); +} + export { buildOptimisticPolicyCategories, buildOptimisticMccGroup, @@ -1882,4 +1897,7 @@ export { setWorkspaceCategoryDescriptionHint, setWorkspaceCategoryEnabled, setWorkspaceRequiresCategory, + setPendingCategorySelection, + completePendingCategorySelection, + clearPendingCategorySelection, }; diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 35a41113fa63..d3b3c7f4bd51 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -1,5 +1,5 @@ import lodashIsEmpty from 'lodash/isEmpty'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import {InteractionManager, View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -22,7 +22,7 @@ import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useThemeStyles from '@hooks/useThemeStyles'; import {getIOURequestPolicyID, setMoneyRequestCategory, updateMoneyRequestCategory} from '@libs/actions/IOU'; import {setDraftSplitTransaction} from '@libs/actions/IOU/Split'; -import {enablePolicyCategories, getPolicyCategories} from '@libs/actions/Policy/Category'; +import {clearPendingCategorySelection, enablePolicyCategories, getPolicyCategories, setPendingCategorySelection} from '@libs/actions/Policy/Category'; import {isCategoryMissing} from '@libs/CategoryUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; @@ -91,6 +91,8 @@ function IOURequestStepCategory({ const canCreateCategoryInSitu = isPolicyAdmin(policy) && !getValidConnectedIntegration(policy); + const [pendingCategorySelection] = useOnyx(ONYXKEYS.PENDING_CATEGORY_SELECTION); + const createCategoryMenuItems = canCreateCategoryInSitu ? [ { @@ -100,6 +102,7 @@ function IOURequestStepCategory({ if (!policyID || !report?.reportID) { return; } + setPendingCategorySelection(transactionID); Navigation.navigate( ROUTES.SETTINGS_CATEGORY_CREATE.getRoute( policyID, @@ -192,6 +195,21 @@ function IOURequestStepCategory({ navigateBack(); }; + const updateCategoryRef = useRef(updateCategory); + updateCategoryRef.current = updateCategory; + + useEffect(() => { + if (pendingCategorySelection?.transactionID !== transactionID || !pendingCategorySelection?.categoryName) { + return; + } + const {categoryName} = pendingCategorySelection; + clearPendingCategorySelection(); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + updateCategoryRef.current({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); + }); + }, [pendingCategorySelection, transactionID]); + return ( Date: Wed, 1 Apr 2026 16:51:11 +0700 Subject: [PATCH 03/12] fix comments and keyboard flicker --- src/libs/actions/IOU/index.ts | 18 +++ src/libs/actions/Policy/Category.ts | 18 --- .../request/step/IOURequestStepCategory.tsx | 121 ++++++++++-------- .../categories/CreateCategoryPage.tsx | 9 +- 4 files changed, 95 insertions(+), 71 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 83b0c187bab6..7d362ab5e705 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -13944,6 +13944,21 @@ function rejectExpenseReport( API.write(WRITE_COMMANDS.REJECT_EXPENSE_REPORT, parameters, {optimisticData, successData, failureData}); } +/** Marks a transaction as waiting for an in-situ category creation to complete. */ +function setPendingCategorySelection(transactionID: string) { + Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, {transactionID}); +} + +/** Records the newly created category name so IOURequestStepCategory can auto-apply it. */ +function completePendingCategorySelection(categoryName: string) { + Onyx.merge(ONYXKEYS.PENDING_CATEGORY_SELECTION, {categoryName}); +} + +/** Clears the pending category selection after it has been applied (or cancelled). */ +function clearPendingCategorySelection() { + Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, null); +} + export { approveMoneyRequest, canApproveIOU, @@ -14068,6 +14083,9 @@ export { getTrackExpenseInformation, getMoneyRequestInformation, getOrCreateOptimisticSplitChatReport, + setPendingCategorySelection, + completePendingCategorySelection, + clearPendingCategorySelection, }; export type { GPSPoint as GpsPoint, diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index f8a5d04ebfcb..a0fe3aeda0dc 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1853,21 +1853,6 @@ function setPolicyCategoryAttendeesRequired(policyID: string, categoryName: stri API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_ATTENDEES_REQUIRED, parameters, onyxData); } -/** Marks a transaction as waiting for an in-situ category creation to complete. */ -function setPendingCategorySelection(transactionID: string) { - Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, {transactionID}); -} - -/** Records the newly created category name so IOURequestStepCategory can auto-apply it. */ -function completePendingCategorySelection(categoryName: string) { - Onyx.merge(ONYXKEYS.PENDING_CATEGORY_SELECTION, {categoryName}); -} - -/** Clears the pending category selection after it has been applied (or cancelled). */ -function clearPendingCategorySelection() { - Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, null); -} - export { buildOptimisticPolicyCategories, buildOptimisticMccGroup, @@ -1897,7 +1882,4 @@ export { setWorkspaceCategoryDescriptionHint, setWorkspaceCategoryEnabled, setWorkspaceRequiresCategory, - setPendingCategorySelection, - completePendingCategorySelection, - clearPendingCategorySelection, }; diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index d3b3c7f4bd51..df7b2d01c7e1 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -1,5 +1,5 @@ import lodashIsEmpty from 'lodash/isEmpty'; -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {InteractionManager, View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -20,9 +20,9 @@ import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useRestartOnReceiptFailure from '@hooks/useRestartOnReceiptFailure'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getIOURequestPolicyID, setMoneyRequestCategory, updateMoneyRequestCategory} from '@libs/actions/IOU'; +import {clearPendingCategorySelection, getIOURequestPolicyID, setMoneyRequestCategory, setPendingCategorySelection, updateMoneyRequestCategory} from '@libs/actions/IOU'; import {setDraftSplitTransaction} from '@libs/actions/IOU/Split'; -import {clearPendingCategorySelection, enablePolicyCategories, getPolicyCategories, setPendingCategorySelection} from '@libs/actions/Policy/Category'; +import {enablePolicyCategories, getPolicyCategories} from '@libs/actions/Policy/Category'; import {isCategoryMissing} from '@libs/CategoryUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; @@ -145,58 +145,80 @@ function IOURequestStepCategory({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [policyID]); - const navigateBack = () => { + const navigateBack = useCallback(() => { Navigation.goBack(backTo); - }; + }, [backTo]); - const updateCategory = (category: ListItem) => { - const categorySearchText = category.searchText ?? ''; - const isSelectedCategory = categorySearchText === categoryForDisplay; - const updatedCategory = isSelectedCategory ? '' : categorySearchText; + const updateCategory = useCallback( + (category: ListItem) => { + const categorySearchText = category.searchText ?? ''; + const isSelectedCategory = categorySearchText === categoryForDisplay; + const updatedCategory = isSelectedCategory ? '' : categorySearchText; - if (transaction) { - // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value - if (isEditingSplit) { - setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {category: updatedCategory}, policy); - navigateBack(); - return; - } + if (transaction) { + // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value + if (isEditingSplit) { + setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {category: updatedCategory}, policy); + navigateBack(); + return; + } - if (isEditing && report) { - updateMoneyRequestCategory({ - transactionID: transaction.transactionID, - transactionThreadReport: report, - parentReport, - parentReportNextStep, - category: updatedCategory, - policy, - policyTagList: policyTags, - policyCategories, - policyRecentlyUsedCategories, - currentUserAccountIDParam, - currentUserEmailParam, - isASAPSubmitBetaEnabled, - hash: currentSearchHash, - }); - navigateBack(); - return; + if (isEditing && report) { + updateMoneyRequestCategory({ + transactionID: transaction.transactionID, + transactionThreadReport: report, + parentReport, + parentReportNextStep, + category: updatedCategory, + policy, + policyTagList: policyTags, + policyCategories, + policyRecentlyUsedCategories, + currentUserAccountIDParam, + currentUserEmailParam, + isASAPSubmitBetaEnabled, + hash: currentSearchHash, + }); + navigateBack(); + return; + } } - } - setMoneyRequestCategory(transactionID, updatedCategory, policy); + setMoneyRequestCategory(transactionID, updatedCategory, policy); - if (action === CONST.IOU.ACTION.CATEGORIZE && !backTo) { - if (report?.reportID) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report.reportID)); + if (action === CONST.IOU.ACTION.CATEGORIZE && !backTo) { + if (report?.reportID) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report.reportID)); + } + return; } - return; - } - navigateBack(); - }; - - const updateCategoryRef = useRef(updateCategory); - updateCategoryRef.current = updateCategory; + navigateBack(); + }, + [ + action, + backTo, + categoryForDisplay, + currentSearchHash, + currentUserAccountIDParam, + currentUserEmailParam, + isASAPSubmitBetaEnabled, + isEditing, + isEditingSplit, + iouType, + navigateBack, + parentReport, + parentReportNextStep, + policy, + policyCategories, + policyRecentlyUsedCategories, + policyTags, + report, + splitDraftTransaction, + transaction, + transactionID, + ], + ); useEffect(() => { if (pendingCategorySelection?.transactionID !== transactionID || !pendingCategorySelection?.categoryName) { @@ -204,11 +226,8 @@ function IOURequestStepCategory({ } const {categoryName} = pendingCategorySelection; clearPendingCategorySelection(); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - updateCategoryRef.current({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); - }); - }, [pendingCategorySelection, transactionID]); + updateCategory({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); + }, [pendingCategorySelection, transactionID, updateCategory]); return ( Date: Mon, 6 Apr 2026 23:19:53 +0700 Subject: [PATCH 04/12] fix auto select created category --- src/pages/workspace/categories/CreateCategoryPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index b5f26549527b..80d42c67ef9a 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -7,7 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {completePendingCategorySelection} from '@libs/actions/IOU'; +import {clearPendingCategorySelection, completePendingCategorySelection} from '@libs/actions/IOU'; import {createPolicyCategory} from '@libs/actions/Policy/Category'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -71,6 +71,8 @@ function CreateCategoryPage({route}: CreateCategoryPageProps) { }); if (pendingCategorySelection) { completePendingCategorySelection(values.categoryName.trim()); + } else { + clearPendingCategorySelection(); } Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(route.params.policyID, backTo) : undefined); }, From c893136c398bd77fa9d83a0670e291a4a1c4f2ff Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 8 Apr 2026 14:02:35 +0700 Subject: [PATCH 05/12] fix ts --- src/pages/iou/request/step/IOURequestStepCategory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 4cfde7b32dc0..c13931cdae82 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -20,7 +20,7 @@ import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useRestartOnReceiptFailure from '@hooks/useRestartOnReceiptFailure'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearPendingCategorySelection, getIOURequestPolicyID, setMoneyRequestCategory} from '@libs/actions/IOU'; +import {clearPendingCategorySelection, getIOURequestPolicyID, setMoneyRequestCategory, setPendingCategorySelection} from '@libs/actions/IOU'; import {setDraftSplitTransaction} from '@libs/actions/IOU/Split'; import {updateMoneyRequestCategory} from '@libs/actions/IOU/UpdateMoneyRequest'; import {enablePolicyCategories, getPolicyCategories} from '@libs/actions/Policy/Category'; From 55338c1d08277c7b2eed6ad993fd25b51b19563e Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 13 Apr 2026 10:44:10 +0700 Subject: [PATCH 06/12] fix the error appears below the category field for a while --- src/pages/iou/request/step/IOURequestStepCategory.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index c13931cdae82..d60600c6d2ac 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -90,7 +90,7 @@ function IOURequestStepCategory({ const categoryForDisplay = isCategoryMissing(transactionCategory) ? '' : transactionCategory; - const canCreateCategoryInSitu = isPolicyAdmin(policy) && !getValidConnectedIntegration(policy); + const canCreateCategoryInSitu = isPolicyAdmin(policy) && !getValidConnectedIntegration(policy) && !!policy?.areCategoriesEnabled; const [pendingCategorySelection] = useOnyx(ONYXKEYS.PENDING_CATEGORY_SELECTION); @@ -226,9 +226,13 @@ function IOURequestStepCategory({ return; } const {categoryName} = pendingCategorySelection; + // Skip the update until policyCategories reflects the new category — this effect will re-run when it does. + if (!policyCategories?.[categoryName]?.enabled) { + return; + } clearPendingCategorySelection(); updateCategory({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); - }, [pendingCategorySelection, transactionID, updateCategory]); + }, [pendingCategorySelection, policyCategories, transactionID, updateCategory]); return ( Date: Wed, 15 Apr 2026 15:50:12 +0700 Subject: [PATCH 07/12] fix: blank page briefly appears after saving a new category --- src/pages/iou/request/step/IOURequestStepCategory.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index d60600c6d2ac..b3781fcafc43 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -231,7 +231,10 @@ function IOURequestStepCategory({ return; } clearPendingCategorySelection(); - updateCategory({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + updateCategory({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); + }); }, [pendingCategorySelection, policyCategories, transactionID, updateCategory]); return ( From 4d8a2d3354245974eac1efe7e7c5ad38cb904865 Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 24 Apr 2026 14:58:35 +0700 Subject: [PATCH 08/12] create new component and fix the navigation bug --- src/ONYXKEYS.ts | 4 - src/ROUTES.ts | 13 ++ src/SCREENS.ts | 1 + .../MoneyRequestConfirmationList.tsx | 3 +- .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 9 + src/libs/Violations/ViolationsUtils.ts | 5 +- src/libs/actions/IOU/index.ts | 18 -- .../request/step/IOURequestStepCategory.tsx | 28 +-- .../step/IOURequestStepCategoryCreate.tsx | 212 ++++++++++++++++++ .../categories/CreateCategoryPage.tsx | 8 - 12 files changed, 245 insertions(+), 58 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b02bdff86b79..db9255398607 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -117,9 +117,6 @@ const ONYXKEYS = { /** Object containing contact method that's going to be added */ PENDING_CONTACT_ACTION: 'pendingContactAction', - /** Stores the category name created in-situ from IOURequestStepCategory so it can be auto-applied to the transaction */ - PENDING_CATEGORY_SELECTION: 'pendingCategorySelection', - /** Store the information of magic code */ VALIDATE_ACTION_CODE: 'validateActionCode', @@ -1351,7 +1348,6 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction; - [ONYXKEYS.PENDING_CATEGORY_SELECTION]: {transactionID: string; categoryName?: string}; [ONYXKEYS.VALIDATE_ACTION_CODE]: OnyxTypes.ValidateMagicCodeAction; [ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE]: OnyxTypes.ValidateDomainTwoFactorCode; [ONYXKEYS.JOINABLE_POLICIES]: OnyxTypes.JoinablePolicies; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5a0da24dd239..f92400a6df39 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1295,6 +1295,19 @@ const ROUTES = { 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 = '') => { + if (!transactionID || !reportID) { + Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_CATEGORY_CREATE route'); + } + // eslint-disable-next-line no-restricted-syntax -- backTo is needed here to track where editing was initiated from (e.g. search/view or r/:reportID) + 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) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 936862a14972..aa859b10ff63 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -354,6 +354,7 @@ const SCREENS = { STEP_UPGRADE: 'Money_Request_Step_Upgrade', STEP_AMOUNT: 'Money_Request_Step_Amount', 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', STEP_DISTANCE: 'Money_Request_Step_Distance', diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index c9a90db7b410..7e0cce68bc09 100644 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -792,7 +792,8 @@ function MoneyRequestConfirmationList({ return; } - if (iouCategory && policyCategories && !policyCategories[iouCategory]?.enabled) { + const isCategoryBeingCreated = policyCategories?.[iouCategory]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + if (iouCategory && policyCategories && !isCategoryBeingCreated && !policyCategories[iouCategory]?.enabled) { setFormError('violations.categoryOutOfPolicy'); return; } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 631f2976e123..91680d790eee 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -178,6 +178,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator 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_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, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index a233c7aa3ab4..1502f68b97be 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1723,6 +1723,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO]: ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.route, [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route, + [SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE]: ROUTES.MONEY_REQUEST_STEP_CATEGORY_CREATE.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION_VERIFY_ACCOUNT]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION_VERIFY_ACCOUNT.route, [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index cf0ccac31cd8..a5fc9a2be70e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1878,6 +1878,15 @@ type MoneyRequestNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo: Routes; }; + [SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE]: { + action: IOUAction; + iouType: Exclude; + transactionID: string; + reportID: string; + reportActionID?: string; + // eslint-disable-next-line no-restricted-syntax -- backTo is needed to track where editing was initiated from (search/view or r/:reportID) + backTo?: Routes; + }; [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: { action: IOUAction; iouType: Exclude; diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 4003f22e0227..fe540361a49a 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -395,7 +395,10 @@ const ViolationsUtils = { const hasCategoryOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'categoryOutOfPolicy'); const hasMissingCategoryViolation = transactionViolations.some((violation) => violation.name === 'missingCategory'); const categoryKey = updatedTransaction.category; - const isCategoryInPolicy = categoryKey ? policyCategories?.[categoryKey]?.enabled : false; + const categoryData = policyCategories?.[categoryKey ?? '']; + // A category being created optimistically (pendingAction === 'add') is treated as valid + // so in-situ creation doesn't trigger a "categoryOutOfPolicy" violation before the server confirms it. + const isCategoryInPolicy = categoryKey ? !!(categoryData?.enabled || categoryData?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) : false; // Add 'categoryOutOfPolicy' violation if category is not in policy if (!hasCategoryOutOfPolicyViolation && !isCategoryMissing(categoryKey) && !isCategoryInPolicy) { diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 29cc3afcb0db..baf851f39bb2 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -3657,21 +3657,6 @@ function getSearchOnyxUpdate({ } } -/** Marks a transaction as waiting for an in-situ category creation to complete. */ -function setPendingCategorySelection(transactionID: string) { - Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, {transactionID}); -} - -/** Records the newly created category name so IOURequestStepCategory can auto-apply it. */ -function completePendingCategorySelection(categoryName: string) { - Onyx.merge(ONYXKEYS.PENDING_CATEGORY_SELECTION, {categoryName}); -} - -/** Clears the pending category selection after it has been applied (or cancelled). */ -function clearPendingCategorySelection() { - Onyx.set(ONYXKEYS.PENDING_CATEGORY_SELECTION, null); -} - export { clearMoneyRequest, createDistanceRequest, @@ -3742,9 +3727,6 @@ export { createSplitsAndOnyxData, getMoneyRequestInformation, getOrCreateOptimisticSplitChatReport, - setPendingCategorySelection, - completePendingCategorySelection, - clearPendingCategorySelection, getTransactionWithPreservedLocalReceiptSource, highlightTransactionOnSearchRouteIfNeeded, }; diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index b3781fcafc43..e947ac798a0e 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -20,7 +20,7 @@ import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useRestartOnReceiptFailure from '@hooks/useRestartOnReceiptFailure'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearPendingCategorySelection, getIOURequestPolicyID, setMoneyRequestCategory, setPendingCategorySelection} from '@libs/actions/IOU'; +import {getIOURequestPolicyID, setMoneyRequestCategory} from '@libs/actions/IOU'; import {setDraftSplitTransaction} from '@libs/actions/IOU/Split'; import {updateMoneyRequestCategory} from '@libs/actions/IOU/UpdateMoneyRequest'; import {enablePolicyCategories, getPolicyCategories} from '@libs/actions/Policy/Category'; @@ -92,8 +92,6 @@ function IOURequestStepCategory({ const canCreateCategoryInSitu = isPolicyAdmin(policy) && !getValidConnectedIntegration(policy) && !!policy?.areCategoriesEnabled; - const [pendingCategorySelection] = useOnyx(ONYXKEYS.PENDING_CATEGORY_SELECTION); - const createCategoryMenuItems = canCreateCategoryInSitu ? [ { @@ -103,13 +101,7 @@ function IOURequestStepCategory({ if (!policyID || !report?.reportID) { return; } - setPendingCategorySelection(transactionID); - Navigation.navigate( - ROUTES.SETTINGS_CATEGORY_CREATE.getRoute( - policyID, - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, report.reportID, backTo, reportActionID), - ), - ); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY_CREATE.getRoute(action, iouType, transactionID, report.reportID, reportActionID, backTo)); }, }, ] @@ -221,22 +213,6 @@ function IOURequestStepCategory({ ], ); - useEffect(() => { - if (pendingCategorySelection?.transactionID !== transactionID || !pendingCategorySelection?.categoryName) { - return; - } - const {categoryName} = pendingCategorySelection; - // Skip the update until policyCategories reflects the new category — this effect will re-run when it does. - if (!policyCategories?.[categoryName]?.enabled) { - return; - } - clearPendingCategorySelection(); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - updateCategory({searchText: categoryName, keyForList: categoryName, text: categoryName, isSelected: false}); - }); - }, [pendingCategorySelection, policyCategories, transactionID, updateCategory]); - return ( & + WithFullTransactionOrNotFoundProps; + +function IOURequestStepCategoryCreate({ + report: reportReal, + reportDraft, + route: { + params: {transactionID, action, iouType, reportID, backTo}, + }, + transaction, +}: IOURequestStepCategoryCreateProps) { + const {translate} = useLocalize(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {isBetaEnabled} = usePermissions(); + const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); + const {currentSearchHash} = useSearchStateContext(); + + const isEditing = action === CONST.IOU.ACTION.EDIT; + const isEditingSplit = (iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.SPLIT_EXPENSE) && isEditing; + + const policyIdReal = getIOURequestPolicyID(transaction, reportReal); + const policyIdDraft = getIOURequestPolicyID(transaction, reportDraft); + const {policy} = usePolicyForTransaction({ + transaction, + reportPolicyID: policyIdReal ?? policyIdDraft, + action, + iouType, + isPerDiemRequest: false, + }); + const policyID = policy?.id; + + const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportReal?.parentReportID ?? reportDraft?.parentReportID)}`); + const [parentReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(reportReal?.parentReportID ?? reportDraft?.parentReportID)}`); + + const report = reportReal ?? reportDraft; + + useRestartOnReceiptFailure(transaction, reportID, iouType, action); + + const policyHasTags = hasTags(policyTags); + + const { + taskReport: setupCategoryTaskReport, + taskParentReport: setupCategoryTaskParentReport, + isOnboardingTaskParentReportArchived: isSetupCategoryTaskParentReportArchived, + hasOutstandingChildTask, + parentReportAction, + } = useOnboardingTaskInformation(CONST.ONBOARDING_TASK_TYPE.SETUP_CATEGORIES); + + const { + taskReport: setupCategoriesAndTagsTaskReport, + taskParentReport: setupCategoriesAndTagsTaskParentReport, + isOnboardingTaskParentReportArchived: isSetupCategoriesAndTagsTaskParentReportArchived, + hasOutstandingChildTask: setupCategoriesAndTagsHasOutstandingChildTask, + parentReportAction: setupCategoriesAndTagsParentReportAction, + } = useOnboardingTaskInformation(CONST.ONBOARDING_TASK_TYPE.SETUP_CATEGORIES_AND_TAGS); + + const createCategory = useCallback( + (values: FormOnyxValues) => { + const categoryName = values.categoryName.trim(); + + // 1. Create the category in the workspace (optimistic update, queued API call). + createPolicyCategory({ + policyID: policyID ?? '', + categoryName, + isSetupCategoriesTaskParentReportArchived: isSetupCategoryTaskParentReportArchived, + setupCategoryTaskReport, + setupCategoryTaskParentReport, + currentUserAccountID: currentUserPersonalDetails.accountID, + hasOutstandingChildTask, + parentReportAction, + setupCategoriesAndTagsTaskReport, + setupCategoriesAndTagsTaskParentReport, + isSetupCategoriesAndTagsTaskParentReportArchived, + setupCategoriesAndTagsHasOutstandingChildTask, + setupCategoriesAndTagsParentReportAction, + policyHasTags, + }); + + // 2. Apply the newly created category to the transaction. + const policyCategoriesWithNewCategory = { + ...policyCategories, + [categoryName]: { + name: categoryName, + enabled: true, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }; + + if (isEditingSplit && transaction) { + setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {category: categoryName}, policy); + } else if (isEditing && report) { + updateMoneyRequestCategory({ + transactionID: transaction?.transactionID ?? transactionID, + transactionThreadReport: report, + parentReport, + parentReportNextStep, + category: categoryName, + policy, + policyTagList: policyTags, + policyCategories: policyCategoriesWithNewCategory, + policyRecentlyUsedCategories, + currentUserAccountIDParam: currentUserPersonalDetails.accountID, + currentUserEmailParam: currentUserPersonalDetails.login ?? '', + isASAPSubmitBetaEnabled, + hash: currentSearchHash, + }); + } else { + setMoneyRequestCategory(transactionID, categoryName, policy); + } + + if (isEditing) { + Navigation.goBack(backTo); + } else { + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, reportID)); + } + }, + [ + action, + backTo, + currentSearchHash, + currentUserPersonalDetails.accountID, + currentUserPersonalDetails.login, + hasOutstandingChildTask, + isASAPSubmitBetaEnabled, + isEditing, + isEditingSplit, + isSetupCategoriesAndTagsTaskParentReportArchived, + isSetupCategoryTaskParentReportArchived, + iouType, + parentReport, + parentReportAction, + parentReportNextStep, + policy, + policyCategories, + policyHasTags, + policyID, + policyRecentlyUsedCategories, + policyTags, + report, + reportID, + setupCategoriesAndTagsHasOutstandingChildTask, + setupCategoriesAndTagsParentReportAction, + setupCategoriesAndTagsTaskParentReport, + setupCategoriesAndTagsTaskReport, + setupCategoryTaskParentReport, + setupCategoryTaskReport, + splitDraftTransaction, + transaction, + transactionID, + ], + ); + + return ( + + Navigation.goBack()} + shouldShowWrapper + testID="IOURequestStepCategoryCreate" + > + + + + ); +} + +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepCategoryCreateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepCategoryCreate); +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepCategoryCreateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategoryCreateWithFullTransactionOrNotFound); +export default IOURequestStepCategoryCreateWithWritableReportOrNotFound; diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index 80d42c67ef9a..733f818475e5 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -7,7 +7,6 @@ import useLocalize from '@hooks/useLocalize'; import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {clearPendingCategorySelection, completePendingCategorySelection} from '@libs/actions/IOU'; import {createPolicyCategory} from '@libs/actions/Policy/Category'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -27,7 +26,6 @@ type CreateCategoryPageProps = function CreateCategoryPage({route}: CreateCategoryPageProps) { const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`); - const [pendingCategorySelection] = useOnyx(ONYXKEYS.PENDING_CATEGORY_SELECTION); const styles = useThemeStyles(); const {translate} = useLocalize(); const backTo = route.params?.backTo; @@ -69,11 +67,6 @@ function CreateCategoryPage({route}: CreateCategoryPageProps) { setupCategoriesAndTagsParentReportAction, policyHasTags, }); - if (pendingCategorySelection) { - completePendingCategorySelection(values.categoryName.trim()); - } else { - clearPendingCategorySelection(); - } Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(route.params.policyID, backTo) : undefined); }, [ @@ -92,7 +85,6 @@ function CreateCategoryPage({route}: CreateCategoryPageProps) { setupCategoriesAndTagsHasOutstandingChildTask, setupCategoriesAndTagsParentReportAction, policyHasTags, - pendingCategorySelection, ], ); From d6e3b4e817c61fd58fb0a4d7752753af1be56358 Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 24 Apr 2026 15:03:40 +0700 Subject: [PATCH 09/12] fix lint --- src/ROUTES.ts | 5 +---- src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f92400a6df39..80b8ace35e6f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1302,10 +1302,7 @@ const ROUTES = { Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_CATEGORY_CREATE route'); } // eslint-disable-next-line no-restricted-syntax -- backTo is needed here to track where editing was initiated from (e.g. search/view or r/:reportID) - return getUrlWithBackToParam( - `${action as string}/${iouType as string}/category/new/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, - backTo, - ); + return getUrlWithBackToParam(`${action as string}/${iouType as string}/category/new/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo); }, }, MONEY_REQUEST_STEP_CATEGORY: { diff --git a/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx b/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx index a74fa3a00187..730b3458e12a 100644 --- a/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx @@ -91,9 +91,13 @@ function IOURequestStepCategoryCreate({ (values: FormOnyxValues) => { const categoryName = values.categoryName.trim(); + if (!policyID) { + return; + } + // 1. Create the category in the workspace (optimistic update, queued API call). createPolicyCategory({ - policyID: policyID ?? '', + policyID, categoryName, isSetupCategoriesTaskParentReportArchived: isSetupCategoryTaskParentReportArchived, setupCategoryTaskReport, From 9b96a1ae5ef7790433dfb81e061d7b8508c7134f Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 24 Apr 2026 16:02:21 +0700 Subject: [PATCH 10/12] fix ts --- src/pages/iou/request/step/withFullTransactionOrNotFound.tsx | 3 ++- src/pages/iou/request/step/withWritableReportOrNotFound.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index fb9c310b7b0b..5d24f5efc0a0 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -56,7 +56,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.ODOMETER_IMAGE | typeof SCREENS.MONEY_REQUEST.STEP_TIME_RATE | typeof SCREENS.MONEY_REQUEST.STEP_HOURS - | typeof SCREENS.MONEY_REQUEST.STEP_HOURS_EDIT; + | typeof SCREENS.MONEY_REQUEST.STEP_HOURS_EDIT + | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE; type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps & PlatformStackScreenProps; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 3fd755feb219..1b9cf9eeb30c 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -59,7 +59,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL | typeof SCREENS.MONEY_REQUEST.STEP_TIME_RATE | typeof SCREENS.MONEY_REQUEST.STEP_HOURS - | typeof SCREENS.MONEY_REQUEST.STEP_HOURS_EDIT; + | typeof SCREENS.MONEY_REQUEST.STEP_HOURS_EDIT + | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY_CREATE; type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & PlatformStackScreenProps; From bf805d409b2577ad0aeecc2de0dbe07e3a1cf0f4 Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 28 Apr 2026 14:40:47 +0700 Subject: [PATCH 11/12] fix comments --- .../request/step/IOURequestStepCategory.tsx | 107 +++++++----------- 1 file changed, 41 insertions(+), 66 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index e947ac798a0e..9e65b8e682bc 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -1,5 +1,5 @@ import lodashIsEmpty from 'lodash/isEmpty'; -import React, {useCallback, useEffect} from 'react'; +import React, {useEffect} from 'react'; import {InteractionManager, View} from 'react-native'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -138,80 +138,55 @@ function IOURequestStepCategory({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [policyID]); - const navigateBack = useCallback(() => { + const navigateBack = () => { Navigation.goBack(backTo); - }, [backTo]); + }; - const updateCategory = useCallback( - (category: ListItem) => { - const categorySearchText = category.searchText ?? ''; - const isSelectedCategory = categorySearchText === categoryForDisplay; - const updatedCategory = isSelectedCategory ? '' : categorySearchText; + const updateCategory = (category: ListItem) => { + const categorySearchText = category.searchText ?? ''; + const isSelectedCategory = categorySearchText === categoryForDisplay; + const updatedCategory = isSelectedCategory ? '' : categorySearchText; - if (transaction) { - // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value - if (isEditingSplit) { - setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {category: updatedCategory}, policy); - navigateBack(); - return; - } + if (transaction) { + // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value + if (isEditingSplit) { + setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {category: updatedCategory}, policy); + navigateBack(); + return; + } - if (isEditing && report) { - updateMoneyRequestCategory({ - transactionID: transaction.transactionID, - transactionThreadReport: report, - parentReport, - parentReportNextStep, - category: updatedCategory, - policy, - policyTagList: policyTags, - policyCategories, - policyRecentlyUsedCategories, - currentUserAccountIDParam, - currentUserEmailParam, - isASAPSubmitBetaEnabled, - hash: currentSearchHash, - }); - navigateBack(); - return; - } + if (isEditing && report) { + updateMoneyRequestCategory({ + transactionID: transaction.transactionID, + transactionThreadReport: report, + parentReport, + parentReportNextStep, + category: updatedCategory, + policy, + policyTagList: policyTags, + policyCategories, + policyRecentlyUsedCategories, + currentUserAccountIDParam, + currentUserEmailParam, + isASAPSubmitBetaEnabled, + hash: currentSearchHash, + }); + navigateBack(); + return; } + } - setMoneyRequestCategory(transactionID, updatedCategory, policy); + setMoneyRequestCategory(transactionID, updatedCategory, policy); - if (action === CONST.IOU.ACTION.CATEGORIZE && !backTo) { - if (report?.reportID) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report.reportID)); - } - return; + if (action === CONST.IOU.ACTION.CATEGORIZE && !backTo) { + if (report?.reportID) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report.reportID)); } + return; + } - navigateBack(); - }, - [ - action, - backTo, - categoryForDisplay, - currentSearchHash, - currentUserAccountIDParam, - currentUserEmailParam, - isASAPSubmitBetaEnabled, - isEditing, - isEditingSplit, - iouType, - navigateBack, - parentReport, - parentReportNextStep, - policy, - policyCategories, - policyRecentlyUsedCategories, - policyTags, - report, - splitDraftTransaction, - transaction, - transactionID, - ], - ); + navigateBack(); + }; return ( Date: Thu, 30 Apr 2026 16:35:40 +0700 Subject: [PATCH 12/12] fix lint --- src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx b/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx index 730b3458e12a..14ecc86d336e 100644 --- a/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategoryCreate.tsx @@ -209,8 +209,6 @@ function IOURequestStepCategoryCreate({ ); } -/* eslint-disable rulesdir/no-negated-variables */ const IOURequestStepCategoryCreateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepCategoryCreate); -/* eslint-disable rulesdir/no-negated-variables */ const IOURequestStepCategoryCreateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategoryCreateWithFullTransactionOrNotFound); export default IOURequestStepCategoryCreateWithWritableReportOrNotFound;