diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 7e1b5fbbae90..0b32d8ee6dc1 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -1,6 +1,11 @@
name: Set up Node
description: Set up Node
+outputs:
+ cache-hit:
+ description: Was there a cache hit on the main node_modules?
+ value: ${{ steps.cache-node-modules.outputs.cache-hit }}
+
runs:
using: composite
steps:
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 7c7b51240fdb..04de0f5b5deb 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -184,6 +184,7 @@ jobs:
run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
- name: Setup Node
+ id: setup-node
uses: ./.github/actions/composite/setupNode
- name: Setup Ruby
@@ -206,7 +207,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/android/app/build.gradle b/android/app/build.gradle
index 28ed5dec455f..99d7a186e7ee 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 1001044314
- versionName "1.4.43-14"
+ versionCode 1001044318
+ versionName "1.4.43-18"
}
flavorDimensions "default"
diff --git a/assets/images/product-illustrations/mushroom-top-hat.svg b/assets/images/product-illustrations/mushroom-top-hat.svg
new file mode 100644
index 000000000000..cb808f7289e0
--- /dev/null
+++ b/assets/images/product-illustrations/mushroom-top-hat.svg
@@ -0,0 +1,142 @@
+
+
+
diff --git a/assets/images/simple-illustrations/simple-illustration__approval.svg b/assets/images/simple-illustrations/simple-illustration__approval.svg
new file mode 100644
index 000000000000..bdef2436958b
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__approval.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg b/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg
new file mode 100644
index 000000000000..fc7082e9932c
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg b/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg
new file mode 100644
index 000000000000..33d1fc0fa044
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg
new file mode 100644
index 000000000000..47d30d54310f
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/workflows.svg b/assets/images/workflows.svg
new file mode 100644
index 000000000000..24156c66eb69
--- /dev/null
+++ b/assets/images/workflows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md
index 09dd4de2867b..3fe5ec41f5f6 100644
--- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md
+++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md
@@ -3,7 +3,7 @@ title: Billing Overview
description: An overview of how billing works in Expensify.
---
# Overview
-Expensify’s billing is based on monthly member activity. At the beginning of each month, you’ll be billed for the previous month’s activity. Your Expensify bill ultimately depends on your plan type, whether you're on an annual subscription or pay-per-use billing, and whether you’re using Expensify Cards.
+Expensify’s billing is based on monthly member activity. At the beginning of each month, you’ll be billed for the previous month’s activity. Your Expensify bill ultimately depends on your plan type, whether you're on an annual subscription or pay-per-use billing, and whether you’re using the Expensify Visa® Commercial Card.
# How billing works in Expensify
Expensify bills the owners of Group Workspaces on the first of each month for the previous month's usage. You can find billing receipts in **Settings > Account > Payments > Billing History**. We recommend that businesses have one billing owner for all of their Group Workspaces.
## Active members
@@ -23,7 +23,7 @@ Bundling the Expensify Card with an annual subscription ensures you pay the lowe
If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per member. This additional 50% discount, when coupled with an annual subscription, brings the price per member to $5 on a Collect plan and $9 on a Control plan.
-Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more. Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account.
+Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more (_applies to USD purchases only_). Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account.
## Savings calculator
To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save!
{% include faq-begin.md %}
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 509e95136326..574657c8c3f4 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.43.14
+ 1.4.43.18
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index e2bd6b2648c0..e4962c94df8d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.43.14
+ 1.4.43.18
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 55c91be9521d..308c4314ee68 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.43
CFBundleVersion
- 1.4.43.14
+ 1.4.43.18
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 91b5708d6485..aab783e8bbb7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.43-14",
+ "version": "1.4.43-18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.43-14",
+ "version": "1.4.43-18",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -237,7 +237,7 @@
"style-loader": "^2.0.0",
"time-analytics-webpack-plugin": "^0.1.17",
"ts-node": "^10.9.2",
- "type-fest": "^3.12.0",
+ "type-fest": "^4.10.2",
"typescript": "^5.3.2",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
@@ -8139,9 +8139,9 @@
}
},
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
- "version": "0.5.10",
- "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
- "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==",
+ "version": "0.5.11",
+ "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz",
+ "integrity": "sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==",
"dev": true,
"dependencies": {
"ansi-html-community": "^0.0.8",
@@ -8161,7 +8161,7 @@
"@types/webpack": "4.x || 5.x",
"react-refresh": ">=0.10.0 <1.0.0",
"sockjs-client": "^1.4.0",
- "type-fest": ">=0.17.0 <4.0.0",
+ "type-fest": ">=0.17.0 <5.0.0",
"webpack": ">=4.43.0 <6.0.0",
"webpack-dev-server": "3.x || 4.x",
"webpack-hot-middleware": "2.x",
@@ -26870,10 +26870,11 @@
}
},
"node_modules/core-js-pure": {
- "version": "3.24.1",
+ "version": "3.36.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz",
+ "integrity": "sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ==",
"dev": true,
"hasInstallScript": true,
- "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -38622,6 +38623,17 @@
"node": ">=8"
}
},
+ "node_modules/jest-watch-typeahead/node_modules/type-fest": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+ "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/jest-watcher": {
"version": "29.4.1",
"license": "MIT",
@@ -50849,11 +50861,12 @@
}
},
"node_modules/type-fest": {
- "version": "3.13.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
- "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "version": "4.10.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.3.tgz",
+ "integrity": "sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==",
+ "dev": true,
"engines": {
- "node": ">=14.16"
+ "node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
diff --git a/package.json b/package.json
index a6c16d3dedd1..f5ff807cdbec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.43-14",
+ "version": "1.4.43-18",
"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.",
@@ -285,7 +285,7 @@
"style-loader": "^2.0.0",
"time-analytics-webpack-plugin": "^0.1.17",
"ts-node": "^10.9.2",
- "type-fest": "^3.12.0",
+ "type-fest": "^4.10.2",
"typescript": "^5.3.2",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index e3cf8f6b172e..8abd4c087b16 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1558,6 +1558,7 @@ const CONST = {
WORKSPACE_INVOICES: 'WorkspaceSendInvoices',
WORKSPACE_TRAVEL: 'WorkspaceBookTravel',
WORKSPACE_MEMBERS: 'WorkspaceManageMembers',
+ WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows',
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
WORKSPACE_SETTINGS: 'WorkspaceSettings',
},
@@ -3111,6 +3112,8 @@ const CONST = {
*/
ADDITIONAL_ALLOWED_CHARACTERS: 20,
+ VALIDATION_REIMBURSEMENT_INPUT_LIMIT: 20,
+
REFERRAL_PROGRAM: {
CONTENT_TYPES: {
MONEY_REQUEST: 'request',
@@ -3308,6 +3311,14 @@ const CONST = {
ADDRESS: 3,
},
},
+
+ EXIT_SURVEY: {
+ REASONS: {
+ FEATURE_NOT_AVAILABLE: 'featureNotAvailable',
+ DONT_UNDERSTAND: 'dontUnderstand',
+ PREFER_CLASSIC: 'preferClassic',
+ },
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index afbcd768b465..f0b400687b12 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -205,6 +205,9 @@ const ONYXKEYS = {
/** Is report data loading? */
IS_LOADING_APP: 'isLoadingApp',
+ /** Is the user in the process of switching to OldDot? */
+ IS_SWITCHING_TO_OLD_DOT: 'isSwitchingToOldDot',
+
/** Is the test tools modal open? */
IS_TEST_TOOLS_MODAL_OPEN: 'isTestToolsModalOpen',
@@ -388,6 +391,10 @@ const ONYXKEYS = {
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm',
PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft',
+ EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm',
+ EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft',
+ EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm',
+ EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft',
},
} as const;
@@ -410,6 +417,8 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm;
[ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm;
[ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.EditTaskForm;
+ [ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM]: FormTypes.ExitSurveyReasonForm;
+ [ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM]: FormTypes.ExitSurveyResponseForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.MoneyRequestDescriptionForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: FormTypes.MoneyRequestMerchantForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.MoneyRequestAmountForm;
@@ -534,6 +543,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean;
[ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean;
[ONYXKEYS.IS_LOADING_APP]: boolean;
+ [ONYXKEYS.IS_SWITCHING_TO_OLD_DOT]: boolean;
[ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer;
[ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string;
[ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 080d8cdd5655..a8786bda3ffb 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -159,6 +159,17 @@ const ROUTES = {
getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const,
},
+ SETTINGS_EXIT_SURVEY_REASON: 'settings/exit-survey/reason',
+ SETTINGS_EXIT_SURVEY_RESPONSE: {
+ route: 'settings/exit-survey/response',
+ getRoute: (reason?: ValueOf, backTo?: string) =>
+ getUrlWithBackToParam(`settings/exit-survey/response${reason ? `?reason=${encodeURIComponent(reason)}` : ''}`, backTo),
+ },
+ SETTINGS_EXIT_SURVEY_CONFIRM: {
+ route: 'settings/exit-survey/confirm',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('settings/exit-survey/confirm', backTo),
+ },
+
KEYBOARD_SHORTCUTS: 'keyboard-shortcuts',
NEW: 'new',
@@ -471,6 +482,10 @@ const ROUTES = {
route: 'workspace/:policyID/settings/currency',
getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
},
+ WORKSPACE_WORKFLOWS: {
+ route: 'workspace/:policyID/workflows',
+ getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const,
+ },
WORKSPACE_CARD: {
route: 'workspace/:policyID/card',
getRoute: (policyID: string) => `workspace/${policyID}/card` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 50ced3ff256a..520895c89c98 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -80,6 +80,12 @@ const SCREENS = {
REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud',
CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address',
},
+
+ EXIT_SURVEY: {
+ REASON: 'Settings_ExitSurvey_Reason',
+ RESPONSE: 'Settings_ExitSurvey_Response',
+ CONFIRM: 'Settings_ExitSurvey_Confirm',
+ },
},
SAVE_THE_WORLD: {
ROOT: 'SaveTheWorld_Root',
@@ -209,6 +215,7 @@ const SCREENS = {
INVITE_MESSAGE: 'Workspace_Invite_Message',
CATEGORIES: 'Workspace_Categories',
CURRENCY: 'Workspace_Profile_Currency',
+ WORKFLOWS: 'Workspace_Workflows',
DESCRIPTION: 'Workspace_Profile_Description',
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
diff --git a/src/components/Alert/index.js b/src/components/Alert/index.js
deleted file mode 100644
index 3fc90433f13e..000000000000
--- a/src/components/Alert/index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import _ from 'underscore';
-
-/**
- * Shows an alert modal with ok and cancel options.
- *
- * @param {String} title The title of the alert
- * @param {String} description The description of the alert
- * @param {Object[]} [options] An array of objects with `style` and `onPress` properties
- */
-export default (title, description, options) => {
- const result = _.filter(window.confirm([title, description], Boolean)).join('\n');
-
- if (result) {
- const confirmOption = _.find(options, ({style}) => style !== 'cancel');
- if (confirmOption && confirmOption.onPress) {
- confirmOption.onPress();
- }
- } else {
- const cancelOption = _.find(options, ({style}) => style === 'cancel');
- if (cancelOption && cancelOption.onPress) {
- cancelOption.onPress();
- }
- }
-};
diff --git a/src/components/Alert/index.native.js b/src/components/Alert/index.native.js
deleted file mode 100644
index 31c837a7dd6b..000000000000
--- a/src/components/Alert/index.native.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import {Alert} from 'react-native';
-
-export default Alert.alert;
diff --git a/src/components/Alert/index.native.tsx b/src/components/Alert/index.native.tsx
new file mode 100644
index 000000000000..b72eff5d9b58
--- /dev/null
+++ b/src/components/Alert/index.native.tsx
@@ -0,0 +1,6 @@
+import {Alert as AlertRN} from 'react-native';
+import type Alert from './types';
+
+const alert: Alert = AlertRN.alert;
+
+export default alert;
diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx
new file mode 100644
index 000000000000..f212f06aa9d3
--- /dev/null
+++ b/src/components/Alert/index.tsx
@@ -0,0 +1,16 @@
+import type Alert from './types';
+
+/** Shows an alert modal with ok and cancel options. */
+const alert: Alert = (title, description, options) => {
+ const result = window.confirm([title, description].filter(Boolean).join('\n'));
+
+ if (result) {
+ const confirmOption = options?.find(({style}) => style !== 'cancel');
+ confirmOption?.onPress?.();
+ } else {
+ const cancelOption = options?.find(({style}) => style === 'cancel');
+ cancelOption?.onPress?.();
+ }
+};
+
+export default alert;
diff --git a/src/components/Alert/types.ts b/src/components/Alert/types.ts
new file mode 100644
index 000000000000..25454abfe8b8
--- /dev/null
+++ b/src/components/Alert/types.ts
@@ -0,0 +1,5 @@
+import type {AlertStatic} from 'react-native';
+
+type Alert = AlertStatic['alert'];
+
+export default Alert;
diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js
index 59928b80c4b1..d4d3d0696c59 100644
--- a/src/components/AttachmentPicker/index.native.js
+++ b/src/components/AttachmentPicker/index.native.js
@@ -1,7 +1,7 @@
import lodashCompact from 'lodash/compact';
import PropTypes from 'prop-types';
import React, {useCallback, useMemo, useRef, useState} from 'react';
-import {Alert, View} from 'react-native';
+import {Alert, Image as RNImage, View} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
import RNDocumentPicker from 'react-native-document-picker';
import {launchImageLibrary} from 'react-native-image-picker';
@@ -57,11 +57,22 @@ const getImagePickerOptions = (type) => {
};
/**
- * See https://github.com/rnmods/react-native-document-picker#options for DocumentPicker configuration options
+ * Return documentPickerOptions based on the type
+ * @param {String} type
+ * @returns {Object}
*/
-const documentPickerOptions = {
- type: [RNDocumentPicker.types.allFiles],
- copyTo: 'cachesDirectory',
+
+const getDocumentPickerOptions = (type) => {
+ if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) {
+ return {
+ type: [RNDocumentPicker.types.images],
+ copyTo: 'cachesDirectory',
+ };
+ }
+ return {
+ type: [RNDocumentPicker.types.allFiles],
+ copyTo: 'cachesDirectory',
+ };
};
/**
@@ -158,7 +169,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
*/
const showDocumentPicker = useCallback(
() =>
- RNDocumentPicker.pick(documentPickerOptions).catch((error) => {
+ RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error) => {
if (RNDocumentPicker.isCancel(error)) {
return;
}
@@ -166,7 +177,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
showGeneralAlert(error.message);
throw error;
}),
- [showGeneralAlert],
+ [showGeneralAlert, type],
);
const menuItemData = useMemo(() => {
@@ -181,7 +192,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
textTranslationKey: 'attachmentPicker.chooseFromGallery',
pickAttachment: () => showImagePicker(launchImageLibrary),
},
- type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE && {
+ {
icon: Expensicons.Paperclip,
textTranslationKey: 'attachmentPicker.chooseDocument',
pickAttachment: showDocumentPicker,
@@ -189,7 +200,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
]);
return data;
- }, [showDocumentPicker, showImagePicker, type, shouldHideCameraOption]);
+ }, [showDocumentPicker, showImagePicker, shouldHideCameraOption]);
const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItemData.length - 1, isActive: isVisible});
@@ -232,22 +243,23 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
onCanceled.current();
return Promise.resolve();
}
-
const fileData = _.first(attachments);
-
- if (fileData.width === -1 || fileData.height === -1) {
- showImageCorruptionAlert();
- return Promise.resolve();
- }
-
- return getDataForUpload(fileData)
- .then((result) => {
- completeAttachmentSelection.current(result);
- })
- .catch((error) => {
- showGeneralAlert(error.message);
- throw error;
- });
+ RNImage.getSize(fileData.uri, (width, height) => {
+ fileData.width = width;
+ fileData.height = height;
+ if (fileData.width === -1 || fileData.height === -1) {
+ showImageCorruptionAlert();
+ return Promise.resolve();
+ }
+ return getDataForUpload(fileData)
+ .then((result) => {
+ completeAttachmentSelection.current(result);
+ })
+ .catch((error) => {
+ showGeneralAlert(error.message);
+ throw error;
+ });
+ });
},
[showGeneralAlert, showImageCorruptionAlert],
);
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index ae98978ffcad..37d0f730c9e9 100644
--- a/src/components/Form/types.ts
+++ b/src/components/Form/types.ts
@@ -7,6 +7,7 @@ import type AmountTextInput from '@components/AmountTextInput';
import type CheckboxWithLabel from '@components/CheckboxWithLabel';
import type CountrySelector from '@components/CountrySelector';
import type Picker from '@components/Picker';
+import type RadioButtons from '@components/RadioButtons';
import type SingleChoiceQuestion from '@components/SingleChoiceQuestion';
import type StatePicker from '@components/StatePicker';
import type TextInput from '@components/TextInput';
@@ -34,7 +35,8 @@ type ValidInputs =
| typeof AmountForm
| typeof BusinessTypePicker
| typeof StatePicker
- | typeof ValuePicker;
+ | typeof ValuePicker
+ | typeof RadioButtons;
type ValueTypeKey = 'string' | 'boolean' | 'date';
type ValueTypeMap = {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
index 2e47b97ec7af..43d1be85d21a 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
+import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import VideoPlayerPreview from '@components/VideoPlayerPreview';
import * as FileUtils from '@libs/fileDownload/FileUtils';
-import {parseReportRouteParams} from '@libs/ReportUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
@@ -22,22 +22,24 @@ function VideoRenderer({tnode, key}: VideoRendererProps) {
const width = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE]);
const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]);
const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]);
- const activeRoute = Navigation.getActiveRoute();
- const {reportID} = parseReportRouteParams(activeRoute);
return (
- {
- const route = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, sourceURL);
- Navigation.navigate(route);
- }}
- />
+
+ {({report}) => (
+ {
+ const route = ROUTES.REPORT_ATTACHMENTS.getRoute(report?.reportID ?? '', sourceURL);
+ Navigation.navigate(route);
+ }}
+ />
+ )}
+
);
}
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 6a2fb1c6b1f6..2a7ed30abf1a 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -145,6 +145,7 @@ import Users from '@assets/images/users.svg';
import VolumeHigh from '@assets/images/volume-high.svg';
import VolumeLow from '@assets/images/volume-low.svg';
import Wallet from '@assets/images/wallet.svg';
+import Workflows from '@assets/images/workflows.svg';
import Workspace from '@assets/images/workspace-default-avatar.svg';
import Wrench from '@assets/images/wrench.svg';
import Zoom from '@assets/images/zoom.svg';
@@ -289,6 +290,7 @@ export {
VolumeHigh,
VolumeLow,
Wallet,
+ Workflows,
Workspace,
Zoom,
Twitter,
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 299b694df3f2..e03b393dc81f 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -16,6 +16,7 @@ import JewelBoxYellow from '@assets/images/product-illustrations/jewel-box--yell
import MagicCode from '@assets/images/product-illustrations/magic-code.svg';
import MoneyEnvelopeBlue from '@assets/images/product-illustrations/money-envelope--blue.svg';
import MoneyMousePink from '@assets/images/product-illustrations/money-mouse--pink.svg';
+import MushroomTopHat from '@assets/images/product-illustrations/mushroom-top-hat.svg';
import PaymentHands from '@assets/images/product-illustrations/payment-hands.svg';
import ReceiptYellow from '@assets/images/product-illustrations/receipt--yellow.svg';
import ReceiptsSearchYellow from '@assets/images/product-illustrations/receipts-search--yellow.svg';
@@ -27,6 +28,7 @@ import TadaBlue from '@assets/images/product-illustrations/tada--blue.svg';
import TadaYellow from '@assets/images/product-illustrations/tada--yellow.svg';
import TeleScope from '@assets/images/product-illustrations/telescope.svg';
import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg';
+import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg';
import BankArrow from '@assets/images/simple-illustrations/simple-illustration__bank-arrow.svg';
import BigRocket from '@assets/images/simple-illustrations/simple-illustration__bigrocket.svg';
import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg';
@@ -57,6 +59,7 @@ import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__o
import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg';
import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg';
import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg';
+import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg';
import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg';
import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg';
import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg';
@@ -65,6 +68,8 @@ import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustrati
import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg';
import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg';
import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
+import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg';
+import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg';
export {
Abracadabra,
@@ -92,6 +97,7 @@ export {
Mailbox,
MoneyEnvelopeBlue,
MoneyMousePink,
+ MushroomTopHat,
ReceiptsSearchYellow,
ReceiptYellow,
ReceiptWrangler,
@@ -133,5 +139,9 @@ export {
LockClosed,
Gears,
QRCode,
+ ReceiptEnvelope,
+ Approval,
+ WalletAlt,
+ Workflows,
House,
};
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index ae225b3db9e9..923337ba9ada 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -52,8 +52,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
return null;
}
- const isHidden = optionItem?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
- if (isHidden && !isFocused && !optionItem?.isPinned) {
+ const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
+ const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction);
+
+ const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+
+ const shouldOverrideHidden = hasBrickError || isFocused || optionItem.isPinned;
+ if (isHidden && !shouldOverrideHidden) {
return null;
}
@@ -74,8 +79,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
- const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
- const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction);
/**
* Show the ReportActionContextMenu modal popover.
*
diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx
index 52464a1453a1..cfcd6acba41f 100644
--- a/src/components/RadioButtonWithLabel.tsx
+++ b/src/components/RadioButtonWithLabel.tsx
@@ -55,7 +55,7 @@ function RadioButtonWithLabel({LabelComponent, style, label = '', hasError = fal
accessible={false}
onPress={onPress}
style={[styles.flexRow, styles.flexWrap, styles.flexShrink1, styles.alignItemsCenter]}
- wrapperStyle={[styles.ml3, styles.pr2, styles.w100]}
+ wrapperStyle={[styles.flex1, styles.ml3, styles.pr2]}
// disable hover style when disabled
hoverDimmingValue={0.8}
pressDimmingValue={0.5}
diff --git a/src/components/RadioButtons.tsx b/src/components/RadioButtons.tsx
index 3407c5ad9afa..90c7d8580b5c 100644
--- a/src/components/RadioButtons.tsx
+++ b/src/components/RadioButtons.tsx
@@ -1,12 +1,16 @@
-import React, {useState} from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
+import React, {forwardRef, useEffect, useState} from 'react';
+import type {ForwardedRef} from 'react';
import {View} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
+import type {MaybePhraseKey} from '@libs/Localize';
+import FormHelpMessage from './FormHelpMessage';
import RadioButtonWithLabel from './RadioButtonWithLabel';
type Choice = {
label: string;
value: string;
+ style?: StyleProp;
};
type RadioButtonsProps = {
@@ -19,33 +23,55 @@ type RadioButtonsProps = {
/** Callback to fire when selecting a radio button */
onPress: (value: string) => void;
+ /** Potential error text provided by a form InputWrapper */
+ errorText?: MaybePhraseKey;
+
/** Style for radio button */
radioButtonStyle?: StyleProp;
+
+ /** Callback executed when input value changes (same as onPress, but required by FormProvider for the sake of saving drafts) */
+ onInputChange?: (value: string) => void;
+
+ /** The checked value, if you're using this component as a controlled input. */
+ value?: string;
};
-function RadioButtons({items, onPress, defaultCheckedValue = '', radioButtonStyle}: RadioButtonsProps) {
+function RadioButtons({items, onPress, defaultCheckedValue = '', radioButtonStyle, errorText, onInputChange = () => {}, value}: RadioButtonsProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const [checkedValue, setCheckedValue] = useState(defaultCheckedValue);
+ useEffect(() => {
+ if (value === checkedValue) {
+ return;
+ }
+ setCheckedValue(value ?? '');
+ }, [checkedValue, value]);
return (
-
- {items.map((item) => (
- {
- setCheckedValue(item.value);
- return onPress(item.value);
- }}
- label={item.label}
- />
- ))}
-
+ <>
+
+ {items.map((item) => (
+ {
+ setCheckedValue(item.value);
+ onInputChange(item.value);
+ return onPress(item.value);
+ }}
+ label={item.label}
+ />
+ ))}
+
+ {!!errorText && }
+ >
);
}
RadioButtons.displayName = 'RadioButtons';
export type {Choice};
-export default RadioButtons;
+export default forwardRef(RadioButtons);
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 8503ba9afa06..d768fe8e5d90 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -86,7 +86,7 @@ function MoneyRequestPreviewContent({
const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
const hasReceipt = TransactionUtils.hasReceipt(transaction);
const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction);
- const hasViolations = TransactionUtils.hasViolation(transaction, transactionViolations);
+ const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '', transactionViolations);
const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction);
const shouldShowRBR = hasViolations || hasFieldErrors;
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
@@ -152,13 +152,13 @@ function MoneyRequestPreviewContent({
let message = translate('iou.cash');
if (hasViolations && transaction) {
- const violations = TransactionUtils.getTransactionViolations(transaction, transactionViolations);
+ const violations = TransactionUtils.getTransactionViolations(transaction.transactionID, transactionViolations);
if (violations?.[0]) {
const violationMessage = ViolationsUtils.getViolationTranslation(violations[0], translate);
const isTooLong = violations.filter((v) => v.type === 'violation').length > 1 || violationMessage.length > 15;
message += ` • ${isTooLong ? translate('violations.reviewRequired') : violationMessage}`;
}
- } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport)) {
+ } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) {
message += ` • ${translate('iou.approved')}`;
} else if (iouReport?.isWaitingOnBankAccount) {
message += ` • ${translate('iou.pending')}`;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index db4d4bd2e956..5e869ac15e1e 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -145,10 +145,7 @@ function MoneyRequestView({
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true));
const {getViolationsForField} = useViolations(transactionViolations ?? []);
- const hasViolations = useCallback(
- (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0,
- [canUseViolations, getViolationsForField],
- );
+ const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]);
let amountDescription = `${translate('iou.amount')}`;
@@ -202,7 +199,7 @@ function MoneyRequestView({
const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction;
const getErrorForField = useCallback(
- (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']) => {
+ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], shouldShowViolations = true) => {
// Checks applied when creating a new money request
// NOTE: receipt field can return multiple violations, so we need to handle it separately
const fieldChecks: Partial> = {
@@ -228,8 +225,9 @@ function MoneyRequestView({
}
// Return violations if there are any
- if (canUseViolations && hasViolations(field, data)) {
- const violations = getViolationsForField(field, data);
+ // At the moment, we only return violations for tags for workspaces with single-level tags
+ if (canUseViolations && shouldShowViolations && hasViolations(field)) {
+ const violations = getViolationsForField(field);
return ViolationsUtils.getViolationTranslation(violations[0], translate);
}
@@ -400,16 +398,8 @@ function MoneyRequestView({
ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, index, transaction?.transactionID ?? '', report.reportID),
)
}
- brickRoadIndicator={
- getErrorForField('tag', {
- tagName: name,
- })
- ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
- : undefined
- }
- error={getErrorForField('tag', {
- tagName: name,
- })}
+ brickRoadIndicator={getErrorForField('tag', {}, policyTagLists.length === 1) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ error={getErrorForField('tag', {}, policyTagLists.length === 1)}
/>
))}
diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx
index c8bf783032ad..3ff844dd80e9 100644
--- a/src/components/SingleChoiceQuestion.tsx
+++ b/src/components/SingleChoiceQuestion.tsx
@@ -4,7 +4,6 @@ import React, {forwardRef} from 'react';
import type {Text as RNText} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import type {MaybePhraseKey} from '@libs/Localize';
-import FormHelpMessage from './FormHelpMessage';
import type {Choice} from './RadioButtons';
import RadioButtons from './RadioButtons';
import Text from './Text';
@@ -32,8 +31,8 @@ function SingleChoiceQuestion({prompt, errorText, possibleAnswers, currentQuesti
items={possibleAnswers}
key={currentQuestionIndex}
onPress={onInputChange}
+ errorText={errorText}
/>
-
>
);
}
diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts
index ce0f0e126252..a0f3d62c3547 100644
--- a/src/components/TextInput/BaseTextInput/types.ts
+++ b/src/components/TextInput/BaseTextInput/types.ts
@@ -51,15 +51,11 @@ type CustomBaseTextInputProps = {
/**
* Autogrow input container length based on the entered text.
- * Note: If you use this prop, the text input has to be controlled
- * by a value prop.
*/
autoGrow?: boolean;
/**
* Autogrow input container height based on the entered text
- * Note: If you use this prop, the text input has to be controlled
- * by a value prop.
*/
autoGrowHeight?: boolean;
diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js
index 23d1aec1817c..801c1b2f44ca 100644
--- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js
+++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.js
@@ -3,6 +3,7 @@ import React, {useCallback, useContext, useMemo, useState} from 'react';
import _ from 'underscore';
import * as Expensicons from '@components/Icon/Expensicons';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import fileDownload from '@libs/fileDownload';
import * as Url from '@libs/Url';
import CONST from '@src/CONST';
@@ -14,6 +15,7 @@ function VideoPopoverMenuContextProvider({children}) {
const {currentVideoPlayerRef} = usePlaybackContext();
const {translate} = useLocalize();
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]);
+ const {isOffline} = useNetwork();
const updatePlaybackSpeed = useCallback(
(speed) => {
@@ -30,32 +32,36 @@ function VideoPopoverMenuContextProvider({children}) {
});
}, [currentVideoPlayerRef]);
- const menuItems = useMemo(
- () => [
- {
+ const menuItems = useMemo(() => {
+ const items = [];
+
+ if (!isOffline) {
+ items.push({
icon: Expensicons.Download,
text: translate('common.download'),
onSelected: () => {
downloadAttachment();
},
- },
- {
- icon: Expensicons.Meter,
- text: translate('videoPlayer.playbackSpeed'),
- subMenuItems: [
- ..._.map(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS, (speed) => ({
- icon: currentPlaybackSpeed === speed ? Expensicons.Checkmark : null,
- text: speed.toString(),
- onSelected: () => {
- updatePlaybackSpeed(speed);
- },
- shouldPutLeftPaddingWhenNoIcon: true,
- })),
- ],
- },
- ],
- [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed],
- );
+ });
+ }
+
+ items.push({
+ icon: Expensicons.Meter,
+ text: translate('videoPlayer.playbackSpeed'),
+ subMenuItems: [
+ ..._.map(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS, (speed) => ({
+ icon: currentPlaybackSpeed === speed ? Expensicons.Checkmark : null,
+ text: speed.toString(),
+ onSelected: () => {
+ updatePlaybackSpeed(speed);
+ },
+ shouldPutLeftPaddingWhenNoIcon: true,
+ })),
+ ],
+ });
+
+ return items;
+ }, [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed, isOffline]);
const contextValue = useMemo(() => ({menuItems, updatePlaybackSpeed}), [menuItems, updatePlaybackSpeed]);
return {children};
diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx
index 74d10945fbcb..560576fdbf5c 100755
--- a/src/components/withKeyboardState.tsx
+++ b/src/components/withKeyboardState.tsx
@@ -8,27 +8,34 @@ import type ChildrenProps from '@src/types/utils/ChildrenProps';
type KeyboardStateContextValue = {
/** Whether the keyboard is open */
isKeyboardShown: boolean;
+
+ /** Height of the keyboard in pixels */
+ keyboardHeight: number;
};
// TODO: Remove - left for backwards compatibility with existing components (https://github.com/Expensify/App/issues/25151)
const keyboardStatePropTypes = {
/** Whether the keyboard is open */
isKeyboardShown: PropTypes.bool.isRequired,
+
+ /** Height of the keyboard in pixels */
+ keyboardHeight: PropTypes.number.isRequired,
};
const KeyboardStateContext = createContext({
isKeyboardShown: false,
+ keyboardHeight: 0,
});
function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
- const [isKeyboardShown, setIsKeyboardShown] = useState(false);
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
- const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
- setIsKeyboardShown(true);
+ const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', (e) => {
+ setKeyboardHeight(e.endCoordinates.height);
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
- setIsKeyboardShown(false);
+ setKeyboardHeight(0);
});
return () => {
@@ -39,9 +46,10 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
const contextValue = useMemo(
() => ({
- isKeyboardShown,
+ keyboardHeight,
+ isKeyboardShown: keyboardHeight !== 0,
}),
- [isKeyboardShown],
+ [keyboardHeight],
);
return {children};
}
diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts
index 1e4a6d4cf2ca..9d5e1e75d7c8 100644
--- a/src/hooks/useNetwork.ts
+++ b/src/hooks/useNetwork.ts
@@ -29,5 +29,5 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {
prevOfflineStatusRef.current = isOffline;
}, [isOffline]);
- return {isOffline};
+ return {isOffline: isOffline ?? false};
}
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index ea825b45bc0b..29b2dcb86718 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -58,19 +58,7 @@ function useViolations(violations: TransactionViolation[]) {
}
return violationGroups ?? new Map();
}, [violations]);
-
- const getViolationsForField = useCallback(
- (field: ViolationField, data?: TransactionViolation['data']) => {
- const currentViolations = violationsByField.get(field) ?? [];
-
- if (data?.tagName) {
- return currentViolations.filter((violation) => violation.data?.tagName === data.tagName);
- }
-
- return currentViolations;
- },
- [violationsByField],
- );
+ const getViolationsForField = useCallback((field: ViolationField) => violationsByField.get(field) ?? [], [violationsByField]);
return {
getViolationsForField,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index ffb764b40e6a..4d7041d4a791 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -860,7 +860,6 @@ export default {
noLogsAvailable: 'No logs available',
logSizeTooLarge: ({size}: LogSizeParams) => `Log size exceeds the limit of ${size} MB. Please use "Save log" to download the log file instead.`,
},
- goToExpensifyClassic: 'Go to Expensify Classic',
security: 'Security',
signOut: 'Sign out',
signOutConfirmationText: "You'll lose any offline changes if you sign-out.",
@@ -1030,6 +1029,25 @@ export default {
},
cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.',
},
+ workflowsPage: {
+ workflowTitle: 'Spend',
+ workflowDescription: 'Configure a workflow from the moment spend occurs, including approval and payment.',
+ delaySubmissionTitle: 'Delay submissions',
+ delaySubmissionDescription: 'Expenses are shared right away for better spend visibility. Set a slower cadence if needed.',
+ submissionFrequency: 'Submission frequency',
+ weeklyFrequency: 'Weekly',
+ monthlyFrequency: 'Monthly',
+ twiceAMonthFrequency: 'Twice a month',
+ byTripFrequency: 'By trip',
+ manuallyFrequency: 'Manually',
+ dailyFrequency: 'Daily',
+ addApprovalsTitle: 'Add approvals',
+ approver: 'Approver',
+ connectBankAccount: 'Connect bank account',
+ addApprovalsDescription: 'Require additional approval before authorizing a payment.',
+ makeOrTrackPaymentsTitle: 'Make or track payments',
+ makeOrTrackPaymentsDescription: 'Add an authorized payer for payments made in Expensify, or simply track payments made elsewhere.',
+ },
reportFraudPage: {
title: 'Report virtual card fraud',
description: 'If your virtual card details have been stolen or compromised, we’ll permanently deactivate your existing card and provide you with a new virtual card and number.',
@@ -1683,6 +1701,7 @@ export default {
workspace: {
common: {
card: 'Cards',
+ workflows: 'Workflows',
workspace: 'Workspace',
edit: 'Edit workspace',
enabled: 'Enabled',
@@ -2359,4 +2378,28 @@ export default {
mute: 'Mute',
unmute: 'Unmute',
},
+ exitSurvey: {
+ header: 'Before you go',
+ reasonPage: {
+ title: "Please tell us why you're leaving",
+ subtitle: 'Before you go, please tell us why you’d like to switch to Expensify Classic.',
+ },
+ reasons: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: "I need a feature that's only available in Expensify Classic.",
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: "I don't understand how to use New Expensify.",
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: 'I understand how to use New Expensify, but I prefer Expensify Classic.',
+ },
+ prompts: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: "What feature do you need that isn't available in New Expensify?",
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: 'What are you trying to do?',
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: 'Why do you prefer Expensify Classic?',
+ },
+ responsePlaceholder: 'Your response',
+ thankYou: 'Thanks for the feedback!',
+ thankYouSubtitle: 'Your responses will help us build a better product to get stuff done. Thank you so much!',
+ goToExpensifyClassic: 'Switch to Expensify Classic',
+ offlineTitle: "Looks like you're stuck here...",
+ offline:
+ "You appear to be offline. Unfortunately, Expensify Classic doesn't work offline, but New Expensify does. If you prefer to use Expensify Classic, try again when you have an internet connection.",
+ },
} satisfies TranslationBase;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index b03cbdd3772b..c9ff087d0de7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -196,7 +196,7 @@ export default {
iAcceptThe: 'Acepto los ',
remove: 'Eliminar',
admin: 'Administrador',
- owner: 'Poseedor',
+ owner: 'Dueño',
dateFormat: 'AAAA-MM-DD',
send: 'Enviar',
notifications: 'Notificaciones',
@@ -859,7 +859,6 @@ export default {
signOut: 'Desconectar',
signOutConfirmationText: 'Si cierras sesión perderás los cambios hechos mientras estabas desconectado',
versionLetter: 'v',
- goToExpensifyClassic: 'Ir a Expensify Classic',
readTheTermsAndPrivacy: {
phrase1: 'Leer los',
phrase2: 'Términos de Servicio',
@@ -1026,6 +1025,25 @@ export default {
},
cardDetailsLoadingFailure: 'Se ha producido un error al cargar los datos de la tarjeta. Comprueba tu conexión a Internet e inténtalo de nuevo.',
},
+ workflowsPage: {
+ workflowTitle: 'Gasto',
+ workflowDescription: 'Configure un flujo de trabajo desde el momento en que se produce el gasto, incluida la aprobación y el pago',
+ delaySubmissionTitle: 'Retrasar envíos',
+ delaySubmissionDescription: 'Los gastos se comparten de inmediato para una mejor visibilidad del gasto. Establece una cadencia más lenta si es necesario.',
+ submissionFrequency: 'Frecuencia de envíos',
+ weeklyFrequency: 'Semanal',
+ monthlyFrequency: 'Mensual',
+ twiceAMonthFrequency: 'Dos veces al mes',
+ byTripFrequency: 'Por viaje',
+ manuallyFrequency: 'Manual',
+ dailyFrequency: 'Diaria',
+ addApprovalsTitle: 'Requerir aprobaciones',
+ approver: 'Aprobador',
+ connectBankAccount: 'Conectar cuenta bancaria',
+ addApprovalsDescription: 'Requiere una aprobación adicional antes de autorizar un pago.',
+ makeOrTrackPaymentsTitle: 'Realizar o seguir pagos',
+ makeOrTrackPaymentsDescription: 'Añade un pagador autorizado para los pagos realizados en Expensify, o simplemente realiza un seguimiento de los pagos realizados en otro lugar.',
+ },
reportFraudPage: {
title: 'Reportar fraude con la tarjeta virtual',
description:
@@ -1707,6 +1725,7 @@ export default {
workspace: {
common: {
card: 'Tarjetas',
+ workflows: 'Flujos de trabajo',
workspace: 'Espacio de trabajo',
edit: 'Editar espacio de trabajo',
enabled: 'Activada',
@@ -2851,4 +2870,28 @@ export default {
mute: 'Silenciar',
unmute: 'Activar sonido',
},
+ exitSurvey: {
+ header: 'Antes de irte',
+ reasonPage: {
+ title: 'Dinos por qué te vas',
+ subtitle: 'Antes de irte, por favor dinos por qué te gustaría cambiarte a Expensify Classic.',
+ },
+ reasons: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: 'Necesito una función que sólo está disponible en Expensify Classic.',
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: 'No entiendo cómo usar New Expensify.',
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: 'Entiendo cómo usar New Expensify, pero prefiero Expensify Classic.',
+ },
+ prompts: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: '¿Qué función necesitas que no esté disponible en New Expensify?',
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: '¿Qué estás tratando de hacer?',
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: '¿Por qué prefieres Expensify Classic?',
+ },
+ responsePlaceholder: 'Su respuesta',
+ thankYou: '¡Gracias por tus comentarios!',
+ thankYouSubtitle: 'Sus respuestas nos ayudarán a crear un mejor producto para hacer las cosas bien. ¡Muchas gracias!',
+ goToExpensifyClassic: 'Cambiar a Expensify Classic',
+ offlineTitle: 'Parece que estás atrapado aquí...',
+ offline:
+ 'Parece que estás desconectado. Desafortunadamente, Expensify Classic no funciona sin conexión, pero New Expensify sí. Si prefieres utilizar Expensify Classic, inténtalo de nuevo cuando tengas conexión a internet.',
+ },
} satisfies EnglishTranslation;
diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts
index c1eb1003a698..62f90a64cf05 100644
--- a/src/libs/API/parameters/CreateDistanceRequestParams.ts
+++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts
@@ -12,6 +12,8 @@ type CreateDistanceRequestParams = {
category?: string;
tag?: string;
billable?: boolean;
+ transactionThreadReportID: string;
+ createdReportActionIDForThread: string;
};
export default CreateDistanceRequestParams;
diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts
index 983394008ba7..b55f9fd7a2a9 100644
--- a/src/libs/API/parameters/RequestMoneyParams.ts
+++ b/src/libs/API/parameters/RequestMoneyParams.ts
@@ -25,6 +25,8 @@ type RequestMoneyParams = {
taxAmount: number;
billable?: boolean;
gpsPoints?: string;
+ transactionThreadReportID: string;
+ createdReportActionIDForThread: string;
};
export default RequestMoneyParams;
diff --git a/src/libs/API/parameters/SendMoneyParams.ts b/src/libs/API/parameters/SendMoneyParams.ts
index b737ba2ea48b..ac6f42de5aa5 100644
--- a/src/libs/API/parameters/SendMoneyParams.ts
+++ b/src/libs/API/parameters/SendMoneyParams.ts
@@ -9,6 +9,8 @@ type SendMoneyParams = {
newIOUReportDetails: string;
createdReportActionID: string;
reportPreviewReportActionID: string;
+ transactionThreadReportID: string;
+ createdReportActionIDForThread: string;
};
export default SendMoneyParams;
diff --git a/src/libs/API/parameters/SetWorkspaceApprovalModeParams.ts b/src/libs/API/parameters/SetWorkspaceApprovalModeParams.ts
new file mode 100644
index 000000000000..df84fbabbf95
--- /dev/null
+++ b/src/libs/API/parameters/SetWorkspaceApprovalModeParams.ts
@@ -0,0 +1,6 @@
+type SetWorkspaceApprovalModeParams = {
+ policyID: string;
+ value: string;
+};
+
+export default SetWorkspaceApprovalModeParams;
diff --git a/src/libs/API/parameters/SetWorkspaceAutoReportingParams.ts b/src/libs/API/parameters/SetWorkspaceAutoReportingParams.ts
new file mode 100644
index 000000000000..a87817986ffa
--- /dev/null
+++ b/src/libs/API/parameters/SetWorkspaceAutoReportingParams.ts
@@ -0,0 +1,6 @@
+type SetWorkspaceAutoReportingParams = {
+ policyID: string;
+ enabled: boolean;
+};
+
+export default SetWorkspaceAutoReportingParams;
diff --git a/src/libs/API/parameters/SwitchToOldDotParams.ts b/src/libs/API/parameters/SwitchToOldDotParams.ts
new file mode 100644
index 000000000000..95449a123dc9
--- /dev/null
+++ b/src/libs/API/parameters/SwitchToOldDotParams.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type SwitchToOldDotParams = {
+ reason?: ValueOf;
+ surveyResponse?: string;
+};
+
+export default SwitchToOldDotParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 2633d795b561..0b0a81eb21f8 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -145,3 +145,6 @@ 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 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 35b03f21c841..17cc366ba3b7 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -8,6 +8,8 @@ import type UpdateBeneficialOwnersForBankAccountParams from './parameters/Update
type ApiRequest = ValueOf;
const WRITE_COMMANDS = {
+ SET_WORKSPACE_AUTO_REPORTING: 'SetWorkspaceAutoReporting',
+ SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode',
DISMISS_REFERRAL_BANNER: 'DismissReferralBanner',
UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale',
RECONNECT_APP: 'ReconnectApp',
@@ -147,6 +149,7 @@ const WRITE_COMMANDS = {
PAY_MONEY_REQUEST: 'PayMoneyRequest',
CANCEL_PAYMENT: 'CancelPayment',
ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount',
+ SWITCH_TO_OLD_DOT: 'SwitchToOldDot',
} as const;
type WriteCommand = ValueOf;
@@ -292,6 +295,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CANCEL_PAYMENT]: Parameters.CancelPaymentParams;
[WRITE_COMMANDS.ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT]: Parameters.AcceptACHContractForBankAccount;
[WRITE_COMMANDS.UPDATE_WORKSPACE_DESCRIPTION]: Parameters.UpdateWorkspaceDescriptionParams;
+ [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING]: Parameters.SetWorkspaceAutoReportingParams;
+ [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams;
+ [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams;
};
const READ_COMMANDS = {
diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts
index 5d925ae1c684..58a82de3df53 100644
--- a/src/libs/GroupChatUtils.ts
+++ b/src/libs/GroupChatUtils.ts
@@ -1,5 +1,6 @@
import type {OnyxEntry} from 'react-native-onyx';
import type {Report} from '@src/types/onyx';
+import localeCompare from './LocaleCompare';
import * as ReportUtils from './ReportUtils';
/**
@@ -11,7 +12,7 @@ function getGroupChatName(report: OnyxEntry): string | undefined {
return participants
.map((participant) => ReportUtils.getDisplayNameForParticipant(participant, isMultipleParticipantReport))
- .sort((first, second) => first?.localeCompare(second ?? '') ?? 0)
+ .sort((first, second) => localeCompare(first ?? '', second ?? ''))
.filter(Boolean)
.join(', ');
}
diff --git a/src/libs/KeyboardShortcut/index.ts b/src/libs/KeyboardShortcut/index.ts
index 44ba54953c40..0571f5e271ab 100644
--- a/src/libs/KeyboardShortcut/index.ts
+++ b/src/libs/KeyboardShortcut/index.ts
@@ -1,6 +1,7 @@
import Str from 'expensify-common/lib/str';
import * as KeyCommand from 'react-native-key-command';
import getOperatingSystem from '@libs/getOperatingSystem';
+import localeCompare from '@libs/LocaleCompare';
import CONST from '@src/CONST';
import bindHandlerToKeydownEvent from './bindHandlerToKeydownEvent';
@@ -32,7 +33,7 @@ type Shortcut = {
const documentedShortcuts: Record = {};
function getDocumentedShortcuts(): Shortcut[] {
- return Object.values(documentedShortcuts).sort((a, b) => a.displayName.localeCompare(b.displayName));
+ return Object.values(documentedShortcuts).sort((a, b) => localeCompare(a.displayName, b.displayName));
}
const keyInputEnter = KeyCommand?.constants?.keyInputEnter?.toString() ?? 'keyInputEnter';
diff --git a/src/libs/LocaleCompare.ts b/src/libs/LocaleCompare.ts
index 5142c5b43d9a..b2c48b410d32 100644
--- a/src/libs/LocaleCompare.ts
+++ b/src/libs/LocaleCompare.ts
@@ -1,19 +1,26 @@
import Onyx from 'react-native-onyx';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-const DEFAULT_LOCALE = 'en';
-
const COLLATOR_OPTIONS: Intl.CollatorOptions = {usage: 'sort', sensitivity: 'base'};
-let collator = new Intl.Collator(DEFAULT_LOCALE, COLLATOR_OPTIONS);
+let collator = new Intl.Collator(CONST.LOCALES.DEFAULT, COLLATOR_OPTIONS);
Onyx.connect({
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
callback: (locale) => {
- collator = new Intl.Collator(locale ?? DEFAULT_LOCALE, COLLATOR_OPTIONS);
+ collator = new Intl.Collator(locale ?? CONST.LOCALES.DEFAULT, COLLATOR_OPTIONS);
},
});
+/**
+ * This is a wrapper around the localeCompare function that uses the preferred locale from the user's settings.
+ *
+ * It re-uses Intl.Collator with static options for performance reasons. See https://github.com/facebook/hermes/issues/867 for more details.
+ * @param a
+ * @param b
+ * @returns -1 if a < b, 1 if a > b, 0 if a === b
+ */
function localeCompare(a: string, b: string) {
return collator.compare(a, b);
}
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index cd75a6d31fdb..2be262aa5f0f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -253,6 +253,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default as React.ComponentType,
[SCREENS.KEYBOARD_SHORTCUTS]: () => require('../../../pages/KeyboardShortcutsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: () => require('../../../pages/settings/ExitSurvey/ExitSurveyReasonPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: () => require('../../../pages/settings/ExitSurvey/ExitSurveyResponsePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: () => require('../../../pages/settings/ExitSurvey/ExitSurveyConfirmPage').default as React.ComponentType,
});
const EnablePaymentsStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index 5e14ad9fca29..262a93da9e33 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -17,6 +17,7 @@ const workspaceSettingsScreens = {
[SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType,
[SCREENS.WORKSPACE.PROFILE]: () => require('../../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType,
[SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType,
[SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.INVOICES]: () => require('../../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
index 47b646f4d150..f4316009b70b 100755
--- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
@@ -7,6 +7,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = {
[SCREENS.WORKSPACE.INITIAL]: [
SCREENS.WORKSPACE.PROFILE,
SCREENS.WORKSPACE.CARD,
+ SCREENS.WORKSPACE.WORKFLOWS,
SCREENS.WORKSPACE.REIMBURSE,
SCREENS.WORKSPACE.BILLS,
SCREENS.WORKSPACE.INVOICES,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 9428379430dd..48d649cc4dd9 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -46,6 +46,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CARD]: {
path: ROUTES.WORKSPACE_CARD.route,
},
+ [SCREENS.WORKSPACE.WORKFLOWS]: {
+ path: ROUTES.WORKSPACE_WORKFLOWS.route,
+ },
[SCREENS.WORKSPACE.REIMBURSE]: {
path: ROUTES.WORKSPACE_REIMBURSE.route,
},
@@ -270,6 +273,15 @@ const config: LinkingOptions['config'] = {
[SCREENS.SETTINGS.SHARE_CODE]: {
path: ROUTES.SETTINGS_SHARE_CODE,
},
+ [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: {
+ path: ROUTES.SETTINGS_EXIT_SURVEY_REASON,
+ },
+ [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: {
+ path: ROUTES.SETTINGS_EXIT_SURVEY_RESPONSE.route,
+ },
+ [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: {
+ path: ROUTES.SETTINGS_EXIT_SURVEY_CONFIRM.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 1aae7dae1a7f..f02bb3bd2aca 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -15,6 +15,7 @@ import type CONST from '@src/CONST';
import type NAVIGATORS from '@src/NAVIGATORS';
import type {HybridAppRoute, Route as Routes} from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
+import type EXIT_SURVEY_REASON_FORM_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
type NavigationRef = NavigationContainerRefWithCurrent;
@@ -59,6 +60,9 @@ type CentralPaneNavigatorParamList = {
[SCREENS.WORKSPACE.CARD]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.WORKFLOWS]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.REIMBURSE]: {
policyID: string;
};
@@ -177,6 +181,14 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: undefined;
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: undefined;
[SCREENS.KEYBOARD_SHORTCUTS]: undefined;
+ [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: undefined;
+ [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: {
+ [EXIT_SURVEY_REASON_FORM_INPUT_IDS.REASON]: ValueOf;
+ backTo: Routes;
+ };
+ [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: {
+ backTo: Routes;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
diff --git a/src/libs/NumberUtils.ts b/src/libs/NumberUtils.ts
index d7eb87a2ed1e..60e5246f5ed2 100644
--- a/src/libs/NumberUtils.ts
+++ b/src/libs/NumberUtils.ts
@@ -69,4 +69,11 @@ function parseFloatAnyLocale(value: string): number {
return parseFloat(value ? value.replace(',', '.') : value);
}
-export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale};
+/**
+ * Given an input number p and another number q, returns the largest number that's less than p and divisible by q.
+ */
+function roundDownToLargestMultiple(p: number, q: number) {
+ return Math.floor(p / q) * q;
+}
+
+export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale, roundDownToLargestMultiple};
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 97b4fc0144c8..8698cd21729c 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -34,6 +34,7 @@ import times from '@src/utils/times';
import Timing from './actions/Timing';
import * as CollectionUtils from './CollectionUtils';
import * as ErrorUtils from './ErrorUtils';
+import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import * as LoginUtils from './LoginUtils';
@@ -871,9 +872,9 @@ function sortTags(tags: Record | Tag[]) {
let sortedTags;
if (Array.isArray(tags)) {
- sortedTags = tags.sort((a, b) => a.name.localeCompare(b.name));
+ sortedTags = tags.sort((a, b) => localeCompare(a.name, b.name));
} else {
- sortedTags = Object.values(tags).sort((a, b) => a.name.localeCompare(b.name));
+ sortedTags = Object.values(tags).sort((a, b) => localeCompare(a.name, b.name));
}
return sortedTags;
@@ -1061,7 +1062,8 @@ function getTagsOptions(tags: Category[]): Option[] {
function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) {
const tagSections = [];
const sortedTags = sortTags(tags);
- const enabledTags = sortedTags.filter((tag) => tag.enabled);
+ const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
+ const enabledTags = [...selectedOptions, ...sortedTags.filter((tag) => tag.enabled && !selectedOptionNames.includes(tag.name))];
const numberOfTags = enabledTags.length;
let indexOffset = 0;
@@ -1109,7 +1111,6 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt
return tagSections;
}
- const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
const filteredRecentlyUsedTags = recentlyUsedTags
.filter((recentlyUsedTag) => {
const tagObject = tags.find((tag) => tag.name === recentlyUsedTag);
@@ -1119,13 +1120,11 @@ function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOpt
const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name));
if (selectedOptions.length) {
- const selectedTagOptions = selectedOptions.map((option) => {
- const tagObject = tags.find((tag) => tag.name === option.name);
- return {
- name: option.name,
- enabled: !!tagObject?.enabled,
- };
- });
+ const selectedTagOptions = selectedOptions.map((option) => ({
+ name: option.name,
+ // Should be marked as enabled to be able to unselect even though the selected category is disabled
+ enabled: true,
+ }));
tagSections.push({
// "Selected" section
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 974ce88a03ec..70f87a8c7373 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -3,9 +3,11 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import Navigation from './Navigation/Navigation';
type MemberEmailsToAccountIDs = Record;
type UnitRate = {rate: number};
@@ -250,6 +252,13 @@ function getPolicyMembersByIdWithoutCurrentUser(policyMembers: OnyxCollection): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED;
}
-function isDeletedAction(reportAction: OnyxEntry): boolean {
+function isDeletedAction(reportAction: OnyxEntry): boolean {
// A deleted comment has either an empty array or an object with html field with empty string as value
const message = reportAction?.message ?? [];
return message.length === 0 || message[0]?.html === '';
@@ -103,8 +104,8 @@ function isDeletedParentAction(reportAction: OnyxEntry): boolean {
return (reportAction?.message?.[0]?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0;
}
-function isReversedTransaction(reportAction: OnyxEntry) {
- return (reportAction?.message?.[0]?.isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0;
+function isReversedTransaction(reportAction: OnyxEntry) {
+ return (reportAction?.message?.[0]?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0;
}
function isPendingRemove(reportAction: OnyxEntry | EmptyObject): boolean {
@@ -184,9 +185,11 @@ function getParentReportAction(report: OnyxEntry | EmptyObject): ReportA
/**
* Determines if the given report action is sent money report action by checking for 'pay' type and presence of IOUDetails object.
*/
-function isSentMoneyReportAction(reportAction: OnyxEntry): boolean {
+function isSentMoneyReportAction(reportAction: OnyxEntry): boolean {
return (
- reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!reportAction?.originalMessage?.IOUDetails
+ reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
+ (reportAction?.originalMessage as IOUMessage)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY &&
+ !!(reportAction?.originalMessage as IOUMessage)?.IOUDetails
);
}
@@ -517,7 +520,7 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null):
* to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp).
* This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY.
*/
-function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] {
+function getSortedReportActionsForDisplay(reportActions: ReportActions | ReportAction[] | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] {
const filteredReportActions = Object.entries(reportActions ?? {})
.filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key))
.map((entry) => entry[1]);
@@ -800,14 +803,6 @@ function getMemberChangeMessageFragment(reportAction: OnyxEntry):
};
}
-/**
- * MARKEDREIMBURSED reportActions come from marking a report as reimbursed in OldDot. For now, we just
- * concat all of the text elements of the message to create the full message.
- */
-function getMarkedReimbursedMessage(reportAction: OnyxEntry): string {
- return reportAction?.message?.map((element) => element.text).join('') ?? '';
-}
-
function getMemberChangeMessagePlainText(reportAction: OnyxEntry): string {
const messageElements = getMemberChangeMessageElements(reportAction);
return messageElements.map((element) => element.content).join('');
@@ -935,7 +930,6 @@ export {
hasRequestFromCurrentAccount,
getFirstVisibleReportActionID,
isMemberChangeAction,
- getMarkedReimbursedMessage,
getMemberChangeMessageFragment,
getMemberChangeMessagePlainText,
isReimbursementDeQueuedAction,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ae6e02e70d29..2daf337ddb80 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -52,6 +52,7 @@ import * as CollectionUtils from './CollectionUtils';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import isReportMessageAttachment from './isReportMessageAttachment';
+import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import linkingConfig from './Navigation/linkingConfig';
@@ -722,12 +723,10 @@ function hasParticipantInArray(report: Report, policyMemberAccountIDs: number[])
/**
* Whether the Money Request report is settled
*/
-function isSettled(reportOrID: Report | OnyxEntry | string | undefined): boolean {
- if (!allReports || !reportOrID) {
+function isSettled(reportID: string | undefined): boolean {
+ if (!allReports || !reportID) {
return false;
}
- const reportID = typeof reportOrID === 'string' ? reportOrID : reportOrID?.reportID;
-
const report: Report | EmptyObject = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {};
if (isEmptyObject(report) || report.isWaitingOnBankAccount) {
return false;
@@ -1443,7 +1442,7 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo
const sortedParticipantDetails = participantDetails.sort((first, second) => {
// First sort by displayName/login
- const displayNameLoginOrder = first[1].localeCompare(second[1]);
+ const displayNameLoginOrder = localeCompare(first[1], second[1]);
if (displayNameLoginOrder !== 0) {
return displayNameLoginOrder;
}
@@ -1701,7 +1700,7 @@ function getDisplayNamesWithTooltips(
})
.sort((first, second) => {
// First sort by displayName/login
- const displayNameLoginOrder = first.displayName.localeCompare(second.displayName);
+ const displayNameLoginOrder = localeCompare(first.displayName, second.displayName);
if (displayNameLoginOrder !== 0) {
return displayNameLoginOrder;
}
@@ -2259,7 +2258,7 @@ function hasMissingSmartscanFields(iouReportID: string): boolean {
/**
* Given a parent IOU report action get report name for the LHN.
*/
-function getTransactionReportName(reportAction: OnyxEntry): string {
+function getTransactionReportName(reportAction: OnyxEntry): string {
if (ReportActionsUtils.isReversedTransaction(reportAction)) {
return Localize.translateLocal('parentReportAction.reversedTransaction');
}
@@ -3763,7 +3762,7 @@ function buildOptimisticTaskReport(
*
* @param moneyRequestReportID - the reportID which the report action belong to
*/
-function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string): OptimisticChatReport {
+function buildTransactionThread(reportAction: OnyxEntry, moneyRequestReportID: string): OptimisticChatReport {
const participantAccountIDs = [...new Set([currentUserAccountID, Number(reportAction?.actorAccountID)])].filter(Boolean) as number[];
return buildOptimisticChatReport(
participantAccountIDs,
@@ -3967,6 +3966,13 @@ function shouldReportBeInOptionList({
return true;
}
+ const reportIsSettled = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
+
+ // Always show IOU reports with violations unless they are reimbursed
+ if (isExpenseRequest(report) && doesReportHaveViolations && !reportIsSettled) {
+ return true;
+ }
+
// Hide only chat threads that haven't been commented on (other threads are actionable)
if (isChatThread(report) && canHideReport && isEmptyChat) {
return false;
@@ -3978,11 +3984,6 @@ function shouldReportBeInOptionList({
return true;
}
- // Always show IOU reports with violations
- if (isExpenseRequest(report) && doesReportHaveViolations) {
- return true;
- }
-
// All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones
if (isInGSDMode) {
return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE;
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 5e7cb85fe528..ee0ec6d9755b 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -5,6 +5,7 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {RecentWaypoint, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
+import type {IOUMessage} from '@src/types/onyx/OriginalMessage';
import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates';
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
@@ -12,6 +13,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {isCorporateCard, isExpensifyCard} from './CardUtils';
import DateUtils from './DateUtils';
import * as NumberUtils from './NumberUtils';
+import type {OptimisticIOUReportAction} from './ReportUtils';
let allTransactions: OnyxCollection = {};
@@ -495,11 +497,11 @@ function hasRoute(transaction: Transaction): boolean {
*
* @deprecated Use Onyx.connect() or withOnyx() instead
*/
-function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject {
+function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject {
let transactionID = '';
if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) {
- transactionID = reportAction.originalMessage?.IOUTransactionID ?? '';
+ transactionID = (reportAction?.originalMessage as IOUMessage)?.IOUTransactionID ?? '';
}
return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {};
@@ -585,20 +587,11 @@ function isOnHold(transaction: OnyxEntry): boolean {
/**
* Checks if any violations for the provided transaction are of type 'violation'
*/
-function hasViolation(transactionOrID: Transaction | OnyxEntry | string, transactionViolations: OnyxCollection): boolean {
- if (!transactionOrID) {
- return false;
- }
- const transactionID = typeof transactionOrID === 'string' ? transactionOrID : transactionOrID.transactionID;
+function hasViolation(transactionID: string, transactionViolations: OnyxCollection): boolean {
return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation'));
}
-function getTransactionViolations(transactionOrID: OnyxEntry | string, transactionViolations: OnyxCollection): TransactionViolation[] | null {
- if (!transactionOrID) {
- return null;
- }
- const transactionID = typeof transactionOrID === 'string' ? transactionOrID : transactionOrID.transactionID;
-
+function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null {
return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null;
}
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index 16b428549156..6153ea62cd0d 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -2,7 +2,6 @@ import reject from 'lodash/reject';
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import type {Phrase, PhraseParameters} from '@libs/Localize';
-import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -31,7 +30,7 @@ const ViolationsUtils = {
// Add 'categoryOutOfPolicy' violation if category is not in policy
if (!hasCategoryOutOfPolicyViolation && categoryKey && !isCategoryInPolicy) {
- newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation', userMessage: ''});
+ newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation'});
}
// Remove 'categoryOutOfPolicy' violation if category is in policy
@@ -46,72 +45,40 @@ const ViolationsUtils = {
// Add 'missingCategory' violation if category is required and not set
if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey) {
- newTransactionViolations.push({name: 'missingCategory', type: 'violation', userMessage: ''});
+ newTransactionViolations.push({name: 'missingCategory', type: 'violation'});
}
}
if (policyRequiresTags) {
- const selectedTags = TransactionUtils.getTagArrayFromName(updatedTransaction.tag ?? '') ?? [];
const policyTagKeys = Object.keys(policyTagList);
- if (policyTagKeys.length === 0) {
- newTransactionViolations.push({
- name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY,
- type: 'violation',
- userMessage: '',
- });
- }
-
- policyTagKeys.forEach((key, index) => {
- const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY && violation.data?.tagName === key);
- const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_TAG && violation.data?.tagName === key);
- const selectedTag = selectedTags[index];
- const isTagInPolicy = Boolean(policyTagList[key]?.tags[selectedTag]?.enabled);
+ // At the moment, we only return violations for tags for workspaces with single-level tags
+ if (policyTagKeys.length === 1) {
+ const policyTagListName = policyTagKeys[0];
+ const policyTags = policyTagList[policyTagListName]?.tags;
+ const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY);
+ const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_TAG);
+ const isTagInPolicy = policyTags ? !!policyTags[updatedTransaction.tag ?? '']?.enabled : false;
// Add 'tagOutOfPolicy' violation if tag is not in policy
- if (!hasTagOutOfPolicyViolation && selectedTag && !isTagInPolicy) {
- newTransactionViolations.push({
- name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY,
- type: 'violation',
- userMessage: '',
- data: {
- tagName: key,
- },
- });
+ if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) {
+ newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: 'violation'});
}
// Remove 'tagOutOfPolicy' violation if tag is in policy
- if (hasTagOutOfPolicyViolation && selectedTag && isTagInPolicy) {
- newTransactionViolations = reject(newTransactionViolations, {
- name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY,
- data: {
- tagName: key,
- },
- });
+ if (hasTagOutOfPolicyViolation && updatedTransaction.tag && isTagInPolicy) {
+ newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY});
}
// Remove 'missingTag' violation if tag is valid according to policy
if (hasMissingTagViolation && isTagInPolicy) {
- newTransactionViolations = reject(newTransactionViolations, {
- name: CONST.VIOLATIONS.MISSING_TAG,
- data: {
- tagName: key,
- },
- });
+ newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.MISSING_TAG});
}
-
// Add 'missingTag violation' if tag is required and not set
- if (!hasMissingTagViolation && !selectedTag && policyRequiresTags) {
- newTransactionViolations.push({
- name: CONST.VIOLATIONS.MISSING_TAG,
- type: 'violation',
- userMessage: '',
- data: {
- tagName: key,
- },
- });
+ if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) {
+ newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'});
}
- });
+ }
}
return {
diff --git a/src/libs/actions/ExitSurvey.ts b/src/libs/actions/ExitSurvey.ts
new file mode 100644
index 000000000000..ef3ecd6d3e31
--- /dev/null
+++ b/src/libs/actions/ExitSurvey.ts
@@ -0,0 +1,78 @@
+import type {OnyxUpdate} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import * as API from '@libs/API';
+import ONYXKEYS from '@src/ONYXKEYS';
+import REASON_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
+import type {ExitReason} from '@src/types/form/ExitSurveyReasonForm';
+import RESPONSE_INPUT_IDS from '@src/types/form/ExitSurveyResponseForm';
+
+let exitReason: ExitReason | undefined;
+let exitSurveyResponse: string | undefined;
+Onyx.connect({
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM,
+ callback: (value) => (exitReason = value?.[REASON_INPUT_IDS.REASON]),
+});
+Onyx.connect({
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM,
+ callback: (value) => (exitSurveyResponse = value?.[RESPONSE_INPUT_IDS.RESPONSE]),
+});
+
+function saveExitReason(reason: ExitReason) {
+ Onyx.set(ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM, {[REASON_INPUT_IDS.REASON]: reason});
+}
+
+function saveResponse(response: string) {
+ Onyx.set(ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM, {[RESPONSE_INPUT_IDS.RESPONSE]: response});
+}
+
+/**
+ * Save the user's response to the mandatory exit survey in the back-end.
+ */
+function switchToOldDot() {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.IS_SWITCHING_TO_OLD_DOT,
+ value: true,
+ },
+ ];
+
+ const finallyData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.IS_SWITCHING_TO_OLD_DOT,
+ value: false,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM_DRAFT,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM_DRAFT,
+ value: null,
+ },
+ ];
+
+ API.write(
+ 'SwitchToOldDot',
+ {
+ reason: exitReason,
+ surveyResponse: exitSurveyResponse,
+ },
+ {optimisticData, finallyData},
+ );
+}
+
+export {saveExitReason, saveResponse, switchToOldDot};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 8a83a9a2cd81..37308c73e724 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -76,6 +76,8 @@ type MoneyRequestInformation = {
createdChatReportActionID: string;
createdIOUReportActionID: string;
reportPreviewAction: OnyxTypes.ReportAction;
+ transactionThreadReportID: string;
+ createdReportActionIDForThread: string;
onyxData: OnyxData;
};
@@ -390,6 +392,8 @@ function buildOnyxDataForMoneyRequest(
optimisticPolicyRecentlyUsedCategories: string[],
optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags,
isNewChatReport: boolean,
+ transactionThreadReport: OptimisticChatReport,
+ transactionThreadCreatedReportAction: OptimisticCreatedReportAction,
shouldCreateNewMoneyRequestReport: boolean,
policy?: OnyxEntry,
policyTagList?: OnyxEntry,
@@ -469,6 +473,19 @@ function buildOnyxDataForMoneyRequest(
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ value: transactionThreadReport,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ value: {
+ [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction,
+ },
+ },
+
// Remove the temporary transaction used during the creation flow
{
onyxMethod: Onyx.METHOD.SET,
@@ -531,6 +548,14 @@ function buildOnyxDataForMoneyRequest(
errorFields: null,
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ value: {
+ pendingFields: null,
+ errorFields: null,
+ },
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
@@ -575,6 +600,16 @@ function buildOnyxDataForMoneyRequest(
},
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ value: {
+ [transactionThreadCreatedReportAction.reportActionID]: {
+ pendingAction: null,
+ errors: null,
+ },
+ },
+ },
);
const failureData: OnyxUpdate[] = [
@@ -605,6 +640,15 @@ function buildOnyxDataForMoneyRequest(
},
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ value: {
+ errorFields: {
+ createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
+ },
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
@@ -614,14 +658,6 @@ function buildOnyxDataForMoneyRequest(
pendingFields: clearedPendingFields,
},
},
-
- // Remove the temporary transaction used during the creation flow
- {
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`,
- value: null,
- },
-
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
@@ -671,6 +707,15 @@ function buildOnyxDataForMoneyRequest(
}),
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ value: {
+ [transactionThreadCreatedReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ },
];
// We don't need to compute violations unless we're on a paid policy
@@ -825,7 +870,8 @@ function getMoneyRequestInformation(
// 1. CREATED action for the chatReport
// 2. CREATED action for the iouReport
// 3. IOU action for the iouReport
- // 4. REPORTPREVIEW action for the chatReport
+ // 4. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread
+ // 5. REPORTPREVIEW action for the chatReport
// Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat
const currentTime = DateUtils.getDBTime();
const optimisticCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail);
@@ -845,6 +891,8 @@ function getMoneyRequestInformation(
false,
currentTime,
);
+ const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, iouReport.reportID);
+ const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail);
let reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
if (reportPreviewAction) {
@@ -888,6 +936,8 @@ function getMoneyRequestInformation(
optimisticPolicyRecentlyUsedCategories,
optimisticPolicyRecentlyUsedTags,
isNewChatReport,
+ optimisticTransactionThread,
+ optimisticCreatedActionForTransactionThread,
shouldCreateNewMoneyRequestReport,
policy,
policyTagList,
@@ -906,6 +956,8 @@ function getMoneyRequestInformation(
createdChatReportActionID: isNewChatReport ? optimisticCreatedActionForChat.reportActionID : '0',
createdIOUReportActionID: shouldCreateNewMoneyRequestReport ? optimisticCreatedActionForIOU.reportActionID : '0',
reportPreviewAction,
+ transactionThreadReportID: optimisticTransactionThread.reportID,
+ createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID,
onyxData: {
optimisticData,
successData,
@@ -941,7 +993,18 @@ function createDistanceRequest(
source: ReceiptGeneric as ReceiptSource,
state: CONST.IOU.RECEIPT_STATE.OPEN,
};
- const {iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation(
+ const {
+ iouReport,
+ chatReport,
+ transaction,
+ iouAction,
+ createdChatReportActionID,
+ createdIOUReportActionID,
+ reportPreviewAction,
+ transactionThreadReportID,
+ createdReportActionIDForThread,
+ onyxData,
+ } = getMoneyRequestInformation(
currentChatReport,
participant,
comment,
@@ -976,6 +1039,8 @@ function createDistanceRequest(
category,
tag,
billable,
+ transactionThreadReportID,
+ createdReportActionIDForThread,
};
API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData);
@@ -1440,27 +1505,39 @@ function requestMoney(
const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report;
const moneyRequestReportID = isMoneyRequestReport ? report.reportID : '';
const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
- const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} =
- getMoneyRequestInformation(
- currentChatReport,
- participant,
- comment,
- amount,
- currency,
- currentCreated,
- merchant,
- receipt,
- undefined,
- category,
- tag,
- billable,
- policy,
- policyTagList,
- policyCategories,
- payeeAccountID,
- payeeEmail,
- moneyRequestReportID,
- );
+ const {
+ payerAccountID,
+ payerEmail,
+ iouReport,
+ chatReport,
+ transaction,
+ iouAction,
+ createdChatReportActionID,
+ createdIOUReportActionID,
+ reportPreviewAction,
+ transactionThreadReportID,
+ createdReportActionIDForThread,
+ onyxData,
+ } = getMoneyRequestInformation(
+ currentChatReport,
+ participant,
+ comment,
+ amount,
+ currency,
+ currentCreated,
+ merchant,
+ receipt,
+ undefined,
+ category,
+ tag,
+ billable,
+ policy,
+ policyTagList,
+ policyCategories,
+ payeeAccountID,
+ payeeEmail,
+ moneyRequestReportID,
+ );
const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID;
const parameters: RequestMoneyParams = {
@@ -1485,9 +1562,10 @@ function requestMoney(
taxCode,
taxAmount,
billable,
-
// This needs to be a string of JSON because of limitations with the fetch() API and nested objects
gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined,
+ transactionThreadReportID,
+ createdReportActionIDForThread,
};
API.write(WRITE_COMMANDS.REQUEST_MONEY, parameters, onyxData);
@@ -1607,6 +1685,11 @@ function createSplitsAndOnyxData(
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`,
value: splitTransaction,
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`,
+ value: null,
+ },
];
const successData: OnyxUpdate[] = [
@@ -1623,11 +1706,6 @@ function createSplitsAndOnyxData(
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`,
value: {pendingAction: null},
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`,
- value: null,
- },
];
if (!existingSplitChatReport) {
@@ -1646,11 +1724,6 @@ function createSplitsAndOnyxData(
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`,
- value: null,
- },
];
if (existingSplitChatReport) {
@@ -1815,6 +1888,10 @@ function createSplitsAndOnyxData(
// Add tag to optimistic policy recently used tags when a participant is a workspace
const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag) : {};
+ // Create optimistic transactionThread
+ const optimisticTransactionThread = ReportUtils.buildTransactionThread(oneOnOneIOUAction, oneOnOneIOUReport.reportID);
+ const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
+
// STEP 5: Build Onyx Data
const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest(
oneOnOneChatReport,
@@ -1828,6 +1905,8 @@ function createSplitsAndOnyxData(
optimisticPolicyRecentlyUsedCategories,
optimisticPolicyRecentlyUsedTags,
isNewOneOnOneChatReport,
+ optimisticTransactionThread,
+ optimisticCreatedActionForTransactionThread,
shouldCreateNewOneOnOneIOUReport,
);
@@ -1842,6 +1921,8 @@ function createSplitsAndOnyxData(
createdChatReportActionID: oneOnOneCreatedActionForChat.reportActionID,
createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID,
reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID,
+ transactionThreadReportID: optimisticTransactionThread.reportID,
+ createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID,
};
splits.push(individualSplit);
@@ -2412,6 +2493,9 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
oneOnOneReportPreviewAction = ReportUtils.buildOptimisticReportPreview(oneOnOneChatReport, oneOnOneIOUReport, '', oneOnOneTransaction);
}
+ const optimisticTransactionThread = ReportUtils.buildTransactionThread(oneOnOneIOUAction, oneOnOneIOUReport.reportID);
+ const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
+
const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest(
oneOnOneChatReport,
oneOnOneIOUReport,
@@ -2424,6 +2508,8 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
[],
{},
isNewOneOnOneChatReport,
+ optimisticTransactionThread,
+ optimisticCreatedActionForTransactionThread,
shouldCreateNewOneOnOneIOUReport,
);
@@ -2438,6 +2524,8 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
createdChatReportActionID: oneOnOneCreatedActionForChat.reportActionID,
createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID,
reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID,
+ transactionThreadReportID: optimisticTransactionThread.reportID,
+ createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID,
});
optimisticData.push(...oneOnOneOptimisticData);
@@ -3124,6 +3212,9 @@ function getSendMoneyParams(
const reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, optimisticIOUReport);
+ const optimisticTransactionThread = ReportUtils.buildTransactionThread(optimisticIOUReportAction, optimisticIOUReport.reportID);
+ const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail);
+
// Change the method to set for new reports because it doesn't exist yet, is faster,
// and we need the data to be available when we navigate to the chat page
const optimisticChatReportData: OnyxUpdate = isNewChat
@@ -3156,6 +3247,11 @@ function getSendMoneyParams(
lastMessageHtml: optimisticIOUReportAction.message?.[0].html,
},
};
+ const optimisticTransactionThreadData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
+ value: optimisticTransactionThread,
+ };
const optimisticIOUReportActionsData: OnyxUpdate = {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`,
@@ -3173,6 +3269,13 @@ function getSendMoneyParams(
[reportPreviewAction.reportActionID]: reportPreviewAction,
},
};
+ const optimisticTransactionThreadReportActionsData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
+ value: {
+ [optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread,
+ },
+ };
const successData: OnyxUpdate[] = [
{
@@ -3198,6 +3301,15 @@ function getSendMoneyParams(
},
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
+ value: {
+ [optimisticCreatedActionForTransactionThread.reportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
];
const failureData: OnyxUpdate[] = [
@@ -3208,6 +3320,24 @@ function getSendMoneyParams(
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
+ value: {
+ errorFields: {
+ createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
+ value: {
+ [optimisticCreatedActionForTransactionThread.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ },
];
let optimisticPersonalDetailListData: OnyxUpdate | EmptyObject = {};
@@ -3272,7 +3402,16 @@ function getSendMoneyParams(
});
}
- const optimisticData: OnyxUpdate[] = [optimisticChatReportData, optimisticIOUReportData, optimisticChatReportActionsData, optimisticIOUReportActionsData, optimisticTransactionData];
+ const optimisticData: OnyxUpdate[] = [
+ optimisticChatReportData,
+ optimisticIOUReportData,
+ optimisticChatReportActionsData,
+ optimisticIOUReportActionsData,
+ optimisticTransactionData,
+ optimisticTransactionThreadData,
+ optimisticTransactionThreadReportActionsData,
+ ];
+
if (!isEmptyObject(optimisticPersonalDetailListData)) {
optimisticData.push(optimisticPersonalDetailListData);
}
@@ -3287,6 +3426,8 @@ function getSendMoneyParams(
newIOUReportDetails,
createdReportActionID: isNewChat ? optimisticCreatedAction.reportActionID : '0',
reportPreviewReportActionID: reportPreviewAction.reportActionID,
+ transactionThreadReportID: optimisticTransactionThread.reportID,
+ createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID,
},
optimisticData,
successData,
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 874ddb6804d7..57cd4a6fc071 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -6,6 +6,7 @@ import lodashClone from 'lodash/clone';
import lodashUnion from 'lodash/union';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import * as API from '@libs/API';
import type {
AddMembersToWorkspaceParams,
@@ -19,6 +20,8 @@ import type {
OpenWorkspaceMembersPageParams,
OpenWorkspaceParams,
OpenWorkspaceReimburseViewParams,
+ SetWorkspaceApprovalModeParams,
+ SetWorkspaceAutoReportingParams,
UpdateWorkspaceAvatarParams,
UpdateWorkspaceCustomUnitAndRateParams,
UpdateWorkspaceDescriptionParams,
@@ -381,6 +384,87 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[]
return announceRoomMembers;
}
+function setWorkspaceAutoReporting(policyID: string, enabled: boolean) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ autoReporting: enabled,
+ pendingFields: {isAutoApprovalEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ autoReporting: !enabled,
+ pendingFields: {isAutoApprovalEnabled: null},
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {isAutoApprovalEnabled: null},
+ },
+ },
+ ];
+
+ const params: SetWorkspaceAutoReportingParams = {policyID, enabled};
+ API.write(WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING, params, {optimisticData, failureData, successData});
+}
+
+function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMode: ValueOf) {
+ const isAutoApprovalEnabled = approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC;
+
+ const value = {
+ approver,
+ approvalMode,
+ isAutoApprovalEnabled,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ ...value,
+ pendingFields: {approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {approvalMode: null},
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {approvalMode: null},
+ },
+ },
+ ];
+
+ const params: SetWorkspaceApprovalModeParams = {policyID, value: JSON.stringify(value)};
+ API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData});
+}
+
/**
* Build optimistic data for removing users from the announcement room
*/
@@ -2134,5 +2218,7 @@ export {
buildOptimisticPolicyRecentlyUsedTags,
createDraftInitialWorkspace,
setWorkspaceInviteMessageDraft,
+ setWorkspaceAutoReporting,
+ setWorkspaceApprovalMode,
updateWorkspaceDescription,
};
diff --git a/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx
index bb352acd4732..ed00fbcff422 100644
--- a/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx
+++ b/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx
@@ -136,7 +136,6 @@ function BankInfo({reimbursementAccount, reimbursementAccountDraft, plaidLinkTok
testID={BankInfo.displayName}
includeSafeAreaPaddingBottom={false}
shouldEnablePickerAvoiding={false}
- shouldEnableMaxHeight
>
)}
{!requiresTwoFactorAuth && (
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
index 9eae888bdd74..b4272f094071 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
@@ -8,6 +8,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -34,6 +35,7 @@ const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.PER
function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}: ConfirmationProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
const isLoading = reimbursementAccount?.isLoading ?? false;
const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);
@@ -112,6 +114,7 @@ function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext,
/>
)}