diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 04de0f5b5deb..818441828bf0 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -198,7 +198,7 @@ jobs:
id: pods-cache
with:
path: ios/Pods
- key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }}
+ key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }}
restore-keys: ${{ runner.os }}-pods-cache-
- name: Compare Podfile.lock and Manifest.lock
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 25a14fb27e1b..9548c3a6e595 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -154,6 +154,7 @@ jobs:
echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc
- name: Setup Node
+ id: setup-node
uses: ./.github/actions/composite/setupNode
- name: Setup XCode
@@ -170,7 +171,7 @@ jobs:
id: pods-cache
with:
path: ios/Pods
- key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock') }}
+ key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }}
restore-keys: ${{ runner.os }}-pods-cache-
- name: Compare Podfile.lock and Manifest.lock
@@ -179,7 +180,7 @@ jobs:
- name: Install cocoapods
uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350
- if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true'
+ if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true'
with:
timeout_minutes: 10
max_attempts: 5
diff --git a/__mocks__/react-native-webview.js b/__mocks__/react-native-webview.ts
similarity index 55%
rename from __mocks__/react-native-webview.js
rename to __mocks__/react-native-webview.ts
index 58875fd5288b..8266c7b1eda0 100644
--- a/__mocks__/react-native-webview.js
+++ b/__mocks__/react-native-webview.ts
@@ -1,6 +1,8 @@
+import type {View as RNView} from 'react-native';
+
jest.mock('react-native-webview', () => {
const {View} = require('react-native');
return {
- WebView: () => View,
+ WebView: () => View as RNView,
};
});
diff --git a/android/app/build.gradle b/android/app/build.gradle
index cff6f004967e..d049e10b0065 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001044400
- versionName "1.4.44-0"
+ versionCode 1001044406
+ versionName "1.4.44-6"
}
flavorDimensions "default"
diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md
index 16e628acbeee..043cc4be1e26 100644
--- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md
+++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md
@@ -81,7 +81,7 @@ Follow the below steps to run reconciliation on the Expensify Card settlements:
- Entry ID: a unique number grouping card payments and transactions settled by those payments
- Amount: the amount debited from the Business Bank Account for payments
- Merchant: the business where a purchase was made
- - Card: refers to the Expensify credit card number and cardholder's email address
+ - Card: refers to the Expensify Card number and cardholder's email address
- Business Account: the business bank account connected to Expensify that the settlement is paid from
- Transaction ID: a special ID that helps Expensify support locate transactions if there's an issue
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index e4e898423238..a1e1fea835af 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.44.0
+ 1.4.44.6
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 661432acfd4c..917242a291ff 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.44.0
+ 1.4.44.6
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index cdd031218474..ea595f9d644e 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.44
CFBundleVersion
- 1.4.44.0
+ 1.4.44.6
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 892a7f4abd28..88ddec8b82d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.44-0",
+ "version": "1.4.44-6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.44-0",
+ "version": "1.4.44-6",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -38,7 +38,6 @@
"@react-native-picker/picker": "2.5.1",
"@react-navigation/material-top-tabs": "^6.6.3",
"@react-navigation/native": "6.1.8",
- "@react-navigation/native-stack": "^6.9.17",
"@react-navigation/stack": "6.3.16",
"@react-ng/bounds-observer": "^0.2.1",
"@rnmapbox/maps": "^10.1.11",
@@ -53,7 +52,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.10.1",
@@ -98,7 +97,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.6",
+ "react-native-onyx": "2.0.7",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -10346,17 +10345,6 @@
"react": "*"
}
},
- "node_modules/@react-navigation/elements": {
- "version": "1.3.21",
- "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.21.tgz",
- "integrity": "sha512-eyS2C6McNR8ihUoYfc166O1D8VYVh9KIl0UQPI8/ZJVsStlfSTgeEEh+WXge6+7SFPnZ4ewzEJdSAHH+jzcEfg==",
- "peerDependencies": {
- "@react-navigation/native": "^6.0.0",
- "react": "*",
- "react-native": "*",
- "react-native-safe-area-context": ">= 3.0.0"
- }
- },
"node_modules/@react-navigation/material-top-tabs": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.3.tgz",
@@ -10388,22 +10376,6 @@
"react-native": "*"
}
},
- "node_modules/@react-navigation/native-stack": {
- "version": "6.9.17",
- "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.17.tgz",
- "integrity": "sha512-X8p8aS7JptQq7uZZNFEvfEcPf6tlK4PyVwYDdryRbG98B4bh2wFQYMThxvqa+FGEN7USEuHdv2mF0GhFKfX0ew==",
- "dependencies": {
- "@react-navigation/elements": "^1.3.21",
- "warn-once": "^0.1.0"
- },
- "peerDependencies": {
- "@react-navigation/native": "^6.0.0",
- "react": "*",
- "react-native": "*",
- "react-native-safe-area-context": ">= 3.0.0",
- "react-native-screens": ">= 3.0.0"
- }
- },
"node_modules/@react-navigation/routers": {
"version": "6.1.9",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
@@ -10430,6 +10402,17 @@
"react-native-screens": ">= 3.0.0"
}
},
+ "node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": {
+ "version": "1.3.17",
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz",
+ "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==",
+ "peerDependencies": {
+ "@react-navigation/native": "^6.0.0",
+ "react": "*",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 3.0.0"
+ }
+ },
"node_modules/@react-ng/bounds-observer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz",
@@ -30775,8 +30758,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167",
- "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb",
+ "integrity": "sha512-nAe0fPbfRn/VYHe6mCp/APmMbda/NiHE3aZq7q0kWhPmz1LVTukeaREmZ7SN8auyLOy9/mS0RIQLeV0AR8vsrA==",
"license": "MIT",
"dependencies": {
"classnames": "2.4.0",
@@ -44507,9 +44490,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.6.tgz",
- "integrity": "sha512-qsCxvNKc+mq/Y74v6Twe7VZxqgfpjBm0997R8OEtCUJEtgAp0riCQ3GvuVVIWYALz3S+ADokEAEPzeFW2frtpw==",
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.7.tgz",
+ "integrity": "sha512-UGMUTSFxYEzNn3wuCGzaf0t6D5XwcE+3J2pYj7wPlbskdcHVLijZZEwgSSDBF7hgNfCuZ+ImetskPNktnf9hkg==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index 5f9a29c4c1c6..f84469fd433e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.44-0",
+ "version": "1.4.44-6",
"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.",
@@ -86,7 +86,6 @@
"@react-native-picker/picker": "2.5.1",
"@react-navigation/material-top-tabs": "^6.6.3",
"@react-navigation/native": "6.1.8",
- "@react-navigation/native-stack": "^6.9.17",
"@react-navigation/stack": "6.3.16",
"@react-ng/bounds-observer": "^0.2.1",
"@rnmapbox/maps": "^10.1.11",
@@ -101,7 +100,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.10.1",
@@ -146,7 +145,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "2.0.6",
+ "react-native-onyx": "2.0.7",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
diff --git a/patches/@react-navigation+native-stack+6.9.17.patch b/patches/@react-navigation+native-stack+6.9.17.patch
deleted file mode 100644
index 933ca6ce792e..000000000000
--- a/patches/@react-navigation+native-stack+6.9.17.patch
+++ /dev/null
@@ -1,39 +0,0 @@
-diff --git a/node_modules/@react-navigation/native-stack/src/types.tsx b/node_modules/@react-navigation/native-stack/src/types.tsx
-index 206fb0b..7a34a8e 100644
---- a/node_modules/@react-navigation/native-stack/src/types.tsx
-+++ b/node_modules/@react-navigation/native-stack/src/types.tsx
-@@ -490,6 +490,14 @@ export type NativeStackNavigationOptions = {
- * Only supported on iOS and Android.
- */
- freezeOnBlur?: boolean;
-+ // partial changes from https://github.com/react-navigation/react-navigation/commit/90cfbf23bcc5259f3262691a9eec6c5b906e5262
-+ // patch can be removed when new version of `native-stack` will be released
-+ /**
-+ * Whether the keyboard should hide when swiping to the previous screen. Defaults to `false`.
-+ *
-+ * Only supported on iOS
-+ */
-+ keyboardHandlingEnabled?: boolean;
- };
-
- export type NativeStackNavigatorProps = DefaultNavigatorOptions<
-diff --git a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx
-index a005c43..03d8b50 100644
---- a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx
-+++ b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx
-@@ -161,6 +161,7 @@ const SceneView = ({
- statusBarTranslucent,
- statusBarColor,
- freezeOnBlur,
-+ keyboardHandlingEnabled,
- } = options;
-
- let {
-@@ -289,6 +290,7 @@ const SceneView = ({
- onNativeDismissCancelled={onNativeDismissCancelled}
- // this prop is available since rn-screens 3.16
- freezeOnBlur={freezeOnBlur}
-+ hideKeyboardOnSwipe={keyboardHandlingEnabled}
- >
-
-
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index a8786bda3ffb..22ebffd52eec 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -526,6 +526,11 @@ const ROUTES = {
route: 'workspace/:policyID/categories',
getRoute: (policyID: string) => `workspace/${policyID}/categories` as const,
},
+ WORKSPACE_CATEGORIES_SETTINGS: {
+ route: 'workspace/:policyID/categories/settings',
+ getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const,
+ },
+
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 520895c89c98..ac75968e68b9 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -219,6 +219,7 @@ const SCREENS = {
DESCRIPTION: 'Workspace_Profile_Description',
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
+ CATEGORIES_SETTINGS: 'Categories_Settings',
},
EDIT_REQUEST: {
diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js
index 0abb1dc4a873..7fa8b364fb0f 100644
--- a/src/components/FlatList/MVCPFlatList.js
+++ b/src/components/FlatList/MVCPFlatList.js
@@ -38,6 +38,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont
const firstVisibleViewRef = React.useRef(null);
const mutationObserverRef = React.useRef(null);
const lastScrollOffsetRef = React.useRef(0);
+ const isListRenderedRef = React.useRef(false);
const getScrollOffset = React.useCallback(() => {
if (scrollRef.current == null) {
@@ -137,6 +138,9 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont
}, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]);
React.useEffect(() => {
+ if (!isListRenderedRef.current) {
+ return;
+ }
requestAnimationFrame(() => {
prepareForMaintainVisibleContentPosition();
setupMutationObserver();
@@ -186,6 +190,10 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont
onScroll={onScrollInternal}
scrollEventThrottle={1}
ref={onRef}
+ onLayout={(e) => {
+ isListRenderedRef.current = true;
+ props.onLayout?.(e);
+ }}
/>
);
});
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index f53e3130f660..fe8cc3506b3f 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -84,8 +84,10 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
let canDeleteRequest = canModifyRequest;
if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) {
- // If it's a paid policy expense report, only allow deleting the request if it's not submitted or the user is the policy admin
- canDeleteRequest = canDeleteRequest && (ReportUtils.isDraftExpenseReport(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy));
+ // If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin
+ canDeleteRequest =
+ canDeleteRequest &&
+ (ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy));
}
const changeMoneyRequestStatus = () => {
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index a75aef3bc981..0af7140e1523 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -189,10 +189,9 @@ function MoneyRequestView({
const hasReceipt = TransactionUtils.hasReceipt(transaction);
let receiptURIs;
- let hasErrors = false;
+ const hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction);
if (hasReceipt) {
receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(transaction);
- hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction);
}
const pendingAction = transaction?.pendingAction;
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 591767234b8b..cbc728ffd1ce 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -138,7 +138,7 @@ function ReportPreview({
const hasReceipts = transactionsWithReceipts.length > 0;
const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
- const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations));
+ const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations));
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction));
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 4d7041d4a791..91f3198ca1e4 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1739,6 +1739,8 @@ export default {
collect: 'Collect',
},
categories: {
+ requiresCategory: 'Members must categorize all spend',
+ enableCategory: 'Enable category',
subtitle: 'Get a better overview of where money is being spent. Use our default categories or add your own.',
emptyCategories: {
title: "You haven't created any categories",
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c9ff087d0de7..d17355e69d55 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1763,6 +1763,8 @@ export default {
collect: 'Recolectar',
},
categories: {
+ requiresCategory: 'Los miembros deben categorizar todos los gastos',
+ enableCategory: 'Activar categoría',
subtitle: 'Obtén una visión general de dónde te gastas el dinero. Utiliza las categorías predeterminadas o añade las tuyas propias.',
emptyCategories: {
title: 'No has creado ninguna categoría',
diff --git a/src/libs/API/parameters/SetWorkspaceRequiresCategoryParams.ts b/src/libs/API/parameters/SetWorkspaceRequiresCategoryParams.ts
new file mode 100644
index 000000000000..5dd0ac6d758d
--- /dev/null
+++ b/src/libs/API/parameters/SetWorkspaceRequiresCategoryParams.ts
@@ -0,0 +1,6 @@
+type SetWorkspaceRequiresCategoryParams = {
+ policyID: string;
+ requiresCategory: boolean;
+};
+
+export default SetWorkspaceRequiresCategoryParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 0b0a81eb21f8..fc24b97ff1f3 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -145,6 +145,7 @@ export type {default as UnHoldMoneyRequestParams} from './UnHoldMoneyRequestPara
export type {default as CancelPaymentParams} from './CancelPaymentParams';
export type {default as AcceptACHContractForBankAccount} from './AcceptACHContractForBankAccount';
export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams';
+export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 17cc366ba3b7..acd57aa75304 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -110,6 +110,7 @@ const WRITE_COMMANDS = {
UPDATE_WORKSPACE_DESCRIPTION: 'UpdateWorkspaceDescription',
CREATE_WORKSPACE: 'CreateWorkspace',
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
+ SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
CREATE_TASK: 'CreateTask',
CANCEL_TASK: 'CancelTask',
EDIT_TASK_ASSIGNEE: 'EditTaskAssignee',
@@ -255,6 +256,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE]: Parameters.UpdateWorkspaceCustomUnitAndRateParams;
[WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
+ [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
[WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams;
[WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index 0ecbe7a03d87..3d0144d8cf77 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -1,9 +1,8 @@
import type {ParamListBase} from '@react-navigation/routers';
import type {StackNavigationOptions} from '@react-navigation/stack';
-import {createStackNavigator} from '@react-navigation/stack';
+import {CardStyleInterpolators, createStackNavigator} from '@react-navigation/stack';
import React, {useMemo} from 'react';
import useThemeStyles from '@hooks/useThemeStyles';
-import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import type {
AddPersonalBankAccountNavigatorParamList,
DetailsNavigatorParamList,
@@ -36,7 +35,6 @@ import type {
import type {ThemeStyles} from '@styles/index';
import type {Screen} from '@src/SCREENS';
import SCREENS from '@src/SCREENS';
-import subRouteOptions from './modalStackNavigatorOptions';
type Screens = Partial React.ComponentType>>;
@@ -46,46 +44,39 @@ type Screens = Partial React.ComponentType>>;
* @param screens key/value pairs where the key is the name of the screen and the value is a functon that returns the lazy-loaded component
* @param getScreenOptions optional function that returns the screen options, override the default options
*/
-function createModalStackNavigatorFactory(factory: typeof createPlatformStackNavigator) {
- return function createNestedModalStackNavigator(
- screens: Screens,
- getScreenOptions?: (styles: ThemeStyles) => StackNavigationOptions,
- ): React.ComponentType {
- const ModalStackNavigator = factory();
-
- function ModalStack() {
- const styles = useThemeStyles();
-
- const defaultSubRouteOptions = useMemo(
- (): StackNavigationOptions => ({
- ...subRouteOptions,
- cardStyle: styles.navigationScreenCardStyle,
- }),
- [styles],
- );
-
- return (
-
- {Object.keys(screens as Required).map((name) => (
- )[name as Screen]}
- />
- ))}
-
- );
- }
-
- ModalStack.displayName = 'ModalStack';
-
- return ModalStack;
- };
+function createModalStackNavigator(screens: Screens, getScreenOptions?: (styles: ThemeStyles) => StackNavigationOptions): React.ComponentType {
+ const ModalStackNavigator = createStackNavigator();
+
+ function ModalStack() {
+ const styles = useThemeStyles();
+
+ const defaultSubRouteOptions = useMemo(
+ (): StackNavigationOptions => ({
+ cardStyle: styles.navigationScreenCardStyle,
+ headerShown: false,
+ cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
+ }),
+ [styles],
+ );
+
+ return (
+
+ {Object.keys(screens as Required).map((name) => (
+ )[name as Screen]}
+ />
+ ))}
+
+ );
+ }
+
+ ModalStack.displayName = 'ModalStack';
+
+ return ModalStack;
}
-const createModalStackNavigator = createModalStackNavigatorFactory(createPlatformStackNavigator);
-const createJSModalStackNavigator = createModalStackNavigatorFactory(createStackNavigator);
-
const MoneyRequestModalStackNavigator = createModalStackNavigator({
[SCREENS.MONEY_REQUEST.START]: () => require('../../../pages/iou/request/IOURequestRedirectToStartPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.CREATE]: () => require('../../../pages/iou/request/IOURequestStartPage').default as React.ComponentType,
@@ -165,7 +156,7 @@ const RoomInviteModalStackNavigator = createModalStackNavigator require('../../../pages/RoomInvitePage').default as React.ComponentType,
});
-const SearchModalStackNavigator = createJSModalStackNavigator({
+const SearchModalStackNavigator = createModalStackNavigator({
[SCREENS.SEARCH_ROOT]: () => require('../../../pages/SearchPage').default as React.ComponentType,
});
@@ -257,6 +248,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileDescriptionPage').default as React.ComponentType,
[SCREENS.WORKSPACE.SHARE]: () => require('../../../pages/workspace/WorkspaceProfileSharePage').default as React.ComponentType,
[SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index 262a93da9e33..1e5d3639a32f 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -1,12 +1,12 @@
+import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
import useThemeStyles from '@hooks/useThemeStyles';
import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper';
import getCurrentUrl from '@libs/Navigation/currentUrl';
-import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import type {CentralPaneNavigatorParamList} from '@navigation/types';
import SCREENS from '@src/SCREENS';
-const Stack = createPlatformStackNavigator();
+const Stack = createStackNavigator();
const url = getCurrentUrl();
const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
similarity index 100%
rename from src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx
rename to src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx
deleted file mode 100644
index 30651e32cbd6..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-function Overlay() {
- return null;
-}
-
-Overlay.displayName = 'Overlay';
-
-export default Overlay;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 550fb947a4e6..c421bdc82028 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -1,4 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
+import {createStackNavigator} from '@react-navigation/stack';
import React, {useMemo, useRef} from 'react';
import {View} from 'react-native';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
@@ -6,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
-import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import type {AuthScreensParamList, RightModalNavigatorParamList} from '@navigation/types';
import type NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
@@ -14,7 +14,7 @@ import Overlay from './Overlay';
type RightModalNavigatorProps = StackScreenProps;
-const Stack = createPlatformStackNavigator();
+const Stack = createStackNavigator();
function RightModalNavigator({navigation}: RightModalNavigatorProps) {
const styles = useThemeStyles();
diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx
index 792a538cfd39..6b1557994627 100644
--- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx
@@ -1,5 +1,5 @@
+import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
-import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import type {PublicScreensParamList} from '@navigation/types';
import LogInWithShortLivedAuthTokenPage from '@pages/LogInWithShortLivedAuthTokenPage';
import AppleSignInDesktopPage from '@pages/signin/AppleSignInDesktopPage';
@@ -12,7 +12,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import defaultScreenOptions from './defaultScreenOptions';
-const RootStack = createPlatformStackNavigator();
+const RootStack = createStackNavigator();
function PublicScreens() {
return (
diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts
deleted file mode 100644
index 17100bc71bda..000000000000
--- a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-const defaultScreenOptions = {
- contentStyle: {
- overflow: 'visible',
- flex: 1,
- },
- headerShown: false,
- animationTypeForReplace: 'push',
- animation: 'slide_from_right',
-};
-
-export default defaultScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts
deleted file mode 100644
index 4015c43c679e..000000000000
--- a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type {StackNavigationOptions} from '@react-navigation/stack';
-
-const defaultScreenOptions: StackNavigationOptions = {
- cardStyle: {
- overflow: 'visible',
- flex: 1,
- },
- headerShown: false,
- animationTypeForReplace: 'push',
-};
-
-export default defaultScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts
deleted file mode 100644
index 2b062fd2f2be..000000000000
--- a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type {NativeStackNavigationOptions} from '@react-navigation/native-stack';
-
-const rightModalNavigatorOptions = (): NativeStackNavigationOptions => ({
- presentation: 'card',
- animation: 'slide_from_right',
-});
-
-export default rightModalNavigatorOptions;
diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts
deleted file mode 100644
index 935c0041b794..000000000000
--- a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type {StackNavigationOptions} from '@react-navigation/stack';
-// eslint-disable-next-line no-restricted-imports
-import getNavigationModalCardStyle from '@styles/utils/getNavigationModalCardStyles';
-
-const rightModalNavigatorOptions = (isSmallScreenWidth: boolean): StackNavigationOptions => ({
- presentation: 'transparentModal',
-
- // We want pop in RHP since there are some flows that would work weird otherwise
- animationTypeForReplace: 'pop',
- cardStyle: {
- ...getNavigationModalCardStyle(),
-
- // This is necessary to cover translated sidebar with overlay.
- width: isSmallScreenWidth ? '100%' : '200%',
- // Excess space should be on the left so we need to position from right.
- right: 0,
- },
-});
-
-export default rightModalNavigatorOptions;
diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
index 5685afec5459..c3a69bbd7ccf 100644
--- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
+++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
@@ -4,7 +4,6 @@ import type {StyleUtilsType} from '@styles/utils';
import variables from '@styles/variables';
import CONFIG from '@src/CONFIG';
import createModalCardStyleInterpolator from './createModalCardStyleInterpolator';
-import getRightModalNavigatorOptions from './getRightModalNavigatorOptions';
type ScreenOptions = Record;
@@ -26,12 +25,23 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
return {
rightModalNavigator: {
...commonScreenOptions,
- ...getRightModalNavigatorOptions(isSmallScreenWidth),
cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ presentation: 'transparentModal',
+
+ // We want pop in RHP since there are some flows that would work weird otherwise
+ animationTypeForReplace: 'pop',
+ cardStyle: {
+ ...StyleUtils.getNavigationModalCardStyle(),
+
+ // This is necessary to cover translated sidebar with overlay.
+ width: isSmallScreenWidth ? '100%' : '200%',
+ // Excess space should be on the left so we need to position from right.
+ right: 0,
+ },
},
leftModalNavigator: {
...commonScreenOptions,
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER),
+ cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER),
presentation: 'transparentModal',
// We want pop in LHP since there are some flows that would work weird otherwise
@@ -49,8 +59,8 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
homeScreen: {
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
- // Note: The card* properties won't be applied on mobile platforms, as they use the native defaults.
cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
width: isSmallScreenWidth ? '100%' : variables.sideBarWidth,
@@ -63,7 +73,6 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
fullScreen: {
...commonScreenOptions,
-
cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
@@ -78,9 +87,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
...commonScreenOptions,
animationEnabled: isSmallScreenWidth,
cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
- // temporary solution - better to hide a keyboard than see keyboard flickering
- // see https://github.com/software-mansion/react-native-screens/issues/2021 for more details
- keyboardHandlingEnabled: true,
+
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
paddingRight: isSmallScreenWidth ? 0 : variables.sideBarWidth,
diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts
deleted file mode 100644
index ca9769fa9972..000000000000
--- a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type {NativeStackNavigationOptions} from '@react-navigation/native-stack';
-
-const defaultSubRouteOptions: NativeStackNavigationOptions = {
- headerShown: false,
- animation: 'slide_from_right',
-};
-
-export default defaultSubRouteOptions;
diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts
deleted file mode 100644
index 280a38b263b7..000000000000
--- a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type {StackNavigationOptions} from '@react-navigation/stack';
-import {CardStyleInterpolators} from '@react-navigation/stack';
-
-const defaultSubRouteOptions: StackNavigationOptions = {
- headerShown: false,
- cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
-};
-
-export default defaultSubRouteOptions;
diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts
deleted file mode 100644
index ef44cefc13c9..000000000000
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import {createNativeStackNavigator} from '@react-navigation/native-stack';
-
-function createPlatformStackNavigator() {
- return createNativeStackNavigator();
-}
-
-export default createPlatformStackNavigator;
diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts
deleted file mode 100644
index 51228295572f..000000000000
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createStackNavigator} from '@react-navigation/stack';
-
-const createPlatformStackNavigator: typeof createStackNavigator = () => createStackNavigator();
-
-export default createPlatformStackNavigator;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 48d649cc4dd9..7a6211ebd283 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -259,6 +259,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
path: ROUTES.WORKSPACE_INVITE_MESSAGE.route,
},
+ [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
+ path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
+ },
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 765ab76fd638..a997e783deb0 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -175,6 +175,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
+ policyID: string;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 15c56eb1dc58..f040270fbf5f 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -951,6 +951,14 @@ function isProcessingReport(report: OnyxEntry | EmptyObject): boolean {
return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED;
}
+/**
+ * Returns true if the policy has `instant` reporting frequency and if the report is still being processed (i.e. submitted state)
+ */
+function isExpenseReportWithInstantSubmittedState(report: OnyxEntry | EmptyObject): boolean {
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? null;
+ return isExpenseReport(report) && isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy);
+}
+
/**
* Check if the report is a single chat report that isn't a thread
* and personal detail of participant is optimistic data
@@ -1260,8 +1268,8 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID:
if (isActionOwner) {
if (!isEmptyObject(report) && isPaidGroupPolicyExpenseReport(report)) {
- // If it's a paid policy expense report, only allow deleting the request if it's not submitted or the user is the policy admin
- return isDraftExpenseReport(report) || PolicyUtils.isPolicyAdmin(policy);
+ // If it's a paid policy expense report, only allow deleting the request if it's a draft or is instantly submitted or the user is the policy admin
+ return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report) || PolicyUtils.isPolicyAdmin(policy);
}
return true;
}
@@ -1471,11 +1479,9 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo
*/
function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = null): Icon {
const workspaceName = getPolicyName(report, false, policy);
- const rootParentReport = getRootParentReport(report);
- const hasCustomAvatar =
- !(isEmptyObject(rootParentReport) || isDefaultRoom(rootParentReport) || isChatRoom(rootParentReport) || isArchivedRoom(rootParentReport)) &&
- allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar;
- const policyExpenseChatAvatarSource = hasCustomAvatar ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar : getDefaultWorkspaceAvatar(workspaceName);
+ const policyExpenseChatAvatarSource = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar
+ ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar
+ : getDefaultWorkspaceAvatar(workspaceName);
const workspaceIcon: Icon = {
source: policyExpenseChatAvatarSource ?? '',
@@ -2227,8 +2233,7 @@ function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewActio
*
*/
function hasMissingSmartscanFields(iouReportID: string): boolean {
- const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
- return transactionsWithReceipts.some((transaction) => TransactionUtils.hasMissingSmartscanFields(transaction));
+ return TransactionUtils.getAllReportTransactions(iouReportID).some((transaction) => TransactionUtils.hasMissingSmartscanFields(transaction));
}
/**
@@ -3299,7 +3304,6 @@ function updateReportPreview(iouReport: OnyxEntry, reportPreviewAction:
const message = getReportPreviewMessage(iouReport, reportPreviewAction);
return {
...reportPreviewAction,
- created: DateUtils.getDBTime(),
message: [
{
html: message,
@@ -4232,7 +4236,7 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
if (isMoneyRequestReport(report)) {
const isOwnExpenseReport = isExpenseReport(report) && isOwnPolicyExpenseChat;
if (isOwnExpenseReport && PolicyUtils.isPaidGroupPolicy(policy)) {
- return isDraftExpenseReport(report);
+ return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report);
}
return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID);
@@ -5034,6 +5038,7 @@ export {
isPublicAnnounceRoom,
isConciergeChatReport,
isProcessingReport,
+ isExpenseReportWithInstantSubmittedState,
isCurrentUserTheOnlyParticipant,
hasAutomatedExpensifyAccountIDs,
hasExpensifyGuidesEmails,
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 3489053951b6..67e31c610369 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -12,6 +12,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {isCorporateCard, isExpensifyCard} from './CardUtils';
import DateUtils from './DateUtils';
import * as NumberUtils from './NumberUtils';
+import {getCleanedTagName} from './PolicyUtils';
import type {OptimisticIOUReportAction} from './ReportUtils';
let allTransactions: OnyxCollection = {};
@@ -409,7 +410,7 @@ function getTag(transaction: OnyxEntry, tagIndex?: number): string
}
function getTagForDisplay(transaction: OnyxEntry, tagIndex?: number): string {
- return getTag(transaction, tagIndex).replace(/[\\\\]:/g, ':');
+ return getCleanedTagName(getTag(transaction, tagIndex));
}
/**
@@ -480,7 +481,7 @@ function isReceiptBeingScanned(transaction: OnyxEntry): boolean {
* Check if the transaction has a non-smartscanning receipt and is missing required fields
*/
function hasMissingSmartscanFields(transaction: OnyxEntry): boolean {
- return Boolean(transaction && hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction));
+ return Boolean(transaction && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction));
}
/**
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index ca43d92bde83..f34f1562a2fc 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -324,9 +324,9 @@ function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string,
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants});
}
-function setMoneyRequestReceipt(transactionID: string, source: string, filename: string, isDraft: boolean) {
+function setMoneyRequestReceipt(transactionID: string, source: string, filename: string, isDraft: boolean, type?: string) {
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
- receipt: {source},
+ receipt: {source, type: type ?? ''},
filename,
});
}
@@ -804,8 +804,8 @@ function getMoneyRequestInformation(
// If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN
needsToBeManuallySubmitted = isFromPaidPolicy && !policy?.harvesting?.enabled;
- // If the linked expense report on paid policy is not draft, we need to create a new draft expense report
- if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) {
+ // If the linked expense report on paid policy is not draft and not instantly submitted, we need to create a new draft expense report
+ if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport) && !ReportUtils.isExpenseReportWithInstantSubmittedState(iouReport)) {
iouReport = null;
}
}
@@ -814,7 +814,7 @@ function getMoneyRequestInformation(
if (isPolicyExpenseChat) {
iouReport = {...iouReport};
if (iouReport?.currency === currency && typeof iouReport.total === 'number') {
- // Because of the Expense reports are stored as negative values, we substract the total from the amount
+ // Because of the Expense reports are stored as negative values, we subtract the total from the amount
iouReport.total -= amount;
}
} else {
@@ -4193,6 +4193,7 @@ function navigateToStartStepIfScanFileCannotBeRead(
iouType: ValueOf,
transactionID: string,
reportID: string,
+ receiptType: string,
) {
if (!receiptFilename || !receiptPath) {
return;
@@ -4206,7 +4207,7 @@ function navigateToStartStepIfScanFileCannotBeRead(
}
IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID);
};
- FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure);
+ FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure, receiptType);
}
/** Save the preferred payment method for a policy */
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 57cd4a6fc071..b9a2e8535b62 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -54,6 +54,7 @@ import type {
} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import type {Attributes, CustomUnit, Rate, Unit} from '@src/types/onyx/Policy';
+import type {OnyxData} from '@src/types/onyx/Request';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -2178,6 +2179,60 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string
return policyID;
}
+const setWorkspaceRequiresCategory = (policyID: string, requiresCategory: boolean) => {
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ requiresCategory,
+ errors: {
+ requiresCategory: null,
+ },
+ pendingFields: {
+ requiresCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ errors: {
+ requiresCategory: null,
+ },
+ pendingFields: {
+ requiresCategory: null,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ requiresCategory: !requiresCategory,
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
+ pendingFields: {
+ requiresCategory: null,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ requiresCategory,
+ };
+
+ API.write('SetWorkspaceRequiresCategory', parameters, onyxData);
+};
+
export {
removeMembers,
addMembersToWorkspace,
@@ -2221,4 +2276,5 @@ export {
setWorkspaceAutoReporting,
setWorkspaceApprovalMode,
updateWorkspaceDescription,
+ setWorkspaceRequiresCategory,
};
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 70ab01f62466..06bd47f3b39b 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -159,7 +159,7 @@ function appendTimeToFileName(fileName: string): string {
* @param path - the blob url of the locally uploaded file
* @param fileName - name of the file to read
*/
-const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}) =>
+const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = () => {}, fileType = '') =>
new Promise((resolve) => {
if (!path) {
resolve();
@@ -176,7 +176,9 @@ const readFileAsync: ReadFileAsync = (path, fileName, onSuccess, onFailure = ()
}
res.blob()
.then((blob) => {
- const file = new File([blob], cleanFileName(fileName), {type: blob.type});
+ // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file.
+ // In this case, let us fallback on fileType provided by the caller of this function.
+ const file = new File([blob], cleanFileName(fileName), {type: blob.type || fileType});
file.source = path;
// For some reason, the File object on iOS does not have a uri property
// so images aren't uploaded correctly to the backend
diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts
index 6d92bddd5816..f09ce495795b 100644
--- a/src/libs/fileDownload/types.ts
+++ b/src/libs/fileDownload/types.ts
@@ -8,7 +8,7 @@ type GetImageResolution = (url: File | Asset) => Promise;
type ExtensionAndFileName = {fileName: string; fileExtension: string};
type SplitExtensionFromFileName = (fileName: string) => ExtensionAndFileName;
-type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void) => Promise;
+type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void, fileType?: string) => Promise;
type AttachmentDetails = {
previewSourceURL: null | string;
diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js
index 6bdea2cb4a27..6345ebf89185 100644
--- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js
+++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js
@@ -203,8 +203,8 @@ function SuggestionMention({
suggestionEndIndex = indexOfFirstSpecialCharOrEmojiAfterTheCursor + selectionEnd;
}
- const newLineIndex = value.lastIndexOf('\n', selectionEnd - 1);
- const leftString = value.substring(newLineIndex + 1, suggestionEndIndex);
+ const afterLastBreakLineIndex = value.lastIndexOf('\n', selectionEnd - 1) + 1;
+ const leftString = value.substring(afterLastBreakLineIndex, suggestionEndIndex);
const words = leftString.split(CONST.REGEX.SPACE_OR_EMOJI);
const lastWord = _.last(words);
const secondToLastWord = words[words.length - 3];
@@ -215,12 +215,12 @@ function SuggestionMention({
// Detect if the last two words contain a mention (two words are needed to detect a mention with a space in it)
if (lastWord.startsWith('@')) {
- atSignIndex = leftString.lastIndexOf(lastWord);
+ atSignIndex = leftString.lastIndexOf(lastWord) + afterLastBreakLineIndex;
suggestionWord = lastWord;
prefix = suggestionWord.substring(1);
} else if (secondToLastWord && secondToLastWord.startsWith('@') && secondToLastWord.length > 1) {
- atSignIndex = leftString.lastIndexOf(secondToLastWord);
+ atSignIndex = leftString.lastIndexOf(secondToLastWord) + afterLastBreakLineIndex;
suggestionWord = `${secondToLastWord} ${lastWord}`;
prefix = suggestionWord.substring(1);
diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx
index e762f07cf20f..4250c874a471 100644
--- a/src/pages/home/report/ReportActionItemFragment.tsx
+++ b/src/pages/home/report/ReportActionItemFragment.tsx
@@ -105,6 +105,7 @@ function ReportActionItemFragment({
source={source}
html={fragment.html ?? ''}
addExtraMargin={!displayAsGroup}
+ styleAsDeleted={!!(isOffline && isPendingDelete)}
/>
);
}
diff --git a/src/pages/home/report/comment/AttachmentCommentFragment.tsx b/src/pages/home/report/comment/AttachmentCommentFragment.tsx
index 3c49cb90a106..fec5ebe92e54 100644
--- a/src/pages/home/report/comment/AttachmentCommentFragment.tsx
+++ b/src/pages/home/report/comment/AttachmentCommentFragment.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage';
import RenderCommentHTML from './RenderCommentHTML';
@@ -8,15 +9,19 @@ type AttachmentCommentFragmentProps = {
source: OriginalMessageSource;
html: string;
addExtraMargin: boolean;
+ styleAsDeleted: boolean;
};
-function AttachmentCommentFragment({addExtraMargin, html, source}: AttachmentCommentFragmentProps) {
+function AttachmentCommentFragment({addExtraMargin, html, source, styleAsDeleted}: AttachmentCommentFragmentProps) {
const styles = useThemeStyles();
+ const isUploading = html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
+ const htmlContent = styleAsDeleted && isUploading ? `${html}` : html;
+
return (
);
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index 0744fbd600a7..1ec793c96244 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -92,6 +92,7 @@ function IOURequestStepConfirmation({
const [receiptFile, setReceiptFile] = useState();
const receiptFilename = lodashGet(transaction, 'filename');
const receiptPath = lodashGet(transaction, 'receipt.source');
+ const receiptType = lodashGet(transaction, 'receipt.type');
const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList;
const transactionTaxAmount = transaction.taxAmount;
const requestType = TransactionUtils.getRequestType(transaction);
@@ -179,8 +180,8 @@ function IOURequestStepConfirmation({
setReceiptFile(receipt);
};
- IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID);
- }, [receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]);
+ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType);
+ }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]);
useEffect(() => {
const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat);
diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js
index a6f3563bd486..675eb0d03448 100644
--- a/src/pages/iou/request/step/IOURequestStepParticipants.js
+++ b/src/pages/iou/request/step/IOURequestStepParticipants.js
@@ -42,13 +42,14 @@ function IOURequestStepParticipants({
const headerTitle = translate(TransactionUtils.getHeaderTitleTranslationKey(transaction));
const receiptFilename = lodashGet(transaction, 'filename');
const receiptPath = lodashGet(transaction, 'receipt.source');
+ const receiptType = lodashGet(transaction, 'receipt.type');
// When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow.
// This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then
// the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process.
useEffect(() => {
- IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID);
- }, [receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]);
+ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID, receiptType);
+ }, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]);
const addParticipant = useCallback(
(val) => {
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js
index b23420b5ef69..181d8edc22f2 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js
@@ -211,7 +211,9 @@ function IOURequestStepScan({
}
// Store the receipt on the transaction object in Onyx
- IOU.setMoneyRequestReceipt(transactionID, file.uri, file.name, action !== CONST.IOU.ACTION.EDIT);
+ // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file.
+ // So, let us also save the file type in receipt for later use during blob fetch
+ IOU.setMoneyRequestReceipt(transactionID, file.uri, file.name, action !== CONST.IOU.ACTION.EDIT, file.type);
if (action === CONST.IOU.ACTION.EDIT) {
updateScanAndNavigate(file, file.uri);
diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js
index dbcb10d47f39..db02a99db067 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.js
+++ b/src/pages/tasks/NewTaskDescriptionPage.js
@@ -1,6 +1,6 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
@@ -42,6 +42,7 @@ const parser = new ExpensiMark();
function NewTaskDescriptionPage(props) {
const styles = useThemeStyles();
const {inputCallbackRef} = useAutoFocusInput();
+ const defaultDescriptionValue = useMemo(() => parser.htmlToMarkdown(parser.replace(props.task.description)), [props.task.description]);
const onSubmit = (values) => {
Task.setDescriptionValue(values.taskDescription);
@@ -85,7 +86,7 @@ function NewTaskDescriptionPage(props) {
parser.htmlToMarkdown(parser.replace(taskDescription)), [taskDescription]);
useEffect(() => {
setTaskTitle(props.task.title);
setTaskDescription(parser.htmlToMarkdown(parser.replace(props.task.description || '')));
- }, [props.task]);
+ }, [props.task.title, props.task.description]);
/**
* @param {Object} values - form input values passed by the Form component
@@ -118,7 +119,7 @@ function NewTaskDetailsPage(props) {
autoGrowHeight
shouldSubmitForm
containerStyles={[styles.autoGrowHeightMultilineInput]}
- defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))}
+ defaultValue={defaultDescriptionValue}
value={taskDescription}
onValueChange={(value) => setTaskDescription(value)}
/>
diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js
index b8b48abd09ff..23a52384af6d 100644
--- a/src/pages/tasks/TaskDescriptionPage.js
+++ b/src/pages/tasks/TaskDescriptionPage.js
@@ -1,6 +1,6 @@
import {useFocusEffect} from '@react-navigation/native';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import React, {useCallback, useRef} from 'react';
+import React, {useCallback, useMemo, useRef} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -80,6 +80,7 @@ function TaskDescriptionPage(props) {
const isOpen = ReportUtils.isOpenTaskReport(props.report);
const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID);
const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen);
+ const defaultDescriptionValue = useMemo(() => parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || ''), [props.report]);
useFocusEffect(
useCallback(() => {
@@ -121,7 +122,7 @@ function TaskDescriptionPage(props) {
name={INPUT_IDS.DESCRIPTION}
label={props.translate('newTaskPage.descriptionOptional')}
accessibilityLabel={props.translate('newTaskPage.descriptionOptional')}
- defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')}
+ defaultValue={defaultDescriptionValue}
ref={(el) => {
if (!el) {
return;
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 28943ddc3b68..c01df71255e0 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -153,7 +153,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
title={formattedCurrency}
description={translate('workspace.editor.currencyInputLabel')}
shouldShowRightIcon={!readOnly}
- disabled={hasVBA ?? readOnly}
+ disabled={hasVBA ? true : readOnly}
wrapperStyle={styles.sectionMenuItemTopDescription}
onPress={onPressCurrency}
shouldGreyOutWhenDisabled={false}
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index b8a65c28806b..7cd9972a6f57 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -3,6 +3,7 @@ import React, {useMemo, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
+import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -16,10 +17,12 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import Navigation from '@libs/Navigation/Navigation';
import type {CentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
@@ -86,6 +89,22 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP
);
+ const navigateToCategorySettings = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
+ };
+
+ const settingsButton = (
+
+
+
+ );
+
return (
@@ -99,7 +118,10 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP
icon={Illustrations.FolderOpen}
title={translate('workspace.common.categories')}
shouldShowBackButton={isSmallScreenWidth}
- />
+ >
+ {!isSmallScreenWidth && settingsButton}
+
+ {isSmallScreenWidth && {settingsButton}}
{translate('workspace.categories.subtitle')}
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
new file mode 100644
index 000000000000..da7558ca9022
--- /dev/null
+++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
@@ -0,0 +1,63 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Switch from '@components/Switch';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {setWorkspaceRequiresCategory} from '@libs/actions/Policy';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type SCREENS from '@src/SCREENS';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
+
+type WorkspaceCategoriesSettingsPageProps = StackScreenProps;
+
+function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const updateWorkspaceRequiresCategory = (value: boolean) => {
+ setWorkspaceRequiresCategory(route.params.policyID, value);
+ };
+
+ return (
+
+
+ {({policy}) => (
+
+
+
+
+
+ {translate('workspace.categories.requiresCategory')}
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+WorkspaceCategoriesSettingsPage.displayName = 'WorkspaceCategoriesSettingsPage';
+
+export default WorkspaceCategoriesSettingsPage;
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index 44f7e529f5eb..1a7541955720 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -66,6 +66,7 @@ type Receipt = {
source?: ReceiptSource;
filename?: string;
state?: ValueOf;
+ type?: string;
};
type Route = {