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 = ( + +