+
+The primary email address on your Expensify account is the email that receives email updates and notifications for your account. You can add a secondary email address in order to
+- Change your primary email to a new one.
+- Connect your personal email address as a secondary login if your primary email address is one from your employer. This allows you to always have access to your Expensify account, even if your employer changes.
+
+{% include info.html %}
+Before you can remove a primary email address, you must add a new one to your Expensify account and make it the primary using the steps below. Email addresses must be added as a secondary login before they can be made the primary.
+{% include end-info.html %}
+
+*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.*
+
+1. Hover over Settings, then click **Account**.
+2. Under the Account Details tab, scroll down to the Secondary Logins section and click **Add Secondary Login**.
+3. Enter the email address or phone number you wish to use as a secondary login. For phone numbers, be sure to include the international code, if applicable.
+4. Find the email or text message from Expensify containing the Magic Code and enter it into the field.
+5. To make the new email address the primary address for your account, click **Make Primary**.
+
+You can keep both logins, or you can click **Remove** next to the old email address to delete it from your account.
+
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 4ed309467f13..097c0ad2679e 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -46,8 +46,8 @@ https://community.expensify.com/discussion/5366/deep-dive-troubleshooting-credit
https://community.expensify.com/discussion/9554/how-to-set-up-global-reimbursemen,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements
https://community.expensify.com/discussion/4463/how-to-remove-or-manage-settings-for-imported-personal-cards,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards
https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards
-https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription
-https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription
+https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://use.expensify.com/
+https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager
https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/getting-started/Employees,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace
@@ -60,3 +60,8 @@ https://help.expensify.com/articles/expensify-classic/account-settings/Preferenc
https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
https://help.expensify.com/articles/expensify-classic/getting-started/Individual-Users,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself
https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription,https://use.expensify.com/
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://use.expensify.com/
+https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://use.expensify.com/
+https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
+https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 5d93786a5ad6..bccea916e01a 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.4.47
+ 1.4.49CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.47.7
+ 1.4.49.0ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index fad0a170d4ab..058476f03a9d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 1.4.47
+ 1.4.49CFBundleSignature????CFBundleVersion
- 1.4.47.7
+ 1.4.49.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 220fdd322c6e..869b3aebab44 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 1.4.47
+ 1.4.49CFBundleVersion
- 1.4.47.7
+ 1.4.49.0NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index cc717e8d6a0f..bfab80fe3148 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.47-7",
+ "version": "1.4.49-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.47-7",
+ "version": "1.4.49-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 5b498cb09dc2..a78c23c3c960 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.47-7",
+ "version": "1.4.49-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 1e3b33d5d760..70fecab70c39 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -6,6 +6,12 @@ import * as KeyCommand from 'react-native-key-command';
import * as Url from './libs/Url';
import SCREENS from './SCREENS';
+type RateAndUnit = {
+ unit: string;
+ rate: number;
+};
+type CurrencyDefaultMileageRate = Record;
+
// Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types.
// Freezing the array ensures that it cannot be unintentionally modified.
const EMPTY_ARRAY = Object.freeze([]);
@@ -313,6 +319,7 @@ const CONST = {
BETA_COMMENT_LINKING: 'commentLinking',
VIOLATIONS: 'violations',
REPORT_FIELDS: 'reportFields',
+ P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
},
BUTTON_STATES: {
@@ -568,6 +575,7 @@ const CONST = {
LIMIT: 50,
TYPE: {
ADDCOMMENT: 'ADDCOMMENT',
+ ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST',
APPROVED: 'APPROVED',
CHRONOSOOOLIST: 'CHRONOSOOOLIST',
CLOSED: 'CLOSED',
@@ -671,6 +679,10 @@ const CONST = {
INVITE: 'invited',
NOTHING: 'nothing',
},
+ ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION: {
+ ACCEPT: 'accept',
+ DECLINE: 'decline',
+ },
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
@@ -1414,6 +1426,7 @@ const CONST = {
MILEAGE_IRS_RATE: 0.655,
DEFAULT_RATE: 'Default Rate',
RATE_DECIMALS: 3,
+ FAKE_P2P_ID: '_FAKE_P2P_ID_',
},
TERMS: {
@@ -1646,6 +1659,7 @@ const CONST = {
FORM_CHARACTER_LIMIT: 50,
LEGAL_NAMES_CHARACTER_LIMIT: 150,
LOGIN_CHARACTER_LIMIT: 254,
+ CATEGORY_NAME_LIMIT: 256,
TITLE_CHARACTER_LIMIT: 100,
DESCRIPTION_LIMIT: 500,
@@ -1726,6 +1740,7 @@ const CONST = {
MAX_64BIT_LEFT_PART: 92233,
MAX_64BIT_MIDDLE_PART: 7203685,
MAX_64BIT_RIGHT_PART: 4775807,
+ INVALID_CATEGORY_NAME: '###',
// When generating a random value to fit in 7 digits (for the `middle` or `right` parts above), this is the maximum value to multiply by Math.random().
MAX_INT_FOR_RANDOM_7_DIGIT_VALUE: 10000000,
@@ -3108,6 +3123,7 @@ const CONST = {
ONYX_UPDATE_TYPES: {
HTTPS: 'https',
PUSHER: 'pusher',
+ AIRSHIP: 'airship',
},
EVENTS: {
SCROLLING: 'scrolling',
@@ -3340,6 +3356,664 @@ const CONST = {
ADDRESS: 3,
},
},
+ CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{
+ "AED": {
+ "rate": 396,
+ "unit": "km"
+ },
+ "AFN": {
+ "rate": 8369,
+ "unit": "km"
+ },
+ "ALL": {
+ "rate": 11104,
+ "unit": "km"
+ },
+ "AMD": {
+ "rate": 56842,
+ "unit": "km"
+ },
+ "ANG": {
+ "rate": 193,
+ "unit": "km"
+ },
+ "AOA": {
+ "rate": 67518,
+ "unit": "km"
+ },
+ "ARS": {
+ "rate": 9873,
+ "unit": "km"
+ },
+ "AUD": {
+ "rate": 85,
+ "unit": "km"
+ },
+ "AWG": {
+ "rate": 195,
+ "unit": "km"
+ },
+ "AZN": {
+ "rate": 183,
+ "unit": "km"
+ },
+ "BAM": {
+ "rate": 177,
+ "unit": "km"
+ },
+ "BBD": {
+ "rate": 216,
+ "unit": "km"
+ },
+ "BDT": {
+ "rate": 9130,
+ "unit": "km"
+ },
+ "BGN": {
+ "rate": 177,
+ "unit": "km"
+ },
+ "BHD": {
+ "rate": 40,
+ "unit": "km"
+ },
+ "BIF": {
+ "rate": 210824,
+ "unit": "km"
+ },
+ "BMD": {
+ "rate": 108,
+ "unit": "km"
+ },
+ "BND": {
+ "rate": 145,
+ "unit": "km"
+ },
+ "BOB": {
+ "rate": 745,
+ "unit": "km"
+ },
+ "BRL": {
+ "rate": 594,
+ "unit": "km"
+ },
+ "BSD": {
+ "rate": 108,
+ "unit": "km"
+ },
+ "BTN": {
+ "rate": 7796,
+ "unit": "km"
+ },
+ "BWP": {
+ "rate": 1180,
+ "unit": "km"
+ },
+ "BYN": {
+ "rate": 280,
+ "unit": "km"
+ },
+ "BYR": {
+ "rate": 2159418,
+ "unit": "km"
+ },
+ "BZD": {
+ "rate": 217,
+ "unit": "km"
+ },
+ "CAD": {
+ "rate": 70,
+ "unit": "km"
+ },
+ "CDF": {
+ "rate": 213674,
+ "unit": "km"
+ },
+ "CHF": {
+ "rate": 100,
+ "unit": "km"
+ },
+ "CLP": {
+ "rate": 77249,
+ "unit": "km"
+ },
+ "CNY": {
+ "rate": 702,
+ "unit": "km"
+ },
+ "COP": {
+ "rate": 383668,
+ "unit": "km"
+ },
+ "CRC": {
+ "rate": 65899,
+ "unit": "km"
+ },
+ "CUC": {
+ "rate": 108,
+ "unit": "km"
+ },
+ "CUP": {
+ "rate": 2776,
+ "unit": "km"
+ },
+ "CVE": {
+ "rate": 6112,
+ "unit": "km"
+ },
+ "CZK": {
+ "rate": 2356,
+ "unit": "km"
+ },
+ "DJF": {
+ "rate": 19151,
+ "unit": "km"
+ },
+ "DKK": {
+ "rate": 673,
+ "unit": "km"
+ },
+ "DOP": {
+ "rate": 6144,
+ "unit": "km"
+ },
+ "DZD": {
+ "rate": 14375,
+ "unit": "km"
+ },
+ "EEK": {
+ "rate": 1576,
+ "unit": "km"
+ },
+ "EGP": {
+ "rate": 1696,
+ "unit": "km"
+ },
+ "ERN": {
+ "rate": 1617,
+ "unit": "km"
+ },
+ "ETB": {
+ "rate": 4382,
+ "unit": "km"
+ },
+ "EUR": {
+ "rate": 3,
+ "unit": "km"
+ },
+ "FJD": {
+ "rate": 220,
+ "unit": "km"
+ },
+ "FKP": {
+ "rate": 77,
+ "unit": "km"
+ },
+ "GBP": {
+ "rate": 45,
+ "unit": "mi"
+ },
+ "GEL": {
+ "rate": 359,
+ "unit": "km"
+ },
+ "GHS": {
+ "rate": 620,
+ "unit": "km"
+ },
+ "GIP": {
+ "rate": 77,
+ "unit": "km"
+ },
+ "GMD": {
+ "rate": 5526,
+ "unit": "km"
+ },
+ "GNF": {
+ "rate": 1081319,
+ "unit": "km"
+ },
+ "GTQ": {
+ "rate": 832,
+ "unit": "km"
+ },
+ "GYD": {
+ "rate": 22537,
+ "unit": "km"
+ },
+ "HKD": {
+ "rate": 837,
+ "unit": "km"
+ },
+ "HNL": {
+ "rate": 2606,
+ "unit": "km"
+ },
+ "HRK": {
+ "rate": 684,
+ "unit": "km"
+ },
+ "HTG": {
+ "rate": 8563,
+ "unit": "km"
+ },
+ "HUF": {
+ "rate": 33091,
+ "unit": "km"
+ },
+ "IDR": {
+ "rate": 1555279,
+ "unit": "km"
+ },
+ "ILS": {
+ "rate": 356,
+ "unit": "km"
+ },
+ "INR": {
+ "rate": 7805,
+ "unit": "km"
+ },
+ "IQD": {
+ "rate": 157394,
+ "unit": "km"
+ },
+ "IRR": {
+ "rate": 4539961,
+ "unit": "km"
+ },
+ "ISK": {
+ "rate": 13518,
+ "unit": "km"
+ },
+ "JMD": {
+ "rate": 15794,
+ "unit": "km"
+ },
+ "JOD": {
+ "rate": 77,
+ "unit": "km"
+ },
+ "JPY": {
+ "rate": 11748,
+ "unit": "km"
+ },
+ "KES": {
+ "rate": 11845,
+ "unit": "km"
+ },
+ "KGS": {
+ "rate": 9144,
+ "unit": "km"
+ },
+ "KHR": {
+ "rate": 437658,
+ "unit": "km"
+ },
+ "KMF": {
+ "rate": 44418,
+ "unit": "km"
+ },
+ "KPW": {
+ "rate": 97043,
+ "unit": "km"
+ },
+ "KRW": {
+ "rate": 121345,
+ "unit": "km"
+ },
+ "KWD": {
+ "rate": 32,
+ "unit": "km"
+ },
+ "KYD": {
+ "rate": 90,
+ "unit": "km"
+ },
+ "KZT": {
+ "rate": 45396,
+ "unit": "km"
+ },
+ "LAK": {
+ "rate": 1010829,
+ "unit": "km"
+ },
+ "LBP": {
+ "rate": 164153,
+ "unit": "km"
+ },
+ "LKR": {
+ "rate": 21377,
+ "unit": "km"
+ },
+ "LRD": {
+ "rate": 18709,
+ "unit": "km"
+ },
+ "LSL": {
+ "rate": 1587,
+ "unit": "km"
+ },
+ "LTL": {
+ "rate": 348,
+ "unit": "km"
+ },
+ "LVL": {
+ "rate": 71,
+ "unit": "km"
+ },
+ "LYD": {
+ "rate": 486,
+ "unit": "km"
+ },
+ "MAD": {
+ "rate": 967,
+ "unit": "km"
+ },
+ "MDL": {
+ "rate": 1910,
+ "unit": "km"
+ },
+ "MGA": {
+ "rate": 406520,
+ "unit": "km"
+ },
+ "MKD": {
+ "rate": 5570,
+ "unit": "km"
+ },
+ "MMK": {
+ "rate": 152083,
+ "unit": "km"
+ },
+ "MNT": {
+ "rate": 306788,
+ "unit": "km"
+ },
+ "MOP": {
+ "rate": 863,
+ "unit": "km"
+ },
+ "MRO": {
+ "rate": 38463,
+ "unit": "km"
+ },
+ "MRU": {
+ "rate": 3862,
+ "unit": "km"
+ },
+ "MUR": {
+ "rate": 4340,
+ "unit": "km"
+ },
+ "MVR": {
+ "rate": 1667,
+ "unit": "km"
+ },
+ "MWK": {
+ "rate": 84643,
+ "unit": "km"
+ },
+ "MXN": {
+ "rate": 2219,
+ "unit": "km"
+ },
+ "MYR": {
+ "rate": 444,
+ "unit": "km"
+ },
+ "MZN": {
+ "rate": 7772,
+ "unit": "km"
+ },
+ "NAD": {
+ "rate": 1587,
+ "unit": "km"
+ },
+ "NGN": {
+ "rate": 42688,
+ "unit": "km"
+ },
+ "NIO": {
+ "rate": 3772,
+ "unit": "km"
+ },
+ "NOK": {
+ "rate": 917,
+ "unit": "km"
+ },
+ "NPR": {
+ "rate": 12474,
+ "unit": "km"
+ },
+ "NZD": {
+ "rate": 151,
+ "unit": "km"
+ },
+ "OMR": {
+ "rate": 42,
+ "unit": "km"
+ },
+ "PAB": {
+ "rate": 108,
+ "unit": "km"
+ },
+ "PEN": {
+ "rate": 401,
+ "unit": "km"
+ },
+ "PGK": {
+ "rate": 380,
+ "unit": "km"
+ },
+ "PHP": {
+ "rate": 5234,
+ "unit": "km"
+ },
+ "PKR": {
+ "rate": 16785,
+ "unit": "km"
+ },
+ "PLN": {
+ "rate": 415,
+ "unit": "km"
+ },
+ "PYG": {
+ "rate": 704732,
+ "unit": "km"
+ },
+ "QAR": {
+ "rate": 393,
+ "unit": "km"
+ },
+ "RON": {
+ "rate": 443,
+ "unit": "km"
+ },
+ "RSD": {
+ "rate": 10630,
+ "unit": "km"
+ },
+ "RUB": {
+ "rate": 8074,
+ "unit": "km"
+ },
+ "RWF": {
+ "rate": 107182,
+ "unit": "km"
+ },
+ "SAR": {
+ "rate": 404,
+ "unit": "km"
+ },
+ "SBD": {
+ "rate": 859,
+ "unit": "km"
+ },
+ "SCR": {
+ "rate": 2287,
+ "unit": "km"
+ },
+ "SDG": {
+ "rate": 41029,
+ "unit": "km"
+ },
+ "SEK": {
+ "rate": 917,
+ "unit": "km"
+ },
+ "SGD": {
+ "rate": 145,
+ "unit": "km"
+ },
+ "SHP": {
+ "rate": 77,
+ "unit": "km"
+ },
+ "SLL": {
+ "rate": 1102723,
+ "unit": "km"
+ },
+ "SOS": {
+ "rate": 62604,
+ "unit": "km"
+ },
+ "SRD": {
+ "rate": 1526,
+ "unit": "km"
+ },
+ "STD": {
+ "rate": 2223309,
+ "unit": "km"
+ },
+ "STN": {
+ "rate": 2232,
+ "unit": "km"
+ },
+ "SVC": {
+ "rate": 943,
+ "unit": "km"
+ },
+ "SYP": {
+ "rate": 82077,
+ "unit": "km"
+ },
+ "SZL": {
+ "rate": 1585,
+ "unit": "km"
+ },
+ "THB": {
+ "rate": 3328,
+ "unit": "km"
+ },
+ "TJS": {
+ "rate": 1230,
+ "unit": "km"
+ },
+ "TMT": {
+ "rate": 378,
+ "unit": "km"
+ },
+ "TND": {
+ "rate": 295,
+ "unit": "km"
+ },
+ "TOP": {
+ "rate": 245,
+ "unit": "km"
+ },
+ "TRY": {
+ "rate": 845,
+ "unit": "km"
+ },
+ "TTD": {
+ "rate": 732,
+ "unit": "km"
+ },
+ "TWD": {
+ "rate": 3055,
+ "unit": "km"
+ },
+ "TZS": {
+ "rate": 250116,
+ "unit": "km"
+ },
+ "UAH": {
+ "rate": 2985,
+ "unit": "km"
+ },
+ "UGX": {
+ "rate": 395255,
+ "unit": "km"
+ },
+ "USD": {
+ "rate": 67,
+ "unit": "mi"
+ },
+ "UYU": {
+ "rate": 4777,
+ "unit": "km"
+ },
+ "UZS": {
+ "rate": 1131331,
+ "unit": "km"
+ },
+ "VEB": {
+ "rate": 679346,
+ "unit": "km"
+ },
+ "VEF": {
+ "rate": 26793449,
+ "unit": "km"
+ },
+ "VES": {
+ "rate": 194381905,
+ "unit": "km"
+ },
+ "VND": {
+ "rate": 2487242,
+ "unit": "km"
+ },
+ "VUV": {
+ "rate": 11748,
+ "unit": "km"
+ },
+ "WST": {
+ "rate": 272,
+ "unit": "km"
+ },
+ "XAF": {
+ "rate": 59224,
+ "unit": "km"
+ },
+ "XCD": {
+ "rate": 291,
+ "unit": "km"
+ },
+ "XOF": {
+ "rate": 59224,
+ "unit": "km"
+ },
+ "XPF": {
+ "rate": 10783,
+ "unit": "km"
+ },
+ "YER": {
+ "rate": 27037,
+ "unit": "km"
+ },
+ "ZAR": {
+ "rate": 1588,
+ "unit": "km"
+ },
+ "ZMK": {
+ "rate": 566489,
+ "unit": "km"
+ },
+ "ZMW": {
+ "rate": 2377,
+ "unit": "km"
+ }
+ }`) as CurrencyDefaultMileageRate,
EXIT_SURVEY: {
REASONS: {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index f6b5c635e4ae..31e22491e2b9 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -1,4 +1,4 @@
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type * as FormTypes from './types/form';
@@ -128,6 +128,9 @@ const ONYXKEYS = {
/** This NVP contains the choice that the user made on the engagement modal */
NVP_INTRO_SELECTED: 'introSelected',
+ /** The NVP with the last distance rate used per policy */
+ NVP_LAST_SELECTED_DISTANCE_RATES: 'lastSelectedDistanceRates',
+
/** Does this user have push notifications enabled for this device? */
PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',
@@ -284,6 +287,7 @@ const ONYXKEYS = {
POLICY_MEMBERS: 'policyMembers_',
POLICY_DRAFTS: 'policyDrafts_',
POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_',
+ POLICY_JOIN_MEMBER: 'policyJoinMember_',
POLICY_CATEGORIES: 'policyCategories_',
POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_',
POLICY_TAGS: 'policyTags_',
@@ -326,6 +330,8 @@ const ONYXKEYS = {
ADD_DEBIT_CARD_FORM: 'addDebitCardForm',
ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft',
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
+ WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate',
+ WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm',
WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft',
@@ -407,6 +413,7 @@ type AllOnyxKeys = DeepValueOf;
type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
+ [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
@@ -481,6 +488,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SELECTED_TAB]: string;
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;
+ [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember;
};
type OnyxValuesMapping = {
@@ -524,6 +532,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
[ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean;
[ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected;
+ [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates;
[ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
[ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData;
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
@@ -582,7 +591,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;
type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
-type OnyxValue = OnyxEntry;
+type OnyxValue = TOnyxKey extends keyof OnyxCollectionValuesMapping ? OnyxCollection : OnyxEntry;
type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index cfc287ba2cdc..2ed9fbc3666e 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -478,6 +478,10 @@ const ROUTES = {
route: 'workspace/:policyID/avatar',
getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const,
},
+ WORKSPACE_JOIN_USER: {
+ route: 'workspace/:policyID/join',
+ getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const,
+ },
WORKSPACE_SETTINGS_CURRENCY: {
route: 'workspace/:policyID/settings/currency',
getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
@@ -546,10 +550,23 @@ const ROUTES = {
route: 'workspace/:policyID/categories/settings',
getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const,
},
+ WORKSPACE_CATEGORY_CREATE: {
+ route: 'workspace/:policyID/categories/new',
+ getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const,
+ },
WORKSPACE_TAGS: {
route: 'workspace/:policyID/tags',
getRoute: (policyID: string) => `workspace/${policyID}/tags` as const,
},
+ WORKSPACE_MEMBER_DETAILS: {
+ route: 'workspace/:policyID/members/:accountID',
+ getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo),
+ },
+ WORKSPACE_MEMBER_ROLE_SELECTION: {
+ route: 'workspace/:policyID/members/:accountID/role-selection',
+ getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo),
+ },
+
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 2369fe435feb..6fc61aec61a0 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -128,6 +128,7 @@ const SCREENS = {
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
SAML_SIGN_IN: 'SAMLSignIn',
+ WORKSPACE_JOIN_USER: 'WorkspaceJoinUser',
MONEY_REQUEST: {
MANUAL_TAB: 'manual',
@@ -223,8 +224,11 @@ const SCREENS = {
DESCRIPTION: 'Workspace_Profile_Description',
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
+ CATEGORY_CREATE: 'Category_Create',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
+ MEMBER_DETAILS: 'Workspace_Member_Details',
+ MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection',
},
EDIT_REQUEST: {
diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx
index 39c91c2a0789..a2e3f5d9948e 100644
--- a/src/components/AddressSearch/index.tsx
+++ b/src/components/AddressSearch/index.tsx
@@ -1,11 +1,12 @@
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
-import {ActivityIndicator, Keyboard, LogBox, ScrollView, View} from 'react-native';
+import {ActivityIndicator, Keyboard, LogBox, View} from 'react-native';
import type {LayoutChangeEvent} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import LocationErrorMessage from '@components/LocationErrorMessage';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js
index e924cb8c13e9..b2c9fed64467 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js
@@ -109,7 +109,6 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) {
isHovered={isModalHovered}
isFocused={isFocused}
optionalVideoDuration={item.duration}
- isUsedInCarousel
/>
diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js
index f6a56dc73088..461548f0d2b1 100755
--- a/src/components/Attachments/AttachmentView/index.js
+++ b/src/components/Attachments/AttachmentView/index.js
@@ -1,7 +1,7 @@
import Str from 'expensify-common/lib/str';
import PropTypes from 'prop-types';
import React, {memo, useEffect, useState} from 'react';
-import {ActivityIndicator, ScrollView, View} from 'react-native';
+import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import * as AttachmentsPropTypes from '@components/Attachments/propTypes';
@@ -9,6 +9,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 5be33e6ff2ec..635645b0035b 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -2,8 +2,12 @@ import React, {useCallback} from 'react';
import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
+import Icon from './Icon';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import Text from './Text';
@@ -31,11 +35,29 @@ type BadgeProps = {
/** Callback to be called on onPress */
onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void;
+
+ /** The icon asset to display to the left of the text */
+ icon?: IconAsset | null;
+
+ /** Any additional styles to pass to the left icon container. */
+ iconStyles?: StyleProp;
};
-function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) {
+function Badge({
+ success = false,
+ error = false,
+ pressable = false,
+ text,
+ environment = CONST.ENVIRONMENT.DEV,
+ badgeStyles,
+ textStyles,
+ onPress = () => {},
+ icon,
+ iconStyles = [],
+}: BadgeProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const theme = useTheme();
const textColorStyles = success || error ? styles.textWhite : undefined;
const Wrapper = pressable ? PressableWithoutFeedback : View;
@@ -53,6 +75,16 @@ function Badge({success = false, error = false, pressable = false, text, environ
aria-label={!pressable ? text : undefined}
accessible={false}
>
+ {icon && (
+
+
+
+ )}
,
+ ref: ForwardedRef,
) {
const theme = useTheme();
const styles = useThemeStyles();
diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx
index 941d63c1bf94..fda0c5441734 100644
--- a/src/components/DistanceEReceipt.tsx
+++ b/src/components/DistanceEReceipt.tsx
@@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import EReceiptBackground from '@assets/images/eReceipt_background.svg';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -15,6 +15,7 @@ import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import ImageSVG from './ImageSVG';
import PendingMapView from './MapView/PendingMapView';
+import ScrollView from './ScrollView';
import Text from './Text';
import ThumbnailImage from './ThumbnailImage';
diff --git a/src/components/DistanceRequest/index.tsx b/src/components/DistanceRequest/index.tsx
index 9900656057ce..8920c9a4a92b 100644
--- a/src/components/DistanceRequest/index.tsx
+++ b/src/components/DistanceRequest/index.tsx
@@ -2,6 +2,7 @@ import type {RouteProp} from '@react-navigation/native';
import lodashIsEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
import type {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx
index dc78a3ce6222..418f3e93eac4 100644
--- a/src/components/DraggableList/index.tsx
+++ b/src/components/DraggableList/index.tsx
@@ -1,7 +1,9 @@
import React, {useCallback} from 'react';
import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd';
import type {OnDragEndResponder} from 'react-beautiful-dnd';
-import {ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView} from 'react-native';
+import ScrollView from '@components/ScrollView';
import useThemeStyles from '@hooks/useThemeStyles';
import type {DraggableListProps} from './types';
import useDraggableInPortal from './useDraggableInPortal';
@@ -37,7 +39,7 @@ function DraggableList(
// eslint-disable-next-line @typescript-eslint/naming-convention
ListFooterComponent,
}: DraggableListProps,
- ref: React.ForwardedRef,
+ ref: React.ForwardedRef,
) {
const styles = useThemeStyles();
/**
diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index 9b68916c4003..88938f31cd79 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -1,6 +1,7 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useRef} from 'react';
-import type {GestureResponderEvent, Role} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Role, Text} from 'react-native';
import {Platform, View} from 'react-native';
import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
@@ -58,12 +59,12 @@ type FloatingActionButtonProps = {
role: Role;
};
-function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) {
+function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) {
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const {translate} = useLocalize();
- const fabPressable = useRef(null);
+ const fabPressable = useRef(null);
const sharedValue = useSharedValue(isActive ? 1 : 0);
const buttonRef = ref;
@@ -112,9 +113,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
{
- fabPressable.current = el;
+ fabPressable.current = el ?? null;
if (buttonRef && 'current' in buttonRef) {
- buttonRef.current = el;
+ buttonRef.current = el ?? null;
}
}}
accessibilityLabel={accessibilityLabel}
diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx
index 5615f3b87cfa..5c2488ca144a 100644
--- a/src/components/Form/FormWrapper.tsx
+++ b/src/components/Form/FormWrapper.tsx
@@ -1,13 +1,15 @@
import React, {useCallback, useMemo, useRef} from 'react';
import type {RefObject} from 'react';
-import type {StyleProp, View, ViewStyle} from 'react-native';
-import {Keyboard, ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native';
+import {Keyboard} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import FormElement from '@components/FormElement';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types';
+import ScrollView from '@components/ScrollView';
import ScrollViewWithContext from '@components/ScrollViewWithContext';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -60,7 +62,7 @@ function FormWrapper({
disablePressOnEnter = true,
}: FormWrapperProps) {
const styles = useThemeStyles();
- const formRef = useRef(null);
+ const formRef = useRef(null);
const formContentRef = useRef(null);
const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]);
diff --git a/src/components/FormScrollView.tsx b/src/components/FormScrollView.tsx
index ade167e9e628..91f5a825a38a 100644
--- a/src/components/FormScrollView.tsx
+++ b/src/components/FormScrollView.tsx
@@ -1,15 +1,16 @@
import type {ForwardedRef} from 'react';
import React from 'react';
-import type {ScrollViewProps} from 'react-native';
-import {ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView, ScrollViewProps} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
+import ScrollView from './ScrollView';
type FormScrollViewProps = ScrollViewProps & {
/** Form elements */
children: React.ReactNode;
};
-function FormScrollView({children, ...rest}: FormScrollViewProps, ref: ForwardedRef) {
+function FormScrollView({children, ...rest}: FormScrollViewProps, ref: ForwardedRef) {
const styles = useThemeStyles();
return (
(null);
- const transferBalanceButtonRef = useRef(null);
+ const anchorRef = useRef(null);
+ const transferBalanceButtonRef = useRef(null);
const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
@@ -111,7 +111,7 @@ function KYCWall({
return;
}
- const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
+ const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current as HTMLDivElement);
const position = getAnchorPosition(buttonPosition);
setPositionAddPaymentMenu(position);
@@ -162,7 +162,7 @@ function KYCWall({
}
// Use event target as fallback if anchorRef is null for safety
- const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLElement);
+ const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLDivElement);
transferBalanceButtonRef.current = targetElement;
@@ -181,7 +181,7 @@ function KYCWall({
return;
}
- const clickedElementLocation = getClickedTargetLocation(targetElement);
+ const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement);
const position = getAnchorPosition(clickedElementLocation);
setPositionAddPaymentMenu(position);
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 102f85ea49b9..5784be21bac3 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -88,7 +88,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
- const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length;
+ const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index fa6de1c2e4f4..68114dcf4e4c 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -217,11 +217,12 @@ function MoneyRequestConfirmationList(props) {
const {onSendMoney, onConfirm, onSelectParticipant} = props;
const {translate, toLocaleDigit} = useLocalize();
const transaction = props.transaction;
- const {canUseViolations} = usePermissions();
+ const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();
const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST;
const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND;
+ const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill);
const isSplitWithScan = isSplitBill && props.isScanRequest;
@@ -721,13 +722,14 @@ function MoneyRequestConfirmationList(props) {
)}
{props.isDistanceRequest && (
Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))}
- disabled={didConfirm || !isTypeRequest}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ disabled={didConfirm || !canEditDistance}
interactive={!props.isReadOnly}
/>
)}
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index fe8cc3506b3f..7f9ab3fe0dc9 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -79,16 +78,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction);
- const isRequestModifiable = !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction);
- const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction);
- let canDeleteRequest = canModifyRequest;
+ const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction);
+ const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction;
- if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) {
- // If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin
- canDeleteRequest =
- canDeleteRequest &&
- (ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy));
- }
+ // If the report supports adding transactions to it, then it also supports deleting transactions from it.
+ const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction;
const changeMoneyRequestStatus = () => {
if (isOnHold) {
@@ -108,7 +102,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
}, [canDeleteRequest]);
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)];
- if (isRequestModifiable) {
+ if (canHoldOrUnholdRequest) {
const isRequestIOU = parentReport?.type === 'iou';
const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU;
const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover;
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index 74a480a2eff7..968e1dfbfdca 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -1,6 +1,7 @@
import {useIsFocused} from '@react-navigation/native';
import {format} from 'date-fns';
import Str from 'expensify-common/lib/str';
+import {isUndefined} from 'lodash';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
@@ -245,11 +246,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const theme = useTheme();
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
- const {canUseViolations} = usePermissions();
+ const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();
const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
+ const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit);
const {unit, rate, currency} = mileageRate;
const distance = lodashGet(transaction, 'routes.route0.distance', 0);
@@ -490,6 +492,31 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
IOU.setMoneyRequestMerchant(transaction.transactionID, distanceMerchant, true);
}, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]);
+ // Auto select the category if there is only one enabled category and it is required
+ useEffect(() => {
+ const enabledCategories = _.filter(policyCategories, (category) => category.enabled);
+ if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
+ return;
+ }
+ IOU.setMoneyRequestCategory(transaction.transactionID, enabledCategories[0].name);
+ }, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]);
+
+ // Auto select the tag if there is only one enabled tag and it is required
+ useEffect(() => {
+ let updatedTagsString = TransactionUtils.getTag(transaction);
+ policyTagLists.forEach((tagList, index) => {
+ const enabledTags = _.filter(tagList.tags, (tag) => tag.enabled);
+ const isTagListRequired = isUndefined(tagList.required) ? false : tagList.required && canUseViolations;
+ if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
+ return;
+ }
+ updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
+ });
+ if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
+ IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString);
+ }
+ }, [policyTagLists, transaction, policyTags, isTagRequired, canUseViolations]);
+
/**
* @param {Object} option
*/
@@ -689,13 +716,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
item: (
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
- disabled={didConfirm || !isTypeRequest}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ disabled={didConfirm || !canEditDistance}
interactive={!isReadOnly}
/>
),
diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx
index d14aec90fa10..0bc9130ea4a8 100644
--- a/src/components/OnyxProvider.tsx
+++ b/src/components/OnyxProvider.tsx
@@ -8,8 +8,8 @@ import createOnyxContext from './createOnyxContext';
const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK);
const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE);
-const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
-const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
+const [withReportActionsDrafts, ReportActionsDraftsProvider, , useReportActionsDrafts] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
+const [withBlockedFromConcierge, BlockedFromConciergeProvider, , useBlockedFromConcierge] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS);
const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME);
@@ -66,5 +66,7 @@ export {
useFrequentlyUsedEmojis,
withPreferredEmojiSkinTone,
PreferredEmojiSkinToneContext,
+ useBlockedFromConcierge,
+ useReportActionsDrafts,
useSession,
};
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index c0258f1252ef..40fb1115ac36 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager';
import Button from '@components/Button';
@@ -9,6 +9,7 @@ import FixedFooter from '@components/FixedFooter';
import FormHelpMessage from '@components/FormHelpMessage';
import OptionsList from '@components/OptionsList';
import ReferralProgramCTA from '@components/ReferralProgramCTA';
+import ScrollView from '@components/ScrollView';
import ShowMoreButton from '@components/ShowMoreButton';
import TextInput from '@components/TextInput';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js
index 10596bb9faf9..97d893b511dd 100644
--- a/src/components/PDFView/PDFPasswordForm.js
+++ b/src/components/PDFView/PDFPasswordForm.js
@@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useRef, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx
index 1bee95532104..c86d3b71c1d9 100644
--- a/src/components/Picker/BasePicker.tsx
+++ b/src/components/Picker/BasePicker.tsx
@@ -1,6 +1,7 @@
import lodashDefer from 'lodash/defer';
import type {ForwardedRef, ReactElement, ReactNode, RefObject} from 'react';
import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {ScrollView} from 'react-native';
import {View} from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index e06037f47b63..314c1ba141c3 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -1,5 +1,6 @@
import type {RefObject} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
import type {PopoverAnchorPosition} from '@components/Modal/types';
import type BaseModalProps from '@components/Modal/types';
import type {WindowDimensionsProps} from '@components/withWindowDimensions/types';
@@ -20,7 +21,7 @@ type PopoverProps = BaseModalProps &
anchorAlignment?: AnchorAlignment;
/** The anchor ref of the popover */
- anchorRef: RefObject;
+ anchorRef: RefObject;
/** Whether disable the animations */
disableAnimation?: boolean;
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index 67481b41d50b..cc6c84477525 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -1,6 +1,7 @@
import type {RefObject} from 'react';
import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types';
const PopoverContext = createContext({
@@ -10,7 +11,7 @@ const PopoverContext = createContext({
isOpen: false,
});
-function elementContains(ref: RefObject | undefined, target: EventTarget | null) {
+function elementContains(ref: RefObject | undefined, target: EventTarget | null) {
if (ref?.current && 'contains' in ref.current && ref?.current?.contains(target as Node)) {
return true;
}
@@ -21,7 +22,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
const [isOpen, setIsOpen] = useState(false);
const activePopoverRef = useRef(null);
- const closePopover = useCallback((anchorRef?: RefObject): boolean => {
+ const closePopover = useCallback((anchorRef?: RefObject): boolean => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return false;
}
diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts
index 2a366ae2a712..5022aee0f843 100644
--- a/src/components/PopoverProvider/types.ts
+++ b/src/components/PopoverProvider/types.ts
@@ -1,5 +1,6 @@
import type {ReactNode, RefObject} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
type PopoverContextProps = {
children: ReactNode;
@@ -8,14 +9,14 @@ type PopoverContextProps = {
type PopoverContextValue = {
onOpen?: (popoverParams: AnchorRef) => void;
popover?: AnchorRef | Record | null;
- close: (anchorRef?: RefObject) => void;
+ close: (anchorRef?: RefObject) => void;
isOpen: boolean;
};
type AnchorRef = {
- ref: RefObject;
- close: (anchorRef?: RefObject) => void;
- anchorRef: RefObject;
+ ref: RefObject;
+ close: (anchorRef?: RefObject) => void;
+ anchorRef: RefObject;
};
export type {PopoverContextProps, PopoverContextValue, AnchorRef};
diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts
index 0d24cdd4bd9f..8fe40119ca61 100644
--- a/src/components/PopoverWithoutOverlay/types.ts
+++ b/src/components/PopoverWithoutOverlay/types.ts
@@ -1,5 +1,6 @@
import type {RefObject} from 'react';
-import type {View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {Text, View} from 'react-native';
import type BaseModalProps from '@components/Modal/types';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
@@ -14,7 +15,7 @@ type PopoverWithoutOverlayProps = ChildrenProps &
};
/** The anchor ref of the popover */
- anchorRef: RefObject;
+ anchorRef: RefObject;
/** A react-native-animatable animation timing for the modal display animation */
animationInTiming?: number;
diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts
index 2dd2e17e0454..9040a844e5a7 100644
--- a/src/components/Pressable/GenericPressable/types.ts
+++ b/src/components/Pressable/GenericPressable/types.ts
@@ -1,5 +1,6 @@
import type {ElementRef, ForwardedRef, RefObject} from 'react';
-import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, Text as RNText, StyleProp, View, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
import type {Shortcut} from '@libs/KeyboardShortcut';
import type CONST from '@src/CONST';
@@ -138,7 +139,7 @@ type PressableProps = RNPressableProps &
noDragArea?: boolean;
};
-type PressableRef = ForwardedRef;
+type PressableRef = ForwardedRef;
export default PressableProps;
export type {PressableRef};
diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts
index aa67d45d66fb..b07c867daeb3 100644
--- a/src/components/PressableWithSecondaryInteraction/types.ts
+++ b/src/components/PressableWithSecondaryInteraction/types.ts
@@ -4,7 +4,7 @@ import type {ParsableStyle} from '@styles/utils/types';
type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps & {
/** The function that should be called when this pressable is pressed */
- onPress: (event?: GestureResponderEvent) => void;
+ onPress?: (event?: GestureResponderEvent) => void;
/** The function that should be called when this pressable is pressedIn */
onPressIn?: (event?: GestureResponderEvent) => void;
diff --git a/src/components/Reactions/ReportActionItemEmojiReactions.tsx b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
index 7e95ab670b7e..c6bf4f9e4016 100644
--- a/src/components/Reactions/ReportActionItemEmojiReactions.tsx
+++ b/src/components/Reactions/ReportActionItemEmojiReactions.tsx
@@ -23,7 +23,7 @@ type ReportActionItemEmojiReactionsProps = WithCurrentUserPersonalDetailsProps &
emojiReactions: OnyxEntry;
/** The user's preferred locale. */
- preferredLocale: OnyxEntry;
+ preferredLocale?: OnyxEntry;
/** The report action that these reactions are for */
reportAction: ReportAction;
@@ -155,7 +155,7 @@ function ReportActionItemEmojiReactions({
shouldDisableOpacity={!!reportAction.pendingAction}
>
(popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref)}
+ ref={(ref) => (popoverReactionListAnchors.current[reaction.reactionEmojiName] = ref ?? null)}
count={reaction.reactionCount}
emojiCodes={reaction.emojiCodes}
onPress={reaction.onPress}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index f0cd8dc1b4b5..60dbfc07966a 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -2,6 +2,7 @@ import Str from 'expensify-common/lib/str';
import React, {useMemo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
@@ -26,7 +27,7 @@ type MoneyReportViewProps = {
report: Report;
/** Policy that the report belongs to */
- policy: Policy;
+ policy: OnyxEntry;
/** Policy report fields */
policyReportFields: PolicyReportField[];
@@ -67,107 +68,111 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont
- {ReportUtils.reportFieldsEnabled(report) &&
- sortedPolicyReportFields.map((reportField) => {
- const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
- const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
- const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
-
- return (
-
- Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))}
- shouldShowRightIcon
- disabled={isFieldDisabled}
- wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
- shouldGreyOutWhenDisabled={false}
- numberOfLinesTitle={0}
- interactive
- shouldStackHorizontally={false}
- onSecondaryInteraction={() => {}}
- hoverAndPressStyle={false}
- titleWithTooltips={[]}
- />
-
- );
- })}
-
-
-
- {translate('common.total')}
-
-
-
- {isSettled && (
-
-
-
- )}
-
- {formattedTotalAmount}
-
-
-
- {Boolean(shouldShowBreakdown) && (
+ {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && (
<>
-
-
-
- {translate('cardTransactions.outOfPocket')}
-
-
-
-
- {formattedOutOfPocketAmount}
-
-
-
-
+ {ReportUtils.reportFieldsEnabled(report) &&
+ sortedPolicyReportFields.map((reportField) => {
+ const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
+ const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
+ const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
+
+ return (
+
+ Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))}
+ shouldShowRightIcon
+ disabled={isFieldDisabled}
+ wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
+ shouldGreyOutWhenDisabled={false}
+ numberOfLinesTitle={0}
+ interactive
+ shouldStackHorizontally={false}
+ onSecondaryInteraction={() => {}}
+ hoverAndPressStyle={false}
+ titleWithTooltips={[]}
+ />
+
+ );
+ })}
+
- {translate('cardTransactions.companySpend')}
+ {translate('common.total')}
+ {isSettled && (
+
+
+
+ )}
- {formattedCompanySpendAmount}
+ {formattedTotalAmount}
+ {Boolean(shouldShowBreakdown) && (
+ <>
+
+
+
+ {translate('cardTransactions.outOfPocket')}
+
+
+
+
+ {formattedOutOfPocketAmount}
+
+
+
+
+
+
+ {translate('cardTransactions.companySpend')}
+
+
+
+
+ {formattedCompanySpendAmount}
+
+
+
+ >
+ )}
+
>
)}
-
);
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 0bd18d8ee7ea..1d048640b30b 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -24,7 +24,6 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
@@ -470,8 +469,8 @@ export default withOnyx {
- const parentReportAction = ReportActionsUtils.getParentReportAction(report);
+ key: ({report, parentReportActions}) => {
+ const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''];
const originalMessage = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage : undefined;
const transactionID = originalMessage?.IOUTransactionID ?? 0;
return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`;
diff --git a/src/components/ReportActionItem/TaskAction.tsx b/src/components/ReportActionItem/TaskAction.tsx
index b10be4e86fe8..7e9262bb4c05 100644
--- a/src/components/ReportActionItem/TaskAction.tsx
+++ b/src/components/ReportActionItem/TaskAction.tsx
@@ -1,20 +1,24 @@
import React from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import * as TaskUtils from '@libs/TaskUtils';
+import type {ReportAction} from '@src/types/onyx';
type TaskActionProps = {
/** Name of the reportAction action */
- actionName: string;
+ action: OnyxEntry;
};
-function TaskAction({actionName}: TaskActionProps) {
+function TaskAction({action}: TaskActionProps) {
const styles = useThemeStyles();
+ const message = TaskUtils.getTaskReportActionMessage(action);
return (
- {TaskUtils.getTaskReportActionMessage(actionName)}
+ {message.html ? ${message.html}`} /> : {message.text}}
);
}
diff --git a/src/components/ScrollView.tsx b/src/components/ScrollView.tsx
new file mode 100644
index 000000000000..a61c592015ee
--- /dev/null
+++ b/src/components/ScrollView.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import type {ForwardedRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import {ScrollView as RNScrollView} from 'react-native';
+import type {ScrollViewProps} from 'react-native';
+
+function ScrollView({children, scrollIndicatorInsets, ...props}: ScrollViewProps, ref: ForwardedRef) {
+ return (
+
+ {children}
+
+ );
+}
+
+ScrollView.displayName = 'ScrollView';
+
+export type {ScrollViewProps};
+
+export default React.forwardRef(ScrollView);
diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx
index 1ac53651a542..1b9bb2c09f56 100644
--- a/src/components/ScrollViewWithContext.tsx
+++ b/src/components/ScrollViewWithContext.tsx
@@ -1,13 +1,14 @@
import type {ForwardedRef, ReactNode} from 'react';
import React, {createContext, forwardRef, useMemo, useRef, useState} from 'react';
-import type {NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps} from 'react-native';
-import {ScrollView} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView, ScrollViewProps} from 'react-native';
+import ScrollView from './ScrollView';
const MIN_SMOOTH_SCROLL_EVENT_THROTTLE = 16;
type ScrollContextValue = {
contentOffsetY: number;
- scrollViewRef: ForwardedRef;
+ scrollViewRef: ForwardedRef;
};
const ScrollContext = createContext({
@@ -28,9 +29,9 @@ type ScrollViewWithContextProps = Partial & {
* Using this wrapper will automatically handle scrolling to the picker's
* when the picker modal is opened
*/
-function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) {
+function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) {
const [contentOffsetY, setContentOffsetY] = useState(0);
- const defaultScrollViewRef = useRef(null);
+ const defaultScrollViewRef = useRef(null);
const scrollViewRef = ref ?? defaultScrollViewRef;
const setContextScrollPosition = (event: NativeSyntheticEvent) => {
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js
index 23d0bb6f816b..92d829e9d0db 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.js
+++ b/src/components/VideoPlayer/BaseVideoPlayer.js
@@ -82,7 +82,7 @@ function BaseVideoPlayer({
setIsPopoverVisible(false);
};
- // fix for iOS mWeb: preventing iOS native player edfault behavior from pausing the video when exiting fullscreen
+ // fix for iOS mWeb: preventing iOS native player default behavior from pausing the video when exiting fullscreen
const preventPausingWhenExitingFullscreen = useCallback(
(isVideoPlaying) => {
if (videoResumeTryNumber.current === 0 || isVideoPlaying) {
@@ -121,6 +121,7 @@ function BaseVideoPlayer({
const handleFullscreenUpdate = useCallback(
(e) => {
onFullscreenUpdate(e);
+
// fix for iOS native and mWeb: when switching to fullscreen and then exiting
// the fullscreen mode while playing, the video pauses
if (!isPlaying || e.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
@@ -139,7 +140,8 @@ function BaseVideoPlayer({
const bindFunctions = useCallback(() => {
currentVideoPlayerRef.current._onPlaybackStatusUpdate = handlePlaybackStatusUpdate;
currentVideoPlayerRef.current._onFullscreenUpdate = handleFullscreenUpdate;
- // update states after binding
+
+ // Update states after binding
currentVideoPlayerRef.current.getStatusAsync().then((status) => {
handlePlaybackStatusUpdate(status);
});
@@ -149,6 +151,7 @@ function BaseVideoPlayer({
if (!isUploading) {
return;
}
+
// If we are uploading a new video, we want to immediately set the video player ref.
currentVideoPlayerRef.current = videoPlayerRef.current;
}, [url, currentVideoPlayerRef, isUploading]);
@@ -162,6 +165,7 @@ function BaseVideoPlayer({
if (shouldUseSharedVideoElementRef.current) {
return;
}
+
// If it's not a shared video player, clear the video player ref.
currentVideoPlayerRef.current = null;
},
diff --git a/src/components/createOnyxContext.tsx b/src/components/createOnyxContext.tsx
index 50cda00b17b4..c19b8006c86c 100644
--- a/src/components/createOnyxContext.tsx
+++ b/src/components/createOnyxContext.tsx
@@ -3,7 +3,7 @@ import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, PropsWithou
import React, {createContext, forwardRef, useContext} from 'react';
import {withOnyx} from 'react-native-onyx';
import getComponentDisplayName from '@libs/getComponentDisplayName';
-import type {OnyxKey, OnyxValue, OnyxValues} from '@src/ONYXKEYS';
+import type {OnyxKey, OnyxValue} from '@src/ONYXKEYS';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
// Provider types
@@ -32,11 +32,11 @@ type CreateOnyxContext = [
WithOnyxKey,
ComponentType, TOnyxKey>>,
React.Context>,
- () => OnyxValues[TOnyxKey],
+ () => NonNullable>,
];
export default (onyxKeyName: TOnyxKey): CreateOnyxContext => {
- const Context = createContext>(null);
+ const Context = createContext>(null as OnyxValue);
function Provider(props: ProviderPropsWithOnyx): ReactNode {
return {props.children};
}
@@ -86,7 +86,7 @@ export default (onyxKeyName: TOnyxKey): CreateOnyxCont
if (value === null) {
throw new Error(`useOnyxContext must be used within a OnyxProvider [key: ${onyxKeyName}]`);
}
- return value;
+ return value as NonNullable>;
};
return [withOnyxKey, ProviderWithOnyx, Context, useOnyxContext];
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 0a52cca62ef5..3575854ee7e2 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1754,6 +1754,7 @@ export default {
workspaceType: 'Workspace type',
workspaceAvatar: 'Workspace avatar',
mustBeOnlineToViewMembers: 'You must be online in order to view members of this workspace.',
+ requested: 'Requested',
},
type: {
free: 'Free',
@@ -1770,6 +1771,10 @@ export default {
subtitle: 'Add a category to organize your spend.',
},
genericFailureMessage: 'An error occurred while updating the category, please try again.',
+ addCategory: 'Add category',
+ categoryRequiredError: 'Category name is required.',
+ existingCategoryError: 'A category with this name already exists.',
+ invalidCategoryName: 'Invalid category name.',
},
tags: {
requiresTag: 'Members must tag all spend',
@@ -1805,6 +1810,9 @@ export default {
genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.',
removeMembersPrompt: 'Are you sure you want to remove these members?',
removeMembersTitle: 'Remove members',
+ removeMemberButtonTitle: 'Remove from workspace',
+ removeMemberPrompt: ({memberName}) => `Are you sure you want to remove ${memberName}`,
+ removeMemberTitle: 'Remove member',
makeMember: 'Make member',
makeAdmin: 'Make admin',
selectAll: 'Select all',
@@ -2196,6 +2204,7 @@ export default {
viewAttachment: 'View attachment',
},
parentReportAction: {
+ deletedReport: '[Deleted report]',
deletedMessage: '[Deleted message]',
deletedRequest: '[Deleted request]',
reversedTransaction: '[Reversed transaction]',
@@ -2239,6 +2248,10 @@ export default {
invite: 'Invite them',
nothing: 'Do nothing',
},
+ actionableMentionJoinWorkspaceOptions: {
+ accept: 'Accept',
+ decline: 'Decline',
+ },
teachersUnitePage: {
teachersUnite: 'Teachers Unite',
joinExpensifyOrg: 'Join Expensify.org in eliminating injustice around the world and help teachers split their expenses for classrooms in need!',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 013255c1e11e..51a83e55fee2 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1778,6 +1778,7 @@ export default {
workspaceType: 'Tipo de espacio de trabajo',
workspaceAvatar: 'Espacio de trabajo avatar',
mustBeOnlineToViewMembers: 'Debes estar en línea para poder ver los miembros de este espacio de trabajo.',
+ requested: 'Solicitado',
},
type: {
free: 'Gratis',
@@ -1794,6 +1795,10 @@ export default {
subtitle: 'Añade una categoría para organizar tu gasto.',
},
genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
+ addCategory: 'Añadir categoría',
+ categoryRequiredError: 'Lo nombre de la categoría es obligatorio.',
+ existingCategoryError: 'Ya existe una categoría con este nombre.',
+ invalidCategoryName: 'Lo nombre de la categoría es invalido.',
},
tags: {
requiresTag: 'Los miembros deben etiquetar todos los gastos',
@@ -1829,6 +1834,9 @@ export default {
genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, inténtalo más tarde.',
removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?',
removeMembersTitle: 'Eliminar miembros',
+ removeMemberButtonTitle: 'Quitar del espacio de trabajo',
+ removeMemberPrompt: ({memberName}) => `¿Estás seguro de que deseas eliminar a ${memberName}`,
+ removeMemberTitle: 'Eliminar miembro',
makeMember: 'Hacer miembro',
makeAdmin: 'Hacer administrador',
selectAll: 'Seleccionar todo',
@@ -2684,6 +2692,7 @@ export default {
viewAttachment: 'Ver archivo adjunto',
},
parentReportAction: {
+ deletedReport: '[Informe eliminado]',
deletedMessage: '[Mensaje eliminado]',
deletedRequest: '[Solicitud eliminada]',
reversedTransaction: '[Transacción anulada]',
@@ -2705,6 +2714,10 @@ export default {
invite: 'Invitar',
nothing: 'No hacer nada',
},
+ actionableMentionJoinWorkspaceOptions: {
+ accept: 'Aceptar',
+ decline: 'Rechazar',
+ },
moderation: {
flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.',
chooseAReason: 'Elige abajo un motivo para reportarlo:',
diff --git a/src/libs/API/parameters/AcceptJoinRequest.ts b/src/libs/API/parameters/AcceptJoinRequest.ts
new file mode 100644
index 000000000000..4c7b6a00b2fb
--- /dev/null
+++ b/src/libs/API/parameters/AcceptJoinRequest.ts
@@ -0,0 +1,5 @@
+type AcceptJoinRequestParams = {
+ requests: string;
+};
+
+export default AcceptJoinRequestParams;
diff --git a/src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts b/src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts
new file mode 100644
index 000000000000..629a66c2e657
--- /dev/null
+++ b/src/libs/API/parameters/CreateWorkspaceCategoriesParams.ts
@@ -0,0 +1,10 @@
+type CreateWorkspaceCategoriesParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array<{name: string;}>
+ */
+ categories: string;
+};
+
+export default CreateWorkspaceCategoriesParams;
diff --git a/src/libs/API/parameters/DeclineJoinRequest.ts b/src/libs/API/parameters/DeclineJoinRequest.ts
new file mode 100644
index 000000000000..da0b147254d8
--- /dev/null
+++ b/src/libs/API/parameters/DeclineJoinRequest.ts
@@ -0,0 +1,5 @@
+type DeclineJoinRequestParams = {
+ requests: string;
+};
+
+export default DeclineJoinRequestParams;
diff --git a/src/libs/API/parameters/JoinPolicyInviteLink.ts b/src/libs/API/parameters/JoinPolicyInviteLink.ts
new file mode 100644
index 000000000000..4b280b8cd8c6
--- /dev/null
+++ b/src/libs/API/parameters/JoinPolicyInviteLink.ts
@@ -0,0 +1,6 @@
+type JoinPolicyInviteLinkParams = {
+ policyID: string;
+ inviterEmail: string;
+};
+
+export default JoinPolicyInviteLinkParams;
diff --git a/src/libs/API/parameters/PayMoneyRequestParams.ts b/src/libs/API/parameters/PayMoneyRequestParams.ts
index edf05b6ce528..4a769f057e10 100644
--- a/src/libs/API/parameters/PayMoneyRequestParams.ts
+++ b/src/libs/API/parameters/PayMoneyRequestParams.ts
@@ -5,6 +5,7 @@ type PayMoneyRequestParams = {
chatReportID: string;
reportActionID: string;
paymentMethodType: PaymentMethodType;
+ amount?: number;
};
export default PayMoneyRequestParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 00e8b5e761ad..f529032130bb 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -149,9 +149,13 @@ export type {default as AcceptACHContractForBankAccount} from './AcceptACHContra
export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams';
export type {default as UpdateWorkspaceMembersRoleParams} from './UpdateWorkspaceMembersRoleParams';
export type {default as SetWorkspaceCategoriesEnabledParams} from './SetWorkspaceCategoriesEnabledParams';
+export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspaceCategoriesParams';
export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams';
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
+export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest';
+export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest';
+export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index ee4ce1ea3670..1b41ced4f1d7 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -115,6 +115,7 @@ const WRITE_COMMANDS = {
CREATE_WORKSPACE: 'CreateWorkspace',
CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment',
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
+ CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
CREATE_TASK: 'CreateTask',
CANCEL_TASK: 'CancelTask',
@@ -156,6 +157,9 @@ const WRITE_COMMANDS = {
CANCEL_PAYMENT: 'CancelPayment',
ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount',
SWITCH_TO_OLD_DOT: 'SwitchToOldDot',
+ JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink',
+ ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest',
+ DECLINE_JOIN_REQUEST: 'DeclineJoinRequest',
} as const;
type WriteCommand = ValueOf;
@@ -264,6 +268,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
+ [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams;
[WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams;
@@ -310,6 +315,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams;
[WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams;
[WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams;
+ [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
+ [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams;
+ [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams;
};
const READ_COMMANDS = {
@@ -386,6 +394,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
OPEN_OLD_DOT_LINK: 'OpenOldDotLink',
REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails',
GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages',
+ JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink',
RECONNECT_APP: 'ReconnectApp',
} as const;
@@ -397,6 +406,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams;
[SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams;
[SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams;
+ [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams;
[SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams;
};
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index a42cb6a8f756..aef615018b4c 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -7,6 +7,7 @@ import * as CurrencyUtils from './CurrencyUtils';
import * as PolicyUtils from './PolicyUtils';
type DefaultMileageRate = {
+ customUnitRateID?: string;
rate?: number;
currency?: string;
unit: Unit;
@@ -38,6 +39,7 @@ function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate |
}
return {
+ customUnitRateID: distanceRate.customUnitRateID,
rate: distanceRate.rate,
currency: distanceRate.currency,
unit: distanceUnit.attributes.unit,
@@ -76,6 +78,27 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string
return convertedDistance.toFixed(2);
}
+/**
+ * @param hasRoute Whether the route exists for the distance request
+ * @param distanceInMeters Distance traveled
+ * @param unit Unit that should be used to display the distance
+ * @param rate Expensable amount allowed per unit
+ * @param translate Translate function
+ * @returns A string that describes the distance traveled
+ */
+function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number, translate: LocaleContextProps['translate']): string {
+ if (!hasRoute || !rate) {
+ return translate('iou.routePending');
+ }
+
+ const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
+ const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers');
+ const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer');
+ const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit;
+
+ return `${distanceInUnits} ${unitString}`;
+}
+
/**
* @param hasRoute Whether the route exists for the distance request
* @param distanceInMeters Distance traveled
@@ -99,15 +122,13 @@ function getDistanceMerchant(
return translate('iou.routePending');
}
- const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
- const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers');
+ const formattedDistance = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate);
const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer');
- const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit;
const ratePerUnit = PolicyUtils.getUnitRateValue({rate}, toLocaleDigit);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `;
- return `${distanceInUnits} ${unitString} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`;
+ return `${formattedDistance} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`;
}
/**
diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts
index cab0f48d75fd..33cda171f24b 100644
--- a/src/libs/EmojiUtils.ts
+++ b/src/libs/EmojiUtils.ts
@@ -242,9 +242,13 @@ function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji
/**
* Given an emoji item object, return an emoji code based on its type.
*/
-const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: number): string => {
+const getEmojiCodeWithSkinColor = (item: Emoji, preferredSkinToneIndex: OnyxEntry): string | undefined => {
const {code, types} = item;
- if (types?.[preferredSkinToneIndex]) {
+ if (!preferredSkinToneIndex) {
+ return;
+ }
+
+ if (typeof preferredSkinToneIndex === 'number' && types?.[preferredSkinToneIndex]) {
return types[preferredSkinToneIndex];
}
@@ -305,7 +309,7 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[]
* Replace any emoji name in a text with the emoji icon.
* If we're on mobile, we also add a space after the emoji granted there's no text after it.
*/
-function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
+function replaceEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
// emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it
const emojisTrie = require('./EmojiTrie').default;
@@ -345,9 +349,9 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF
// Set the cursor to the end of the last replaced Emoji. Note that we position after
// the extra space, if we added one.
- cursorPosition = newText.indexOf(emoji) + emojiReplacement.length;
+ cursorPosition = newText.indexOf(emoji) + (emojiReplacement?.length ?? 0);
- newText = newText.replace(emoji, emojiReplacement);
+ newText = newText.replace(emoji, emojiReplacement ?? '');
}
}
@@ -369,7 +373,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF
/**
* Find all emojis in a text and replace them with their code.
*/
-function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
+function replaceAndExtractEmojis(text: string, preferredSkinTone: OnyxEntry = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji {
const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang);
return {
diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts
index 20313ee8912d..784d339a4a0d 100644
--- a/src/libs/ErrorUtils.ts
+++ b/src/libs/ErrorUtils.ts
@@ -180,3 +180,5 @@ export {
getMicroSecondOnyxErrorObject,
isReceiptError,
};
+
+export type {OnyxDataWithErrors};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index fc89b53fbefd..6f5dcdf9cda9 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -63,6 +63,7 @@ const loadConciergePage = () => require('../../../pages/ConciergePage').default
const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default as React.ComponentType;
const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default as React.ComponentType;
const loadReportAvatar = () => require('../../../pages/ReportAvatar').default as React.ComponentType;
+const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default as React.ComponentType;
let timezone: Timezone | null;
let currentAccountID = -1;
@@ -356,6 +357,14 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f
options={screenOptions.fullScreen}
component={DesktopSignInRedirectPage}
/>
+
);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index 545641957c9a..978e338796ea 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -251,6 +251,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: () => require('../../../pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index f38ec213a466..58d9efb43df5 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -11,6 +11,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as Session from '@libs/actions/Session';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import Navigation from '@libs/Navigation/Navigation';
@@ -47,7 +48,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
// When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method.
// To prevent this, the value of the bottomTabRoute?.name is checked here
bottomTabRoute?.name === SCREENS.WORKSPACE.INITIAL ||
- (currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR)
+ Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) ||
+ Session.isAnonymousUser()
) {
return;
}
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index 20c426a74c71..2ca4c5178a5e 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -45,7 +45,7 @@ function parseAndLogRoute(state: NavigationState) {
const focusedRoute = findFocusedRoute(state);
- if (focusedRoute?.name !== SCREENS.NOT_FOUND) {
+ if (focusedRoute?.name !== SCREENS.NOT_FOUND && focusedRoute?.name !== SCREENS.SAML_SIGN_IN) {
updateLastVisitedPath(currentPath);
}
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index 7959999ee813..5bc7d52230a8 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -4,9 +4,9 @@ import SCREENS from '@src/SCREENS';
const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = {
[SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE],
[SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT],
- [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE],
+ [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION],
[SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET],
- [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
+ [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS],
};
export default CENTRAL_PANE_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 3ceb3c1ac7df..8a24dc177a80 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -22,6 +22,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.PROFILE_AVATAR]: ROUTES.PROFILE_AVATAR.route,
[SCREENS.WORKSPACE_AVATAR]: ROUTES.WORKSPACE_AVATAR.route,
[SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route,
+ [SCREENS.WORKSPACE_JOIN_USER]: ROUTES.WORKSPACE_JOIN_USER.route,
// Sidebar
[NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: {
@@ -280,6 +281,15 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
+ [SCREENS.WORKSPACE.MEMBER_DETAILS]: {
+ path: ROUTES.WORKSPACE_MEMBER_DETAILS.route,
+ },
+ [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: {
+ path: ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.route,
+ },
+ [SCREENS.WORKSPACE.CATEGORY_CREATE]: {
+ path: ROUTES.WORKSPACE_CATEGORY_CREATE.route,
+ },
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 19a691582333..a3b2727a2725 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -93,6 +93,7 @@ type CentralPaneNavigatorParamList = {
};
[SCREENS.WORKSPACE.TAGS]: {
policyID: string;
+ categoryName: string;
};
};
@@ -197,6 +198,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.CATEGORY_CREATE]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.CATEGORY_SETTINGS]: {
policyID: string;
categoryName: string;
@@ -204,6 +208,16 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.MEMBER_DETAILS]: {
+ policyID: string;
+ accountID: string;
+ backTo: Routes;
+ };
+ [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION]: {
+ policyID: string;
+ accountID: string;
+ backTo: Routes;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
@@ -567,6 +581,10 @@ type AuthScreensParamList = SharedScreensParamList & {
[SCREENS.WORKSPACE_AVATAR]: {
policyID: string;
};
+ [SCREENS.WORKSPACE_JOIN_USER]: {
+ policyID: string;
+ email: string;
+ };
[SCREENS.REPORT_AVATAR]: {
reportID: string;
};
diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts
index d6ec246eddf7..40778f38c0d4 100644
--- a/src/libs/Notification/PushNotification/NotificationType.ts
+++ b/src/libs/Notification/PushNotification/NotificationType.ts
@@ -18,6 +18,8 @@ type ReportCommentNotificationData = {
shouldScrollToLastUnread?: boolean;
roomName?: string;
onyxData?: OnyxServerUpdate[];
+ lastUpdateID?: number;
+ previousUpdateID?: number;
};
/**
diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
index 813e0aecbd5c..7f86d3ddb9ac 100644
--- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
+++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
@@ -1,4 +1,5 @@
import Onyx from 'react-native-onyx';
+import * as OnyxUpdates from '@libs/actions/OnyxUpdates';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils';
@@ -6,8 +7,10 @@ import {extractPolicyIDFromPath} from '@libs/PolicyUtils';
import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils';
import Visibility from '@libs/Visibility';
import * as Modal from '@userActions/Modal';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import backgroundRefresh from './backgroundRefresh';
import PushNotification from './index';
@@ -27,9 +30,28 @@ Onyx.connect({
* Setup reportComment push notification callbacks.
*/
export default function subscribeToReportCommentPushNotifications() {
- PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => {
+ PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => {
Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID});
- Onyx.update(onyxData ?? []);
+
+ if (onyxData && lastUpdateID && previousUpdateID) {
+ Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0});
+
+ const updates: OnyxUpdatesFromServer = {
+ type: CONST.ONYX_UPDATE_TYPES.AIRSHIP,
+ lastUpdateID,
+ previousUpdateID,
+ updates: [
+ {
+ eventType: 'eventType',
+ data: onyxData,
+ },
+ ],
+ };
+ OnyxUpdates.applyOnyxUpdatesReliably(updates);
+ } else {
+ Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0});
+ }
+
backgroundRefresh();
});
diff --git a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts
index 9c7e6402d69b..82410b120df2 100644
--- a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts
+++ b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts
@@ -1,6 +1,7 @@
-import type {OnyxValue} from '@src/ONYXKEYS';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Report} from '@src/types/onyx';
-export default function reportWithoutHasDraftSelector(report: OnyxValue<'report_'>) {
+export default function reportWithoutHasDraftSelector(report: OnyxEntry) {
if (!report) {
return report;
}
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 07f0df962455..fd803a508b4a 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -107,7 +107,7 @@ type Hierarchy = Record;
selectedOptions?: Option[];
maxRecentReportsToShow?: number;
excludeLogins?: string[];
@@ -156,7 +156,6 @@ type SectionForSearchTerm = {
section: CategorySection;
newIndexOffset: number;
};
-
type GetOptions = {
recentReports: ReportUtils.OptionData[];
personalDetails: ReportUtils.OptionData[];
@@ -533,7 +532,6 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
// some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action
const lastOriginalReportAction = lastReportActions[report?.reportID ?? ''] ?? null;
let lastMessageTextFromReport = '';
- const lastActionName = lastReportAction?.actionName ?? '';
if (ReportUtils.isArchivedRoom(report)) {
const archiveReason =
@@ -585,12 +583,8 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
} else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) {
const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(report?.reportID, lastReportAction);
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true);
- } else if (
- lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
- lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ||
- lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED
- ) {
- lastMessageTextFromReport = lastReportAction?.message?.[0].text ?? '';
+ } else if (ReportActionUtils.isTaskAction(lastReportAction)) {
+ lastMessageTextFromReport = TaskUtils.getTaskReportActionMessage(lastReportAction).text;
} else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
} else if (ReportActionUtils.isApprovedOrSubmittedReportAction(lastReportAction)) {
@@ -1441,7 +1435,8 @@ function getOptions(
const {parentReportID, parentReportActionID} = report ?? {};
const canGetParentReport = parentReportID && parentReportActionID && allReportActions;
const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null;
- const doesReportHaveViolations = betas.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction);
+ const doesReportHaveViolations =
+ (betas?.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction)) ?? false;
return ReportUtils.shouldReportBeInOptionList({
report,
@@ -1805,7 +1800,7 @@ function getIOUConfirmationOptionsFromParticipants(participants: Participant[],
function getFilteredOptions(
reports: OnyxCollection,
personalDetails: OnyxEntry,
- betas: Beta[] = [],
+ betas: OnyxEntry = [],
searchValue = '',
selectedOptions: Array> = [],
excludeLogins: string[] = [],
@@ -1852,9 +1847,9 @@ function getFilteredOptions(
*/
function getShareDestinationOptions(
- reports: Record,
+ reports: Record,
personalDetails: OnyxEntry,
- betas: Beta[] = [],
+ betas: OnyxEntry = [],
searchValue = '',
selectedOptions: Array> = [],
excludeLogins: string[] = [],
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index c9f386f5bd7a..26df03134fd5 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas);
}
+function canUseP2PDistanceRequests(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas);
+}
+
function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas);
}
@@ -44,5 +48,6 @@ export default {
canUseLinkPreviews,
canUseViolations,
canUseReportFields,
+ canUseP2PDistanceRequests,
canUseWorkflowsDelayedSubmission,
};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 5b916148c6ee..f6534e075773 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -91,7 +91,9 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb
*/
function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean {
return (
- !!policy && policy?.isPolicyExpenseChatEnabled && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0)
+ !!policy &&
+ (policy?.isPolicyExpenseChatEnabled || Boolean(policy?.isJoinRequestPending)) &&
+ (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0)
);
}
@@ -227,7 +229,7 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean {
* Checks if policy's scheduled submit / auto reporting frequency is "instant".
* Note: Free policies have "instant" submit always enabled.
*/
-function isInstantSubmitEnabled(policy: OnyxEntry): boolean {
+function isInstantSubmitEnabled(policy: OnyxEntry | EmptyObject): boolean {
return policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT || policy?.type === CONST.POLICY.TYPE.FREE;
}
@@ -242,6 +244,13 @@ function extractPolicyIDFromPath(path: string) {
return path.match(CONST.REGEX.POLICY_ID_FROM_PATH)?.[1];
}
+/**
+ * Whether the policy has active accounting integration connections
+ */
+function hasAccountingConnections(policy: OnyxEntry) {
+ return Boolean(policy?.connections);
+}
+
function getPathWithoutPolicyID(path: string) {
return path.replace(CONST.REGEX.PATH_WITHOUT_POLICY_ID, '/');
}
@@ -263,6 +272,7 @@ function goBackFromInvalidPolicy() {
export {
getActivePolicies,
+ hasAccountingConnections,
hasPolicyMemberError,
hasPolicyError,
hasPolicyErrorFields,
diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts
index bc48111eadc5..3cb15c0f3fc3 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher.ts
@@ -5,7 +5,7 @@ import type {LiteralUnion, ValueOf} from 'type-fest';
import Log from '@libs/Log';
import type CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {OnyxUpdateEvent, OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
+import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import TYPE from './EventType';
import Pusher from './library';
@@ -22,8 +22,6 @@ type Args = {
authEndpoint: string;
};
-type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer;
-
type UserIsTypingEvent = ReportUserIsTyping & {
userLogin?: string;
};
@@ -37,7 +35,7 @@ type PusherEventMap = {
[TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
};
-type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : PushJSON;
+type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : OnyxUpdatesFromServer;
type EventCallbackError = {type: ValueOf; data: {code: number}};
@@ -413,4 +411,4 @@ export {
getPusherSocketID,
};
-export type {EventCallbackError, States, PushJSON, UserIsTypingEvent, UserIsLeavingRoomEvent};
+export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent};
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 1ee75eb9c2f6..2bd79adef516 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -1,10 +1,10 @@
import type {OnyxUpdate} from 'react-native-onyx';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
+import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import Log from './Log';
import NetworkConnection from './NetworkConnection';
import * as Pusher from './Pusher/pusher';
-import type {PushJSON} from './Pusher/pusher';
type Callback = (data: OnyxUpdate[]) => Promise;
@@ -25,10 +25,10 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis
/**
* Abstraction around subscribing to private user channel events. Handles all logs and errors automatically.
*/
-function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: PushJSON) => void) {
+function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer) => void) {
const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const;
- function logPusherEvent(pushJSON: PushJSON) {
+ function logPusherEvent(pushJSON: OnyxUpdatesFromServer) {
Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON);
}
@@ -36,7 +36,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
NetworkConnection.triggerReconnectionCallbacks('Pusher re-subscribed to private user channel');
}
- function onEventPush(pushJSON: PushJSON) {
+ function onEventPush(pushJSON: OnyxUpdatesFromServer) {
logPusherEvent(pushJSON);
onEvent(pushJSON);
}
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 0b6c9671d69c..b12469941fd9 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -6,7 +6,15 @@ import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ActionName, ChangeLog, IOUMessage, OriginalMessageActionableMentionWhisper, OriginalMessageIOU, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage';
+import type {
+ ActionName,
+ ChangeLog,
+ IOUMessage,
+ OriginalMessageActionableMentionWhisper,
+ OriginalMessageIOU,
+ OriginalMessageJoinPolicyChangeLog,
+ OriginalMessageReimbursementDequeued,
+} from '@src/types/onyx/OriginalMessage';
import type Report from '@src/types/onyx/Report';
import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
@@ -382,10 +390,6 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key:
return false;
}
- if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKEDITED) {
- return false;
- }
-
// Filter out any unsupported reportAction types
if (!supportedActionTypes.includes(reportAction.actionName)) {
return false;
@@ -675,7 +679,8 @@ function isTaskAction(reportAction: OnyxEntry): boolean {
return (
reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ||
- reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED
+ reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ||
+ reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKEDITED
);
}
@@ -832,7 +837,7 @@ function hasRequestFromCurrentAccount(reportID: string, currentAccountID: number
* Checks if a given report action corresponds to an actionable mention whisper.
* @param reportAction
*/
-function isActionableMentionWhisper(reportAction: OnyxEntry): boolean {
+function isActionableMentionWhisper(reportAction: OnyxEntry): reportAction is ReportActionBase & OriginalMessageActionableMentionWhisper {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER;
}
@@ -884,6 +889,26 @@ function isCurrentActionUnread(report: Report | EmptyObject, reportAction: Repor
return isReportActionUnread(reportAction, lastReadTime) && (!prevReportAction || !isReportActionUnread(prevReportAction, lastReadTime));
}
+/**
+ * Checks if a given report action corresponds to a join request action.
+ * @param reportAction
+ */
+function isActionableJoinRequest(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST;
+}
+
+/**
+ * Checks if any report actions correspond to a join request action that is still pending.
+ * @param reportID
+ */
+function isActionableJoinRequestPending(reportID: string): boolean {
+ const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(reportID)));
+ const findPendingRequest = sortedReportActions.find(
+ (reportActionItem) => isActionableJoinRequest(reportActionItem) && (reportActionItem as OriginalMessageJoinPolicyChangeLog)?.originalMessage?.choice === '',
+ );
+ return !!findPendingRequest;
+}
+
function isApprovedOrSubmittedReportAction(action: OnyxEntry | EmptyObject) {
return [CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.SUBMITTED].some((type) => type === action?.actionName);
}
@@ -949,6 +974,8 @@ export {
isActionableMentionWhisper,
getActionableMentionWhisperMessage,
isCurrentActionUnread,
+ isActionableJoinRequest,
+ isActionableJoinRequestPending,
};
export type {LastVisibleMessage};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 2bfcb6712109..e062a4857e19 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -28,6 +28,7 @@ import type {
ReportAction,
ReportMetadata,
Session,
+ Task,
Transaction,
TransactionViolation,
} from '@src/types/onyx';
@@ -515,6 +516,14 @@ Onyx.connect({
},
});
+function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource {
+ return currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID);
+}
+
+function getCurrentUserDisplayNameOrEmail(): string | undefined {
+ return currentUserPersonalDetails?.displayName ?? currentUserEmail;
+}
+
function getChatType(report: OnyxEntry | Participant | EmptyObject): ValueOf | undefined {
return report?.chatType;
}
@@ -959,14 +968,6 @@ function isProcessingReport(report: OnyxEntry | EmptyObject): boolean {
return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED;
}
-/**
- * Returns true if the policy has `instant` reporting frequency and if the report is still being processed (i.e. submitted state)
- */
-function isExpenseReportWithInstantSubmittedState(report: OnyxEntry | EmptyObject): boolean {
- const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? null;
- return isExpenseReport(report) && isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy);
-}
-
/**
* Check if the report is a single chat report that isn't a thread
* and personal detail of participant is optimistic data
@@ -1075,6 +1076,20 @@ function findLastAccessedReport(
return adminReport ?? sortedReports.at(-1) ?? null;
}
+/**
+ * Whether the provided report has expenses
+ */
+function hasExpenses(reportID?: string): boolean {
+ return !!Object.values(allTransactions ?? {}).find((transaction) => `${transaction?.reportID}` === `${reportID}`);
+}
+
+/**
+ * Whether the provided report is a closed expense report with no expenses
+ */
+function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean {
+ return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && isExpenseReport(report) && !hasExpenses(report.reportID);
+}
+
/**
* Whether the provided report is an archived room
*/
@@ -1082,6 +1097,16 @@ function isArchivedRoom(report: OnyxEntry | EmptyObject): boolean {
return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED;
}
+/**
+ * Whether the provided report is the admin's room
+ */
+function isJoinRequestInAdminRoom(report: OnyxEntry): boolean {
+ if (!report) {
+ return false;
+ }
+ return ReportActionsUtils.isActionableJoinRequestPending(report.reportID);
+}
+
/**
* Checks if the current user is allowed to comment on the given report.
*/
@@ -1279,6 +1304,29 @@ function getChildReportNotificationPreference(reportAction: OnyxEntry): boolean {
+ if (!isMoneyRequestReport(moneyRequestReport)) {
+ return false;
+ }
+
+ if (isReportApproved(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) {
+ return false;
+ }
+
+ if (isGroupPolicy(moneyRequestReport) && isProcessingReport(moneyRequestReport) && !PolicyUtils.isInstantSubmitEnabled(getPolicy(moneyRequestReport?.policyID))) {
+ return false;
+ }
+
+ return true;
+}
+
/**
* Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a
* policy admin
@@ -1293,14 +1341,13 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID:
// For now, users cannot delete split actions
const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
- if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (!isEmptyObject(report) && isReportApproved(report))) {
+ if (isSplitAction) {
return false;
}
if (isActionOwner) {
- if (!isEmptyObject(report) && isPaidGroupPolicyExpenseReport(report)) {
- // If it's a paid policy expense report, only allow deleting the request if it's a draft or is instantly submitted or the user is the policy admin
- return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report) || PolicyUtils.isPolicyAdmin(policy);
+ if (!isEmptyObject(report) && isMoneyRequestReport(report)) {
+ return canAddOrDeleteTransactions(report);
}
return true;
}
@@ -1820,7 +1867,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -1887,6 +1934,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | Op
return false;
}
+ if (isJoinRequestInAdminRoom(optionOrReport)) {
+ return true;
+ }
+
if (isArchivedRoom(optionOrReport) || isArchivedRoom(getReport(optionOrReport.parentReportID))) {
return false;
}
@@ -2583,6 +2634,10 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu
return parentReportActionMessage;
}
+ if (isClosedExpenseReportWithNoExpenses(report)) {
+ return Localize.translateLocal('parentReportAction.deletedReport');
+ }
+
if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) {
return Localize.translateLocal('parentReportAction.deletedTask');
}
@@ -2983,11 +3038,10 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency);
const policy = getPolicy(policyID);
- const isFree = policy?.type === CONST.POLICY.TYPE.FREE;
+ const isInstantSubmitEnabled = PolicyUtils.isInstantSubmitEnabled(policy);
- // Define the state and status of the report based on whether the policy is free or paid
- const stateNum = isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN;
- const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN;
+ const stateNum = isInstantSubmitEnabled ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN;
+ const statusNum = isInstantSubmitEnabled ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN;
const expenseReport: OptimisticExpenseReport = {
reportID: generateReportID(),
@@ -3158,14 +3212,14 @@ function buildOptimisticIOUReportAction(
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: getIOUReportActionMessage(iouReportID, type, amount, comment, currency, paymentType, isSettlingUp),
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3191,14 +3245,14 @@ function buildOptimisticApprovedReportAction(amount: number, currency: string, e
actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.APPROVED, Math.abs(amount), '', currency),
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3233,14 +3287,14 @@ function buildOptimisticMovedReportAction(fromPolicyID: string, toPolicyID: stri
actionName: CONST.REPORT.ACTIONS.TYPE.MOVED,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: movedActionMessage,
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3266,14 +3320,14 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatar(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: getIOUReportActionMessage(expenseReportID, CONST.REPORT.ACTIONS.TYPE.SUBMITTED, Math.abs(amount), '', currency),
person: [
{
style: 'strong',
- text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
type: 'TEXT',
},
],
@@ -3339,7 +3393,7 @@ function buildOptimisticModifiedExpenseReportAction(
actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
created: DateUtils.getDBTime(),
isAttachment: false,
message: [
@@ -3422,7 +3476,7 @@ function buildOptimisticTaskReportAction(taskReportID: string, actionName: Origi
actionName,
actorAccountID: currentUserAccountID,
automatic: false,
- avatar: currentUserPersonalDetails?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
originalMessage,
message: [
@@ -3498,10 +3552,6 @@ function buildOptimisticChatReport(
};
}
-function getCurrentUserAvatarOrDefault(): UserUtils.AvatarSource {
- return allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID);
-}
-
/**
* Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically
* @param [created] - Action created time
@@ -3528,7 +3578,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction: string, created
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
@@ -3564,7 +3614,7 @@ function buildOptimisticRenamedRoomReportAction(newName: string, oldName: string
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
originalMessage: {
@@ -3605,11 +3655,11 @@ function buildOptimisticHoldReportAction(comment: string, created = DateUtils.ge
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
- avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
created,
shouldShow: true,
};
@@ -3636,42 +3686,79 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
- avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: getCurrentUserAvatarOrDefault(),
created,
shouldShow: true,
};
}
-/**
- * Returns the necessary reportAction onyx data to indicate that a task report has been edited
- */
-function buildOptimisticEditedTaskReportAction(emailEditingTask: string): OptimisticEditedTaskReportAction {
+function buildOptimisticEditedTaskFieldReportAction({title, description}: Task): OptimisticEditedTaskReportAction {
+ // We do not modify title & description in one request, so we need to create a different optimistic action for each field modification
+ let field = '';
+ let value = '';
+ if (title !== undefined) {
+ field = 'task title';
+ value = title;
+ } else if (description !== undefined) {
+ field = 'description';
+ value = description;
+ }
+
+ let changelog = 'edited this task';
+ if (field && value) {
+ changelog = `updated the ${field} to ${value}`;
+ } else if (field) {
+ changelog = `removed the ${field}`;
+ }
+
return {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
actorAccountID: currentUserAccountID,
message: [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ text: changelog,
+ html: changelog,
+ },
+ ],
+ person: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: emailEditingTask,
+ text: getCurrentUserDisplayNameOrEmail(),
},
+ ],
+ automatic: false,
+ avatar: getCurrentUserAvatarOrDefault(),
+ created: DateUtils.getDBTime(),
+ shouldShow: false,
+ };
+}
+
+function buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID: number): OptimisticEditedTaskReportAction {
+ return {
+ reportActionID: NumberUtils.rand64(),
+ actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ actorAccountID: currentUserAccountID,
+ message: [
{
- type: CONST.REPORT.MESSAGE.TYPE.TEXT,
- style: 'normal',
- text: ' edited this task',
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ text: `assigned to ${getDisplayNameForParticipant(assigneeAccountID)}`,
+ html: `assigned to `,
},
],
person: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
automatic: false,
@@ -3714,7 +3801,7 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail,
+ text: getCurrentUserDisplayNameOrEmail(),
},
],
reportActionID: NumberUtils.rand64(),
@@ -3979,7 +4066,7 @@ function shouldReportBeInOptionList({
report: OnyxEntry;
currentReportId: string;
isInGSDMode: boolean;
- betas: Beta[];
+ betas: OnyxEntry;
policies: OnyxCollection;
excludeEmptyChats: boolean;
doesReportHaveViolations: boolean;
@@ -4138,7 +4225,13 @@ function chatIncludesChronos(report: OnyxEntry | EmptyObject): boolean {
* - It's an ADDCOMMENT that is not an attachment
*/
function canFlagReportAction(reportAction: OnyxEntry, reportID: string | undefined): boolean {
- const report = getReport(reportID);
+ let report = getReport(reportID);
+
+ // If the childReportID exists in reportAction and is equal to the reportID,
+ // the report action being evaluated is the parent report action in a thread, and we should get the parent report to evaluate instead.
+ if (reportAction?.childReportID?.toString() === reportID?.toString()) {
+ report = getReport(report?.parentReportID);
+ }
const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID;
const isOriginalMessageHaveHtml =
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT ||
@@ -4313,7 +4406,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
return false;
}
- // In case of expense reports, we have to look at the parent workspace chat to get the isOwnPolicyExpenseChat property
let isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat ?? false;
if (isExpenseReport(report) && getParentReport(report)) {
isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat);
@@ -4327,12 +4419,8 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
// User can request money in any IOU report, unless paid, but user can only request money in an expense report
// which is tied to their workspace chat.
if (isMoneyRequestReport(report)) {
- const isOwnExpenseReport = isExpenseReport(report) && isOwnPolicyExpenseChat;
- if (isOwnExpenseReport && PolicyUtils.isPaidGroupPolicy(policy)) {
- return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report);
- }
-
- return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID);
+ const canAddTransactions = canAddOrDeleteTransactions(report);
+ return isGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions;
}
// In case of policy expense chat, users can only request money from their own policy expense chat
@@ -5088,6 +5176,17 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry | undefined | null, chatReport: OnyxEntry | null): boolean {
+ return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport);
+}
+
export {
getReportParticipantsTitle,
isReportMessageAttachment,
@@ -5111,6 +5210,7 @@ export {
getPolicyName,
getPolicyType,
isArchivedRoom,
+ isClosedExpenseReportWithNoExpenses,
isExpensifyOnlyParticipantInReport,
canCreateTaskInReport,
isPolicyExpenseChatAdmin,
@@ -5119,7 +5219,6 @@ export {
isPublicAnnounceRoom,
isConciergeChatReport,
isProcessingReport,
- isExpenseReportWithInstantSubmittedState,
isCurrentUserTheOnlyParticipant,
hasAutomatedExpensifyAccountIDs,
hasExpensifyGuidesEmails,
@@ -5159,7 +5258,8 @@ export {
buildOptimisticClosedReportAction,
buildOptimisticCreatedReportAction,
buildOptimisticRenamedRoomReportAction,
- buildOptimisticEditedTaskReportAction,
+ buildOptimisticEditedTaskFieldReportAction,
+ buildOptimisticChangedTaskAssigneeReportAction,
buildOptimisticIOUReport,
buildOptimisticApprovedReportAction,
buildOptimisticMovedReportAction,
@@ -5291,6 +5391,9 @@ export {
canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
+ isJoinRequestInAdminRoom,
+ canAddOrDeleteTransactions,
+ shouldCreateNewMoneyRequestReport,
};
export type {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 8d53e992cb2d..3aa4cb63df9a 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -328,7 +328,7 @@ function getOptionData({
const newName = lastAction?.originalMessage?.newName ?? '';
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
} else if (ReportActionsUtils.isTaskAction(lastAction)) {
- result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction.actionName);
+ result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction).text;
} else if (
lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ||
lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM ||
@@ -386,6 +386,12 @@ function getOptionData({
result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report);
+ if (ReportActionsUtils.isActionableJoinRequestPending(report.reportID)) {
+ result.isPinned = true;
+ result.isUnread = true;
+ result.brickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
+ }
+
if (!hasMultipleParticipants) {
result.accountID = personalDetail?.accountID;
result.login = personalDetail?.login;
diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts
index 623d449db885..81a079003d0e 100644
--- a/src/libs/TaskUtils.ts
+++ b/src/libs/TaskUtils.ts
@@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
+import type {Message} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
import * as CollectionUtils from './CollectionUtils';
import * as Localize from './Localize';
@@ -22,16 +23,21 @@ Onyx.connect({
/**
* Given the Task reportAction name, return the appropriate message to be displayed and copied to clipboard.
*/
-function getTaskReportActionMessage(actionName: string): string {
- switch (actionName) {
+function getTaskReportActionMessage(action: OnyxEntry): Pick {
+ switch (action?.actionName) {
case CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED:
- return Localize.translateLocal('task.messages.completed');
+ return {text: Localize.translateLocal('task.messages.completed')};
case CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED:
- return Localize.translateLocal('task.messages.canceled');
+ return {text: Localize.translateLocal('task.messages.canceled')};
case CONST.REPORT.ACTIONS.TYPE.TASKREOPENED:
- return Localize.translateLocal('task.messages.reopened');
+ return {text: Localize.translateLocal('task.messages.reopened')};
+ case CONST.REPORT.ACTIONS.TYPE.TASKEDITED:
+ return {
+ text: action?.message?.[0].text ?? '',
+ html: action?.message?.[0].html,
+ };
default:
- return Localize.translateLocal('task.task');
+ return {text: Localize.translateLocal('task.task')};
}
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 5f9657755b02..cb3aa20ab6a7 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -27,6 +27,7 @@ import type {
import {WRITE_COMMANDS} from '@libs/API/types';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
+import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as IOUUtils from '@libs/IOUUtils';
@@ -222,12 +223,22 @@ Onyx.connect({
},
});
+let lastSelectedDistanceRates: OnyxEntry = {};
+Onyx.connect({
+ key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES,
+ callback: (value) => {
+ lastSelectedDistanceRates = value;
+ },
+});
+
/**
* Initialize money request info
* @param reportID to attach the transaction to
+ * @param policy
+ * @param isFromGlobalCreate
* @param iouRequestType one of manual/scan/distance
*/
-function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
+function initMoneyRequest(reportID: string, policy: OnyxEntry, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
// Generate a brand new transactionID
const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID;
// Disabling this line since currentDate can be an empty string
@@ -241,6 +252,12 @@ function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequ
waypoint0: {},
waypoint1: {},
};
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null;
+ let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
+ if (ReportUtils.isPolicyExpenseChat(report)) {
+ customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? '';
+ }
+ comment.customUnit = {customUnitRateID};
}
// Store the transaction in Onyx and mark it as not saved so it can be cleaned up later
@@ -828,37 +845,26 @@ function getMoneyRequestInformation(
// STEP 2: Get the money request report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report.
// If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic money request report.
let iouReport: OnyxEntry = null;
- const shouldCreateNewMoneyRequestReport = !moneyRequestReportID && (!chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport));
if (moneyRequestReportID) {
iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`] ?? null;
- } else if (!shouldCreateNewMoneyRequestReport) {
+ } else {
iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`] ?? null;
}
- let isFromPaidPolicy = false;
- if (isPolicyExpenseChat) {
- isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy ?? null);
-
- // If the linked expense report on paid policy is not draft and not instantly submitted, we need to create a new draft expense report
- if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport) && !ReportUtils.isExpenseReportWithInstantSubmittedState(iouReport)) {
- iouReport = null;
- }
- }
+ const shouldCreateNewMoneyRequestReport = ReportUtils.shouldCreateNewMoneyRequestReport(iouReport, chatReport);
- if (iouReport) {
- if (isPolicyExpenseChat) {
- iouReport = {...iouReport};
- if (iouReport?.currency === currency && typeof iouReport.total === 'number') {
- // Because of the Expense reports are stored as negative values, we subtract the total from the amount
- iouReport.total -= amount;
- }
- } else {
- iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency);
- }
- } else {
+ if (!iouReport || shouldCreateNewMoneyRequestReport) {
iouReport = isPolicyExpenseChat
? ReportUtils.buildOptimisticExpenseReport(chatReport.reportID, chatReport.policyID ?? '', payeeAccountID, amount, currency)
: ReportUtils.buildOptimisticIOUReport(payeeAccountID, payerAccountID, amount, chatReport.reportID, currency);
+ } else if (isPolicyExpenseChat) {
+ iouReport = {...iouReport};
+ if (iouReport?.currency === currency && typeof iouReport.total === 'number') {
+ // Because of the Expense reports are stored as negative values, we subtract the total from the amount
+ iouReport.total -= amount;
+ }
+ } else {
+ iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency);
}
// STEP 3: Build optimistic receipt and transaction
@@ -1843,10 +1849,8 @@ function createSplitsAndOnyxData(
}
// STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one
- // For Control policy expense chats, if the report is already approved, create a new expense report
let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null;
- const shouldCreateNewOneOnOneIOUReport =
- !oneOnOneIOUReport || (isOwnPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport));
+ const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport);
if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) {
oneOnOneIOUReport = isOwnPolicyExpenseChat
@@ -2484,8 +2488,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
}
let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport?.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null;
- const shouldCreateNewOneOnOneIOUReport =
- !oneOnOneIOUReport || (isPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport));
+ const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport);
if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) {
oneOnOneIOUReport = isPolicyExpenseChat
@@ -3638,6 +3641,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT
chatReportID: chatReport.reportID,
reportActionID: optimisticIOUReportAction.reportActionID,
paymentMethodType,
+ amount: Math.abs(total),
},
optimisticData,
successData,
diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts
index ab0dea960b27..b4554f9461ce 100644
--- a/src/libs/actions/OnyxUpdateManager.ts
+++ b/src/libs/actions/OnyxUpdateManager.ts
@@ -41,7 +41,8 @@ export default () => {
if (
!(typeof value === 'object' && !!value) ||
!('type' in value) ||
- (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && value.updates))
+ (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) &&
+ !((value.type === CONST.ONYX_UPDATE_TYPES.PUSHER || value.type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && value.updates))
) {
console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue');
Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null);
diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts
index cfb4735f0638..ab26ad330b6f 100644
--- a/src/libs/actions/OnyxUpdates.ts
+++ b/src/libs/actions/OnyxUpdates.ts
@@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {Merge} from 'type-fest';
import Log from '@libs/Log';
+import * as SequentialQueue from '@libs/Network/SequentialQueue';
import PusherUtils from '@libs/PusherUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -107,7 +108,7 @@ function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFrom
if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) {
return applyHTTPSOnyxUpdates(request, response);
}
- if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) {
+ if ((type === CONST.ONYX_UPDATE_TYPES.PUSHER || type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && updates) {
return applyPusherOnyxUpdates(updates);
}
}
@@ -141,5 +142,17 @@ function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean {
return lastUpdateIDAppliedToClient < previousUpdateID;
}
+function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer) {
+ const previousUpdateID = Number(updates.previousUpdateID) || 0;
+ if (!doesClientNeedToBeUpdated(previousUpdateID)) {
+ apply(updates);
+ return;
+ }
+
+ // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates.
+ SequentialQueue.pause();
+ saveUpdateInformation(updates);
+}
+
// eslint-disable-next-line import/prefer-default-export
-export {saveUpdateInformation, doesClientNeedToBeUpdated, apply};
+export {saveUpdateInformation, doesClientNeedToBeUpdated, apply, applyOnyxUpdatesReliably};
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index aa64611b210f..f6a1ec3ec340 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -57,7 +57,8 @@ import type {
ReportAction,
Transaction,
} from '@src/types/onyx';
-import type {Errors} from '@src/types/onyx/OnyxCommon';
+import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
+import type {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage';
import type {Attributes, CustomUnit, Rate, Unit} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
@@ -749,9 +750,9 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`,
value: {
- ...memberRoles.reduce((member: Record, current) => {
+ ...memberRoles.reduce((member: Record, current) => {
// eslint-disable-next-line no-param-reassign
- member[current.accountID] = {role: current?.role};
+ member[current.accountID] = {role: current?.role, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE};
return member;
}, {}),
errors: null,
@@ -764,6 +765,11 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`,
value: {
+ ...memberRoles.reduce((member: Record, current) => {
+ // eslint-disable-next-line no-param-reassign
+ member[current.accountID] = {role: current?.role, pendingAction: null};
+ return member;
+ }, {}),
errors: null,
},
},
@@ -964,6 +970,37 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData});
}
+/**
+ * Invite member to the specified policyID
+ * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
+ */
+function inviteMemberToWorkspace(policyID: string, inviterEmail: string) {
+ const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const;
+
+ const optimisticMembersState = {policyID, inviterEmail};
+ const failureMembersState = {policyID, inviterEmail};
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: memberJoinKey,
+ value: optimisticMembersState,
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: memberJoinKey,
+ value: {...failureMembersState, errors: ErrorUtils.getMicroSecondOnyxError('common.genericEditFailureMessage')},
+ },
+ ];
+
+ const params = {policyID, inviterEmail};
+
+ API.write(WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK, params, {optimisticData, failureData});
+}
+
/**
* Updates a workspace avatar image
*/
@@ -2435,6 +2472,56 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor
API.write('SetWorkspaceCategoriesEnabled', parameters, onyxData);
}
+function createPolicyCategory(policyID: string, categoryName: string) {
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ name: categoryName,
+ enabled: true,
+ errors: null,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ errors: null,
+ pendingAction: null,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.genericFailureMessage'),
+ pendingAction: null,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ categories: JSON.stringify([{name: categoryName}]),
+ };
+
+ API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData);
+}
+
function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
const onyxData: OnyxData = {
optimisticData: [
@@ -2503,6 +2590,123 @@ function clearCategoryErrors(policyID: string, categoryName: string) {
});
}
+/**
+ * Accept user join request to a workspace
+ */
+function acceptJoinRequest(reportID: string, reportAction: OnyxEntry) {
+ const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT;
+ if (!reportAction) {
+ return;
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice: ''},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ requests: JSON.stringify({
+ [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
+ requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
+ },
+ }),
+ };
+
+ API.write(WRITE_COMMANDS.ACCEPT_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
+}
+
+/**
+ * Decline user join request to a workspace
+ */
+function declineJoinRequest(reportID: string, reportAction: OnyxEntry) {
+ if (!reportAction) {
+ return;
+ }
+ const choice = CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE;
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ originalMessage: {choice: ''},
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const parameters = {
+ requests: JSON.stringify({
+ [(reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage']).policyID]: {
+ requests: [{accountID: reportAction?.actorAccountID, adminsRoomMessageReportActionID: reportAction.reportActionID}],
+ },
+ }),
+ };
+
+ API.write(WRITE_COMMANDS.DECLINE_JOIN_REQUEST, parameters, {optimisticData, failureData, successData});
+}
+
export {
removeMembers,
updateWorkspaceMembersRole,
@@ -2552,5 +2756,9 @@ export {
updateWorkspaceDescription,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
+ inviteMemberToWorkspace,
+ acceptJoinRequest,
+ declineJoinRequest,
+ createPolicyCategory,
clearCategoryErrors,
};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 7ad12cf3e1ed..94fe324d306a 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -113,15 +113,31 @@ Onyx.connect({
},
});
+// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection = {};
+
+// map of reportID to the ID of the oldest reportAction for that report
+const oldestReportActions: Record = {};
+
+// map of report to the ID of the newest action for that report
+const newestReportActions: Record = {};
+
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
- callback: (action, key) => {
- if (!key || !action) {
+ callback: (actions, key) => {
+ if (!key || !actions) {
return;
}
const reportID = CollectionUtils.extractCollectionItemID(key);
- allReportActions[reportID] = action;
+ allReportActions[reportID] = actions;
+ const sortedActions = ReportActionsUtils.getSortedReportActions(Object.values(actions));
+
+ if (sortedActions.length === 0) {
+ return;
+ }
+
+ oldestReportActions[reportID] = sortedActions[0].reportActionID;
+ newestReportActions[reportID] = sortedActions[sortedActions.length - 1].reportActionID;
},
});
@@ -879,7 +895,7 @@ function reconnect(reportID: string) {
* Gets the older actions that have not been read yet.
* Normally happens when you scroll up on a chat, and the actions have not been read yet.
*/
-function getOlderActions(reportID: string, reportActionID: string) {
+function getOlderActions(reportID: string) {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -912,7 +928,7 @@ function getOlderActions(reportID: string, reportActionID: string) {
const parameters: GetOlderActionsParams = {
reportID,
- reportActionID,
+ reportActionID: oldestReportActions[reportID],
};
API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData});
@@ -922,7 +938,7 @@ function getOlderActions(reportID: string, reportActionID: string) {
* Gets the newer actions that have not been read yet.
* Normally happens when you are not located at the bottom of the list and scroll down on a chat.
*/
-function getNewerActions(reportID: string, reportActionID: string) {
+function getNewerActions(reportID: string) {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -955,7 +971,7 @@ function getNewerActions(reportID: string, reportActionID: string) {
const parameters: GetNewerActionsParams = {
reportID,
- reportActionID,
+ reportActionID: newestReportActions[reportID],
};
API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData});
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index e328460c37eb..27c7f3e36fd4 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -399,7 +399,7 @@ function reopenTask(taskReport: OnyxEntry) {
function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task) {
// Create the EditedReportAction on the task
- const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail);
+ const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskFieldReportAction({title, description});
// Sometimes title or description is undefined, so we need to check for that, and we provide it to multiple functions
const reportName = (title ?? report?.reportName)?.trim();
@@ -429,6 +429,11 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task
];
const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ value: {[editTaskReportAction.reportActionID]: {pendingAction: null}},
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
@@ -467,16 +472,22 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task
API.write(WRITE_COMMANDS.EDIT_TASK, parameters, {optimisticData, successData, failureData});
}
-function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: OnyxEntry = null) {
+function editTaskAssignee(
+ report: OnyxTypes.Report,
+ ownerAccountID: number,
+ assigneeEmail: string,
+ assigneeAccountID: number | null = 0,
+ assigneeChatReport: OnyxEntry = null,
+) {
// Create the EditedReportAction on the task
- const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail);
+ const editTaskReportAction = ReportUtils.buildOptimisticChangedTaskAssigneeReportAction(assigneeAccountID ?? 0);
const reportName = report.reportName?.trim();
let assigneeChatReportOnyxData;
const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : '0';
const optimisticReport: OptimisticReport = {
reportName,
- managerID: assigneeAccountID || report.managerID,
+ managerID: assigneeAccountID ?? report.managerID,
pendingFields: {
...(assigneeAccountID && {managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
},
@@ -499,6 +510,11 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi
];
const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
+ value: {[editTaskReportAction.reportActionID]: {pendingAction: null}},
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`,
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 7b146f7447bb..fdd657f801f2 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -578,35 +578,15 @@ function subscribeToUserEvents() {
// Handles the mega multipleEvents from Pusher which contains an array of single events.
// Each single event is passed to PusherUtils in order to trigger the callbacks for that event
PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => {
- // The data for this push event comes in two different formats:
- // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete
- // - The data is an array of objects, where each object is an onyx update
- // Example: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]
- // 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on
- // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above)
- // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]}
- if (Array.isArray(pushJSON)) {
- Log.warn('Received pusher event with array format');
- pushJSON.forEach((multipleEvent) => {
- PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data);
- });
- return;
- }
-
+ // The data for the update is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above)
+ // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]}
const updates = {
type: CONST.ONYX_UPDATE_TYPES.PUSHER,
lastUpdateID: Number(pushJSON.lastUpdateID || 0),
updates: pushJSON.updates ?? [],
previousUpdateID: Number(pushJSON.previousUpdateID || 0),
};
- if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) {
- OnyxUpdates.apply(updates);
- return;
- }
-
- // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates.
- SequentialQueue.pause();
- OnyxUpdates.saveUpdateInformation(updates);
+ OnyxUpdates.applyOnyxUpdatesReliably(updates);
});
// Handles Onyx updates coming from Pusher through the mega multipleEvents.
diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts
index 3dc5924d023a..9fe6e8f018d8 100644
--- a/src/libs/calculateAnchorPosition.ts
+++ b/src/libs/calculateAnchorPosition.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-imports */
-import type {Text as RNText, View} from 'react-native';
import type {ValueOf} from 'type-fest';
+import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
@@ -13,9 +13,9 @@ type AnchorOrigin = {
/**
* Gets the x,y position of the passed in component for the purpose of anchoring another component to it.
*/
-export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise {
+export default function calculateAnchorPosition(anchorComponent: ContextMenuAnchor, anchorOrigin?: AnchorOrigin): Promise {
return new Promise((resolve) => {
- if (!anchorComponent) {
+ if (!anchorComponent || !('measureInWindow' in anchorComponent)) {
resolve({horizontal: 0, vertical: 0});
return;
}
diff --git a/src/libs/focusTextInputAfterAnimation/index.android.ts b/src/libs/focusTextInputAfterAnimation/index.android.ts
index 31c748f5daa4..cca8a6588103 100644
--- a/src/libs/focusTextInputAfterAnimation/index.android.ts
+++ b/src/libs/focusTextInputAfterAnimation/index.android.ts
@@ -19,7 +19,7 @@ import type FocusTextInputAfterAnimation from './types';
*/
const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef, animationLength = 0) => {
setTimeout(() => {
- inputRef.focus();
+ inputRef?.focus();
}, animationLength);
};
diff --git a/src/libs/focusTextInputAfterAnimation/index.ts b/src/libs/focusTextInputAfterAnimation/index.ts
index 3f7c6555b5ce..66d0c35c1a63 100644
--- a/src/libs/focusTextInputAfterAnimation/index.ts
+++ b/src/libs/focusTextInputAfterAnimation/index.ts
@@ -4,7 +4,7 @@ import type FocusTextInputAfterAnimation from './types';
* This library is a no-op for all platforms except for Android and iOS and will immediately focus the given input without any delays.
*/
const focusTextInputAfterAnimation: FocusTextInputAfterAnimation = (inputRef) => {
- inputRef.focus();
+ inputRef?.focus();
};
export default focusTextInputAfterAnimation;
diff --git a/src/libs/focusTextInputAfterAnimation/types.ts b/src/libs/focusTextInputAfterAnimation/types.ts
index a6a14165598b..bfe29317c1ef 100644
--- a/src/libs/focusTextInputAfterAnimation/types.ts
+++ b/src/libs/focusTextInputAfterAnimation/types.ts
@@ -1,5 +1,5 @@
import type {TextInput} from 'react-native';
-type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement, animationLength: number) => void;
+type FocusTextInputAfterAnimation = (inputRef: TextInput | HTMLInputElement | undefined, animationLength: number) => void;
export default FocusTextInputAfterAnimation;
diff --git a/src/libs/getClickedTargetLocation/types.ts b/src/libs/getClickedTargetLocation/types.ts
index 7b1e85e63b17..eed10238be2d 100644
--- a/src/libs/getClickedTargetLocation/types.ts
+++ b/src/libs/getClickedTargetLocation/types.ts
@@ -1,5 +1,5 @@
type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y';
-type GetClickedTargetLocation = (target: Element) => Pick;
+type GetClickedTargetLocation = (target: HTMLDivElement) => Pick;
export default GetClickedTargetLocation;
diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts
index fd03adcffd93..330ba4470097 100644
--- a/src/libs/isReportMessageAttachment.ts
+++ b/src/libs/isReportMessageAttachment.ts
@@ -8,15 +8,15 @@ import type {Message} from '@src/types/onyx/ReportAction';
*
* @param reportActionMessage report action's message as text, html and translationKey
*/
-export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean {
- if (!text || !html) {
+export default function isReportMessageAttachment(message: Message | undefined): boolean {
+ if (!message?.text || !message.html) {
return false;
}
- if (translationKey && text === CONST.ATTACHMENT_MESSAGE_TEXT) {
- return translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT;
+ if (message.translationKey && message.text === CONST.ATTACHMENT_MESSAGE_TEXT) {
+ return message?.translationKey === CONST.TRANSLATION_KEYS.ATTACHMENT;
}
const regex = new RegExp(` ${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}="(.*)"`, 'i');
- return (text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(text)) && (!!html.match(regex) || html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML);
+ return (message.text === CONST.ATTACHMENT_MESSAGE_TEXT || !!Str.isVideo(message.text)) && (!!message.html.match(regex) || message.html === CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML);
}
diff --git a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts
index dbf2829a6c28..c8ef72ca15e7 100644
--- a/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts
+++ b/src/libs/migrations/KeyReportActionsDraftByReportActionID.ts
@@ -47,6 +47,7 @@ export default function () {
// If newReportActionsDrafts[newOnyxKey] isn't set, fall back on the migrated draft if there is one
const currentActionsDrafts = newReportActionsDrafts[newOnyxKey] ?? allReportActionsDrafts[newOnyxKey];
+
newReportActionsDrafts[newOnyxKey] = {
...currentActionsDrafts,
[reportActionID]: reportActionDraft,
diff --git a/src/libs/navigateAfterJoinRequest/index.desktop.ts b/src/libs/navigateAfterJoinRequest/index.desktop.ts
new file mode 100644
index 000000000000..47180c6a1368
--- /dev/null
+++ b/src/libs/navigateAfterJoinRequest/index.desktop.ts
@@ -0,0 +1,8 @@
+import Navigation from '@navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+
+const navigateAfterJoinRequest = () => {
+ Navigation.goBack(undefined, false, true);
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
+};
+export default navigateAfterJoinRequest;
diff --git a/src/libs/navigateAfterJoinRequest/index.ts b/src/libs/navigateAfterJoinRequest/index.ts
new file mode 100644
index 000000000000..b9e533208ec2
--- /dev/null
+++ b/src/libs/navigateAfterJoinRequest/index.ts
@@ -0,0 +1,8 @@
+import Navigation from '@navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+
+const navigateAfterJoinRequest = () => {
+ Navigation.goBack(undefined, false, true);
+ Navigation.navigate(ROUTES.ALL_SETTINGS);
+};
+export default navigateAfterJoinRequest;
diff --git a/src/libs/navigateAfterJoinRequest/index.web.ts b/src/libs/navigateAfterJoinRequest/index.web.ts
new file mode 100644
index 000000000000..47180c6a1368
--- /dev/null
+++ b/src/libs/navigateAfterJoinRequest/index.web.ts
@@ -0,0 +1,8 @@
+import Navigation from '@navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+
+const navigateAfterJoinRequest = () => {
+ Navigation.goBack(undefined, false, true);
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
+};
+export default navigateAfterJoinRequest;
diff --git a/src/pages/DetailsPage.tsx b/src/pages/DetailsPage.tsx
index a9adb5310e58..b3b0f0782ba0 100755
--- a/src/pages/DetailsPage.tsx
+++ b/src/pages/DetailsPage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import AttachmentModal from '@components/AttachmentModal';
@@ -15,6 +15,7 @@ import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.js
index 9fa3a4becea3..a55816d207be 100644
--- a/src/pages/EnablePayments/TermsStep.js
+++ b/src/pages/EnablePayments/TermsStep.js
@@ -1,9 +1,9 @@
import React, {useEffect, useState} from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/FlagCommentPage.tsx b/src/pages/FlagCommentPage.tsx
index 00c38dabc4ec..216196c17d55 100644
--- a/src/pages/FlagCommentPage.tsx
+++ b/src/pages/FlagCommentPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {SvgProps} from 'react-native-svg';
import type {ValueOf} from 'type-fest';
@@ -9,6 +9,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/GetAssistancePage.tsx b/src/pages/GetAssistancePage.tsx
index 948e0c239de9..b543524fc68e 100644
--- a/src/pages/GetAssistancePage.tsx
+++ b/src/pages/GetAssistancePage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import type {MenuItemWithLink} from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/KeyboardShortcutsPage.tsx b/src/pages/KeyboardShortcutsPage.tsx
index 9b70defbf8af..d68643e74a5a 100644
--- a/src/pages/KeyboardShortcutsPage.tsx
+++ b/src/pages/KeyboardShortcutsPage.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx
index f27c821abd8c..559da335cf13 100644
--- a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx
+++ b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx
@@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -7,6 +7,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import type {MenuItemProps} from '@components/MenuItem';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
index 747b23e943ca..3c7520b850b4 100644
--- a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
+++ b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -8,6 +8,7 @@ import LottieAnimations from '@components/LottieAnimations';
import type {MenuItemProps} from '@components/MenuItem';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx
index 0a6a2659ffb6..1893f81da2fe 100644
--- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx
@@ -1,11 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 486bf53b6a28..a9c350102424 100755
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React, {useEffect} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import AutoUpdateTime from '@components/AutoUpdateTime';
@@ -16,6 +16,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js
index 75ae02587486..e18155ea6139 100644
--- a/src/pages/ReimbursementAccount/BankAccountStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountStep.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -11,6 +11,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
diff --git a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx
index b128d6dc75e8..af4b251952de 100644
--- a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx
@@ -1,10 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx
index 2b742ad65699..4228b1da9d12 100644
--- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx
+++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx
@@ -1,10 +1,11 @@
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx
index 25ce2d7b81da..42bf43d78910 100644
--- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx
+++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx
@@ -1,11 +1,12 @@
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
index 6a94a7b456f3..a5b839118edc 100644
--- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
+++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
@@ -1,6 +1,5 @@
import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import React, {useMemo} from 'react';
-import {ScrollView} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
@@ -8,6 +7,7 @@ import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx b/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx
index 2bf76d714cf5..65f7f14d6c91 100644
--- a/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx
+++ b/src/pages/ReimbursementAccount/ConnectBankAccount/components/FinishChatCard.tsx
@@ -1,9 +1,9 @@
import React from 'react';
-import {ScrollView} from 'react-native';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
index d1ac0989ae38..9c28fe928d33 100644
--- a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
+++ b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
@@ -1,7 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -10,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx
index fd2f05493098..4c4bd9a20b71 100644
--- a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx
+++ b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import {ScrollView} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -10,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
index b4272f094071..f05bb70bcd5a 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
@@ -1,10 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
index 8cca56779059..fac405090de7 100644
--- a/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
+++ b/src/pages/ReimbursementAccount/RequestorOnfidoStep.js
@@ -1,12 +1,12 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Onfido from '@components/Onfido';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Growl from '@libs/Growl';
diff --git a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
index d17166365a39..cb9763b5cc25 100644
--- a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
+++ b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
@@ -1,5 +1,5 @@
import React, {useCallback} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
@@ -8,6 +8,7 @@ import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
// @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript.
import Onfido from '@components/Onfido';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Growl from '@libs/Growl';
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index e94c0cc80952..d96e01c1a4d3 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -1,7 +1,7 @@
import {useRoute} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -17,6 +17,7 @@ import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import RoomHeaderAvatars from '@components/RoomHeaderAvatars';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index f2bba4b17a9a..4f1bac01b556 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -1,5 +1,5 @@
import React, {useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {ImageSourcePropType} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
@@ -10,6 +10,7 @@ import MenuItem from '@components/MenuItem';
import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts
index e9440ab932d6..6f177098c2c4 100644
--- a/src/pages/home/ReportScreenContext.ts
+++ b/src/pages/home/ReportScreenContext.ts
@@ -1,8 +1,9 @@
import type {RefObject, SyntheticEvent} from 'react';
import {createContext} from 'react';
-import type {FlatList, GestureResponderEvent, View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {FlatList, GestureResponderEvent, Text, View} from 'react-native';
-type ReactionListAnchor = View | HTMLDivElement | null;
+type ReactionListAnchor = View | Text | HTMLDivElement | null;
type ReactionListEvent = GestureResponderEvent | MouseEvent | SyntheticEvent;
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 4f6e0548eb72..974a8824f5ff 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -22,7 +22,7 @@ import type {Beta, ReportAction, ReportActions} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions';
import ContextMenuActions from './ContextMenuActions';
-import type {ContextMenuType} from './ReportActionContextMenu';
+import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu';
import {hideContextMenu, showContextMenu} from './ReportActionContextMenu';
type BaseReportActionContextMenuOnyxProps = {
@@ -64,7 +64,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & {
type?: ContextMenuType;
/** Target node which is the target of ContentMenu */
- anchor?: MutableRefObject;
+ anchor?: MutableRefObject;
/** Flag to check if the chat participant is Chronos */
isChronosReport?: boolean;
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 831b32def2bb..ffdbcab577b7 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -1,7 +1,8 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import type {MutableRefObject} from 'react';
import React from 'react';
-import type {GestureResponderEvent} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Text, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -13,6 +14,7 @@ import EmailUtils from '@libs/EmailUtils';
import * as Environment from '@libs/Environment/Environment';
import fileDownload from '@libs/fileDownload';
import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails';
+import * as Localize from '@libs/Localize';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
import Navigation from '@libs/Navigation/Navigation';
import Permissions from '@libs/Permissions';
@@ -28,6 +30,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
import type {Beta, ReportAction, ReportActionReactions, Report as ReportType} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
+import type {ContextMenuAnchor} from './ReportActionContextMenu';
import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';
/** Gets the HTML version of the message in an action */
@@ -52,7 +55,7 @@ type ShouldShow = (
reportAction: OnyxEntry,
isArchivedRoom: boolean,
betas: OnyxEntry,
- menuTarget: MutableRefObject | undefined,
+ menuTarget: MutableRefObject | undefined,
isChronosReport: boolean,
reportID: string,
isPinnedChat: boolean,
@@ -69,6 +72,8 @@ type ContextMenuActionPayload = {
close: () => void;
openContextMenu: () => void;
interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void;
+ anchor?: MutableRefObject;
+ checkIfContextMenuActive?: () => void;
openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void;
event?: GestureResponderEvent | MouseEvent | KeyboardEvent;
setIsEmojiPickerActive?: (state: boolean) => void;
@@ -342,9 +347,8 @@ const ContextMenuActions: ContextMenuAction[] = [
// `ContextMenuItem` with `successText` and `successIcon` which will fall back to
// the `text` and `icon`
onPress: (closePopover, {reportAction, selection, reportID}) => {
- const isTaskAction = ReportActionsUtils.isTaskAction(reportAction);
const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction);
- const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionHtml(reportAction);
+ const messageHtml = getActionHtml(reportAction);
const messageText = ReportActionsUtils.getReportActionMessageText(reportAction);
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
@@ -354,6 +358,9 @@ const ContextMenuActions: ContextMenuAction[] = [
const iouReport = ReportUtils.getReport(ReportActionsUtils.getIOUReportIDFromReportActionPreview(reportAction));
const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction);
Clipboard.setString(displayMessage);
+ } else if (ReportActionsUtils.isTaskAction(reportAction)) {
+ const displayMessage = TaskUtils.getTaskReportActionMessage(reportAction).text;
+ Clipboard.setString(displayMessage);
} else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) {
const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportID, reportAction);
Clipboard.setString(modifyExpenseMessage);
@@ -376,6 +383,10 @@ const ContextMenuActions: ContextMenuAction[] = [
} else if (ReportActionsUtils.isActionableMentionWhisper(reportAction)) {
const mentionWhisperMessage = ReportActionsUtils.getActionableMentionWhisperMessage(reportAction);
setClipboardMessage(mentionWhisperMessage);
+ } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
+ Clipboard.setString(Localize.translateLocal('iou.heldRequest', {comment: reportAction.message?.[1]?.text ?? ''}));
+ } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
+ Clipboard.setString(Localize.translateLocal('iou.unheldRequest'));
} else if (content) {
setClipboardMessage(content);
} else if (messageText) {
@@ -399,7 +410,7 @@ const ContextMenuActions: ContextMenuAction[] = [
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
// Only hide the copylink menu item when context menu is opened over img element.
- const isAttachmentTarget = menuTarget?.current?.tagName === 'IMG' && isAttachment;
+ const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment;
return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction);
},
onPress: (closePopover, {reportAction, reportID}) => {
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
index 98b38dcb6968..b7c3d6214094 100644
--- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
+++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
@@ -1,6 +1,6 @@
import type {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu';
-type MiniReportActionContextMenuProps = Omit & {
+type MiniReportActionContextMenuProps = Omit & {
/** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */
displayAsGroup?: boolean;
};
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index 862d5f01c2fc..931b87704ce5 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -67,8 +67,8 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef(null);
const anchorRef = useRef(null);
const dimensionsEventListener = useRef(null);
- const contextMenuAnchorRef = useRef(null);
- const contextMenuTargetNode = useRef(null);
+ const contextMenuAnchorRef = useRef(null);
+ const contextMenuTargetNode = useRef(null);
const onPopoverShow = useRef(() => {});
const onPopoverHide = useRef(() => {});
@@ -83,7 +83,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef
new Promise((resolve) => {
- if (contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') {
+ if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') {
contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y}));
} else {
resolve({x: 0, y: 0});
@@ -169,7 +169,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {
const {pageX = 0, pageY = 0} = extractPointerEvent(event);
contextMenuAnchorRef.current = contextMenuAnchor;
- contextMenuTargetNode.current = event.target as HTMLElement;
+ contextMenuTargetNode.current = event.target as HTMLDivElement;
if (shouldCloseOnTarget) {
anchorRef.current = event.target as HTMLDivElement;
} else {
diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
index f2537c56a5af..21c1eea18e03 100644
--- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
+++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts
@@ -16,7 +16,7 @@ type OnCancel = () => void;
type ContextMenuType = ValueOf;
-type ContextMenuAnchor = View | RNText | null | undefined;
+type ContextMenuAnchor = View | RNText | HTMLDivElement | null | undefined;
type ShowContextMenu = (
type: ContextMenuType,
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.tsx
similarity index 52%
rename from src/pages/home/report/ReportActionItem.js
rename to src/pages/home/report/ReportActionItem.tsx
index 12588845214b..a37138b43039 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -1,9 +1,10 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import lodashIsEqual from 'lodash/isEqual';
import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
+import type {GestureResponderEvent, TextInput} from 'react-native';
import {InteractionManager, View} from 'react-native';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {Emoji} from '@assets/emojis/types';
import Button from '@components/Button';
import DisplayNames from '@components/DisplayNames';
import Hoverable from '@components/Hoverable';
@@ -12,11 +13,11 @@ import * as Expensicons from '@components/Icon/Expensicons';
import InlineSystemMessage from '@components/InlineSystemMessage';
import KYCWall from '@components/KYCWall';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '@components/OnyxProvider';
+import {useBlockedFromConcierge, usePersonalDetails, useReportActionsDrafts} from '@components/OnyxProvider';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
-import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes';
import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions';
import RenderHTML from '@components/RenderHTML';
+import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions';
import MoneyReportView from '@components/ReportActionItem/MoneyReportView';
@@ -30,14 +31,13 @@ import TaskView from '@components/ReportActionItem/TaskView';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import UnreadActionIndicator from '@components/UnreadActionIndicator';
-import withLocalize from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useReportScrollManager from '@hooks/useReportScrollManager';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import ControlSelection from '@libs/ControlSelection';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -49,11 +49,10 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import SelectionScraper from '@libs/SelectionScraper';
-import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes';
import {ReactionListContext} from '@pages/home/ReportScreenContext';
-import reportPropTypes from '@pages/reportPropTypes';
import * as BankAccounts from '@userActions/BankAccounts';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
+import * as Policy from '@userActions/Policy';
import * as store from '@userActions/ReimbursementAccount/store';
import * as Report from '@userActions/Report';
import * as ReportActions from '@userActions/ReportActions';
@@ -62,6 +61,9 @@ import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {OriginalMessageActionableMentionWhisper, OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
@@ -75,88 +77,113 @@ import ReportActionItemMessage from './ReportActionItemMessage';
import ReportActionItemMessageEdit from './ReportActionItemMessageEdit';
import ReportActionItemSingle from './ReportActionItemSingle';
import ReportActionItemThread from './ReportActionItemThread';
-import reportActionPropTypes from './reportActionPropTypes';
import ReportAttachmentsContext from './ReportAttachmentsContext';
-const propTypes = {
- ...windowDimensionsPropTypes,
+const getDraftMessage = (drafts: OnyxCollection, reportID: string, action: OnyxTypes.ReportAction): string | undefined => {
+ const originalReportID = ReportUtils.getOriginalReportID(reportID, action);
+ const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`;
+ const draftMessage = drafts?.[draftKey]?.[action.reportActionID];
+ return typeof draftMessage === 'string' ? draftMessage : draftMessage?.message;
+};
+
+type ReportActionItemOnyxProps = {
+ /** Stores user's preferred skin tone */
+ preferredSkinTone: OnyxEntry;
+
+ /** IOU report for this action, if any */
+ iouReport: OnyxEntry;
+
+ emojiReactions: OnyxEntry;
+ /** The user's wallet account */
+ userWallet: OnyxEntry;
+
+ /** All the report actions belonging to the report's parent */
+ parentReportActions: OnyxEntry;
+
+ /** All policy report fields */
+ policyReportFields: OnyxEntry;
+
+ /** The policy which the user has access to and which the report is tied to */
+ policy: OnyxEntry;
+};
+
+type ReportActionItemProps = {
/** Report for this action */
- report: reportPropTypes.isRequired,
+ report: OnyxTypes.Report;
/** All the data of the action item */
- action: PropTypes.shape(reportActionPropTypes).isRequired,
+ action: OnyxTypes.ReportAction;
/** Should the comment have the appearance of being grouped with the previous comment? */
- displayAsGroup: PropTypes.bool.isRequired,
+ displayAsGroup: boolean;
/** Is this the most recent IOU Action? */
- isMostRecentIOUReportAction: PropTypes.bool.isRequired,
+ isMostRecentIOUReportAction: boolean;
/** Should we display the new marker on top of the comment? */
- shouldDisplayNewMarker: PropTypes.bool.isRequired,
+ shouldDisplayNewMarker: boolean;
/** Determines if the avatar is displayed as a subscript (positioned lower than normal) */
- shouldShowSubscriptAvatar: PropTypes.bool,
+ shouldShowSubscriptAvatar?: boolean;
/** Position index of the report action in the overall report FlatList view */
- index: PropTypes.number.isRequired,
-
- /** Draft message - if this is set the comment is in 'edit' mode */
- draftMessage: PropTypes.string,
-
- /** Stores user's preferred skin tone */
- preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- emojiReactions: EmojiReactionsPropTypes,
-
- /** IOU report for this action, if any */
- iouReport: reportPropTypes,
+ index: number;
/** Flag to show, hide the thread divider line */
- shouldHideThreadDividerLine: PropTypes.bool,
+ shouldHideThreadDividerLine?: boolean;
- /** The user's wallet account */
- userWallet: userWalletPropTypes,
-
- /** All the report actions belonging to the report's parent */
- parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+ linkedReportActionID?: string;
/** Callback to be called on onPress */
- onPress: PropTypes.func,
-};
-
-const defaultProps = {
- draftMessage: undefined,
- preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
- emojiReactions: {},
- shouldShowSubscriptAvatar: false,
- iouReport: undefined,
- shouldHideThreadDividerLine: false,
- userWallet: {},
- parentReportActions: {},
- onPress: undefined,
-};
-
-function ReportActionItem(props) {
+ onPress?: () => void;
+} & ReportActionItemOnyxProps;
+
+const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU =>
+ actionObj?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU;
+
+function ReportActionItem({
+ action,
+ report,
+ linkedReportActionID,
+ displayAsGroup,
+ emojiReactions,
+ index,
+ iouReport,
+ isMostRecentIOUReportAction,
+ parentReportActions,
+ preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE,
+ shouldDisplayNewMarker,
+ userWallet,
+ shouldHideThreadDividerLine = false,
+ shouldShowSubscriptAvatar = false,
+ policyReportFields,
+ policy,
+ onPress = undefined,
+}: ReportActionItemProps) {
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const blockedFromConcierge = useBlockedFromConcierge();
+ const reportActionDrafts = useReportActionsDrafts();
+ const draftMessage = useMemo(() => getDraftMessage(reportActionDrafts, report.reportID, action), [action, report.reportID, reportActionDrafts]);
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
- const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID));
- const [isEmojiPickerActive, setIsEmojiPickerActive] = useState();
+ const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID));
+ const [isEmojiPickerActive, setIsEmojiPickerActive] = useState();
const [isHidden, setIsHidden] = useState(false);
- const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
+ const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
const reactionListRef = useContext(ReactionListContext);
const {updateHiddenAttachments} = useContext(ReportAttachmentsContext);
- const textInputRef = useRef();
- const popoverAnchorRef = useRef();
- const downloadedPreviews = useRef([]);
- const prevDraftMessage = usePrevious(props.draftMessage);
- const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
- const originalReport = props.report.reportID === originalReportID ? props.report : ReportUtils.getReport(originalReportID);
- const isReportActionLinked = props.linkedReportActionID && props.action.reportActionID && props.linkedReportActionID === props.action.reportActionID;
+ const textInputRef = useRef();
+ const popoverAnchorRef = useRef(null);
+ const downloadedPreviews = useRef([]);
+ const prevDraftMessage = usePrevious(draftMessage);
+ const originalReportID = ReportUtils.getOriginalReportID(report.reportID, action);
+ const originalReport = report.reportID === originalReportID ? report : ReportUtils.getReport(originalReportID);
+ const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID;
const reportScrollManager = useReportScrollManager();
@@ -164,117 +191,120 @@ function ReportActionItem(props) {
() => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.hoverComponentBG) : {}),
[StyleUtils, isReportActionLinked, theme.hoverComponentBG],
);
- const originalMessage = lodashGet(props.action, 'originalMessage', {});
- const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(props.action);
- const prevActionResolution = usePrevious(lodashGet(props.action, 'originalMessage.resolution', null));
+
+ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action);
+ const prevActionResolution = usePrevious(ReportActionsUtils.isActionableMentionWhisper(action) ? action.originalMessage.resolution : null);
// IOUDetails only exists when we are sending money
- const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails');
+ const isSendingMoney = isIOUReport(action) && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails;
const updateHiddenState = useCallback(
- (isHiddenValue) => {
+ (isHiddenValue: boolean) => {
setIsHidden(isHiddenValue);
- const isAttachment = ReportUtils.isReportMessageAttachment(_.last(props.action.message));
+ const isAttachment = ReportUtils.isReportMessageAttachment(action.message?.at(-1));
if (!isAttachment) {
return;
}
- updateHiddenAttachments(props.action.reportActionID, isHiddenValue);
+ updateHiddenAttachments(action.reportActionID, isHiddenValue);
},
- [props.action.reportActionID, props.action.message, updateHiddenAttachments],
+ [action.reportActionID, action.message, updateHiddenAttachments],
);
useEffect(
() => () => {
// ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components,
// we should also hide them when the current component is destroyed
- if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) {
+ if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) {
ReportActionContextMenu.hideContextMenu();
ReportActionContextMenu.hideDeleteModal();
}
- if (EmojiPickerAction.isActive(props.action.reportActionID)) {
+ if (EmojiPickerAction.isActive(action.reportActionID)) {
EmojiPickerAction.hideEmojiPicker(true);
}
- if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) {
- reactionListRef.current.hideReactionList();
+ if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) {
+ reactionListRef?.current?.hideReactionList();
}
},
- [props.action.reportActionID, reactionListRef],
+ [action.reportActionID, reactionListRef],
);
useEffect(() => {
// We need to hide EmojiPicker when this is a deleted parent action
- if (!isDeletedParentAction || !EmojiPickerAction.isActive(props.action.reportActionID)) {
+ if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) {
return;
}
EmojiPickerAction.hideEmojiPicker(true);
- }, [isDeletedParentAction, props.action.reportActionID]);
+ }, [isDeletedParentAction, action.reportActionID]);
useEffect(() => {
- if (!_.isUndefined(prevDraftMessage) || _.isUndefined(props.draftMessage)) {
+ if (prevDraftMessage !== undefined || draftMessage === undefined) {
return;
}
focusTextInputAfterAnimation(textInputRef.current, 100);
- }, [prevDraftMessage, props.draftMessage]);
+ }, [prevDraftMessage, draftMessage]);
useEffect(() => {
if (!Permissions.canUseLinkPreviews()) {
return;
}
- const urls = ReportActionsUtils.extractLinksFromMessageHtml(props.action);
- if (_.isEqual(downloadedPreviews.current, urls) || props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ const urls = ReportActionsUtils.extractLinksFromMessageHtml(action);
+ if (lodashIsEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
return;
}
downloadedPreviews.current = urls;
- Report.expandURLPreview(props.report.reportID, props.action.reportActionID);
- }, [props.action, props.report.reportID]);
+ Report.expandURLPreview(report.reportID, action.reportActionID);
+ }, [action, report.reportID]);
useEffect(() => {
- if (_.isUndefined(props.draftMessage) || !ReportActionsUtils.isDeletedAction(props.action)) {
+ if (draftMessage === undefined || !ReportActionsUtils.isDeletedAction(action)) {
return;
}
- Report.deleteReportActionDraft(props.report.reportID, props.action);
- }, [props.draftMessage, props.action, props.report.reportID]);
+ Report.deleteReportActionDraft(report.reportID, action);
+ }, [draftMessage, action, report.reportID]);
// Hide the message if it is being moderated for a higher offense, or is hidden by a moderator
// Removed messages should not be shown anyway and should not need this flow
- const latestDecision = lodashGet(props, ['action', 'message', 0, 'moderationDecision', 'decision'], '');
+ const latestDecision = action.message?.[0].moderationDecision?.decision ?? '';
useEffect(() => {
- if (props.action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) {
+ if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT) {
return;
}
// Hide reveal message button and show the message if latestDecision is changed to empty
- if (_.isEmpty(latestDecision)) {
+ if (!latestDecision) {
setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
setIsHidden(false);
return;
}
setModerationDecision(latestDecision);
- if (!_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], latestDecision) && !ReportActionsUtils.isPendingRemove(props.action)) {
+ if (
+ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision) &&
+ !ReportActionsUtils.isPendingRemove(action)
+ ) {
setIsHidden(true);
return;
}
setIsHidden(false);
- }, [latestDecision, props.action]);
+ }, [latestDecision, action]);
const toggleContextMenuFromActiveReportAction = useCallback(() => {
- setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(props.action.reportActionID));
- }, [props.action.reportActionID]);
+ setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID));
+ }, [action.reportActionID]);
/**
* Show the ReportActionContextMenu modal popover.
*
- * @param {Object} [event] - A press event.
+ * @param [event] - A press event.
*/
const showPopover = useCallback(
- (event) => {
+ (event: GestureResponderEvent | MouseEvent) => {
// Block menu on the message being Edited or if the report action item has errors
- if (!_.isUndefined(props.draftMessage) || !_.isEmpty(props.action.errors)) {
+ if (draftMessage !== undefined || !isEmptyObject(action.errors)) {
return;
}
@@ -284,11 +314,11 @@ function ReportActionItem(props) {
CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
event,
selection,
- popoverAnchorRef,
- props.report.reportID,
- props.action.reportActionID,
+ popoverAnchorRef.current,
+ report.reportID,
+ action.reportActionID,
originalReportID,
- props.draftMessage,
+ draftMessage ?? '',
() => setIsContextMenuActive(true),
toggleContextMenuFromActiveReportAction,
ReportUtils.isArchivedRoom(originalReport),
@@ -297,168 +327,184 @@ function ReportActionItem(props) {
false,
[],
false,
- setIsEmojiPickerActive,
+ setIsEmojiPickerActive as () => void,
);
},
- [props.draftMessage, props.action, props.report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID],
+ [draftMessage, action, report.reportID, toggleContextMenuFromActiveReportAction, originalReport, originalReportID],
);
// Handles manual scrolling to the bottom of the chat when the last message is an actionable mention whisper and it's resolved.
// This fixes an issue where InvertedFlatList fails to auto scroll down and results in an empty space at the bottom of the chat in IOS.
useEffect(() => {
- if (props.index !== 0 || !ReportActionsUtils.isActionableMentionWhisper(props.action)) {
+ if (index !== 0 || !ReportActionsUtils.isActionableMentionWhisper(action)) {
return;
}
- if (prevActionResolution !== lodashGet(props.action, 'originalMessage.resolution', null)) {
- reportScrollManager.scrollToIndex(props.index);
+ if (ReportActionsUtils.isActionableMentionWhisper(action) && prevActionResolution !== (action.originalMessage.resolution ?? null)) {
+ reportScrollManager.scrollToIndex(index);
}
- }, [props.index, props.action, prevActionResolution, reportScrollManager]);
+ }, [index, action, prevActionResolution, reportScrollManager]);
const toggleReaction = useCallback(
- (emoji) => {
- Report.toggleEmojiReaction(props.report.reportID, props.action, emoji, props.emojiReactions);
+ (emoji: Emoji) => {
+ Report.toggleEmojiReaction(report.reportID, action, emoji, emojiReactions);
},
- [props.report, props.action, props.emojiReactions],
+ [report, action, emojiReactions],
);
const contextValue = useMemo(
() => ({
- anchor: popoverAnchorRef,
- report: props.report,
- action: props.action,
+ anchor: popoverAnchorRef.current,
+ report,
+ action,
checkIfContextMenuActive: toggleContextMenuFromActiveReportAction,
}),
- [props.report, props.action, toggleContextMenuFromActiveReportAction],
+ [report, action, toggleContextMenuFromActiveReportAction],
);
- const actionableItemButtons = useMemo(() => {
- if (!(ReportActionsUtils.isActionableMentionWhisper(props.action) && !lodashGet(props.action, 'originalMessage.resolution', null))) {
+ const actionableItemButtons: ActionableItem[] = useMemo(() => {
+ const isWhisperResolution = (action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution !== null;
+ const isJoinChoice = (action?.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage'])?.choice === '';
+
+ if (!((ReportActionsUtils.isActionableMentionWhisper(action) && isWhisperResolution) || (ReportActionsUtils.isActionableJoinRequest(action) && isJoinChoice))) {
return [];
}
+
+ if (ReportActionsUtils.isActionableJoinRequest(action)) {
+ return [
+ {
+ text: 'actionableMentionJoinWorkspaceOptions.accept',
+ key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT}`,
+ onPress: () => Policy.acceptJoinRequest(report.reportID, action),
+ isPrimary: true,
+ },
+ {
+ text: 'actionableMentionJoinWorkspaceOptions.decline',
+ key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE}`,
+ onPress: () => Policy.declineJoinRequest(report.reportID, action),
+ },
+ ];
+ }
return [
{
text: 'actionableMentionWhisperOptions.invite',
- key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
- onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE),
+ key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
+ onPress: () => Report.resolveActionableMentionWhisper(report.reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE),
isPrimary: true,
},
{
text: 'actionableMentionWhisperOptions.nothing',
- key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
- onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING),
+ key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
+ onPress: () => Report.resolveActionableMentionWhisper(report.reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING),
},
];
- }, [props.action, props.report.reportID]);
+ }, [action, report.reportID]);
/**
* Get the content of ReportActionItem
- * @param {Boolean} hovered whether the ReportActionItem is hovered
- * @param {Boolean} isWhisper whether the report action is a whisper
- * @param {Boolean} hasErrors whether the report action has any errors
- * @returns {Object} child component(s)
+ * @param hovered whether the ReportActionItem is hovered
+ * @param isWhisper whether the report action is a whisper
+ * @param hasErrors whether the report action has any errors
+ * @returns child component(s)
*/
- const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => {
+ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => {
let children;
// Show the MoneyRequestPreview for when request was created, bill was split or money was sent
if (
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
- originalMessage &&
+ isIOUReport(action) &&
+ action.originalMessage &&
// For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message
- (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney)
+ (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney)
) {
// There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID
- const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0';
+ const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0';
children = (
);
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) {
- children = (
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) {
+ children = ReportUtils.isClosedExpenseReportWithNoExpenses(iouReport) ? (
+ ${translate('parentReportAction.deletedReport')}`} />
+ ) : (
);
- } else if (
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ||
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED
- ) {
- children = ;
- } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) {
+ } else if (ReportActionsUtils.isTaskAction(action)) {
+ children = ;
+ } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) {
children = (
);
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) {
- const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID));
- const paymentType = lodashGet(props.action, 'originalMessage.paymentType', '');
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) {
+ const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[report.ownerAccountID ?? -1]);
+ const paymentType = action.originalMessage.paymentType ?? '';
- const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID);
+ const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(report.reportID) && !ReportUtils.isSettled(report.reportID);
const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
const shouldShowEnableWalletButton =
- isSubmitterOfUnsettledReport &&
- (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) &&
- paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
+ isSubmitterOfUnsettledReport && (isEmptyObject(userWallet) || userWallet?.tierName === CONST.WALLET.TIER_NAME.SILVER) && paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY;
children = (
<>
{shouldShowAddCreditBankAccountButton && (
);
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) {
- children = ;
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
- children = ;
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
- children = ;
- } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
- children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) {
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) {
+ children = ;
+ } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
+ children = ;
} else {
const hasBeenFlagged =
- !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision) &&
- !ReportActionsUtils.isPendingRemove(props.action);
+ ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) &&
+ !ReportActionsUtils.isPendingRemove(action);
children = (
- {_.isUndefined(props.draftMessage) ? (
-
+ {draftMessage === undefined ? (
+
{hasBeenFlagged && (
@@ -499,7 +545,7 @@ function ReportActionItem(props) {
style={[styles.buttonSmallText, styles.userSelectNone]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
- {isHidden ? props.translate('moderation.revealMessage') : props.translate('moderation.hideMessage')}
+ {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')}
)}
@@ -508,49 +554,46 @@ function ReportActionItem(props) {
for example: Invite a user mentioned but not a member of the room
https://github.com/Expensify/App/issues/32741
*/}
- {actionableItemButtons.length > 0 && (
-
- )}
+ {actionableItemButtons.length > 0 && }
) : (
)}
);
}
- const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0);
+ const numberOfThreadReplies = action.childVisibleActionCount ?? 0;
- const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID);
- const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID));
- const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {};
+ const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID);
+ const oldestFourAccountIDs =
+ action.childOldestFourAccountIDs
+ ?.split(',')
+ .map((accountID) => Number(accountID))
+ .filter((accountID): accountID is number => typeof accountID === 'number') ?? [];
+ const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {};
return (
<>
{children}
- {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && (
-
- !_.isEmpty(item))} />
+ {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && (
+
+ !isEmptyObject(item))} />
)}
- {!ReportActionsUtils.isMessageDeleted(props.action) && (
+ {!ReportActionsUtils.isMessageDeleted(action) && (
{
if (Session.isAnonymousUser()) {
@@ -571,9 +614,9 @@ function ReportActionItem(props) {
{shouldDisplayThreadReplies && (
{
+ const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => {
const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors);
- if (!_.isUndefined(props.draftMessage)) {
+ if (draftMessage !== undefined) {
return {content};
}
- if (!props.displayAsGroup) {
+ if (!displayAsGroup) {
return (
item === moderationDecision) &&
+ !ReportActionsUtils.isPendingRemove(action)
}
>
{content}
@@ -621,23 +664,23 @@ function ReportActionItem(props) {
return {content};
};
- if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
- const parentReportAction = props.parentReportActions[props.report.parentReportActionID];
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
+ const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null;
if (ReportActionsUtils.isTransactionThread(parentReportAction)) {
const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction);
if (ReportActionsUtils.isDeletedParentAction(parentReportAction) || isReversedTransaction) {
return (
-
+
-
-
+
+ ${props.translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`}
+ html={`${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`}
/>
@@ -649,25 +692,25 @@ function ReportActionItem(props) {
return (
);
}
- if (ReportUtils.isTaskReport(props.report)) {
- if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) {
+ if (ReportUtils.isTaskReport(report)) {
+ if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) {
return (
-
+
-
-
+
+
- ${props.translate('parentReportAction.deletedTask')}`} />
+ ${translate('parentReportAction.deletedTask')}`} />
@@ -676,25 +719,25 @@ function ReportActionItem(props) {
);
}
return (
-
+
-
+
);
}
- if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) {
+ if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) {
return (
-
+
);
@@ -702,96 +745,94 @@ function ReportActionItem(props) {
return (
);
}
- if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
- return ;
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
+ return ;
}
- if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) {
+ if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) {
return (
);
}
// For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true
// Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet
- if (
- props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
- lodashGet(props.report, 'isWaitingOnBankAccount', false) &&
- originalMessage &&
- originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY &&
- !isSendingMoney
- ) {
+ if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) {
return null;
}
// if action is actionable mention whisper and resolved by user, then we don't want to render anything
- if (ReportActionsUtils.isActionableMentionWhisper(props.action) && lodashGet(props.action, 'originalMessage.resolution', null)) {
+ if (ReportActionsUtils.isActionableMentionWhisper(action) && (action.originalMessage.resolution ?? null)) {
return null;
}
// We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them.
// This is a temporary solution needed for comment-linking.
// The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt.
- if (ReportActionsUtils.isWhisperActionTargetedToOthers(props.action)) {
+ if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) {
return null;
}
- const hasErrors = !_.isEmpty(props.action.errors);
- const whisperedToAccountIDs = props.action.whisperedToAccountIDs || [];
+ const hasErrors = !isEmptyObject(action.errors);
+ const whisperedToAccountIDs = action.whisperedToAccountIDs ?? [];
const isWhisper = whisperedToAccountIDs.length > 0;
const isMultipleParticipant = whisperedToAccountIDs.length > 1;
const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs);
- const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : [];
+ const whisperedToPersonalDetails = isWhisper
+ ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[])
+ : [];
const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : [];
return (
props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPress={onPress}
+ style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]}
+ onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onSecondaryInteraction={showPopover}
- preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors}
+ preventDefaultContextMenu={draftMessage === undefined && !hasErrors}
withoutFocusOnSecondaryInteraction
- accessibilityLabel={props.translate('accessibilityHints.chatMessage')}
+ accessibilityLabel={translate('accessibilityHints.chatMessage')}
+ accessible
>
{(hovered) => (
- {props.shouldDisplayNewMarker && }
+ {shouldDisplayNewMarker && }
-
+ ReportActions.clearReportActionErrors(props.report.reportID, props.action)}
+ onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)}
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
pendingAction={
- !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')
+ draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined)
}
- shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)}
- errors={ErrorUtils.getLatestErrorMessageField(props.action)}
+ shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)}
+ errors={ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)}
errorRowStyles={[styles.ml10, styles.mr2]}
- needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)}
+ needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)}
shouldDisableStrikeThrough
>
{isWhisper && (
@@ -804,11 +845,11 @@ function ReportActionItem(props) {
/>
- {props.translate('reportActionContextMenu.onlyVisible')}
+ {translate('reportActionContextMenu.onlyVisible')}
)}
- {renderReportActionItem(hovered || isReportActionLinked, isWhisper, hasErrors)}
+ {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)}
)}
-
+ {/* @ts-expect-error TODO check if there is a field on the reportAction object */}
+
);
}
-ReportActionItem.propTypes = propTypes;
-ReportActionItem.defaultProps = defaultProps;
-
-export default compose(
- withWindowDimensions,
- withLocalize,
- withNetwork(),
- withBlockedFromConcierge({propName: 'blockedFromConcierge'}),
- withReportActionsDrafts({
- propName: 'draftMessage',
- transformValue: (drafts, props) => {
- const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action);
- const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`;
- return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']);
- },
- }),
- withOnyx({
- preferredSkinTone: {
- key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
- initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE,
- },
- iouReport: {
- key: ({action}) => {
- const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action);
- return iouReportID ? `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}` : undefined;
- },
- initialValue: {},
- },
- policyReportFields: {
- key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined),
- initialValue: [],
+export default withOnyx({
+ preferredSkinTone: {
+ key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
+ initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE,
+ },
+ iouReport: {
+ key: ({action}) => {
+ const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action);
+ return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID ?? ''}`;
},
- policy: {
- key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined),
- initialValue: {},
- },
- emojiReactions: {
- key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`,
- initialValue: {},
- },
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- parentReportActions: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`,
- canEvict: false,
- },
- }),
-)(
+ initialValue: {} as OnyxTypes.Report,
+ },
+ policyReportFields: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID ?? ''}`,
+ initialValue: {},
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID ?? ''}`,
+ initialValue: {} as OnyxTypes.Policy,
+ },
+ emojiReactions: {
+ key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`,
+ initialValue: {},
+ },
+ userWallet: {
+ key: ONYXKEYS.USER_WALLET,
+ },
+ parentReportActions: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? 0}`,
+ canEvict: false,
+ },
+})(
memo(ReportActionItem, (prevProps, nextProps) => {
- const prevParentReportAction = prevProps.parentReportActions[prevProps.report.parentReportActionID];
- const nextParentReportAction = nextProps.parentReportActions[nextProps.report.parentReportActionID];
+ const prevParentReportAction = prevProps.parentReportActions?.[prevProps.report.parentReportActionID ?? ''];
+ const nextParentReportAction = nextProps.parentReportActions?.[nextProps.report.parentReportActionID ?? ''];
return (
prevProps.displayAsGroup === nextProps.displayAsGroup &&
- prevProps.draftMessage === nextProps.draftMessage &&
prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction &&
prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker &&
- _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
- _.isEqual(prevProps.action, nextProps.action) &&
- _.isEqual(prevProps.iouReport, nextProps.iouReport) &&
- _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) &&
- _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) &&
- _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) &&
- lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') &&
- lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') &&
- lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') &&
- lodashGet(prevProps.report, 'parentReportActionID') === lodashGet(nextProps.report, 'parentReportActionID') &&
- prevProps.translate === nextProps.translate &&
+ lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) &&
+ lodashIsEqual(prevProps.action, nextProps.action) &&
+ lodashIsEqual(prevProps.iouReport, nextProps.iouReport) &&
+ lodashIsEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) &&
+ lodashIsEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) &&
+ lodashIsEqual(prevProps.report.errorFields, nextProps.report.errorFields) &&
+ prevProps.report?.statusNum === nextProps.report?.statusNum &&
+ prevProps.report?.stateNum === nextProps.report?.stateNum &&
+ prevProps.report?.parentReportID === nextProps.report?.parentReportID &&
+ prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID &&
// TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport
ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) &&
prevProps.action.actionName === nextProps.action.actionName &&
@@ -906,13 +929,13 @@ export default compose(
ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) &&
prevProps.report.managerID === nextProps.report.managerID &&
prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine &&
- lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) &&
- lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) &&
+ prevProps.report?.total === nextProps.report?.total &&
+ prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal &&
prevProps.linkedReportActionID === nextProps.linkedReportActionID &&
- _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
- _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) &&
- _.isEqual(prevProps.policy, nextProps.policy) &&
- _.isEqual(prevParentReportAction, nextParentReportAction)
+ lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
+ lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) &&
+ lodashIsEqual(prevProps.policy, nextProps.policy) &&
+ lodashIsEqual(prevParentReportAction, nextParentReportAction)
);
}),
);
diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx
index 35141a42b726..a28f2af24448 100644
--- a/src/pages/home/report/ReportActionItemBasicMessage.tsx
+++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx
@@ -5,7 +5,7 @@ import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
-type ReportActionItemBasicMessageProps = ChildrenProps & {
+type ReportActionItemBasicMessageProps = Partial & {
message: string;
};
diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx
index 95578c10e816..4fe52f6adf41 100644
--- a/src/pages/home/report/ReportActionItemCreated.tsx
+++ b/src/pages/home/report/ReportActionItemCreated.tsx
@@ -35,7 +35,7 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & {
/** The id of the policy */
// eslint-disable-next-line react/no-unused-prop-types
- policyID: string;
+ policyID: string | undefined;
};
function ReportActionItemCreated(props: ReportActionItemCreatedProps) {
const styles = useThemeStyles();
diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx
index e16d94eb7db7..04391bb19cd5 100644
--- a/src/pages/home/report/ReportActionItemFragment.tsx
+++ b/src/pages/home/report/ReportActionItemFragment.tsx
@@ -70,6 +70,7 @@ const MUTED_ACTIONS = [
CONST.REPORT.ACTIONS.TYPE.IOU,
CONST.REPORT.ACTIONS.TYPE.APPROVED,
CONST.REPORT.ACTIONS.TYPE.MOVED,
+ CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST,
] as ActionName[];
function ReportActionItemFragment({
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index 2c9a4cbd21e8..fbf2da69aa31 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -5,6 +5,7 @@ import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Keyboard, View} from 'react-native';
import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import Composer from '@components/Composer';
import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton';
@@ -58,7 +59,7 @@ type ReportActionItemMessageEditProps = {
shouldDisableEmojiPicker?: boolean;
/** Stores user's preferred skin tone */
- preferredSkinTone?: number;
+ preferredSkinTone?: OnyxEntry;
};
// native ids
@@ -69,7 +70,7 @@ const isMobileSafari = Browser.isMobileSafari();
function ReportActionItemMessageEdit(
{action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps,
- forwardedRef: ForwardedRef,
+ forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>,
) {
const theme = useTheme();
const styles = useThemeStyles();
diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx
index af1c4e85104e..4a041fc495c0 100644
--- a/src/pages/home/report/ReportActionItemParentAction.tsx
+++ b/src/pages/home/report/ReportActionItemParentAction.tsx
@@ -83,7 +83,6 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide
onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)}
>
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))}
report={ancestor.report}
action={ancestor.reportAction}
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 741422cc7e82..696cd7a7d850 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -1,6 +1,7 @@
import React, {useCallback, useMemo} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
@@ -29,7 +30,7 @@ import ReportActionItemFragment from './ReportActionItemFragment';
type ReportActionItemSingleProps = Partial & {
/** All the data of the action */
- action: ReportAction;
+ action: OnyxEntry;
/** Styles for the outermost View */
wrapperStyle?: StyleProp;
@@ -38,7 +39,7 @@ type ReportActionItemSingleProps = Partial & {
report: Report;
/** IOU Report for this action, if any */
- iouReport?: Report;
+ iouReport?: OnyxEntry;
/** Show header for action */
showHeader?: boolean;
@@ -77,12 +78,12 @@ function ReportActionItemSingle({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
- const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID;
+ const actorAccountID = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action?.actorAccountID;
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {};
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
- const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]);
+ const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]);
const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors);
let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID);
@@ -90,7 +91,7 @@ function ReportActionItemSingle({
displayName = ReportUtils.getPolicyName(report);
actorHint = displayName;
avatarSource = ReportUtils.getWorkspaceAvatar(report);
- } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) {
+ } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) {
// We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their
// details. This will be improved upon when the Copilot feature is implemented.
const delegateDetails = personalDetails[action.delegateAccountID];
@@ -141,7 +142,7 @@ function ReportActionItemSingle({
text: displayName,
},
]
- : action.person;
+ : action?.person;
const reportID = report?.reportID;
const iouReportID = iouReport?.reportID;
@@ -155,14 +156,14 @@ function ReportActionItemSingle({
Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID));
return;
}
- showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID));
+ showUserDetails(action?.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID));
}
- }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]);
+ }, [isWorkspaceActor, reportID, actorAccountID, action?.delegateAccountID, iouReportID, displayAllActors]);
const shouldDisableDetailPage = useMemo(
() =>
CONST.RESTRICTED_ACCOUNT_IDS.includes(actorAccountID ?? 0) ||
- (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)),
+ (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action?.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)),
[action, isWorkspaceActor, actorAccountID],
);
@@ -189,7 +190,7 @@ function ReportActionItemSingle({
return (
@@ -237,13 +238,13 @@ function ReportActionItemSingle({
{personArray?.map((fragment, index) => (
))}
@@ -255,7 +256,7 @@ function ReportActionItemSingle({
>{`${status?.emojiCode}`}
)}
-
+
) : null}
{children}
diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx
index f7c7e5fcf91d..c0dbe2a3825d 100644
--- a/src/pages/home/report/ReportActionItemThread.tsx
+++ b/src/pages/home/report/ReportActionItemThread.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {GestureResponderEvent} from 'react-native';
import {View} from 'react-native';
import MultipleAvatars from '@components/MultipleAvatars';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
@@ -26,7 +27,7 @@ type ReportActionItemThreadProps = {
isHovered: boolean;
/** The function that should be called when the thread is LongPressed or right-clicked */
- onSecondaryInteraction: () => void;
+ onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void;
};
function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) {
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index 5e9d863dd62d..ca3ee7d2ab6a 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -194,7 +194,7 @@ function ReportActionsView(props) {
return;
}
// Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments
- Report.getOlderActions(reportID, oldestReportAction.reportActionID);
+ Report.getOlderActions(reportID);
}, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]);
/**
@@ -223,10 +223,9 @@ function ReportActionsView(props) {
return;
}
- const newestReportAction = _.first(props.reportActions);
- Report.getNewerActions(reportID, newestReportAction.reportActionID);
+ Report.getNewerActions(reportID);
}, 500),
- [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID, hasNewestReportAction],
+ [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, hasNewestReportAction],
);
/**
diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx
index a9e284329421..7151cc84e735 100644
--- a/src/pages/home/sidebar/AllSettingsScreen.tsx
+++ b/src/pages/home/sidebar/AllSettingsScreen.tsx
@@ -1,11 +1,11 @@
import React, {useMemo} from 'react';
-import {ScrollView} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Breadcrumbs from '@components/Breadcrumbs';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js
index 8d7272df63e9..62b1adf1fb8c 100644
--- a/src/pages/iou/MoneyRequestSelectorPage.js
+++ b/src/pages/iou/MoneyRequestSelectorPage.js
@@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TabSelector from '@components/TabSelector/TabSelector';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -62,6 +63,7 @@ const defaultProps = {
function MoneyRequestSelectorPage(props) {
const styles = useThemeStyles();
const [isDraggingOver, setIsDraggingOver] = useState(false);
+ const {canUseP2PDistanceRequests} = usePermissions();
const iouType = lodashGet(props.route, 'params.iouType', '');
const reportID = lodashGet(props.route, 'params.reportID', '');
@@ -75,7 +77,7 @@ function MoneyRequestSelectorPage(props) {
const isFromGlobalCreate = !reportID;
const isExpenseChat = ReportUtils.isPolicyExpenseChat(props.report);
const isExpenseReport = ReportUtils.isExpenseReport(props.report);
- const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate;
+ const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate;
const resetMoneyRequestInfo = () => {
const moneyRequestID = `${iouType}${reportID}`;
diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js
index 8e50577ede1f..b1ae257b792f 100644
--- a/src/pages/iou/request/IOURequestStartPage.js
+++ b/src/pages/iou/request/IOURequestStartPage.js
@@ -13,6 +13,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TabSelector from '@components/TabSelector/TabSelector';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -80,6 +81,7 @@ function IOURequestStartPage({
};
const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction));
const previousIOURequestType = usePrevious(transactionRequestType.current);
+ const {canUseP2PDistanceRequests} = usePermissions();
const isFromGlobalCreate = _.isEmpty(report.reportID);
useFocusEffect(
@@ -102,12 +104,12 @@ function IOURequestStartPage({
if (transaction.reportID === reportID) {
return;
}
- IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current);
- }, [transaction, reportID, iouType, isFromGlobalCreate]);
+ IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, transactionRequestType.current);
+ }, [transaction, policy, reportID, iouType, isFromGlobalCreate]);
const isExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isExpenseReport = ReportUtils.isExpenseReport(report);
- const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate;
+ const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate;
// Allow the user to create the request if we are creating the request in global menu or the report can create the request
const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType);
@@ -124,10 +126,10 @@ function IOURequestStartPage({
if (iouType === CONST.IOU.TYPE.SPLIT && transaction.isFromGlobalCreate) {
IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST, newIouType);
}
- IOU.initMoneyRequest(reportID, isFromGlobalCreate, newIouType);
+ IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, newIouType);
transactionRequestType.current = newIouType;
},
- [previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate],
+ [policy, previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate],
);
if (!transaction.transactionID) {
diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
index 95dda131eab7..fb3a4d9457d5 100644
--- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
+++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
@@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import usePermissions from '@hooks/usePermissions';
import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -90,6 +91,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
+ const {canUseP2PDistanceRequests} = usePermissions();
const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : '';
@@ -120,18 +122,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
// sees the option to request money from their admin on their own Workspace Chat.
iouType === CONST.IOU.TYPE.REQUEST,
- // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features.
- iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
+ canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
false,
{},
[],
false,
{},
[],
-
- // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now.
- // This functionality is being built here: https://github.com/Expensify/App/issues/23291
- iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
+ canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
false,
);
@@ -182,7 +180,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
}
return [newSections, chatOptions];
- }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]);
+ }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, canUseP2PDistanceRequests, translate]);
/**
* Adds a single participant to the request
@@ -257,7 +255,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
// the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants
const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat);
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
- const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE;
+ const isAllowedToSplit = canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE;
const handleConfirmSelection = useCallback(() => {
if (shouldShowSplitBillErrorMessage) {
diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js
index af1de64f8930..1b53dab12fa3 100644
--- a/src/pages/iou/request/step/IOURequestStepTag.js
+++ b/src/pages/iou/request/step/IOURequestStepTag.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import categoryPropTypes from '@components/categoryPropTypes';
import TagPicker from '@components/TagPicker';
@@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
import {canEditMoneyRequest} from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
@@ -78,10 +80,12 @@ function IOURequestStepTag({
const tag = TransactionUtils.getTag(transaction, tagIndex);
const isEditing = action === CONST.IOU.ACTION.EDIT;
const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
+ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
const parentReportAction = parentReportActions[report.parentReportActionID];
+ const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
// eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundPage = isEditing && !canEditMoneyRequest(parentReportAction);
+ const shouldShowNotFoundPage = !shouldShowTag || (isEditing && !canEditMoneyRequest(parentReportAction));
const navigateBack = () => {
Navigation.goBack(backTo);
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx
index cb1f73ae2207..55bf77e9ae88 100644
--- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx
@@ -1,11 +1,12 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native';
import type {ValueOf} from 'type-fest';
import BigNumberPad from '@components/BigNumberPad';
import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
+import ScrollView from '@components/ScrollView';
import TextInputWithCurrencySymbol from '@components/TextInputWithCurrencySymbol';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 3fde970327d7..1ad6488aeee9 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import usePermissions from '@hooks/usePermissions';
import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -94,6 +95,7 @@ function MoneyRequestParticipantsSelector({
const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
+ const {canUseP2PDistanceRequests} = usePermissions();
const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
@@ -113,8 +115,7 @@ function MoneyRequestParticipantsSelector({
// sees the option to request money from their admin on their own Workspace Chat.
iouType === CONST.IOU.TYPE.REQUEST,
- // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features.
- !isDistanceRequest,
+ canUseP2PDistanceRequests || !isDistanceRequest,
false,
{},
[],
@@ -123,7 +124,7 @@ function MoneyRequestParticipantsSelector({
[],
// We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now.
// This functionality is being built here: https://github.com/Expensify/App/issues/23291
- !isDistanceRequest,
+ canUseP2PDistanceRequests || !isDistanceRequest,
true,
);
return {
@@ -131,7 +132,7 @@ function MoneyRequestParticipantsSelector({
personalDetails: chatOptions.personalDetails,
userToInvite: chatOptions.userToInvite,
};
- }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]);
+ }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]);
/**
* Returns the sections needed for the OptionsSelector
@@ -272,7 +273,7 @@ function MoneyRequestParticipantsSelector({
// the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants
const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat);
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
- const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND;
+ const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND;
const handleConfirmSelection = useCallback(() => {
if (shouldShowSplitBillErrorMessage) {
diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx
index 3346b044ceca..0c087b2c93d6 100644
--- a/src/pages/settings/AboutPage/AboutPage.tsx
+++ b/src/pages/settings/AboutPage/AboutPage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, StyleProp, ViewStyle} from 'react-native';
import DeviceInfo from 'react-native-device-info';
@@ -9,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
diff --git a/src/pages/settings/AppDownloadLinks.tsx b/src/pages/settings/AppDownloadLinks.tsx
index 352b3772923a..e4165178ff2f 100644
--- a/src/pages/settings/AppDownloadLinks.tsx
+++ b/src/pages/settings/AppDownloadLinks.tsx
@@ -1,11 +1,11 @@
import React, {useRef} from 'react';
-import {ScrollView} from 'react-native';
import type {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import type {MenuItemProps} from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
index 7459819afd99..14739c4ffc52 100644
--- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
+++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect} from 'react';
-import {View} from 'react-native';
+import {NativeModules, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Icon from '@components//Icon';
@@ -84,6 +84,12 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS
text={translate('exitSurvey.goToExpensifyClassic')}
onPress={() => {
ExitSurvey.switchToOldDot();
+
+ if (NativeModules.HybridAppModule) {
+ NativeModules.HybridAppModule.closeReactNativeApp();
+ return;
+ }
+
Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX);
}}
isLoading={isLoading ?? false}
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index b29fd600ae16..2f2343027cf0 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -1,7 +1,7 @@
import {useNavigationState} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
-import {NativeModules, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -174,23 +174,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
],
};
- if (NativeModules.HybridAppModule) {
- const hybridAppMenuItems: MenuData[] = [
- {
- translationKey: 'initialSettingsPage.returnToClassic' as const,
- icon: Expensicons.RotateLeft,
- shouldShowRightIcon: true,
- iconRight: Expensicons.NewWindow,
- action: () => {
- NativeModules.HybridAppModule.closeReactNativeApp();
- },
- },
- ...defaultMenu.items,
- ].filter((item) => item.translationKey !== 'initialSettingsPage.signOut' && item.translationKey !== 'exitSurvey.goToExpensifyClassic');
-
- return {sectionStyle: styles.accountSettingsSectionContainer, sectionTranslationKey: 'initialSettingsPage.account', items: hybridAppMenuItems};
- }
-
return defaultMenu;
}, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, signOut]);
diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.js
index 0fd6121fe512..36a26ccffaa2 100755
--- a/src/pages/settings/Preferences/PreferencesPage.js
+++ b/src/pages/settings/Preferences/PreferencesPage.js
@@ -1,13 +1,14 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Switch from '@components/Switch';
import Text from '@components/Text';
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
index 18589beb6353..2ba4fc33580b 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
+import {InteractionManager, Keyboard, View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -13,6 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
index 5d150e782c44..3851ef7153fb 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
@@ -1,7 +1,7 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React, {useCallback} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index 968d9e502806..2fa133f41616 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useEffect} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
@@ -10,6 +10,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemGroup from '@components/MenuItemGroup';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
diff --git a/src/pages/settings/Report/ReportSettingsPage.tsx b/src/pages/settings/Report/ReportSettingsPage.tsx
index 54057f7c05bb..383cbbcb0833 100644
--- a/src/pages/settings/Report/ReportSettingsPage.tsx
+++ b/src/pages/settings/Report/ReportSettingsPage.tsx
@@ -1,12 +1,13 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import DisplayNames from '@components/DisplayNames';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx
index 8600c9e08471..01563e586792 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.tsx
+++ b/src/pages/settings/Security/SecuritySettingsPage.tsx
@@ -1,11 +1,12 @@
import React, {useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
index d6c7a1abcd4f..b4c1bc249c81 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
@@ -1,5 +1,5 @@
import React, {useEffect, useState} from 'react';
-import {ActivityIndicator, ScrollView, View} from 'react-native';
+import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
@@ -7,6 +7,7 @@ import FormHelpMessage from '@components/FormHelpMessage';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
index 59c145f9e348..ad9a4060af45 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx
@@ -1,8 +1,9 @@
import React, {useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
index d9998c777f3b..58e7d98d69de 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
@@ -1,5 +1,5 @@
import React, {useEffect, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import Button from '@components/Button';
@@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import {useSession} from '@components/OnyxProvider';
import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle';
import QRCode from '@components/QRCode';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx
index a8b676f6c379..097b2cf28ed0 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx
+++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect, useMemo, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx
index 93ead17e9523..85b7bef0550c 100644
--- a/src/pages/settings/Wallet/TransferBalancePage.tsx
+++ b/src/pages/settings/Wallet/TransferBalancePage.tsx
@@ -1,5 +1,5 @@
import React, {useEffect} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
index b9f49049d51a..88236e06f9a9 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
@@ -2,7 +2,7 @@ import _ from 'lodash';
import type {ForwardedRef, RefObject} from 'react';
import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
import type {GestureResponderEvent} from 'react-native';
-import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native';
+import {ActivityIndicator, Dimensions, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
import Button from '@components/Button';
@@ -18,6 +18,7 @@ import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Popover from '@components/Popover';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
@@ -74,7 +75,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi
});
const addPaymentMethodAnchorRef = useRef(null);
- const paymentMethodButtonRef = useRef(null);
+ const paymentMethodButtonRef = useRef(null);
const [anchorPosition, setAnchorPosition] = useState({
anchorPositionHorizontal: 0,
anchorPositionVertical: 0,
@@ -163,7 +164,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi
setShouldShowDefaultDeleteMenu(false);
return;
}
- paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLElement;
+ paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement;
// The delete/default menu
if (accountType) {
diff --git a/src/pages/signin/SAMLSignInPage/index.tsx b/src/pages/signin/SAMLSignInPage/index.tsx
index 701c2917bea6..1ff9d02672be 100644
--- a/src/pages/signin/SAMLSignInPage/index.tsx
+++ b/src/pages/signin/SAMLSignInPage/index.tsx
@@ -7,7 +7,7 @@ import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types';
function SAMLSignInPage({credentials}: SAMLSignInPageProps) {
useEffect(() => {
- window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self');
+ window.location.replace(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`);
}, [credentials?.login]);
return ;
diff --git a/src/pages/signin/SignInPageLayout/index.tsx b/src/pages/signin/SignInPageLayout/index.tsx
index b65da7eba0a5..3532c17181db 100644
--- a/src/pages/signin/SignInPageLayout/index.tsx
+++ b/src/pages/signin/SignInPageLayout/index.tsx
@@ -1,8 +1,11 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+// eslint-disable-next-line no-restricted-imports
+import type {ScrollView as RNScrollView} from 'react-native';
+import {View} from 'react-native';
import SignInGradient from '@assets/images/home-fade-gradient.svg';
import ImageSVG from '@components/ImageSVG';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -38,7 +41,7 @@ function SignInPageLayout(
const StyleUtils = useStyleUtils();
const {preferredLocale} = useLocalize();
const {top: topInsets, bottom: bottomInsets} = useSafeAreaInsets();
- const scrollViewRef = useRef(null);
+ const scrollViewRef = useRef(null);
const prevPreferredLocale = usePrevious(preferredLocale);
const {windowHeight, isMediumScreenWidth, isLargeScreenWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
index f77285190e62..352c08115114 100644
--- a/src/pages/tasks/NewTaskPage.js
+++ b/src/pages/tasks/NewTaskPage.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -155,14 +156,7 @@ function NewTaskPage(props) {
Navigation.goBack(ROUTES.NEW_TASK_DETAILS);
}}
/>
-
+ ;
/** Grab the Share destination of the Task */
- task: PropTypes.shape({
- /** Share destination of the Task */
- shareDestination: PropTypes.string,
-
- /** The task report if it's currently being edited */
- report: reportPropTypes,
- }),
-
- /** The policy of root parent report */
- rootParentReportPolicy: PropTypes.shape({
- /** The role of current user */
- role: PropTypes.string,
- }),
+ task: OnyxEntry;
};
-const defaultProps = {
- reports: {},
- task: {},
- rootParentReportPolicy: {},
+type UseOptions = {
+ reports: OnyxCollection;
};
-function useOptions({reports}) {
+type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps;
+
+function useOptions({reports}: UseOptions) {
const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const betas = useBetas();
const [isLoading, setIsLoading] = useState(true);
@@ -78,7 +69,7 @@ function useOptions({reports}) {
);
const headerMessage = OptionsListUtils.getHeaderMessage(
- (recentReports.length || 0 + personalDetails.length || 0) !== 0 || currentUserOption,
+ (recentReports?.length || 0) + (personalDetails?.length || 0) !== 0 || Boolean(currentUserOption),
Boolean(userToInvite),
debouncedSearchValue,
);
@@ -99,20 +90,20 @@ function useOptions({reports}) {
return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue};
}
-function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
+function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) {
const styles = useThemeStyles();
- const route = useRoute();
+ const route = useRoute>();
const {translate} = useLocalize();
const session = useSession();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task});
+ const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports});
const onChangeText = (newSearchTerm = '') => {
setSearchValue(newSearchTerm);
};
- const report = useMemo(() => {
- if (!route.params || !route.params.reportID) {
+ const report: OnyxEntry = useMemo(() => {
+ if (!route.params?.reportID) {
return null;
}
if (report && !ReportUtils.isTaskReport(report)) {
@@ -120,7 +111,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
Navigation.dismissModal(report.reportID);
});
}
- return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`];
+ return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`] ?? null;
}, [reports, route]);
const sections = useMemo(() => {
@@ -155,17 +146,29 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
if (userToInvite) {
sectionsList.push({
+ title: '',
data: [userToInvite],
shouldShow: true,
indexOffset,
});
}
- return sectionsList;
- }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]);
+ return sectionsList.map((section) => ({
+ ...section,
+ data: section.data.map((option) => ({
+ ...option,
+ text: option.text ?? '',
+ alternateText: option.alternateText ?? undefined,
+ keyForList: option.keyForList ?? '',
+ isDisabled: option.isDisabled ?? undefined,
+ login: option.login ?? undefined,
+ shouldShowSubscript: option.shouldShowSubscript ?? undefined,
+ })),
+ }));
+ }, [currentUserOption, personalDetails, recentReports, translate, userToInvite]);
const selectReport = useCallback(
- (option) => {
+ (option: ListItem) => {
if (!option) {
return;
}
@@ -173,25 +176,35 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
// Check to see if we're editing a task and if so, update the assignee
if (report) {
if (option.accountID !== report.managerID) {
- const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option));
+ const assigneeChatReport = TaskActions.setAssigneeValue(
+ option?.login ?? '',
+ option?.accountID ?? -1,
+ report.reportID,
+ OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? ''}),
+ );
// Pass through the selected assignee
- Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport);
+ TaskActions.editTaskAssignee(report, session?.accountID ?? 0, option?.login ?? '', option?.accountID, assigneeChatReport);
}
Navigation.dismissModal(report.reportID);
// If there's no report, we're creating a new task
} else if (option.accountID) {
- Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option));
+ TaskActions.setAssigneeValue(
+ option?.login ?? '',
+ option.accountID,
+ task?.shareDestination ?? '',
+ OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? undefined}),
+ );
Navigation.goBack(ROUTES.NEW_TASK);
}
},
- [session.accountID, task.shareDestination, report],
+ [session?.accountID, task?.shareDestination, report],
);
- const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]);
+ const handleBackButtonPress = useCallback(() => (route.params?.reportID ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]);
const isOpen = ReportUtils.isOpenTaskReport(report);
- const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', ''));
+ const canModifyTask = TaskActions.canModifyTask(report, currentUserPersonalDetails.accountID);
const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
return (
@@ -199,7 +212,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
includeSafeAreaPaddingBottom={false}
testID={TaskAssigneeSelectorModal.displayName}
>
- {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
+ {({didScreenTransitionEnd}) => (
@@ -225,26 +237,14 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
}
TaskAssigneeSelectorModal.displayName = 'TaskAssigneeSelectorModal';
-TaskAssigneeSelectorModal.propTypes = propTypes;
-TaskAssigneeSelectorModal.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withOnyx({
- rootParentReportPolicy: {
- key: ({reports, route}) => {
- const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`];
- const rootParentReport = ReportUtils.getRootParentReport(report);
- return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`;
- },
- selector: (policy) => lodashPick(policy, ['role']),
- },
- }),
-)(TaskAssigneeSelectorModal);
+const TaskAssigneeSelectorModalWithOnyx = withOnyx({
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(TaskAssigneeSelectorModal);
+
+export default withCurrentUserPersonalDetails(TaskAssigneeSelectorModalWithOnyx);
diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.tsx
similarity index 61%
rename from src/pages/tasks/TaskDescriptionPage.js
rename to src/pages/tasks/TaskDescriptionPage.tsx
index b8b48abd09ff..e08d6380bb18 100644
--- a/src/pages/tasks/TaskDescriptionPage.js
+++ b/src/pages/tasks/TaskDescriptionPage.tsx
@@ -2,53 +2,43 @@ import {useFocusEffect} from '@react-navigation/native';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
-import reportPropTypes from '@pages/reportPropTypes';
+import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/EditTaskForm';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- /** The report currently being looked at */
- report: reportPropTypes,
-
- /* Onyx Props */
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- report: {},
-};
+type TaskDescriptionPageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps;
const parser = new ExpensiMark();
-function TaskDescriptionPage(props) {
+
+function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescriptionPageProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- const validate = useCallback((values) => {
+ const validate = useCallback((values: FormOnyxValues): FormInputErrors => {
const errors = {};
- if (values.description.length > CONST.DESCRIPTION_LIMIT) {
+ if (values?.description && values.description?.length > CONST.DESCRIPTION_LIMIT) {
ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]);
}
@@ -56,30 +46,30 @@ function TaskDescriptionPage(props) {
}, []);
const submit = useCallback(
- (values) => {
- // props.report.description might contain CRLF from the server
- if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(props.report.description)) {
+ (values: FormOnyxValues) => {
+ // report.description might contain CRLF from the server
+ if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(report?.description) && !isEmptyObject(report)) {
// Set the description of the report in the store and then call EditTask API
// to update the description of the report on the server
- Task.editTask(props.report, {description: values.description});
+ Task.editTask(report, {description: values.description});
}
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
},
- [props],
+ [report],
);
- if (!ReportUtils.isTaskReport(props.report)) {
+ if (!ReportUtils.isTaskReport(report)) {
Navigation.isNavigationReady().then(() => {
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
});
}
- const inputRef = useRef(null);
- const focusTimeoutRef = useRef(null);
+ const inputRef = useRef(null);
+ const focusTimeoutRef = useRef(null);
- const isOpen = ReportUtils.isOpenTaskReport(props.report);
- const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID);
- const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen);
+ const isOpen = ReportUtils.isOpenTaskReport(report);
+ const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID);
+ const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
useFocusEffect(
useCallback(() => {
@@ -104,13 +94,13 @@ function TaskDescriptionPage(props) {
testID={TaskDescriptionPage.displayName}
>
-
+
@@ -119,14 +109,14 @@ function TaskDescriptionPage(props) {
role={CONST.ROLE.PRESENTATION}
inputID={INPUT_IDS.DESCRIPTION}
name={INPUT_IDS.DESCRIPTION}
- label={props.translate('newTaskPage.descriptionOptional')}
- accessibilityLabel={props.translate('newTaskPage.descriptionOptional')}
- defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')}
- ref={(el) => {
- if (!el) {
+ label={translate('newTaskPage.descriptionOptional')}
+ accessibilityLabel={translate('newTaskPage.descriptionOptional')}
+ defaultValue={parser.htmlToMarkdown((report && parser.replace(report?.description ?? '')) || '')}
+ ref={(element: AnimatedTextInputRef) => {
+ if (!element) {
return;
}
- inputRef.current = el;
+ inputRef.current = element;
updateMultilineInputRange(inputRef.current);
}}
autoGrowHeight
@@ -140,17 +130,8 @@ function TaskDescriptionPage(props) {
);
}
-TaskDescriptionPage.propTypes = propTypes;
-TaskDescriptionPage.defaultProps = defaultProps;
TaskDescriptionPage.displayName = 'TaskDescriptionPage';
-export default compose(
- withLocalize,
- withCurrentUserPersonalDetails,
- withReportOrNotFound(),
- withOnyx({
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
- },
- }),
-)(TaskDescriptionPage);
+const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskDescriptionPage);
+
+export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails);
diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx
similarity index 62%
rename from src/pages/tasks/TaskShareDestinationSelectorModal.js
rename to src/pages/tasks/TaskShareDestinationSelectorModal.tsx
index b62440b22967..5b56e58752ac 100644
--- a/src/pages/tasks/TaskShareDestinationSelectorModal.js
+++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx
@@ -1,9 +1,7 @@
-import keys from 'lodash/keys';
-import reduce from 'lodash/reduce';
-import PropTypes from 'prop-types';
import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {usePersonalDetails} from '@components/OnyxProvider';
import ScreenWrapper from '@components/ScreenWrapper';
@@ -13,51 +11,45 @@ import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Report from '@libs/actions/Report';
+import * as ReportActions from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import reportPropTypes from '@pages/reportPropTypes';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Report} from '@src/types/onyx';
-const propTypes = {
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
- /** Whether or not we are searching for reports on the server */
- isSearchingForReports: PropTypes.bool,
-};
+type TaskShareDestinationSelectorModalOnyxProps = {
+ reports: OnyxCollection;
-const defaultProps = {
- reports: {},
- isSearchingForReports: false,
+ isSearchingForReports: OnyxEntry;
};
-const selectReportHandler = (option) => {
- if (!option || !option.reportID) {
+type TaskShareDestinationSelectorModalProps = TaskShareDestinationSelectorModalOnyxProps;
+
+const selectReportHandler = (option: unknown) => {
+ const optionItem = option as ReportUtils.OptionData;
+
+ if (!optionItem || !optionItem?.reportID) {
return;
}
- Task.setShareDestinationValue(option.reportID);
+ Task.setShareDestinationValue(optionItem?.reportID);
Navigation.goBack(ROUTES.NEW_TASK);
};
-const reportFilter = (reports) =>
- reduce(
- keys(reports),
- (filtered, reportKey) => {
- const report = reports[reportKey];
- if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) {
- return {...filtered, [reportKey]: report};
- }
- return filtered;
- },
- {},
- );
+const reportFilter = (reports: OnyxCollection) =>
+ Object.keys(reports ?? {}).reduce((filtered, reportKey) => {
+ const report: OnyxEntry = reports?.[reportKey] ?? null;
+ if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) {
+ return {...filtered, [reportKey]: report};
+ }
+ return filtered;
+ }, {});
-function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
+function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) {
const styles = useThemeStyles();
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
const {translate} = useLocalize();
@@ -73,13 +65,29 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue);
- const sections = recentReports && recentReports.length > 0 ? [{data: recentReports, shouldShow: true}] : [];
+ const sections =
+ recentReports && recentReports.length > 0
+ ? [
+ {
+ data: recentReports.map((option) => ({
+ ...option,
+ text: option.text ?? '',
+ alternateText: option.alternateText ?? undefined,
+ keyForList: option.keyForList ?? '',
+ isDisabled: option.isDisabled ?? undefined,
+ login: option.login ?? undefined,
+ shouldShowSubscript: option.shouldShowSubscript ?? undefined,
+ })),
+ shouldShow: true,
+ },
+ ]
+ : [];
return {sections, headerMessage};
}, [personalDetails, reports, debouncedSearchValue]);
useEffect(() => {
- Report.searchInServer(debouncedSearchValue);
+ ReportActions.searchInServer(debouncedSearchValue);
}, [debouncedSearchValue]);
return (
@@ -87,7 +95,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
includeSafeAreaPaddingBottom={false}
testID="TaskShareDestinationSelectorModal"
>
- {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
+ {({didScreenTransitionEnd}) => (
<>
@@ -115,10 +122,8 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) {
}
TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal';
-TaskShareDestinationSelectorModal.propTypes = propTypes;
-TaskShareDestinationSelectorModal.defaultProps = defaultProps;
-export default withOnyx({
+export default withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.tsx
similarity index 50%
rename from src/pages/tasks/TaskTitlePage.js
rename to src/pages/tasks/TaskTitlePage.tsx
index 370baab7cd89..009983beac3e 100644
--- a/src/pages/tasks/TaskTitlePage.js
+++ b/src/pages/tasks/TaskTitlePage.tsx
@@ -1,98 +1,85 @@
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
-import reportPropTypes from '@pages/reportPropTypes';
+import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound';
import * as Task from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/EditTaskForm';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- /** The report currently being looked at */
- report: reportPropTypes,
+type TaskTitlePageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps;
- /* Onyx Props */
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- report: {},
-};
-
-function TaskTitlePage(props) {
+function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) {
const styles = useThemeStyles();
- /**
- * @param {Object} values
- * @param {String} values.title
- * @returns {Object} - An object containing the errors for each inputID
- */
- const validate = useCallback((values) => {
- const errors = {};
+ const {translate} = useLocalize();
+
+ const validate = useCallback(({title}: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
- if (_.isEmpty(values.title)) {
+ if (!title) {
errors.title = 'newTaskPage.pleaseEnterTaskName';
- } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) {
- ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
}, []);
const submit = useCallback(
- (values) => {
- if (values.title !== props.report.reportName) {
+ (values: FormOnyxValues) => {
+ if (values.title !== report?.reportName && !isEmptyObject(report)) {
// Set the title of the report in the store and then call EditTask API
// to update the title of the report on the server
- Task.editTask(props.report, {title: values.title});
+ Task.editTask(report, {title: values.title});
}
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
},
- [props],
+ [report],
);
- if (!ReportUtils.isTaskReport(props.report)) {
+ if (!ReportUtils.isTaskReport(report)) {
Navigation.isNavigationReady().then(() => {
- Navigation.dismissModal(props.report.reportID);
+ Navigation.dismissModal(report?.reportID);
});
}
- const inputRef = useRef(null);
- const isOpen = ReportUtils.isOpenTaskReport(props.report);
- const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID);
- const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen);
+ const inputRef = useRef(null);
+ const isOpen = ReportUtils.isOpenTaskReport(report);
+ const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID);
+ const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
return (
inputRef.current && inputRef.current.focus()}
+ onEntryTransitionEnd={() => {
+ inputRef?.current?.focus();
+ }}
shouldEnableMaxHeight
testID={TaskTitlePage.displayName}
>
{({didScreenTransitionEnd}) => (
-
+
@@ -101,17 +88,17 @@ function TaskTitlePage(props) {
role={CONST.ROLE.PRESENTATION}
inputID={INPUT_IDS.TITLE}
name={INPUT_IDS.TITLE}
- label={props.translate('task.title')}
- accessibilityLabel={props.translate('task.title')}
- defaultValue={(props.report && props.report.reportName) || ''}
- ref={(el) => {
- if (!el) {
+ label={translate('task.title')}
+ accessibilityLabel={translate('task.title')}
+ defaultValue={report?.reportName ?? ''}
+ ref={(element: AnimatedTextInputRef) => {
+ if (!element) {
return;
}
if (!inputRef.current && didScreenTransitionEnd) {
- el.focus();
+ element.focus();
}
- inputRef.current = el;
+ inputRef.current = element;
}}
/>
@@ -122,17 +109,8 @@ function TaskTitlePage(props) {
);
}
-TaskTitlePage.propTypes = propTypes;
-TaskTitlePage.defaultProps = defaultProps;
TaskTitlePage.displayName = 'TaskTitlePage';
-export default compose(
- withLocalize,
- withCurrentUserPersonalDetails,
- withReportOrNotFound(),
- withOnyx({
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
- },
- }),
-)(TaskTitlePage);
+const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskTitlePage);
+
+export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails);
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index d2565022075a..c4f4d6399dbd 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useState} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx
new file mode 100644
index 000000000000..8167e6fc1ebf
--- /dev/null
+++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx
@@ -0,0 +1,80 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect, useRef} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxCollection} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useThemeStyles from '@hooks/useThemeStyles';
+import navigateAfterJoinRequest from '@libs/navigateAfterJoinRequest';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import Navigation from '@navigation/Navigation';
+import type {AuthScreensParamList} from '@navigation/types';
+import * as PolicyAction from '@userActions/Policy';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Policy} from '@src/types/onyx';
+
+type WorkspaceJoinUserPageOnyxProps = {
+ /** The list of this user's policies */
+ policies: OnyxCollection;
+};
+
+type WorkspaceJoinUserPageRoute = {route: StackScreenProps['route']};
+type WorkspaceJoinUserPageProps = WorkspaceJoinUserPageRoute & WorkspaceJoinUserPageOnyxProps;
+
+let isJoinLinkUsed = false;
+
+function WorkspaceJoinUserPage({route, policies}: WorkspaceJoinUserPageProps) {
+ const styles = useThemeStyles();
+ const policyID = route?.params?.policyID;
+ const inviterEmail = route?.params?.email;
+ const policy = ReportUtils.getPolicy(policyID);
+ const isUnmounted = useRef(false);
+
+ useEffect(() => {
+ if (!isJoinLinkUsed) {
+ return;
+ }
+ Navigation.goBack(undefined, false, true);
+ }, []);
+
+ useEffect(() => {
+ if (!policy || !policies || isUnmounted.current || isJoinLinkUsed) {
+ return;
+ }
+ const isPolicyMember = PolicyUtils.isPolicyMember(policyID, policies as Record);
+ if (isPolicyMember) {
+ Navigation.goBack(undefined, false, true);
+ return;
+ }
+ PolicyAction.inviteMemberToWorkspace(policyID, inviterEmail);
+ isJoinLinkUsed = true;
+ Navigation.isNavigationReady().then(() => {
+ if (isUnmounted.current) {
+ return;
+ }
+ navigateAfterJoinRequest();
+ });
+ }, [policy, policyID, policies, inviterEmail]);
+
+ useEffect(
+ () => () => {
+ isUnmounted.current = true;
+ },
+ [],
+ );
+
+ return (
+
+
+
+ );
+}
+
+WorkspaceJoinUserPage.displayName = 'WorkspaceJoinUserPage';
+export default withOnyx({
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+})(WorkspaceJoinUserPage);
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index e47bc4a09be4..3970533870c1 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -254,6 +254,19 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
[selectedEmployees, addUser, removeUser],
);
+ /** Opens the member details page */
+ const openMemberDetails = useCallback(
+ (item: MemberOption) => {
+ if (!isPolicyAdmin || !PolicyUtils.isPaidGroupPolicy(policy)) {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID));
+ return;
+ }
+
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID, Navigation.getActiveRoute()));
+ },
+ [isPolicyAdmin, policy, route.params.policyID],
+ );
+
/**
* Dismisses the errors on one item
*/
@@ -417,22 +430,24 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
},
];
- if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) {
- options.push({
- text: translate('workspace.people.makeMember'),
- value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER,
- icon: Expensicons.User,
- onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER),
- });
- }
+ if (PolicyUtils.isPaidGroupPolicy(policy)) {
+ if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) {
+ options.push({
+ text: translate('workspace.people.makeMember'),
+ value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER,
+ icon: Expensicons.User,
+ onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER),
+ });
+ }
- if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) {
- options.push({
- text: translate('workspace.people.makeAdmin'),
- value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN,
- icon: Expensicons.MakeAdmin,
- onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN),
- });
+ if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) {
+ options.push({
+ text: translate('workspace.people.makeAdmin'),
+ value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN,
+ icon: Expensicons.MakeAdmin,
+ onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN),
+ });
+ }
}
return options;
@@ -463,7 +478,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
onPress={inviteUser}
text={translate('workspace.invite.member')}
icon={Expensicons.Plus}
- iconStyles={{transform: [{scale: 0.6}]}}
+ iconStyles={StyleUtils.getTransformScaleStyle(0.6)}
innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]}
style={[isSmallScreenWidth && styles.flexGrow1]}
/>
@@ -523,13 +538,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
headerMessage={getHeaderMessage()}
headerContent={getHeaderContent()}
- onSelectRow={(item) => {
- if (!isPolicyAdmin) {
- Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID));
- return;
- }
- toggleUser(item.accountID);
- }}
+ onSelectRow={openMemberDetails}
+ onCheckboxPress={(item) => toggleUser(item.accountID)}
onSelectAll={() => toggleAllUsers(data)}
onDismissError={dismissError}
showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))}
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 796f32c343f2..9d90557b1d37 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -1,15 +1,17 @@
-import React, {useCallback} from 'react';
+import React, {useCallback, useState} from 'react';
import type {ImageStyle, StyleProp} from 'react-native';
-import {Image, ScrollView, StyleSheet, View} from 'react-native';
+import {Image, StyleSheet, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import Button from '@components/Button';
+import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
@@ -74,6 +76,19 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
[policy?.avatar, policyName, styles.alignSelfCenter, styles.avatarXLarge],
);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const confirmDeleteAndHideModal = useCallback(() => {
+ if (!policy?.id || !policyName) {
+ return;
+ }
+
+ Policy.deleteWorkspace(policy?.id, policyName);
+
+ PolicyUtils.goBackFromInvalidPolicy();
+
+ setIsDeleteModalOpen(false);
+ }, [policy?.id, policyName]);
return (
{!readOnly && (
-
+
+ setIsDeleteModalOpen(true)}
+ medium
+ />
)}
+ setIsDeleteModalOpen(false)}
+ prompt={translate('workspace.common.deleteConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
)}
diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx
index dd03436042ca..56fd8b783742 100644
--- a/src/pages/workspace/WorkspaceProfileSharePage.tsx
+++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx
@@ -1,5 +1,5 @@
import React, {useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import type {ImageSourcePropType} from 'react-native';
import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import ContextMenuItem from '@components/ContextMenuItem';
@@ -9,6 +9,7 @@ import MenuItem from '@components/MenuItem';
import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index d1edf7f2f783..948cd2fd83c4 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo, useState} from 'react';
-import {FlatList, ScrollView, View} from 'react-native';
+import {FlatList, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -17,6 +17,7 @@ import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {PressableWithoutFeedback} from '@components/Pressable';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -51,6 +52,7 @@ type WorkspaceItem = Required> &
policyID?: string;
adminRoom?: string | null;
announceRoom?: string | null;
+ isJoinRequestPending?: boolean;
};
// eslint-disable-next-line react/no-unused-prop-types
@@ -115,11 +117,12 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- const {isSmallScreenWidth} = useWindowDimensions();
+ const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [policyIDToDelete, setPolicyIDToDelete] = useState();
const [policyNameToDelete, setPolicyNameToDelete] = useState();
+ const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth;
const confirmDeleteAndHideModal = () => {
if (!policyIDToDelete || !policyNameToDelete) {
@@ -191,8 +194,9 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
workspaceIcon={item.icon}
ownerAccountID={item.ownerAccountID}
workspaceType={item.type}
+ isJoinRequestPending={item?.isJoinRequestPending}
rowStyles={hovered && styles.hoveredComponentBG}
- layoutWidth={isSmallScreenWidth ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE}
+ layoutWidth={isLessThanMediumScreen ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE}
brickRoadIndicator={item.brickRoadIndicator}
shouldDisableThreeDotsMenu={item.disabled}
/>
@@ -201,11 +205,11 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
);
},
- [isSmallScreenWidth, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate],
+ [isLessThanMediumScreen, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate],
);
const listHeaderComponent = useCallback(() => {
- if (isSmallScreenWidth) {
+ if (isLessThanMediumScreen) {
return ;
}
@@ -238,7 +242,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
);
- }, [isSmallScreenWidth, styles, translate]);
+ }, [isLessThanMediumScreen, styles, translate]);
const policyRooms = useMemo(() => {
if (!reports || isEmptyObject(reports)) {
@@ -283,8 +287,28 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
return Object.values(policies)
.filter((policy): policy is PolicyType => PolicyUtils.shouldShowPolicy(policy, !!isOffline))
- .map(
- (policy): WorkspaceItem => ({
+ .map((policy): WorkspaceItem => {
+ if (policy?.isJoinRequestPending && policy?.policyDetailsForNonMembers) {
+ const policyInfo = Object.values(policy.policyDetailsForNonMembers)[0];
+ const id = Object.keys(policy.policyDetailsForNonMembers)[0];
+ return {
+ title: policyInfo.name,
+ icon: policyInfo.avatar ? policyInfo.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name),
+ disabled: true,
+ ownerAccountID: policyInfo.ownerAccountID,
+ type: policyInfo.type,
+ iconType: policyInfo.avatar ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_ICON,
+ iconFill: theme.textLight,
+ fallbackIcon: Expensicons.FallbackWorkspaceAvatar,
+ policyID: id,
+ role: CONST.POLICY.ROLE.USER,
+ errors: null,
+ action: () => null,
+ dismissError: () => null,
+ isJoinRequestPending: true,
+ };
+ }
+ return {
title: policy.name,
icon: policy.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name),
action: () => Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)),
@@ -307,8 +331,8 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
ownerAccountID: policy.ownerAccountID,
role: policy.role,
type: policy.type,
- }),
- )
+ };
+ })
.sort((a, b) => localeCompare(a.title, b.title));
}, [reimbursementAccount?.errors, policies, isOffline, theme.textLight, allPolicyMembers, policyRooms]);
@@ -323,7 +347,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
>
Navigation.goBack()}
>
-
+ Navigation.goBack()}
>
-
+
{title}
- {isNarrow && (
- <>
-
-
+
+
+ >
+ ) : (
+
- >
- )}
+ ))}
{!!ownerDetails && (
@@ -183,14 +196,14 @@ function WorkspacesListRow({
>
)}
-
+
-
+
+ {isJoinRequestPending && !isNarrow && (
+
+
+
+ )}
- {isWide && (
+ {isWide && !isJoinRequestPending && (
<>
diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx
new file mode 100644
index 000000000000..cfe28ba292b0
--- /dev/null
+++ b/src/pages/workspace/categories/CreateCategoryPage.tsx
@@ -0,0 +1,110 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {Keyboard} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspaceCategoryCreateForm';
+import type {PolicyCategories} from '@src/types/onyx';
+
+type WorkspaceCreateCategoryPageOnyxProps = {
+ /** All policy categories */
+ policyCategories: OnyxEntry;
+};
+
+type CreateCategoryPageProps = WorkspaceCreateCategoryPageOnyxProps & StackScreenProps;
+
+function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+ const categoryName = values.categoryName.trim();
+
+ if (!ValidationUtils.isRequiredFulfilled(categoryName)) {
+ errors.categoryName = 'workspace.categories.categoryRequiredError';
+ } else if (policyCategories?.[categoryName]) {
+ errors.categoryName = 'workspace.categories.existingCategoryError';
+ } else if (categoryName === CONST.INVALID_CATEGORY_NAME) {
+ errors.categoryName = 'workspace.categories.invalidCategoryName';
+ } else if ([...categoryName].length > CONST.CATEGORY_NAME_LIMIT) {
+ // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
+ ErrorUtils.addErrorMessage(errors, 'categoryName', ['common.error.characterLimitExceedCounter', {length: [...categoryName].length, limit: CONST.CATEGORY_NAME_LIMIT}]);
+ }
+
+ return errors;
+ },
+ [policyCategories],
+ );
+
+ const createCategory = useCallback(
+ (values: FormOnyxValues) => {
+ Policy.createPolicyCategory(route.params.policyID, values.categoryName.trim());
+ Keyboard.dismiss();
+ Navigation.goBack();
+ },
+ [route.params.policyID],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+CreateCategoryPage.displayName = 'CreateCategoryPage';
+
+export default withOnyx({
+ policyCategories: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route?.params?.policyID}`,
+ },
+})(CreateCategoryPage);
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index a66642ff7d5e..52d18d8de276 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -18,7 +18,9 @@ import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import localeCompare from '@libs/LocaleCompare';
import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
import type {CentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
@@ -38,13 +40,16 @@ type PolicyForList = {
};
type WorkspaceCategoriesOnyxProps = {
+ /** The policy the user is accessing. */
+ policy: OnyxEntry;
+
/** Collection of categories attached to a policy */
policyCategories: OnyxEntry;
};
type WorkspaceCategoriesPageProps = WorkspaceCategoriesOnyxProps & StackScreenProps;
-function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesPageProps) {
+function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCategoriesPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
const styles = useThemeStyles();
const theme = useTheme();
@@ -64,24 +69,29 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP
const categoryList = useMemo(
() =>
- Object.values(policyCategories ?? {}).map((value) => ({
- value: value.name,
- text: value.name,
- keyForList: value.name,
- isSelected: !!selectedCategories[value.name],
- rightElement: (
-
- {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
-
-
+ Object.values(policyCategories ?? {})
+ .sort((a, b) => localeCompare(a.name, b.name))
+ .map((value) => ({
+ value: value.name,
+ text: value.name,
+ keyForList: value.name,
+ isSelected: !!selectedCategories[value.name],
+ pendingAction: value.pendingAction,
+ rightElement: (
+
+
+ {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
+
+
+
+
-
- ),
- })),
- [policyCategories, selectedCategories, styles.alignSelfCenter, styles.disabledText, styles.flexRow, styles.p1, styles.pl2, theme.icon, translate],
+ ),
+ })),
+ [policyCategories, selectedCategories, styles.alignSelfCenter, styles.flexRow, styles.label, styles.p1, styles.pl2, styles.textSupporting, theme.icon, translate],
);
const toggleCategory = (category: PolicyForList) => {
@@ -111,10 +121,24 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.text));
};
+ const navigateToCreateCategoryPage = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_CREATE.getRoute(route.params.policyID));
+ };
+
const isLoading = !isOffline && policyCategories === undefined;
- const settingsButton = (
+ const headerButtons = (
+ {!PolicyUtils.hasAccountingConnections(policy) && (
+
+ )}
- {!isSmallScreenWidth && settingsButton}
+ {!isSmallScreenWidth && headerButtons}
- {isSmallScreenWidth && {settingsButton}}
+ {isSmallScreenWidth && {headerButtons}}
{translate('workspace.categories.subtitle')}
@@ -181,6 +205,9 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP
WorkspaceCategoriesPage.displayName = 'WorkspaceCategoriesPage';
export default withOnyx({
+ policy: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`,
+ },
policyCategories: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`,
},
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
new file mode 100644
index 000000000000..d44ff8baa08b
--- /dev/null
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -0,0 +1,152 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import Avatar from '@components/Avatar';
+import Button from '@components/Button';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as UserUtils from '@libs/UserUtils';
+import Navigation from '@navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
+
+type WorkspacePolicyOnyxProps = {
+ /** Personal details of all users */
+ personalDetails: OnyxEntry;
+};
+
+type WorkspaceMemberDetailsPageProps = WithPolicyAndFullscreenLoadingProps & WorkspacePolicyOnyxProps & StackScreenProps;
+
+function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, route}: WorkspaceMemberDetailsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+
+ const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false);
+
+ const accountID = Number(route.params.accountID);
+ const policyID = route.params.policyID;
+ const backTo = route.params.backTo ?? ('' as Route);
+
+ const member = policyMembers?.[accountID];
+ const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
+ const avatar = details.avatar ?? UserUtils.getDefaultAvatar();
+ const fallbackIcon = details.fallbackIcon ?? '';
+ const displayName = details.displayName ?? '';
+
+ const askForConfirmationToRemove = () => {
+ setIsRemoveMemberConfirmModalVisible(true);
+ };
+
+ const removeUser = useCallback(() => {
+ Policy.removeMembers([accountID], route.params.policyID);
+ setIsRemoveMemberConfirmModalVisible(false);
+ Navigation.goBack(backTo);
+ }, [accountID, backTo, route.params.policyID]);
+
+ const navigateToProfile = useCallback(() => {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute()));
+ }, [accountID]);
+
+ const openRoleSelectionModal = useCallback(() => {
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBER_ROLE_SELECTION.getRoute(route.params.policyID, accountID, Navigation.getActiveRoute()));
+ }, [accountID, route.params.policyID]);
+
+ return (
+
+
+
+ Navigation.goBack(backTo)}
+ />
+
+
+
+
+
+ {Boolean(details.displayName ?? '') && (
+
+ {displayName}
+
+ )}
+
+ setIsRemoveMemberConfirmModalVisible(false)}
+ prompt={translate('workspace.people.removeMemberPrompt', {memberName: displayName})}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
+
+WorkspaceMemberDetailsPage.displayName = 'WorkspaceMemberDetailsPage';
+
+export default withPolicyAndFullscreenLoading(
+ withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ })(WorkspaceMemberDetailsPage),
+);
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx
new file mode 100644
index 000000000000..ea8c6aaf144a
--- /dev/null
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsRoleSelectionPage.tsx
@@ -0,0 +1,87 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import type {Route} from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type WorkspaceMemberDetailsPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+
+type ListItemType = {
+ value: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
+ text: string;
+ isSelected: boolean;
+ keyForList: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
+};
+
+function WorkspaceMemberDetailsRoleSelectionPage({policyMembers, route}: WorkspaceMemberDetailsPageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const accountID = Number(route.params.accountID) ?? 0;
+ const policyID = route.params.policyID;
+ const backTo = route.params.backTo ?? ('' as Route);
+ const member = policyMembers?.[accountID];
+
+ const items: ListItemType[] = [
+ {
+ value: CONST.POLICY.ROLE.ADMIN,
+ text: translate('common.admin'),
+ isSelected: member?.role === CONST.POLICY.ROLE.ADMIN,
+ keyForList: CONST.POLICY.ROLE.ADMIN,
+ },
+ {
+ value: CONST.POLICY.ROLE.USER,
+ text: translate('common.member'),
+ isSelected: member?.role === CONST.POLICY.ROLE.USER,
+ keyForList: CONST.POLICY.ROLE.USER,
+ },
+ ];
+
+ const changeRole = ({value}: ListItemType) => {
+ if (!member) {
+ return;
+ }
+
+ Policy.updateWorkspaceMembersRole(route.params.policyID, [accountID], value);
+ Navigation.goBack(backTo);
+ };
+
+ return (
+
+
+
+ Navigation.goBack(backTo)}
+ />
+
+ item.isSelected)?.keyForList}
+ />
+
+
+
+
+ );
+}
+
+WorkspaceMemberDetailsRoleSelectionPage.displayName = 'WorkspaceMemberDetailsRoleSelectionPage';
+
+export default withPolicyAndFullscreenLoading(WorkspaceMemberDetailsRoleSelectionPage);
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
index a23073bbc884..bc728992b811 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
@@ -1,11 +1,12 @@
import React, {useEffect, useMemo} from 'react';
-import {ScrollView, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {withNetwork} from '@components/OnyxProvider';
+import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -110,14 +111,7 @@ function WorkspaceRateAndUnitPage(props: WorkspaceRateAndUnitPageProps) {
shouldShowBackButton
>
{() => (
-
+ ;
+
+type WorkspaceCategoryCreateForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.CATEGORY_NAME]: string;
+ }
+>;
+
+export type {WorkspaceCategoryCreateForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 1ff8d0df2031..3c1462574b9d 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -34,6 +34,7 @@ export type {SettingsStatusSetForm} from './SettingsStatusSetForm';
export type {WaypointForm} from './WaypointForm';
export type {WorkspaceInviteMessageForm} from './WorkspaceInviteMessageForm';
export type {WorkspaceRateAndUnitForm} from './WorkspaceRateAndUnitForm';
+export type {WorkspaceCategoryCreateForm} from './WorkspaceCategoryCreateForm';
export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm';
export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm';
export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm';
diff --git a/src/types/onyx/LastSelectedDistanceRates.ts b/src/types/onyx/LastSelectedDistanceRates.ts
new file mode 100644
index 000000000000..1db1cf32b160
--- /dev/null
+++ b/src/types/onyx/LastSelectedDistanceRates.ts
@@ -0,0 +1,3 @@
+type LastSelectedDistanceRates = Record;
+
+export default LastSelectedDistanceRates;
diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts
index f6104ed470a0..3c6933da19ba 100644
--- a/src/types/onyx/OnyxUpdatesFromServer.ts
+++ b/src/types/onyx/OnyxUpdatesFromServer.ts
@@ -13,7 +13,7 @@ type OnyxUpdateEvent = {
};
type OnyxUpdatesFromServer = {
- type: 'https' | 'pusher';
+ type: 'https' | 'pusher' | 'airship';
lastUpdateID: number | string;
previousUpdateID: number | string;
request?: Request;
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 06c2d2e6abce..e3ba941482a0 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -23,6 +23,7 @@ type OriginalMessageActionName =
| 'TASKCOMPLETED'
| 'TASKEDITED'
| 'TASKREOPENED'
+ | 'ACTIONABLEJOINREQUEST'
| 'ACTIONABLEMENTIONWHISPER'
| ValueOf;
type OriginalMessageApproved = {
@@ -219,6 +220,17 @@ type OriginalMessagePolicyChangeLog = {
originalMessage: ChangeLog;
};
+type OriginalMessageJoinPolicyChangeLog = {
+ actionName: typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST;
+ originalMessage: {
+ choice: string;
+ email: string;
+ inviterEmail: string;
+ lastModified: string;
+ policyID: string;
+ };
+};
+
type OriginalMessageRoomChangeLog = {
actionName: ValueOf;
originalMessage: ChangeLog;
@@ -258,7 +270,9 @@ type OriginalMessageModifiedExpense = {
type OriginalMessageReimbursementQueued = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED;
- originalMessage: unknown;
+ originalMessage: {
+ paymentType: DeepValueOf;
+ };
};
type OriginalMessageReimbursementDequeued = {
@@ -294,6 +308,7 @@ type OriginalMessage =
| OriginalMessageRoomChangeLog
| OriginalMessagePolicyChangeLog
| OriginalMessagePolicyTask
+ | OriginalMessageJoinPolicyChangeLog
| OriginalMessageModifiedExpense
| OriginalMessageReimbursementQueued
| OriginalMessageReimbursementDequeued
@@ -315,6 +330,7 @@ export type {
OriginalMessageCreated,
OriginalMessageRenamed,
OriginalMessageAddComment,
+ OriginalMessageJoinPolicyChangeLog,
OriginalMessageActionableMentionWhisper,
OriginalMessageChronosOOOList,
OriginalMessageSource,
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index b005a9d2756f..61b63106ad8a 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -82,6 +82,20 @@ type Connection = {
type AutoReportingOffset = number | ValueOf;
+type PendingJoinRequestPolicy = {
+ isJoinRequestPending: boolean;
+ policyDetailsForNonMembers: Record<
+ string,
+ OnyxCommon.OnyxValueWithOfflineFeedback<{
+ name: string;
+ ownerAccountID: number;
+ ownerEmail: string;
+ type: ValueOf;
+ avatar?: string;
+ }>
+ >;
+};
+
type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
{
/** The ID of the policy */
@@ -244,10 +258,10 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the Connections feature is enabled */
areConnectionsEnabled?: boolean;
- },
+ } & Partial,
'generalSettings' | 'addWorkspaceRoom'
>;
export default Policy;
-export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault};
+export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault, PendingJoinRequestPolicy};
diff --git a/src/types/onyx/PolicyJoinMember.ts b/src/types/onyx/PolicyJoinMember.ts
new file mode 100644
index 000000000000..263be6c21dc2
--- /dev/null
+++ b/src/types/onyx/PolicyJoinMember.ts
@@ -0,0 +1,17 @@
+import type * as OnyxCommon from './OnyxCommon';
+
+type PolicyJoinMember = {
+ /** Role of the user in the policy */
+ policyID?: string;
+
+ /** Email of the user inviting the new member */
+ inviterEmail?: string;
+
+ /**
+ * Errors from api calls on the specific user
+ * {: 'error message', : 'error message 2'}
+ */
+ errors?: OnyxCommon.Errors;
+};
+
+export default PolicyJoinMember;
diff --git a/src/types/onyx/Task.ts b/src/types/onyx/Task.ts
index 2aec786d77ad..50a871b7ea07 100644
--- a/src/types/onyx/Task.ts
+++ b/src/types/onyx/Task.ts
@@ -2,7 +2,7 @@ import type Report from './Report';
type Task = {
/** Title of the Task */
- title: string;
+ title?: string;
/** Description of the Task */
description?: string;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 6846fc302639..d12b0a22bba2 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -19,6 +19,7 @@ import type IntroSelected from './IntroSelected';
import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs';
import type IOU from './IOU';
import type LastPaymentMethod from './LastPaymentMethod';
+import type LastSelectedDistanceRates from './LastSelectedDistanceRates';
import type Locale from './Locale';
import type {LoginList} from './Login';
import type Login from './Login';
@@ -26,6 +27,7 @@ import type MapboxAccessToken from './MapboxAccessToken';
import type Modal from './Modal';
import type Network from './Network';
import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer';
+import type {DecisionName, OriginalMessageIOU} from './OriginalMessage';
import type PersonalBankAccount from './PersonalBankAccount';
import type {PersonalDetailsList} from './PersonalDetails';
import type PersonalDetails from './PersonalDetails';
@@ -33,6 +35,7 @@ import type PlaidData from './PlaidData';
import type Policy from './Policy';
import type {TaxRate, TaxRates, TaxRatesWithDefault} from './Policy';
import type {PolicyCategories, PolicyCategory} from './PolicyCategory';
+import type PolicyJoinMember from './PolicyJoinMember';
import type {PolicyMembers} from './PolicyMember';
import type PolicyMember from './PolicyMember';
import type {PolicyReportField, PolicyReportFields} from './PolicyReportField';
@@ -44,7 +47,7 @@ import type RecentlyUsedTags from './RecentlyUsedTags';
import type RecentWaypoint from './RecentWaypoint';
import type ReimbursementAccount from './ReimbursementAccount';
import type Report from './Report';
-import type {ReportActions} from './ReportAction';
+import type {ReportActionBase, ReportActions} from './ReportAction';
import type ReportAction from './ReportAction';
import type ReportActionReactions from './ReportActionReactions';
import type ReportActionsDraft from './ReportActionsDraft';
@@ -151,7 +154,12 @@ export type {
PolicyReportField,
PolicyReportFields,
RecentlyUsedReportFields,
+ DecisionName,
+ OriginalMessageIOU,
+ ReportActionBase,
LastPaymentMethod,
+ LastSelectedDistanceRates,
InvitedEmailsToAccountIDs,
Log,
+ PolicyJoinMember,
};
diff --git a/src/utils/arrayDifference.ts b/src/utils/arrayDifference.ts
new file mode 100644
index 000000000000..d011f8d5067b
--- /dev/null
+++ b/src/utils/arrayDifference.ts
@@ -0,0 +1,9 @@
+/**
+ * This function is an equivalent of _.difference, it takes two arrays and returns the difference between them.
+ * It returns an array of items that are in the first array but not in the second array.
+ */
+function arrayDifference(array1: TItem[], array2: TItem[]): TItem[] {
+ return [array1, array2].reduce((a, b) => a.filter((c) => !b.includes(c)));
+}
+
+export default arrayDifference;
diff --git a/tests/e2e/utils/installApp.ts b/tests/e2e/utils/installApp.ts
index b443344e6f02..dc6a9d64053f 100644
--- a/tests/e2e/utils/installApp.ts
+++ b/tests/e2e/utils/installApp.ts
@@ -17,7 +17,7 @@ export default function (packageName: string, path: string, platform = 'android'
execAsync(`adb uninstall ${packageName}`)
.catch((error: ExecException) => {
// Ignore errors
- Logger.warn('Failed to uninstall app:', error);
+ Logger.warn('Failed to uninstall app:', error.message);
})
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.finally(() => execAsync(`adb install ${path}`))
diff --git a/tests/e2e/utils/logger.js b/tests/e2e/utils/logger.ts
similarity index 66%
rename from tests/e2e/utils/logger.js
rename to tests/e2e/utils/logger.ts
index e120c90482b5..ebe8fc05e66a 100644
--- a/tests/e2e/utils/logger.js
+++ b/tests/e2e/utils/logger.ts
@@ -1,7 +1,6 @@
/* eslint-disable import/no-import-module-exports */
import fs from 'fs';
import path from 'path';
-import _ from 'underscore';
import CONFIG from '../config';
const COLOR_DIM = '\x1b[2m';
@@ -12,7 +11,7 @@ const COLOR_GREEN = '\x1b[32m';
const getDateString = () => `[${Date()}] `;
-const writeToLogFile = (...args) => {
+const writeToLogFile = (...args: string[]) => {
if (!fs.existsSync(CONFIG.LOG_FILE)) {
// Check that the directory exists
const logDir = path.dirname(CONFIG.LOG_FILE);
@@ -24,45 +23,46 @@ const writeToLogFile = (...args) => {
}
fs.appendFileSync(
CONFIG.LOG_FILE,
- `${_.map(args, (arg) => {
- if (typeof arg === 'string') {
- // Remove color codes from arg, because they are not supported in log files
- // eslint-disable-next-line no-control-regex
- return arg.replace(/\x1b\[\d+m/g, '');
- }
- return arg;
- })
+ `${args
+ .map((arg) => {
+ if (typeof arg === 'string') {
+ // Remove color codes from arg, because they are not supported in log files
+ // eslint-disable-next-line no-control-regex
+ return arg.replace(/\x1b\[\d+m/g, '');
+ }
+ return arg;
+ })
.join(' ')
.trim()}\n`,
);
};
-const log = (...args) => {
+const log = (...args: string[]) => {
const argsWithTime = [getDateString(), ...args];
console.debug(...argsWithTime);
writeToLogFile(...argsWithTime);
};
-const info = (...args) => {
+const info = (...args: string[]) => {
log('▶️', ...args);
};
-const success = (...args) => {
+const success = (...args: string[]) => {
const lines = ['✅', COLOR_GREEN, ...args, COLOR_RESET];
log(...lines);
};
-const warn = (...args) => {
+const warn = (...args: string[]) => {
const lines = ['⚠️', COLOR_YELLOW, ...args, COLOR_RESET];
log(...lines);
};
-const note = (...args) => {
+const note = (...args: string[]) => {
const lines = [COLOR_DIM, ...args, COLOR_RESET];
log(...lines);
};
-const error = (...args) => {
+const error = (...args: string[]) => {
const lines = ['🔴', COLOR_RED, ...args, COLOR_RESET];
log(...lines);
};
diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts
index 2b2bdbc6b57a..a57eaf593cff 100644
--- a/tests/perf-test/SidebarUtils.perf-test.ts
+++ b/tests/perf-test/SidebarUtils.perf-test.ts
@@ -1,3 +1,4 @@
+import {rand} from '@ngneat/falso';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {measureFunction} from 'reassure';
@@ -13,16 +14,26 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import createCollection from '../utils/collections/createCollection';
import createPersonalDetails from '../utils/collections/personalDetails';
import createRandomPolicy from '../utils/collections/policies';
-import createRandomReportAction from '../utils/collections/reportActions';
+import createRandomReportAction, {getRandomDate} from '../utils/collections/reportActions';
import createRandomReport from '../utils/collections/reports';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
-const getMockedReports = (length = 500) =>
- createCollection(
- (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
- (index) => createRandomReport(index),
- length,
- );
+const REPORTS_COUNT = 15000;
+const REPORT_TRESHOLD = 5;
+const PERSONAL_DETAILS_LIST_COUNT = 1000;
+
+const allReports = createCollection(
+ (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
+ (index) => ({
+ ...createRandomReport(index),
+ type: rand(Object.values(CONST.REPORT.TYPE)),
+ lastVisibleActionCreated: getRandomDate(),
+ // add status and state to every 5th report to mock nonarchived reports
+ statusNum: index % REPORT_TRESHOLD ? 0 : CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: index % REPORT_TRESHOLD ? 0 : CONST.REPORT.STATE_NUM.APPROVED,
+ }),
+ REPORTS_COUNT,
+);
const reportActions = createCollection(
(item) => `${item.reportActionID}`,
@@ -32,9 +43,48 @@ const reportActions = createCollection(
const personalDetails = createCollection(
(item) => item.accountID,
(index) => createPersonalDetails(index),
+ PERSONAL_DETAILS_LIST_COUNT,
+);
+
+const policies = createCollection(
+ (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`,
+ (index) => createRandomPolicy(index),
);
-const mockedResponseMap = getMockedReports(1000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>;
+const mockedBetas = Object.values(CONST.BETAS);
+
+const allReportActions = Object.fromEntries(
+ Object.keys(reportActions).map((key) => [
+ `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`,
+ {
+ [reportActions[key].reportActionID]: {
+ errors: reportActions[key].errors ?? [],
+ message: [
+ {
+ moderationDecision: {
+ decision: reportActions[key].message?.[0]?.moderationDecision?.decision,
+ },
+ },
+ ],
+ reportActionID: reportActions[key].reportActionID,
+ },
+ },
+ ]),
+) as unknown as OnyxCollection;
+
+const currentReportId = '1';
+const transactionViolations = {} as OnyxCollection;
+
+const reportKeys = Object.keys(allReports);
+const reportIDsWithErrors = reportKeys.reduce((errorsMap, reportKey) => {
+ const report = allReports[reportKey];
+ const allReportsActions = allReportActions?.[reportKey.replace('report_', 'reportActions_')] ?? null;
+ const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions) || {};
+ if (isEmptyObject(errors)) {
+ return errorsMap;
+ }
+ return {...errorsMap, [reportKey.replace('report_', '')]: errors};
+}, {});
describe('SidebarUtils', () => {
beforeAll(() => {
@@ -44,7 +94,7 @@ describe('SidebarUtils', () => {
});
Onyx.multiSet({
- ...mockedResponseMap,
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
});
});
@@ -52,7 +102,7 @@ describe('SidebarUtils', () => {
Onyx.clear();
});
- test('[SidebarUtils] getOptionData on 1k reports', async () => {
+ test('[SidebarUtils] getOptionData', async () => {
const report = createRandomReport(1);
const preferredLocale = 'en';
const policy = createRandomPolicy(1);
@@ -73,54 +123,33 @@ describe('SidebarUtils', () => {
);
});
- test('[SidebarUtils] getOrderedReportIDs on 1k reports', async () => {
- const currentReportId = '1';
- const allReports = getMockedReports();
- const betas = [CONST.BETAS.DEFAULT_ROOMS];
- const transactionViolations = {} as OnyxCollection;
-
- const policies = createCollection(
- (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`,
- (index) => createRandomPolicy(index),
+ test('[SidebarUtils] getOrderedReportIDs on 15k reports for default priorityMode', async () => {
+ await waitForBatchedUpdates();
+ await measureFunction(() =>
+ SidebarUtils.getOrderedReportIDs(
+ currentReportId,
+ allReports,
+ mockedBetas,
+ policies,
+ CONST.PRIORITY_MODE.DEFAULT,
+ allReportActions,
+ transactionViolations,
+ undefined,
+ undefined,
+ reportIDsWithErrors,
+ ),
);
+ });
- const allReportActions = Object.fromEntries(
- Object.keys(reportActions).map((key) => [
- key,
- {
- [reportActions[key].reportActionID]: {
- errors: reportActions[key].errors ?? [],
- message: [
- {
- moderationDecision: {
- decision: reportActions[key].message?.[0]?.moderationDecision?.decision,
- },
- },
- ],
- },
- },
- ]),
- ) as unknown as OnyxCollection;
-
- const reportKeys = Object.keys(allReports);
- const reportIDsWithErrors = reportKeys.reduce((errorsMap, reportKey) => {
- const report = allReports[reportKey];
- const allReportsActions = allReportActions?.[reportKey.replace('report_', 'reportActions_')] ?? null;
- const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions) || {};
- if (isEmptyObject(errors)) {
- return errorsMap;
- }
- return {...errorsMap, [reportKey.replace('report_', '')]: errors};
- }, {});
-
+ test('[SidebarUtils] getOrderedReportIDs on 15k reports for GSD priorityMode', async () => {
await waitForBatchedUpdates();
await measureFunction(() =>
SidebarUtils.getOrderedReportIDs(
currentReportId,
allReports,
- betas,
+ mockedBetas,
policies,
- CONST.PRIORITY_MODE.DEFAULT,
+ CONST.PRIORITY_MODE.GSD,
allReportActions,
transactionViolations,
undefined,
diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js
index 6051f04f570e..9c2ff134f21a 100644
--- a/tests/ui/UnreadIndicatorsTest.js
+++ b/tests/ui/UnreadIndicatorsTest.js
@@ -21,6 +21,7 @@ import * as Pusher from '../../src/libs/Pusher/pusher';
import PusherConnectionManager from '../../src/libs/PusherConnectionManager';
import ONYXKEYS from '../../src/ONYXKEYS';
import appSetup from '../../src/setup';
+import PusherHelper from '../utils/PusherHelper';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
@@ -276,6 +277,9 @@ describe('Unread Indicators', () => {
afterEach(() => {
jest.clearAllMocks();
Onyx.clear();
+
+ // Unsubscribe to pusher channels
+ PusherHelper.teardown();
});
it('Display bold in the LHN for unread chat and new line indicator above the chat message when we navigate to it', () =>
@@ -375,54 +379,48 @@ describe('Unread Indicators', () => {
const NEW_REPORT_FIST_MESSAGE_CREATED_DATE = addSeconds(NEW_REPORT_CREATED_DATE, 1);
const createdReportActionID = NumberUtils.rand64();
const commentReportActionID = NumberUtils.rand64();
- const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${USER_A_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`);
- channel.emit(Pusher.TYPE.MULTIPLE_EVENTS, [
+ PusherHelper.emitOnyxUpdate([
{
- eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
- data: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`,
- value: {
- reportID: NEW_REPORT_ID,
- reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
- lastReadTime: '',
- lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()),
- lastMessageText: 'Comment 1',
- lastActorAccountID: USER_C_ACCOUNT_ID,
- participantAccountIDs: [USER_C_ACCOUNT_ID],
- type: CONST.REPORT.TYPE.CHAT,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`,
- value: {
- [createdReportActionID]: {
- actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
- automatic: false,
- created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
- reportActionID: createdReportActionID,
- },
- [commentReportActionID]: {
- actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
- actorAccountID: USER_C_ACCOUNT_ID,
- person: [{type: 'TEXT', style: 'strong', text: 'User C'}],
- created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
- message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}],
- reportActionID: commentReportActionID,
- },
- },
- shouldNotify: true,
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`,
+ value: {
+ reportID: NEW_REPORT_ID,
+ reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
+ lastReadTime: '',
+ lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()),
+ lastMessageText: 'Comment 1',
+ lastActorAccountID: USER_C_ACCOUNT_ID,
+ participantAccountIDs: [USER_C_ACCOUNT_ID],
+ type: CONST.REPORT.TYPE.CHAT,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`,
+ value: {
+ [createdReportActionID]: {
+ actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
+ automatic: false,
+ created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
+ reportActionID: createdReportActionID,
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- value: {
- [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'),
- },
+ [commentReportActionID]: {
+ actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
+ actorAccountID: USER_C_ACCOUNT_ID,
+ person: [{type: 'TEXT', style: 'strong', text: 'User C'}],
+ created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING),
+ message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}],
+ reportActionID: commentReportActionID,
},
- ],
+ },
+ shouldNotify: true,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'),
+ },
},
]);
return waitForBatchedUpdates();
diff --git a/tests/unit/GithubUtilsTest.ts b/tests/unit/GithubUtilsTest.ts
index 794139286527..24e56402f1ea 100644
--- a/tests/unit/GithubUtilsTest.ts
+++ b/tests/unit/GithubUtilsTest.ts
@@ -376,6 +376,7 @@ describe('GithubUtils', () => {
login: 'hubot',
},
],
+ merged_by: {login: 'octocat'},
},
{
number: 7,
@@ -392,14 +393,8 @@ describe('GithubUtils', () => {
color: 'f29513',
},
],
- assignees: [
- {
- login: 'octocat',
- },
- {
- login: 'hubot',
- },
- ],
+ assignees: [],
+ merged_by: {login: 'hubot'},
},
];
const mockGithub = jest.fn(() => ({
@@ -446,7 +441,8 @@ describe('GithubUtils', () => {
const internalQAHeader = '\r\n\r\n**Internal QA:**';
const lineBreak = '\r\n';
const lineBreakDouble = '\r\n\r\n';
- const assignOctocatHubot = ' - @octocat @hubot';
+ const assignOctocat = ' - @octocat';
+ const assignHubot = ' - @hubot';
const deployerVerificationsHeader = '\r\n**Deployer verifications:**';
// eslint-disable-next-line max-len
const timingDashboardVerification =
@@ -468,8 +464,8 @@ describe('GithubUtils', () => {
`${lineBreak}`;
test('Test no verified PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${openCheckbox}${basePRList[0]}` +
@@ -482,12 +478,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test some verified PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, [basePRList[0]]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, [basePRList[0]]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${closedCheckbox}${basePRList[0]}` +
@@ -500,12 +497,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test all verified PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${allVerifiedExpectedOutput}` +
`${lineBreak}${deployerVerificationsHeader}` +
`${lineBreak}${openCheckbox}${timingDashboardVerification}` +
@@ -513,12 +511,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test no resolved deploy blockers', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${allVerifiedExpectedOutput}` +
`${lineBreak}${deployBlockerHeader}` +
`${lineBreak}${openCheckbox}${baseDeployBlockerList[0]}` +
@@ -529,12 +528,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}${lineBreak}` +
`${lineBreak}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test some resolved deploy blockers', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, [baseDeployBlockerList[0]]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, [baseDeployBlockerList[0]]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${allVerifiedExpectedOutput}` +
`${lineBreak}${deployBlockerHeader}` +
`${lineBreak}${closedCheckbox}${baseDeployBlockerList[0]}` +
@@ -545,12 +545,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test all resolved deploy blockers', () => {
- githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, baseDeployBlockerList).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, basePRList, basePRList, baseDeployBlockerList, baseDeployBlockerList).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${closedCheckbox}${basePRList[2]}` +
`${lineBreak}${closedCheckbox}${basePRList[0]}` +
@@ -566,12 +567,13 @@ describe('GithubUtils', () => {
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual([]);
});
});
test('Test internalQA PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${openCheckbox}${basePRList[0]}` +
@@ -579,20 +581,21 @@ describe('GithubUtils', () => {
`${lineBreak}${closedCheckbox}${basePRList[4]}` +
`${lineBreak}${closedCheckbox}${basePRList[5]}` +
`${lineBreak}${internalQAHeader}` +
- `${lineBreak}${openCheckbox}${internalQAPRList[0]}${assignOctocatHubot}` +
- `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}` +
+ `${lineBreak}${openCheckbox}${internalQAPRList[0]}${assignOctocat}` +
+ `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignHubot}` +
`${lineBreakDouble}${deployerVerificationsHeader}` +
`${lineBreak}${openCheckbox}${timingDashboardVerification}` +
`${lineBreak}${openCheckbox}${firebaseVerification}` +
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual(['octocat', 'hubot']);
});
});
test('Test some verified internalQA PRs', () => {
- githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList], [], [], [], [internalQAPRList[0]]).then((issueBody: string) => {
- expect(issueBody).toBe(
+ githubUtils.generateStagingDeployCashBody(tag, [...basePRList, ...internalQAPRList], [], [], [], [internalQAPRList[0]]).then((issue) => {
+ expect(issue.issueBody).toBe(
`${baseExpectedOutput}` +
`${openCheckbox}${basePRList[2]}` +
`${lineBreak}${openCheckbox}${basePRList[0]}` +
@@ -600,14 +603,15 @@ describe('GithubUtils', () => {
`${lineBreak}${closedCheckbox}${basePRList[4]}` +
`${lineBreak}${closedCheckbox}${basePRList[5]}` +
`${lineBreak}${internalQAHeader}` +
- `${lineBreak}${closedCheckbox}${internalQAPRList[0]}${assignOctocatHubot}` +
- `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignOctocatHubot}` +
+ `${lineBreak}${closedCheckbox}${internalQAPRList[0]}${assignOctocat}` +
+ `${lineBreak}${openCheckbox}${internalQAPRList[1]}${assignHubot}` +
`${lineBreakDouble}${deployerVerificationsHeader}` +
`${lineBreak}${openCheckbox}${timingDashboardVerification}` +
`${lineBreak}${openCheckbox}${firebaseVerification}` +
`${lineBreak}${openCheckbox}${ghVerification}` +
`${lineBreakDouble}${ccApplauseLeads}`,
);
+ expect(issue.issueAssignees).toEqual(['octocat', 'hubot']);
});
});
});
diff --git a/tests/unit/LocalizeTests.js b/tests/unit/LocalizeTests.ts
similarity index 100%
rename from tests/unit/LocalizeTests.js
rename to tests/unit/LocalizeTests.ts
diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts
index f28e2f2c3f14..14c749fc92de 100644
--- a/tests/unit/ReportActionsUtilsTest.ts
+++ b/tests/unit/ReportActionsUtilsTest.ts
@@ -285,8 +285,7 @@ describe('ReportActionsUtils', () => {
reportActionID: '1661970171066216',
actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED,
originalMessage: {
- html: 'Hello world',
- whisperedTo: [],
+ paymentType: 'ACH',
},
message: [{html: 'Waiting for the bank account', type: 'Action type', text: 'Action text'}],
},
@@ -303,7 +302,6 @@ describe('ReportActionsUtils', () => {
];
const result = ReportActionsUtils.getSortedReportActionsForDisplay(input);
- input.pop();
expect(result).toStrictEqual(input);
});
@@ -389,8 +387,7 @@ describe('ReportActionsUtils', () => {
reportActionID: '1661970171066216',
actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED,
originalMessage: {
- html: 'Hello world',
- whisperedTo: [],
+ paymentType: 'ACH',
},
message: [{html: 'Waiting for the bank account', type: 'Action type', text: 'Action text'}],
},
diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js
index a5b0a5d3c151..9fbea1df862e 100644
--- a/tests/unit/ReportUtilsTest.js
+++ b/tests/unit/ReportUtilsTest.js
@@ -410,21 +410,26 @@ describe('ReportUtils', () => {
});
});
- it("it is a non-open expense report tied to user's own paid policy expense chat", () => {
- Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, {
- reportID: '101',
- chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
- isOwnPolicyExpenseChat: true,
- }).then(() => {
+ it("it is a submitted report tied to user's own policy expense chat and the policy does not have Instant Submit frequency", () => {
+ const paidPolicy = {
+ id: '3f54cca8',
+ type: CONST.POLICY.TYPE.TEAM,
+ };
+ Promise.all([
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy),
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, {
+ reportID: '101',
+ chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+ isOwnPolicyExpenseChat: true,
+ }),
+ ]).then(() => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.EXPENSE,
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
parentReportID: '101',
- };
- const paidPolicy = {
- type: CONST.POLICY.TYPE.TEAM,
+ policyID: paidPolicy.id,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]);
expect(moneyRequestOptions.length).toBe(0);
@@ -498,7 +503,7 @@ describe('ReportUtils', () => {
});
});
- it("it is an open expense report tied to user's own paid policy expense chat", () => {
+ it("it is an open expense report tied to user's own policy expense chat", () => {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}103`, {
reportID: '103',
chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
@@ -542,6 +547,33 @@ describe('ReportUtils', () => {
expect(moneyRequestOptions.length).toBe(1);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true);
});
+
+ it("it is a submitted expense report in user's own policyExpenseChat and the policy has Instant Submit frequency", () => {
+ const paidPolicy = {
+ id: 'ef72dfeb',
+ type: CONST.POLICY.TYPE.TEAM,
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT,
+ };
+ Promise.all([
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy),
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, {
+ reportID: '101',
+ chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+ isOwnPolicyExpenseChat: true,
+ }),
+ ]).then(() => {
+ const report = {
+ ...LHNTestUtils.getFakeReport(),
+ type: CONST.REPORT.TYPE.EXPENSE,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ parentReportID: '101',
+ policyID: paidPolicy.id,
+ };
+ const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]);
+ expect(moneyRequestOptions.length).toBe(1);
+ });
+ });
});
describe('return multiple money request option if', () => {
diff --git a/tests/unit/TranslateTest.js b/tests/unit/TranslateTest.ts
similarity index 63%
rename from tests/unit/TranslateTest.js
rename to tests/unit/TranslateTest.ts
index d23fa52fc798..0be29a29cb12 100644
--- a/tests/unit/TranslateTest.js
+++ b/tests/unit/TranslateTest.ts
@@ -1,12 +1,15 @@
-import {AnnotationError} from '@actions/core';
-import _ from 'underscore';
-import CONFIG from '../../src/CONFIG';
-import CONST from '../../src/CONST';
-import * as translations from '../../src/languages/translations';
-import * as Localize from '../../src/libs/Localize';
-
-const originalTranslations = _.clone(translations);
-translations.default = {
+/* eslint-disable @typescript-eslint/naming-convention */
+import CONFIG from '@src/CONFIG';
+import CONST from '@src/CONST';
+import * as translations from '@src/languages/translations';
+import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types';
+import * as Localize from '@src/libs/Localize';
+import asMutable from '@src/types/utils/asMutable';
+import arrayDifference from '@src/utils/arrayDifference';
+
+const originalTranslations = {...translations};
+
+asMutable(translations).default = {
[CONST.LOCALES.EN]: translations.flattenObject({
testKey1: 'English',
testKey2: 'Test Word 2',
@@ -24,80 +27,86 @@ translations.default = {
describe('translate', () => {
it('Test present key in full locale', () => {
- expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey1')).toBe('Spanish ES');
+ expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey1' as TranslationPaths)).toBe('Spanish ES');
});
it('Test when key is not found in full locale, but present in language', () => {
- expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey2')).toBe('Spanish Word 2');
- expect(Localize.translate(CONST.LOCALES.ES, 'testKey2')).toBe('Spanish Word 2');
+ expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2');
+ expect(Localize.translate(CONST.LOCALES.ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2');
});
it('Test when key is not found in full locale and language, but present in default', () => {
- expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey3')).toBe('Test Word 3');
+ expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey3' as TranslationPaths)).toBe('Test Word 3');
});
test('Test when key is not found in default', () => {
- expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toThrow(Error);
+ expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toThrow(Error);
});
test('Test when key is not found in default (Production Mode)', () => {
const ORIGINAL_IS_IN_PRODUCTION = CONFIG.IS_IN_PRODUCTION;
- CONFIG.IS_IN_PRODUCTION = true;
- expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toBe('testKey4');
- CONFIG.IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION;
+ asMutable(CONFIG).IS_IN_PRODUCTION = true;
+ expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toBe('testKey4');
+ asMutable(CONFIG).IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION;
});
it('Test when translation value is a function', () => {
const expectedValue = 'With variable Test Variable';
const testVariable = 'Test Variable';
- expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction', {testVariable})).toBe(expectedValue);
+ // @ts-expect-error - TranslationPaths doesn't include testKeyGroup.testFunction as a valid key
+ expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction' as TranslationPaths, {testVariable})).toBe(expectedValue);
});
});
describe('Translation Keys', () => {
- function traverseKeyPath(source, path, keyPaths) {
- const pathArray = keyPaths || [];
+ function traverseKeyPath(source: TranslationFlatObject, path?: string, keyPaths?: string[]): string[] {
+ const pathArray = keyPaths ?? [];
const keyPath = path ? `${path}.` : '';
- _.each(_.keys(source), (key) => {
- if (_.isObject(source[key]) && !_.isFunction(source[key])) {
+ (Object.keys(source) as Array).forEach((key) => {
+ if (typeof source[key] === 'object' && typeof source[key] !== 'function') {
+ // @ts-expect-error - We are modifying the translations object for testing purposes
traverseKeyPath(source[key], keyPath + key, pathArray);
} else {
pathArray.push(keyPath + key);
}
});
+
return pathArray;
}
+
const excludeLanguages = [CONST.LOCALES.EN, CONST.LOCALES.ES_ES];
- const languages = _.without(_.keys(originalTranslations.default), ...excludeLanguages);
+ const languages = Object.keys(originalTranslations.default).filter((ln) => !excludeLanguages.some((excludeLanguage) => excludeLanguage === ln));
const mainLanguage = originalTranslations.default.en;
const mainLanguageKeys = traverseKeyPath(mainLanguage);
- _.each(languages, (ln) => {
- const languageKeys = traverseKeyPath(originalTranslations.default[ln]);
+ languages.forEach((ln) => {
+ const languageKeys = traverseKeyPath(originalTranslations.default[ln as keyof typeof originalTranslations.default]);
it(`Does ${ln} locale have all the keys`, () => {
- const hasAllKeys = _.difference(mainLanguageKeys, languageKeys);
+ const hasAllKeys = arrayDifference(mainLanguageKeys, languageKeys);
if (hasAllKeys.length) {
console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`);
- AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`);
+ Error(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`);
}
expect(hasAllKeys).toEqual([]);
});
it(`Does ${ln} locale have unused keys`, () => {
- const hasAllKeys = _.difference(languageKeys, mainLanguageKeys);
+ const hasAllKeys = arrayDifference(languageKeys, mainLanguageKeys);
if (hasAllKeys.length) {
console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`);
- AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`);
+ Error(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`);
}
expect(hasAllKeys).toEqual([]);
});
});
});
+type ReportContentArgs = {content: string};
+
describe('flattenObject', () => {
it('It should work correctly', () => {
- const func = ({content}) => `This is the content: ${content}`;
+ const func = ({content}: ReportContentArgs) => `This is the content: ${content}`;
const simpleObject = {
common: {
yes: 'Yes',
diff --git a/tests/unit/createOrUpdateStagingDeployTest.js b/tests/unit/createOrUpdateStagingDeployTest.ts
similarity index 97%
rename from tests/unit/createOrUpdateStagingDeployTest.js
rename to tests/unit/createOrUpdateStagingDeployTest.ts
index 75600f0d4fc8..38ba4942a785 100644
--- a/tests/unit/createOrUpdateStagingDeployTest.js
+++ b/tests/unit/createOrUpdateStagingDeployTest.ts
@@ -1,7 +1,8 @@
/**
* @jest-environment node
*/
-import * as core from '@actions/core';
+
+/* eslint-disable @typescript-eslint/naming-convention */
import * as fns from 'date-fns';
import {vol} from 'memfs';
import path from 'path';
@@ -19,7 +20,9 @@ const mockGetPullRequestsMergedBetween = jest.fn();
beforeAll(() => {
// Mock core module
- core.getInput = mockGetInput;
+ jest.mock('@actions/core', () => ({
+ getInput: mockGetInput,
+ }));
// Mock octokit module
const moctokit = {
@@ -49,8 +52,8 @@ beforeAll(() => {
list: jest.fn().mockResolvedValue([]),
},
},
- paginate: jest.fn().mockImplementation((objectMethod) => objectMethod().then(({data}) => data)),
- };
+ paginate: jest.fn().mockImplementation((objectMethod: () => Promise<{data: unknown}>) => objectMethod().then(({data}) => data)),
+ } as typeof GithubUtils.octokit;
GithubUtils.internalOctokit = moctokit;
// Mock GitUtils
@@ -295,7 +298,7 @@ describe('createOrUpdateStagingDeployCash', () => {
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
issue_number: openStagingDeployCashBefore.number,
- // eslint-disable-next-line max-len
+ // eslint-disable-next-line max-len, @typescript-eslint/naming-convention
html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`,
// eslint-disable-next-line max-len
body:
@@ -371,7 +374,7 @@ describe('createOrUpdateStagingDeployCash', () => {
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
issue_number: openStagingDeployCashBefore.number,
- // eslint-disable-next-line max-len
+ // eslint-disable-next-line max-len, @typescript-eslint/naming-convention
html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`,
// eslint-disable-next-line max-len
body:
diff --git a/tests/unit/enhanceParametersTest.js b/tests/unit/enhanceParametersTest.ts
similarity index 96%
rename from tests/unit/enhanceParametersTest.js
rename to tests/unit/enhanceParametersTest.ts
index 6829732f1633..bf2756767401 100644
--- a/tests/unit/enhanceParametersTest.js
+++ b/tests/unit/enhanceParametersTest.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/naming-convention */
import Onyx from 'react-native-onyx';
import CONFIG from '../../src/CONFIG';
import enhanceParameters from '../../src/libs/Network/enhanceParameters';
diff --git a/tests/unit/nativeVersionUpdaterTest.js b/tests/unit/nativeVersionUpdaterTest.ts
similarity index 100%
rename from tests/unit/nativeVersionUpdaterTest.js
rename to tests/unit/nativeVersionUpdaterTest.ts
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 4ee3e63b13e7..dcd144e77596 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -26,12 +26,17 @@ function setup() {
function emitOnyxUpdate(args: OnyxUpdate[]) {
const channel = Pusher.getChannel(CHANNEL_NAME);
- channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, [
- {
- eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
- data: args,
- },
- ]);
+ channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, {
+ type: 'pusher',
+ lastUpdateID: null,
+ previousUpdateID: null,
+ updates: [
+ {
+ eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
+ data: args,
+ },
+ ],
+ });
}
function teardown() {
diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts
index bb14a2c7a41b..dcfa896f1ae4 100644
--- a/tests/utils/collections/reportActions.ts
+++ b/tests/utils/collections/reportActions.ts
@@ -80,3 +80,5 @@ export default function createRandomReportAction(index: number): ReportAction {
isAttachment: randBoolean(),
};
}
+
+export {getRandomDate};