diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 351a11506d68..4100a13f8bee 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -98,7 +98,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c
- [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
- [ ] If a new CSS style is added I verified that:
- [ ] A similar style doesn't already exist
- - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)`)
+ - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)`)
- [ ] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
- [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases)
- [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
diff --git a/__mocks__/@ua/react-native-airship.js b/__mocks__/@ua/react-native-airship.js
index 1672c064f9be..29be662e96a1 100644
--- a/__mocks__/@ua/react-native-airship.js
+++ b/__mocks__/@ua/react-native-airship.js
@@ -31,7 +31,7 @@ const Airship = {
},
contact: {
identify: jest.fn(),
- getNamedUserId: jest.fn(),
+ getNamedUserId: () => Promise.resolve(undefined),
reset: jest.fn(),
},
};
diff --git a/android/app/build.gradle b/android/app/build.gradle
index b62ed8228022..f224d895e2fa 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -91,8 +91,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001040800
- versionName "1.4.8-0"
+ versionCode 1001040801
+ versionName "1.4.8-1"
}
flavorDimensions "default"
diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md
index 32d3919efbe4..8467b97c29fb 100644
--- a/contributingGuides/NAVIGATION.md
+++ b/contributingGuides/NAVIGATION.md
@@ -1,6 +1,6 @@
# Overview
-The navigation in the App consists of a top-level Stack Navigator (called `RootStack`) with each of its `Screen` components handling different high-level flow. All those flows can be seen in `AuthScreens.js` file.
+The navigation in the App consists of a top-level Stack Navigator (called `RootStack`) with each of its `Screen` components handling different high-level flow. All those flows can be seen in `AuthScreens.tsx` file.
## Terminology
@@ -20,11 +20,11 @@ Navigation Actions - User actions correspond to resulting navigation actions tha
## Adding RHP flows
-Most of the time, if you want to add some of the flows concerning one of your reports, e.g. `Money Request` from a user, you will most probably use `RightModalNavigator.js` and `ModalStackNavigators.js` file:
+Most of the time, if you want to add some of the flows concerning one of your reports, e.g. `Money Request` from a user, you will most probably use `RightModalNavigator.tsx` and `ModalStackNavigators.tsx` file:
-- Since each of those flows is kind of a modal stack, if you want to add a page to the existing flow, you should just add a page to the correct stack in `ModalStackNavigators.js`.
+- Since each of those flows is kind of a modal stack, if you want to add a page to the existing flow, you should just add a page to the correct stack in `ModalStackNavigators.tsx`.
-- If you want to create new flow, add a `Screen` in `RightModalNavigator.js` and make new modal in `ModalStackNavigators.js` with chosen pages.
+- If you want to create new flow, add a `Screen` in `RightModalNavigator.tsx` and make new modal in `ModalStackNavigators.tsx` with chosen pages.
When creating RHP flows, you have to remember a couple things:
@@ -196,4 +196,4 @@ The action for the first step created with `getMinimalAction` looks like this:
```
### Deeplinking
-There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones.
\ No newline at end of file
+There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones.
diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md
index 16c8f88927b1..68088f623f8d 100644
--- a/contributingGuides/REVIEWER_CHECKLIST.md
+++ b/contributingGuides/REVIEWER_CHECKLIST.md
@@ -46,7 +46,7 @@
- [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
- [ ] If a new CSS style is added I verified that:
- [ ] A similar style doesn't already exist
- - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG`)
+ - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG`)
- [ ] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
- [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases)
- [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
diff --git a/docs/articles/expensify-classic/account-settings/Notification-Troubbleshooting.md b/docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Notification-Troubbleshooting.md
rename to docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md
diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md
similarity index 98%
rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md
rename to docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md
index f2ff837d7638..fa5879d85ea8 100644
--- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md
+++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md
@@ -1,6 +1,6 @@
---
title: Company-Card-Settings.md
-description: Company card settings
+description: Once you connect your cards, customize the configuration using company card settings.
---
# Overview
Once you’ve imported your company cards via commercial card feed, direct bank feed, or CSV import, the next step is to configure the cards’ settings.
diff --git a/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md
index 2db69d0a8791..8243833dcc23 100644
--- a/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md
+++ b/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md
@@ -1,5 +1,5 @@
---
-title: Your Expensify Partner Manager
+title: Expensify Partner Support
description: Understanding support for our partners
---
@@ -10,6 +10,7 @@ Our well-rounded support methodology is designed to provide comprehensive assist
## 1. ExpensifyApproved! University
**Purpose:** Equip your team with a comprehensive understanding of Expensify.
+
**Benefits:**
- Foundation-level knowledge about the platform.
- 3 CPE credits upon successful completion (US-only).
@@ -17,16 +18,39 @@ Our well-rounded support methodology is designed to provide comprehensive assist
- Visit university.Expensify.com to access our comprehensive training program.
## 2. Partner Manager
-**Role:** A designated liaison for your firm.
+**Role:**
+A Partner Manager is a dedicated point of contact for your firm Partner Managers support our accounting partners by providing recommendations for client’s accounts, assisting with firm-wide training, and ensuring partners receive the full benefits of our partnership program. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency.
+
+
**Key Responsibilities:**
- Handle any escalations promptly.
- Organize firm-wide training sessions.
- Assist with strategic planning and the introduction of new features.
- Once you've completed the ExpensifyApproved! University, log in to your Expensify account. Click on the "Support" option to connect with your dedicated Partner Manager.
+**How do I know if I have a Partner Manager?**
+
+For your firm to be assigned a Partner Manager, you must complete the ExpensifyApproved! University training course. Every external accountant or bookkeeper who completes the training is automatically enrolled in our program and receives all the benefits, including access to the Partner Manager. So everyone at your firm must complete the training to receive the maximum benefit.
+
+You can check to see if you’ve completed the course and enrolled in the ExpensifyApproved! Accountants program simply by logging into your Expensify account. In the bottom left-hand corner of the website, you will see the ExpensifyApproved! logo.
+
+**How do I contact my Partner Manager?**
+1. Signing in to new.expensify.com and searching for your Partner Manager
+2. Replying to or clicking the chat link on any email you get from your Partner Manager
+
+**How do I know if my Partner Manager is online?**
+
+You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours.
+
+**Can I get on a call with my Partner Manager?**
+
+Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm-wide training, and client setups.
+
+We recommend continuing to work with Concierge for general support questions, as this team is always online and available to help immediately.
## 3. Client Setup Specialist
**Purpose:** Ensure smooth onboarding for every client you refer.
+
**Duties:**
- Comprehensive assistance with setting up Expensify.
- Help with configuring accounting integrations.
@@ -35,6 +59,7 @@ Our well-rounded support methodology is designed to provide comprehensive assist
## 4. Client Account Manager
**Role:** Dedicated support for ongoing client needs.
+
**Responsibilities:**
- Address day-to-day product inquiries.
- Assist clients in navigating and optimizing their use of Expensify.
@@ -42,6 +67,7 @@ Our well-rounded support methodology is designed to provide comprehensive assist
## 5. Concierge chat support
**Availability:** Real-time support for any urgent inquiries.
+
**Features:**
- Immediate assistance with an average response time of under two minutes.
- Available to both accountants and clients for all product-related questions.
diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Creating-Per-Diem-Expenses.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md
similarity index 89%
rename from docs/articles/expensify-classic/workspace-and-domain-settings/Creating-Per-Diem-Expenses.md
rename to docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md
index 214188e35137..1b537839af77 100644
--- a/docs/articles/expensify-classic/workspace-and-domain-settings/Creating-Per-Diem-Expenses.md
+++ b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md
@@ -1,17 +1,17 @@
---
-title: Creating Per Diem Expenses
+title: Per-Diem-Expenses
description: How to create Per Diem expenses on mobile and web.
---
# Overview
What are Per Diems? Per diems, short for "per diem allowance" or "daily allowance," are fixed daily payments provided by your employer to cover expenses incurred during business or work-related travel. These allowances simplify expense tracking and reimbursement for meals, lodging, and incidental expenses during a trip. Per Diems can be masterfully tracked in Expensify!
-## How To create per diem expenses
+## How to create per diem expenses
To add per diem expenses, you need three pieces of information:
1. Where did you go? - Specify your travel destination.
2. How long were you away? - Define the period you're claiming for.
-3. Which rate did you use? - Select the appropriate per diem rate.
+3. Which rate did you use? - Select the appropriate per diem rate (this is set by your employer).
### Step 1: On either the web or mobile app, click New Expense and choose Per Diem
@@ -31,15 +31,15 @@ Finally, submit your Per Diem expense for approval, and you'll be on your way to
# FAQ
-## Can I Edit My Per Diems?
+## Can I edit my per diem expenses?
Per Diems cannot be amended. To make changes, delete the expense and recreate it as needed.
-## What If My Admin Requires Daily Per Diems?
+## What if my admin requires daily per diems?
No problem! Create a separate Per Diem expense for each day of your trip.
-## I Have Questions About the Amount I'm Due
+## I have questions about the amount I'm due
Reach out to your internal Admin team, as they've configured the rates in your policy to meet specific requirements.
-## Can I Add Start and End Times to a Per Diem?
+## Can I add start and end times to per diems?
Unfortunately, you cannot add start and end times to Per Diems in Expensify.
By following these steps, you can efficiently create and manage your Per Diem expenses in Expensify, making the process of tracking and getting reimbursed hassle-free.
diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem.md
deleted file mode 100644
index 780e5969c441..000000000000
--- a/docs/articles/expensify-classic/get-paid-back/Per-Diem.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Per Diem
-description: Per Diem
----
-## Resource Coming Soon!
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md b/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md
similarity index 100%
rename from docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md
rename to docs/articles/expensify-classic/integrations/travel-integrations/Navan.md
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md
index 3ee1c8656b4b..16da0c0caa5b 100644
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md
+++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md
@@ -1,5 +1,24 @@
---
-title: Coming Soon
-description: Coming Soon
+title: Uber integration
+description: Connecting your Uber account to Expensify
---
-## Resource Coming Soon!
+## Overview
+
+Link Expensify directly to your Uber account so your Uber for Business receipts populate automatically in Expensify.
+
+# How to connect Uber to Expensify
+
+You can do this right in the Uber app:
+
+1. Head to Account > Business hub > Get started
+2. Tap Create an individual account > Get started
+3. Enter your business email and tap Next
+4. Select the payment card you'd like to use for your business profile
+5. Choose how frequently you’d like to receive travel summaries
+6. Select Expensify as your expense provider
+Expensify and Uber are now connected!
+
+Now, every time you use Uber for Business – be it for rides or meals – the receipt will be imported and scanned into Expensify automatically.
+
+{:width="100%"}
+{:width="100%"}
diff --git a/docs/assets/images/Uber1.png b/docs/assets/images/Uber1.png
new file mode 100644
index 000000000000..d5a7d651c6b9
Binary files /dev/null and b/docs/assets/images/Uber1.png differ
diff --git a/docs/assets/images/Uber2.png b/docs/assets/images/Uber2.png
new file mode 100644
index 000000000000..27ac9925a900
Binary files /dev/null and b/docs/assets/images/Uber2.png differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 6c66552f2325..7c3fbf13697a 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.8.0
+ 1.4.8.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index d54abec37b98..0d2561b67b74 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.8.0
+ 1.4.8.1
diff --git a/ios/expensify_chat_adhoc.mobileprovision.gpg b/ios/expensify_chat_adhoc.mobileprovision.gpg
index 1dae451f168c..f4691df10d67 100644
Binary files a/ios/expensify_chat_adhoc.mobileprovision.gpg and b/ios/expensify_chat_adhoc.mobileprovision.gpg differ
diff --git a/package-lock.json b/package-lock.json
index 1d6333ad719e..51dc9df3a5f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.8-0",
+ "version": "1.4.8-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.8-0",
+ "version": "1.4.8-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -96,7 +96,7 @@
"react-native-pdf": "^6.7.3",
"react-native-performance": "^5.1.0",
"react-native-permissions": "^3.9.3",
- "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
+ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8",
"react-native-plaid-link-sdk": "10.8.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
@@ -44514,9 +44514,9 @@
}
},
"node_modules/react-native-picker-select": {
- "version": "8.0.4",
- "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
- "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==",
+ "version": "8.1.0",
+ "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8",
+ "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==",
"license": "MIT",
"dependencies": {
"lodash.isequal": "^4.5.0"
@@ -84854,9 +84854,9 @@
"requires": {}
},
"react-native-picker-select": {
- "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
- "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==",
- "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
+ "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8",
+ "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==",
+ "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8",
"requires": {
"lodash.isequal": "^4.5.0"
}
diff --git a/package.json b/package.json
index 1453e85fef53..ac02f2db5f82 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.8-0",
+ "version": "1.4.8-1",
"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.",
@@ -143,7 +143,7 @@
"react-native-pdf": "^6.7.3",
"react-native-performance": "^5.1.0",
"react-native-permissions": "^3.9.3",
- "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2",
+ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8",
"react-native-plaid-link-sdk": "10.8.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.9+005+image-header-support.patch
new file mode 100644
index 000000000000..4652e22662f0
--- /dev/null
+++ b/patches/react-native-web+0.19.9+005+image-header-support.patch
@@ -0,0 +1,200 @@
+diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js
+index 95355d5..19109fc 100644
+--- a/node_modules/react-native-web/dist/exports/Image/index.js
++++ b/node_modules/react-native-web/dist/exports/Image/index.js
+@@ -135,7 +135,22 @@ function resolveAssetUri(source) {
+ }
+ return uri;
+ }
+-var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
++function raiseOnErrorEvent(uri, _ref) {
++ var onError = _ref.onError,
++ onLoadEnd = _ref.onLoadEnd;
++ if (onError) {
++ onError({
++ nativeEvent: {
++ error: "Failed to load resource " + uri + " (404)"
++ }
++ });
++ }
++ if (onLoadEnd) onLoadEnd();
++}
++function hasSourceDiff(a, b) {
++ return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers);
++}
++var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => {
+ var ariaLabel = props['aria-label'],
+ blurRadius = props.blurRadius,
+ defaultSource = props.defaultSource,
+@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
+ }
+ }, function error() {
+ updateState(ERRORED);
+- if (onError) {
+- onError({
+- nativeEvent: {
+- error: "Failed to load resource " + uri + " (404)"
+- }
+- });
+- }
+- if (onLoadEnd) {
+- onLoadEnd();
+- }
++ raiseOnErrorEvent(uri, {
++ onError,
++ onLoadEnd
++ });
+ });
+ }
+ function abortPendingRequest() {
+@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => {
+ suppressHydrationWarning: true
+ }), hiddenImage, createTintColorSVG(tintColor, filterRef.current));
+ });
+-Image.displayName = 'Image';
++BaseImage.displayName = 'Image';
++
++/**
++ * This component handles specifically loading an image source with headers
++ * default source is never loaded using headers
++ */
++var ImageWithHeaders = /*#__PURE__*/React.forwardRef((props, ref) => {
++ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource`
++ var nextSource = props.source;
++ var _React$useState3 = React.useState(''),
++ blobUri = _React$useState3[0],
++ setBlobUri = _React$useState3[1];
++ var request = React.useRef({
++ cancel: () => {},
++ source: {
++ uri: '',
++ headers: {}
++ },
++ promise: Promise.resolve('')
++ });
++ var onError = props.onError,
++ onLoadStart = props.onLoadStart,
++ onLoadEnd = props.onLoadEnd;
++ React.useEffect(() => {
++ if (!hasSourceDiff(nextSource, request.current.source)) {
++ return;
++ }
++
++ // When source changes we want to clean up any old/running requests
++ request.current.cancel();
++ if (onLoadStart) {
++ onLoadStart();
++ }
++
++ // Store a ref for the current load request so we know what's the last loaded source,
++ // and so we can cancel it if a different source is passed through props
++ request.current = ImageLoader.loadWithHeaders(nextSource);
++ request.current.promise.then(uri => setBlobUri(uri)).catch(() => raiseOnErrorEvent(request.current.source.uri, {
++ onError,
++ onLoadEnd
++ }));
++ }, [nextSource, onLoadStart, onError, onLoadEnd]);
++
++ // Cancel any request on unmount
++ React.useEffect(() => request.current.cancel, []);
++ var propsToPass = _objectSpread(_objectSpread({}, props), {}, {
++ // `onLoadStart` is called from the current component
++ // We skip passing it down to prevent BaseImage raising it a 2nd time
++ onLoadStart: undefined,
++ // Until the current component resolves the request (using headers)
++ // we skip forwarding the source so the base component doesn't attempt
++ // to load the original source
++ source: blobUri ? _objectSpread(_objectSpread({}, nextSource), {}, {
++ uri: blobUri
++ }) : undefined
++ });
++ return /*#__PURE__*/React.createElement(BaseImage, _extends({
++ ref: ref
++ }, propsToPass));
++});
+
+ // $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
+-var ImageWithStatics = Image;
++var ImageWithStatics = /*#__PURE__*/React.forwardRef((props, ref) => {
++ if (props.source && props.source.headers) {
++ return /*#__PURE__*/React.createElement(ImageWithHeaders, _extends({
++ ref: ref
++ }, props));
++ }
++ return /*#__PURE__*/React.createElement(BaseImage, _extends({
++ ref: ref
++ }, props));
++});
+ ImageWithStatics.getSize = function (uri, success, failure) {
+ ImageLoader.getSize(uri, success, failure);
+ };
+diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js
+index bc06a87..e309394 100644
+--- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js
++++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js
+@@ -76,7 +76,7 @@ var ImageLoader = {
+ var image = requests["" + requestId];
+ if (image) {
+ var naturalHeight = image.naturalHeight,
+- naturalWidth = image.naturalWidth;
++ naturalWidth = image.naturalWidth;
+ if (naturalHeight && naturalWidth) {
+ success(naturalWidth, naturalHeight);
+ complete = true;
+@@ -102,11 +102,19 @@ var ImageLoader = {
+ id += 1;
+ var image = new window.Image();
+ image.onerror = onError;
+- image.onload = e => {
++ image.onload = nativeEvent => {
+ // avoid blocking the main thread
+- var onDecode = () => onLoad({
+- nativeEvent: e
+- });
++ var onDecode = () => {
++ // Append `source` to match RN's ImageLoadEvent interface
++ nativeEvent.source = {
++ uri: image.src,
++ width: image.naturalWidth,
++ height: image.naturalHeight
++ };
++ onLoad({
++ nativeEvent
++ });
++ };
+ if (typeof image.decode === 'function') {
+ // Safari currently throws exceptions when decoding svgs.
+ // We want to catch that error and allow the load handler
+@@ -120,6 +128,32 @@ var ImageLoader = {
+ requests["" + id] = image;
+ return id;
+ },
++ loadWithHeaders(source) {
++ var uri;
++ var abortController = new AbortController();
++ var request = new Request(source.uri, {
++ headers: source.headers,
++ signal: abortController.signal
++ });
++ request.headers.append('accept', 'image/*');
++ var promise = fetch(request).then(response => response.blob()).then(blob => {
++ uri = URL.createObjectURL(blob);
++ return uri;
++ }).catch(error => {
++ if (error.name === 'AbortError') {
++ return '';
++ }
++ throw error;
++ });
++ return {
++ promise,
++ source,
++ cancel: () => {
++ abortController.abort();
++ URL.revokeObjectURL(uri);
++ }
++ };
++ },
+ prefetch(uri) {
+ return new Promise((resolve, reject) => {
+ ImageLoader.load(uri, () => {
diff --git a/src/App.js b/src/App.js
index 2caa6b9ffc29..e273dcce1e47 100644
--- a/src/App.js
+++ b/src/App.js
@@ -53,6 +53,9 @@ function App() {
diff --git a/src/CONST.ts b/src/CONST.ts
index 3d69c83c5c22..283195562e49 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -483,6 +483,7 @@ const CONST = {
MAX_REPORT_PREVIEW_RECEIPTS: 3,
},
REPORT: {
+ MAX_COUNT_BEFORE_FOCUS_UPDATE: 30,
MAXIMUM_PARTICIPANTS: 8,
SPLIT_REPORTID: '-2',
ACTIONS: {
diff --git a/src/Expensify.js b/src/Expensify.js
index 1b692f86a197..aece93c0ff4d 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -7,6 +7,7 @@ import _ from 'underscore';
import ConfirmModal from './components/ConfirmModal';
import DeeplinkWrapper from './components/DeeplinkWrapper';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
+import FocusModeNotification from './components/FocusModeNotification';
import GrowlNotification from './components/GrowlNotification';
import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import SplashScreenHider from './components/SplashScreenHider';
@@ -76,6 +77,9 @@ const propTypes = {
/** Whether the app is waiting for the server's response to determine if a room is public */
isCheckingPublicRoom: PropTypes.bool,
+ /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */
+ focusModeNotification: PropTypes.bool,
+
...withLocalizePropTypes,
};
@@ -88,6 +92,7 @@ const defaultProps = {
isSidebarLoaded: false,
screenShareRequest: null,
isCheckingPublicRoom: true,
+ focusModeNotification: false,
};
const SplashScreenHiddenContext = React.createContext({});
@@ -221,6 +226,7 @@ function Expensify(props) {
isVisible
/>
) : null}
+ {props.focusModeNotification ? : null}
>
)}
@@ -261,6 +267,10 @@ export default compose(
screenShareRequest: {
key: ONYXKEYS.SCREEN_SHARE_REQUEST,
},
+ focusModeNotification: {
+ key: ONYXKEYS.FOCUS_MODE_NOTIFICATION,
+ initWithStoredValues: false,
+ },
}),
)(Expensify);
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index a983ec5acba5..9cd55b41455b 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -152,6 +152,12 @@ const ONYXKEYS = {
/** The user's cash card and imported cards (including the Expensify Card) */
CARD_LIST: 'cardList',
+ /** Whether the user has tried focus mode yet */
+ NVP_TRY_FOCUS_MODE: 'tryFocusMode',
+
+ /** Boolean flag used to display the focus mode notification */
+ FOCUS_MODE_NOTIFICATION: 'focusModeNotification',
+
/** Stores information about the user's saved statements */
WALLET_STATEMENT: 'walletStatement',
@@ -383,6 +389,8 @@ type OnyxValues = {
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf;
[ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge;
[ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string;
+ [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean;
+ [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
[ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record;
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
[ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
@@ -390,7 +398,7 @@ type OnyxValues = {
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
[ONYXKEYS.PLAID_LINK_TOKEN]: string;
[ONYXKEYS.ONFIDO_TOKEN]: string;
- [ONYXKEYS.NVP_PREFERRED_LOCALE]: ValueOf;
+ [ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale;
[ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet;
[ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido;
[ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails;
@@ -408,6 +416,7 @@ type OnyxValues = {
[ONYXKEYS.IS_LOADING_PAYMENT_METHODS]: boolean;
[ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean;
[ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean;
+ [ONYXKEYS.IS_LOADING_APP]: boolean;
[ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer;
[ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string;
[ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean;
@@ -420,6 +429,7 @@ type OnyxValues = {
[ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken;
[ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer;
[ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number;
+ [ONYXKEYS.DEMO_INFO]: OnyxTypes.DemoInfo;
[ONYXKEYS.MAX_CANVAS_AREA]: number;
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index f4cbcf4f2564..c0d3df82e228 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -2,6 +2,7 @@
* This is a file containing constants for all of the screen names. In most cases, we should use the routes for
* navigation. But there are situations where we may need to access screen names directly.
*/
+import DeepValueOf from './types/utils/DeepValueOf';
const PROTECTED_SCREENS = {
HOME: 'Home',
@@ -22,6 +23,25 @@ const SCREENS = {
WORKSPACES: 'Settings_Workspaces',
SECURITY: 'Settings_Security',
STATUS: 'Settings_Status',
+ PROFILE: 'Settings_Profile',
+ PRONOUNS: 'Settings_Pronouns',
+ DISPLAY_NAME: 'Settings_Display_Name',
+ TIMEZONE: 'Settings_Timezone',
+ TIMEZONE_SELECT: 'Settings_Timezone_Select',
+ CONTACT_METHODS: 'Settings_ContactMethods',
+ CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails',
+ NEW_CONTACT_METHOD: 'Settings_NewContactMethod',
+ SHARE_CODE: 'Settings_Share_Code',
+ ABOUT: 'Settings_About',
+ APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links',
+ LOUNGE_ACCESS: 'Settings_Lounge_Access',
+
+ PERSONAL_DETAILS_INITIAL: 'Settings_PersonalDetails_Initial',
+ PERSONAL_DETAILS_LEGAL_NAME: 'Settings_PersonalDetails_LegalName',
+ PERSONAL_DETAILS_DATE_OF_BIRTH: 'Settings_PersonalDetails_DateOfBirth',
+ PERSONAL_DETAILS_ADDRESS: 'Settings_PersonalDetails_Address',
+ PERSONAL_DETAILS_ADDRESS_COUNTRY: 'Settings_PersonalDetails_Address_Country',
+
WALLET: 'Settings_Wallet',
WALLET_DOMAIN_CARD: 'Settings_Wallet_DomainCard',
WALLET_CARD_GET_PHYSICAL: {
@@ -30,15 +50,166 @@ const SCREENS = {
ADDRESS: 'Settings_Card_Get_Physical_Address',
CONFIRM: 'Settings_Card_Get_Physical_Confirm',
},
+ WALLET_TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance',
+ WALLET_CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account',
+ WALLET_ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments',
+ WALLET_CARD_ACTIVATE: 'Settings_Wallet_Card_Activate',
+ WALLET_REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud',
+ WALLET_CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address',
+
+ ADD_DEBIT_CARD: 'Settings_Add_Debit_Card',
+ ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account',
+ PREFERENCES_PRIORITY_MODE: 'Settings_Preferences_PriorityMode',
+ PREFERENCES_LANGUAGE: 'Settings_Preferences_Language',
+ PREFERENCES_THEME: 'Settings_Preferences_Theme',
+ CLOSE: 'Settings_Close',
+ STATUS_SET: 'Settings_Status_Set',
+ TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth',
+ REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged',
},
SAVE_THE_WORLD: {
ROOT: 'SaveTheWorld_Root',
},
+ RIGHT_MODAL: {
+ SETTINGS: 'Settings',
+ NEW_CHAT: 'NewChat',
+ SEARCH: 'Search',
+ DETAILS: 'Details',
+ PROFILE: 'Profile',
+ REPORT_DETAILS: 'Report_Details',
+ REPORT_SETTINGS: 'Report_Settings',
+ REPORT_WELCOME_MESSAGE: 'Report_WelcomeMessage',
+ PARTICIPANTS: 'Participants',
+ MONEY_REQUEST: 'MoneyRequest',
+ NEW_TASK: 'NewTask',
+ TEACHERS_UNITE: 'TeachersUnite',
+ TASK_DETAILS: 'Task_Details',
+ ENABLE_PAYMENTS: 'EnablePayments',
+ SPLIT_DETAILS: 'SplitDetails',
+ ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount',
+ WALLET_STATEMENT: 'Wallet_Statement',
+ FLAG_COMMENT: 'Flag_Comment',
+ EDIT_REQUEST: 'EditRequest',
+ SIGN_IN: 'SignIn',
+ PRIVATE_NOTES: 'Private_Notes',
+ ROOM_MEMBERS: 'RoomMembers',
+ ROOM_INVITE: 'RoomInvite',
+ REFERRAL: 'Referral',
+ },
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
SAML_SIGN_IN: 'SAMLSignIn',
+
+ MONEY_REQUEST: {
+ ROOT: 'Money_Request',
+ AMOUNT: 'Money_Request_Amount',
+ PARTICIPANTS: 'Money_Request_Participants',
+ CONFIRMATION: 'Money_Request_Confirmation',
+ CURRENCY: 'Money_Request_Currency',
+ DATE: 'Money_Request_Date',
+ DESCRIPTION: 'Money_Request_Description',
+ CATEGORY: 'Money_Request_Category',
+ TAG: 'Money_Request_Tag',
+ MERCHANT: 'Money_Request_Merchant',
+ WAYPOINT: 'Money_Request_Waypoint',
+ EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
+ DISTANCE: 'Money_Request_Distance',
+ RECEIPT: 'Money_Request_Receipt',
+ },
+
+ IOU_SEND: {
+ ADD_BANK_ACCOUNT: 'IOU_Send_Add_Bank_Account',
+ ADD_DEBIT_CARD: 'IOU_Send_Add_Debit_Card',
+ ENABLE_PAYMENTS: 'IOU_Send_Enable_Payments',
+ },
+
+ REPORT_SETTINGS: {
+ ROOT: 'Report_Settings_Root',
+ ROOM_NAME: 'Report_Settings_Room_Name',
+ NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences',
+ WRITE_CAPABILITY: 'Report_Settings_Write_Capability',
+ },
+
+ NEW_TASK: {
+ ROOT: 'NewTask_Root',
+ TASK_ASSIGNEE_SELECTOR: 'NewTask_TaskAssigneeSelector',
+ TASK_SHARE_DESTINATION_SELECTOR: 'NewTask_TaskShareDestinationSelector',
+ DETAILS: 'NewTask_Details',
+ TITLE: 'NewTask_Title',
+ DESCRIPTION: 'NewTask_Description',
+ },
+
+ TASK: {
+ TITLE: 'Task_Title',
+ DESCRIPTION: 'Task_Description',
+ ASSIGNEE: 'Task_Assignee',
+ },
+
+ PRIVATE_NOTES: {
+ VIEW: 'PrivateNotes_View',
+ LIST: 'PrivateNotes_List',
+ EDIT: 'PrivateNotes_Edit',
+ },
+
+ REPORT_DETAILS: {
+ ROOT: 'Report_Details_Root',
+ SHARE_CODE: 'Report_Details_Share_Code',
+ },
+
+ WORKSPACE: {
+ INITIAL: 'Workspace_Initial',
+ SETTINGS: 'Workspace_Settings',
+ CARD: 'Workspace_Card',
+ REIMBURSE: 'Workspace_Reimburse',
+ RATE_AND_UNIT: 'Workspace_RateAndUnit',
+ BILLS: 'Workspace_Bills',
+ INVOICES: 'Workspace_Invoices',
+ TRAVEL: 'Workspace_Travel',
+ MEMBERS: 'Workspace_Members',
+ INVITE: 'Workspace_Invite',
+ INVITE_MESSAGE: 'Workspace_Invite_Message',
+ CURRENCY: 'Workspace_Settings_Currency',
+ },
+
+ EDIT_REQUEST: {
+ ROOT: 'EditRequest_Root',
+ CURRENCY: 'EditRequest_Currency',
+ },
+
+ I_KNOW_A_TEACHER: 'I_Know_A_Teacher',
+ INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal',
+ I_AM_A_TEACHER: 'I_Am_A_Teacher',
+
+ ENABLE_PAYMENTS_ROOT: 'EnablePayments_Root',
+ ADD_PERSONAL_BANK_ACCOUNT_ROOT: 'AddPersonalBankAccount_Root',
+ REIMBURSEMENT_ACCOUNT_ROOT: 'Reimbursement_Account_Root',
+ WALLET_STATEMENT_ROOT: 'WalletStatement_Root',
+ SIGN_IN_ROOT: 'SignIn_Root',
+ DETAILS_ROOT: 'Details_Root',
+ PROFILE_ROOT: 'Profile_Root',
+ REPORT_WELCOME_MESSAGE_ROOT: 'Report_WelcomeMessage_Root',
+ REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root',
+ ROOM_MEMBERS_ROOT: 'RoomMembers_Root',
+ ROOM_INVITE_ROOT: 'RoomInvite_Root',
+ SEARCH_ROOT: 'Search_Root',
+ NEW_CHAT_ROOT: 'NewChat_Root',
+ FLAG_COMMENT_ROOT: 'FlagComment_Root',
+
+ SPLIT_DETAILS: {
+ ROOT: 'SplitDetails_Root',
+ EDIT_REQUEST: 'SplitDetails_Edit_Request',
+ EDIT_CURRENCY: 'SplitDetails_Edit_Currency',
+ },
+
+ REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount',
+ GET_ASSISTANCE: 'GetAssistance',
+ REFERRAL_DETAILS: 'Referral_Details',
+ KEYBOARD_SHORTCUTS: 'KeyboardShortcuts',
} as const;
+type Screen = DeepValueOf;
+
export default SCREENS;
export {PROTECTED_SCREENS};
+export type {Screen};
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index c18b706e1acf..68d529c4a78d 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -168,7 +168,7 @@ function AddPlaidBankAccount({
value: account.plaidAccountID,
label: `${account.addressName} ${account.mask}`,
}));
- const {icon, iconSize, iconStyles} = getBankIcon({themeStyles: styles});
+ const {icon, iconSize, iconStyles} = getBankIcon({styles});
const plaidErrors = lodashGet(plaidData, 'errors');
const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : '';
const bankName = lodashGet(plaidData, 'bankName');
diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.js
index a955cf821f7f..6f5148edd436 100644
--- a/src/components/AddressSearch/CurrentLocationButton.js
+++ b/src/components/AddressSearch/CurrentLocationButton.js
@@ -25,8 +25,8 @@ const defaultProps = {
};
function CurrentLocationButton({onPress, isDisabled}) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const {translate} = useLocalize();
return (
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index fc0e2c1348d5..57b0c6466a7f 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -90,9 +90,6 @@ const propTypes = {
/** Denotes whether it is a workspace avatar or not */
isWorkspaceAvatar: PropTypes.bool,
-
- /** Whether it is a receipt attachment or not */
- isReceiptAttachment: PropTypes.bool,
};
const defaultProps = {
@@ -110,7 +107,6 @@ const defaultProps = {
onModalHide: () => {},
onCarouselAttachmentChange: () => {},
isWorkspaceAvatar: false,
- isReceiptAttachment: false,
};
function AttachmentModal(props) {
@@ -122,6 +118,7 @@ function AttachmentModal(props) {
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false);
const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired);
+ const [isAttachmentReceipt, setIsAttachmentReceipt] = useState(null);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState('');
const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null);
const [source, setSource] = useState(props.source);
@@ -157,6 +154,7 @@ function AttachmentModal(props) {
(attachment) => {
setSource(attachment.source);
setFile(attachment.file);
+ setIsAttachmentReceipt(attachment.isReceipt);
setIsAuthTokenRequired(attachment.isAuthTokenRequired);
onCarouselAttachmentChange(attachment);
},
@@ -359,7 +357,7 @@ function AttachmentModal(props) {
const sourceForAttachmentView = props.source || source;
const threeDotsMenuItems = useMemo(() => {
- if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) {
+ if (!isAttachmentReceipt || !props.parentReport || !props.parentReportActions) {
return [];
}
const menuItems = [];
@@ -394,17 +392,17 @@ function AttachmentModal(props) {
}
return menuItems;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]);
+ }, [isAttachmentReceipt, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]);
// There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment.
- // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false.
+ // isAttachmentReceipt will be null until its certain what the file is, in which case it will then be true|false.
let headerTitle = props.headerTitle;
let shouldShowDownloadButton = false;
let shouldShowThreeDotsButton = false;
- if (!_.isNull(props.isReceiptAttachment)) {
- headerTitle = translate(props.isReceiptAttachment ? 'common.receipt' : 'common.attachment');
- shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !props.isReceiptAttachment && !isOffline;
- shouldShowThreeDotsButton = props.isReceiptAttachment && isModalOpen;
+ if (!_.isNull(isAttachmentReceipt)) {
+ headerTitle = translate(isAttachmentReceipt ? 'common.receipt' : 'common.attachment');
+ shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !isAttachmentReceipt && !isOffline;
+ shouldShowThreeDotsButton = isAttachmentReceipt && isModalOpen;
}
return (
@@ -445,7 +443,7 @@ function AttachmentModal(props) {
shouldOverlay
/>
- {!_.isEmpty(props.report) && !props.isReceiptAttachment ? (
+ {!_.isEmpty(props.report) ? (
)}
- {props.isReceiptAttachment && (
+ {isAttachmentReceipt && (
)}
- {!props.isReceiptAttachment && (
+ {!isAttachmentReceipt && (
{
- if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) {
+ if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) {
return;
}
+ // We're handling receipts differently here because receipt images are not
+ // part of the report action message, the images are constructed client-side
+ if (ReportActionsUtils.isMoneyRequestAction(action)) {
+ const transactionID = lodashGet(action, ['originalMessage', 'IOUTransactionID']);
+ if (!transactionID) {
+ return;
+ }
+
+ if (TransactionUtils.hasReceipt(transaction)) {
+ const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction);
+ const isLocalFile = typeof image === 'string' && _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => image.startsWith(prefix));
+ attachments.unshift({
+ source: tryResolveUrlFromApiRoot(image),
+ isAuthTokenRequired: !isLocalFile,
+ file: {name: transaction.filename},
+ isReceipt: true,
+ transactionID,
+ });
+ return;
+ }
+ }
+
const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], '');
const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN;
const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`);
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 1696f4adf0b4..141e619e489e 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -1,3 +1,4 @@
+import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {FlatList, Keyboard, PixelRatio, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
@@ -27,7 +28,7 @@ const viewabilityConfig = {
itemVisiblePercentThreshold: 95,
};
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, transaction}) {
const styles = useThemeStyles();
const scrollRef = useRef(null);
@@ -38,12 +39,21 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const [attachments, setAttachments] = useState([]);
const [activeSource, setActiveSource] = useState(source);
const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
+ const [isReceipt, setIsReceipt] = useState(false);
- const compareImage = useCallback((attachment) => attachment.source === source, [source]);
+ const compareImage = useCallback(
+ (attachment) => {
+ if (attachment.isReceipt && isReceipt) {
+ return attachment.transactionID === transaction.transactionID;
+ }
+ return attachment.source === source;
+ },
+ [source, isReceipt, transaction],
+ );
useEffect(() => {
const parentReportAction = parentReportActions[report.parentReportActionID];
- const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions);
+ const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions, transaction);
const initialPage = _.findIndex(attachmentsFromReport, compareImage);
@@ -78,10 +88,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
// to get the index of the current page
const entry = _.first(viewableItems);
if (!entry) {
+ setIsReceipt(false);
setActiveSource(null);
return;
}
+ setIsReceipt(entry.item.isReceipt);
setPage(entry.index);
setActiveSource(entry.item.source);
@@ -229,6 +241,15 @@ export default compose(
canEvict: false,
},
}),
+ // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file
+ withOnyx({
+ transaction: {
+ key: ({report, parentReportActions}) => {
+ const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]);
+ return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`;
+ },
+ },
+ }),
withLocalize,
withWindowDimensions,
)(AttachmentCarousel);
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index 4a62335a492d..6bf4e63c01e7 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -1,3 +1,4 @@
+import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Keyboard, PixelRatio, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
@@ -17,7 +18,7 @@ import extractAttachmentsFromReport from './extractAttachmentsFromReport';
import AttachmentCarouselPager from './Pager';
import useCarouselArrows from './useCarouselArrows';
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, transaction, onClose}) {
const styles = useThemeStyles();
const pagerRef = useRef(null);
@@ -27,12 +28,21 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const [activeSource, setActiveSource] = useState(source);
const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true);
const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows();
+ const [isReceipt, setIsReceipt] = useState(false);
- const compareImage = useCallback((attachment) => attachment.source === source, [source]);
+ const compareImage = useCallback(
+ (attachment) => {
+ if (attachment.isReceipt && isReceipt) {
+ return attachment.transactionID === transaction.transactionID;
+ }
+ return attachment.source === source;
+ },
+ [source, isReceipt, transaction],
+ );
useEffect(() => {
const parentReportAction = parentReportActions[report.parentReportActionID];
- const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions);
+ const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions, transaction);
const initialPage = _.findIndex(attachmentsFromReport, compareImage);
@@ -67,7 +77,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const item = attachments[newPageIndex];
setPage(newPageIndex);
+ setIsReceipt(item.isReceipt);
setActiveSource(item.source);
+
onNavigate(item);
},
[setShouldShowArrows, attachments, onNavigate],
@@ -174,5 +186,14 @@ export default compose(
canEvict: false,
},
}),
+ // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file
+ withOnyx({
+ transaction: {
+ key: ({report, parentReportActions}) => {
+ const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]);
+ return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`;
+ },
+ },
+ }),
withLocalize,
)(AttachmentCarousel);
diff --git a/src/components/Attachments/propTypes.js b/src/components/Attachments/propTypes.js
index 698a41de9648..13adc468ce64 100644
--- a/src/components/Attachments/propTypes.js
+++ b/src/components/Attachments/propTypes.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]);
const attachmentFilePropType = PropTypes.shape({
- name: PropTypes.string,
+ name: PropTypes.string.isRequired,
});
const attachmentPropType = PropTypes.shape({
@@ -13,7 +13,7 @@ const attachmentPropType = PropTypes.shape({
source: attachmentSourcePropType.isRequired,
/** File object can be an instance of File or Object */
- file: attachmentFilePropType,
+ file: attachmentFilePropType.isRequired,
});
const attachmentsPropType = PropTypes.arrayOf(attachmentPropType);
diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
index f24b82d8e867..efde2b24992f 100644
--- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
+++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
@@ -39,8 +39,8 @@ function BaseAutoCompleteSuggestions(
}: AutoCompleteSuggestionsProps,
ref: ForwardedRef,
) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const rowHeight = useSharedValue(0);
const scrollRef = useRef>(null);
/**
diff --git a/src/components/AvatarSkeleton.js b/src/components/AvatarSkeleton.tsx
similarity index 100%
rename from src/components/AvatarSkeleton.js
rename to src/components/AvatarSkeleton.tsx
diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx
index e97d73d28c8c..6e5ad8970f1a 100644
--- a/src/components/Banner.tsx
+++ b/src/components/Banner.tsx
@@ -41,8 +41,8 @@ type BannerProps = {
};
function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const {translate} = useLocalize();
return (
diff --git a/src/components/BaseMiniContextMenuItem.js b/src/components/BaseMiniContextMenuItem.js
index 035d8bf4b981..3252938e4ca5 100644
--- a/src/components/BaseMiniContextMenuItem.js
+++ b/src/components/BaseMiniContextMenuItem.js
@@ -51,8 +51,8 @@ const defaultProps = {
* @returns {JSX.Element}
*/
function BaseMiniContextMenuItem(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
return (
{
@@ -170,7 +174,7 @@ function Button(
);
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, {
- isActive: pressOnEnter,
+ isActive: pressOnEnter && !shouldDisableEnterShortcut,
shouldBubble: allowBubble,
priority: enterKeyEventListenerPriority,
shouldPreventDefault: false,
diff --git a/src/components/ColorSchemeWrapper/index.tsx b/src/components/ColorSchemeWrapper/index.tsx
index 577ccf9f3794..2909f1ffbe9f 100644
--- a/src/components/ColorSchemeWrapper/index.tsx
+++ b/src/components/ColorSchemeWrapper/index.tsx
@@ -5,9 +5,9 @@ import useThemeStyles from '@styles/useThemeStyles';
function ColorSchemeWrapper({children}: React.PropsWithChildren): React.ReactElement {
const theme = useTheme();
- const themeStyles = useThemeStyles();
+ const styles = useThemeStyles();
- return {children};
+ return {children};
}
export default ColorSchemeWrapper;
diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js
index eb227de36a54..9852e607562b 100644
--- a/src/components/Composer/index.ios.js
+++ b/src/components/Composer/index.ios.js
@@ -66,8 +66,8 @@ const defaultProps = {
function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) {
const textInput = useRef(null);
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
/**
* Set the TextInput Ref
diff --git a/src/components/ConfirmationPage.js b/src/components/ConfirmationPage.tsx
similarity index 56%
rename from src/components/ConfirmationPage.js
rename to src/components/ConfirmationPage.tsx
index ac56ea3d22e9..12e8b40a0f25 100644
--- a/src/components/ConfirmationPage.js
+++ b/src/components/ConfirmationPage.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@styles/useThemeStyles';
@@ -6,61 +5,52 @@ import Button from './Button';
import FixedFooter from './FixedFooter';
import Lottie from './Lottie';
import LottieAnimations from './LottieAnimations';
+import DotLottieAnimation from './LottieAnimations/types';
import Text from './Text';
-const propTypes = {
+type ConfirmationPageProps = {
/** The asset to render */
- // eslint-disable-next-line react/forbid-prop-types
- animation: PropTypes.object,
+ animation?: DotLottieAnimation;
/** Heading of the confirmation page */
- heading: PropTypes.string,
+ heading: string;
/** Description of the confirmation page */
- description: PropTypes.string,
+ description: string;
/** The text for the button label */
- buttonText: PropTypes.string,
+ buttonText?: string;
/** A function that is called when the button is clicked on */
- onButtonPress: PropTypes.func,
+ onButtonPress?: () => void;
/** Whether we should show a confirmation button */
- shouldShowButton: PropTypes.bool,
+ shouldShowButton?: boolean;
};
-const defaultProps = {
- animation: LottieAnimations.Fireworks,
- heading: '',
- description: '',
- buttonText: '',
- onButtonPress: () => {},
- shouldShowButton: false,
-};
-
-function ConfirmationPage(props) {
+function ConfirmationPage({animation = LottieAnimations.Fireworks, heading, description, buttonText = '', onButtonPress = () => {}, shouldShowButton = false}: ConfirmationPageProps) {
const styles = useThemeStyles();
+
return (
<>
- {props.heading}
- {props.description}
+ {heading}
+ {description}
- {props.shouldShowButton && (
+ {shouldShowButton && (
)}
@@ -68,8 +58,6 @@ function ConfirmationPage(props) {
);
}
-ConfirmationPage.propTypes = propTypes;
-ConfirmationPage.defaultProps = defaultProps;
ConfirmationPage.displayName = 'ConfirmationPage';
export default ConfirmationPage;
diff --git a/src/components/ConfirmedRoute.js b/src/components/ConfirmedRoute.js
index 4c4c34851afb..4ddd537fdd7d 100644
--- a/src/components/ConfirmedRoute.js
+++ b/src/components/ConfirmedRoute.js
@@ -42,8 +42,8 @@ function ConfirmedRoute({mapboxAccessToken, transaction}) {
const {route0: route} = transaction.routes || {};
const waypoints = lodashGet(transaction, 'comment.waypoints', {});
const coordinates = lodashGet(route, 'geometry.coordinates', []);
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const getWaypointMarkers = useCallback(
(waypointsData) => {
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index 7059e0161001..a5a8985e3978 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -54,8 +54,8 @@ const defaultProps = {
};
function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, description, isAnonymousAction, isFocused, innerRef}) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const {windowWidth} = useWindowDimensions();
const [isThrottledButtonActive, setThrottledButtonInactive] = useThrottledButtonState();
diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js
index 82204e095f4a..eaa6a8b45b33 100644
--- a/src/components/DatePicker/CalendarPicker/index.js
+++ b/src/components/DatePicker/CalendarPicker/index.js
@@ -1,4 +1,3 @@
-import {withTheme} from '@storybook/theming';
import {addMonths, endOfDay, endOfMonth, format, getYear, isSameDay, parseISO, setDate, setYear, startOfDay, startOfMonth, subMonths} from 'date-fns';
import Str from 'expensify-common/lib/str';
import PropTypes from 'prop-types';
@@ -9,7 +8,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import {withThemePropTypes} from '@components/withTheme';
+import withTheme, {withThemePropTypes} from '@components/withTheme';
import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles';
import compose from '@libs/compose';
import DateUtils from '@libs/DateUtils';
@@ -265,4 +264,4 @@ class CalendarPicker extends React.PureComponent {
CalendarPicker.propTypes = propTypes;
CalendarPicker.defaultProps = defaultProps;
-export default compose(withLocalize, withThemeStyles, withTheme)(CalendarPicker);
+export default compose(withLocalize, withTheme, withThemeStyles)(CalendarPicker);
diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js
index f61c4624db32..2926d6346b1b 100644
--- a/src/components/EmojiPicker/EmojiPickerButton.js
+++ b/src/components/EmojiPicker/EmojiPickerButton.js
@@ -31,8 +31,8 @@ const defaultProps = {
};
function EmojiPickerButton(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const emojiPopoverAnchor = useRef(null);
useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []);
diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js
index fe659612c846..6fd24adf04aa 100644
--- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js
+++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js
@@ -26,8 +26,8 @@ const defaultProps = {
};
function EmojiPickerButtonDropdown(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const emojiPopoverAnchor = useRef(null);
useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []);
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index 5a2211b70ba1..2c77b393c2b9 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -17,6 +17,7 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
import compose from '@libs/compose';
import * as EmojiUtils from '@libs/EmojiUtils';
import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
+import * as ReportUtils from '@libs/ReportUtils';
import * as StyleUtils from '@styles/StyleUtils';
import useThemeStyles from '@styles/useThemeStyles';
import * as User from '@userActions/User';
@@ -307,13 +308,17 @@ function EmojiPickerMenu(props) {
}
const emoji = lodashGet(item, ['types', preferredSkinTone], item.code);
onEmojiSelected(emoji, item);
+ // On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer.
+ keyBoardEvent.preventDefault();
+ // On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again.
+ keyBoardEvent.stopPropagation();
return;
}
// Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input
// is not focused, so that the navigation and tab cycling can be done using the keyboard without
// interfering with the input behaviour.
- if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) {
+ if (!ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) {
setIsUsingKeyboardMovement(true);
return;
}
diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx
index f141624699ed..6917d3dec185 100644
--- a/src/components/EmojiSuggestions.tsx
+++ b/src/components/EmojiSuggestions.tsx
@@ -43,8 +43,8 @@ type EmojiSuggestionsProps = {
const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`;
function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
/**
* Render an emoji suggestion menu item component.
*/
diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx
index 475de82fac35..4b32e1b8ce81 100644
--- a/src/components/FixedFooter.tsx
+++ b/src/components/FixedFooter.tsx
@@ -10,7 +10,7 @@ type FixedFooterProps = {
style?: StyleProp;
};
-function FixedFooter({style = [], children}: FixedFooterProps) {
+function FixedFooter({style, children}: FixedFooterProps) {
const styles = useThemeStyles();
return {children};
}
diff --git a/src/components/FocusModeNotification.js b/src/components/FocusModeNotification.js
new file mode 100644
index 000000000000..37d8e4848b98
--- /dev/null
+++ b/src/components/FocusModeNotification.js
@@ -0,0 +1,47 @@
+import React, {useEffect} from 'react';
+import useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
+import styles from '@styles/styles';
+import * as Link from '@userActions/Link';
+import * as User from '@userActions/User';
+import CONST from '@src/CONST';
+import ConfirmModal from './ConfirmModal';
+import Text from './Text';
+import TextLinkWithRef from './TextLink';
+
+function FocusModeNotification() {
+ const {environmentURL} = useEnvironment();
+ const {translate} = useLocalize();
+ useEffect(() => {
+ User.updateChatPriorityMode(CONST.PRIORITY_MODE.GSD, true);
+ }, []);
+ const href = `${environmentURL}/settings/preferences/priority-mode`;
+ return (
+
+ {translate('focusModeUpdateModal.prompt')}
+ {
+ User.clearFocusModeNotification();
+ Link.openLink(href, environmentURL);
+ }}
+ >
+ {translate('common.here')}
+
+ .
+
+ }
+ isVisible
+ />
+ );
+}
+
+FocusModeNotification.displayName = 'FocusModeNotification';
+export default FocusModeNotification;
diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.js
index 86850a8af96d..faf1ec9cfa16 100644
--- a/src/components/GrowlNotification/index.js
+++ b/src/components/GrowlNotification/index.js
@@ -21,8 +21,8 @@ function GrowlNotification(_, ref) {
const [bodyText, setBodyText] = useState('');
const [type, setType] = useState('success');
const [duration, setDuration] = useState();
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const types = {
[CONST.GROWL.SUCCESS]: {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
index 63973ea43e19..8f1406439be9 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
@@ -6,15 +6,10 @@ import AnchorForCommentsOnly from '@components/AnchorForCommentsOnly';
import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils';
import Text from '@components/Text';
import useEnvironment from '@hooks/useEnvironment';
-import Navigation from '@libs/Navigation/Navigation';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
-import * as Url from '@libs/Url';
import useThemeStyles from '@styles/useThemeStyles';
import * as Link from '@userActions/Link';
-import * as Session from '@userActions/Session';
-import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
import htmlRendererPropTypes from './htmlRendererPropTypes';
function AnchorRenderer(props) {
@@ -26,50 +21,8 @@ function AnchorRenderer(props) {
const displayName = lodashGet(props.tnode, 'domNode.children[0].data', '');
const parentStyle = lodashGet(props.tnode, 'parent.styles.nativeTextRet', {});
const attrHref = htmlAttribs.href || '';
- const attrPath = Url.getPathFromURL(attrHref);
- const hasSameOrigin = Url.hasSameExpensifyOrigin(attrHref, environmentURL);
- const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(attrHref, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(attrHref, CONFIG.EXPENSIFY.STAGING_API_ROOT);
- const internalNewExpensifyPath =
- (Url.hasSameExpensifyOrigin(attrHref, CONST.NEW_EXPENSIFY_URL) ||
- Url.hasSameExpensifyOrigin(attrHref, CONST.STAGING_NEW_EXPENSIFY_URL) ||
- attrHref.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) &&
- !CONST.PATHS_TO_TREAT_AS_EXTERNAL.includes(attrPath)
- ? attrPath
- : '';
- const internalExpensifyPath =
- hasExpensifyOrigin && !attrPath.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL_PATHNAME) && !attrPath.startsWith(CONFIG.EXPENSIFY.DEVPORTAL_URL_PATHNAME) && attrPath;
- const navigateToLink = () => {
- // There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
- // https://www.expensify.com.dev/newdotreport?reportID=3429600449838908 and they have a target="_blank" attribute. This is so that when a user is on OldDot,
- // clicking on the link will open the chat in NewDot. However, when a user is in NewDot and clicks on the concierge link, the link needs to be handled differently.
- // Normally, the link would be sent to Link.openOldDotLink() and opened in a new tab, and that's jarring to the user. Since the intention is to link to a specific NewDot chat,
- // the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab.
- if (hasExpensifyOrigin && attrHref.indexOf('newdotreport?reportID=') > -1) {
- const reportID = attrHref.split('newdotreport?reportID=').pop();
- const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID);
- Navigation.navigate(reportRoute);
- return;
- }
-
- // If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
- // instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
- if (internalNewExpensifyPath && hasSameOrigin) {
- if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(internalNewExpensifyPath)) {
- Session.signOutAndRedirectToSignIn();
- return;
- }
- Navigation.navigate(internalNewExpensifyPath);
- return;
- }
-
- // If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in.
- // As attachments also use expensify.com we don't want it working the same as links.
- if (internalExpensifyPath && !isAttachment) {
- Link.openOldDotLink(internalExpensifyPath);
- return;
- }
- Link.openExternalLink(attrHref);
- };
+ const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref);
+ const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref);
if (!HTMLEngineUtils.isChildOfComment(props.tnode)) {
// This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click.
@@ -78,7 +31,7 @@ function AnchorRenderer(props) {
return (
Link.openLink(attrHref, environmentURL, isAttachment)}
suppressHighlighting
>
@@ -109,7 +62,7 @@ function AnchorRenderer(props) {
key={props.key}
displayName={displayName}
// Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling
- onPress={internalNewExpensifyPath || internalExpensifyPath ? navigateToLink : undefined}
+ onPress={internalNewExpensifyPath || internalExpensifyPath ? Link.openLink : undefined}
>
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js
index fc53c8c51611..0027a557ab02 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js
@@ -28,8 +28,8 @@ const propTypes = {
};
function MentionUserRenderer(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const {translate} = useLocalize();
const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']);
const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid');
diff --git a/src/components/HeaderPageLayout.js b/src/components/HeaderPageLayout.js
index 260f98d208eb..35b03f925831 100644
--- a/src/components/HeaderPageLayout.js
+++ b/src/components/HeaderPageLayout.js
@@ -57,12 +57,12 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty
const {isOffline} = useNetwork();
const appBGColor = StyleUtils.getBackgroundColorStyle(theme.appBG);
const {titleColor, iconFill} = useMemo(() => {
- const isColorfulBackground = (backgroundColor || theme.appBG) !== theme.appBG;
+ const isColorfulBackground = (backgroundColor || theme.appBG) !== theme.appBG && (backgroundColor || theme.highlightBG) !== theme.highlightBG;
return {
titleColor: isColorfulBackground ? theme.textColorfulBackground : undefined,
iconFill: isColorfulBackground ? theme.iconColorfulBackground : undefined,
};
- }, [backgroundColor, theme.appBG, theme.iconColorfulBackground, theme.textColorfulBackground]);
+ }, [backgroundColor, theme.appBG, theme.highlightBG, theme.iconColorfulBackground, theme.textColorfulBackground]);
return (
func,
shouldNavigateToTopMostReport = false,
}) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState();
const {translate} = useLocalize();
const {isKeyboardShown} = useKeyboardState();
diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts
index ece9a865629b..5e4c0192ca86 100644
--- a/src/components/Icon/BankIcons.ts
+++ b/src/components/Icon/BankIcons.ts
@@ -1,13 +1,13 @@
import {SvgProps} from 'react-native-svg';
import GenericBank from '@assets/images/bankicons/generic-bank-account.svg';
import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg';
-import {ThemeStyles} from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import {BankIcon, BankName, BankNameKey} from '@src/types/onyx/Bank';
type BankIconParams = {
- themeStyles: ThemeStyles;
+ styles: ThemeStyles;
bankName?: BankName;
isCard?: boolean;
};
@@ -115,7 +115,8 @@ function getBankNameKey(bankName: string): BankNameKey {
/**
* Returns Bank Icon Object that matches to existing bank icons or default icons
*/
-export default function getBankIcon({themeStyles, bankName, isCard = false}: BankIconParams): BankIcon {
+
+export default function getBankIcon({styles, bankName, isCard = false}: BankIconParams): BankIcon {
const bankIcon: BankIcon = {
icon: isCard ? GenericBankCard : GenericBank,
};
@@ -130,11 +131,11 @@ export default function getBankIcon({themeStyles, bankName, isCard = false}: Ban
// For default Credit Card icon the icon size should not be set.
if (!isCard) {
bankIcon.iconSize = variables.iconSizeExtraLarge;
- bankIcon.iconStyles = [themeStyles.bankIconContainer];
+ bankIcon.iconStyles = [styles.bankIconContainer];
} else {
bankIcon.iconHeight = variables.bankCardHeight;
bankIcon.iconWidth = variables.bankCardWidth;
- bankIcon.iconStyles = [themeStyles.assignedCardsIconContainer];
+ bankIcon.iconStyles = [styles.assignedCardsIconContainer];
}
return bankIcon;
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index 98449c838b67..25bd0a083be0 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -1,7 +1,7 @@
import React, {PureComponent} from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
import withTheme, {ThemeProps} from '@components/withTheme';
-import withThemeStyles, {ThemeStylesProps} from '@components/withThemeStyles';
+import withThemeStyles, {type ThemeStylesProps} from '@components/withThemeStyles';
import * as StyleUtils from '@styles/StyleUtils';
import variables from '@styles/variables';
import IconWrapperStyles from './IconWrapperStyles';
diff --git a/src/components/Image/BaseImage.js b/src/components/Image/BaseImage.js
new file mode 100644
index 000000000000..cd2326900c6c
--- /dev/null
+++ b/src/components/Image/BaseImage.js
@@ -0,0 +1,29 @@
+import React, {useCallback} from 'react';
+import {Image as RNImage} from 'react-native';
+import {defaultProps, imagePropTypes} from './imagePropTypes';
+
+function BaseImage({onLoad, ...props}) {
+ const imageLoadedSuccessfully = useCallback(
+ ({nativeEvent}) => {
+ // We override `onLoad`, so both web and native have the same signature
+ const {width, height} = nativeEvent.source;
+ onLoad({nativeEvent: {width, height}});
+ },
+ [onLoad],
+ );
+
+ return (
+
+ );
+}
+
+BaseImage.propTypes = imagePropTypes;
+BaseImage.defaultProps = defaultProps;
+BaseImage.displayName = 'BaseImage';
+
+export default BaseImage;
diff --git a/src/components/Image/BaseImage.native.js b/src/components/Image/BaseImage.native.js
new file mode 100644
index 000000000000..a621947267a1
--- /dev/null
+++ b/src/components/Image/BaseImage.native.js
@@ -0,0 +1,3 @@
+import RNFastImage from 'react-native-fast-image';
+
+export default RNFastImage;
diff --git a/src/components/Image/index.js b/src/components/Image/index.js
index ef1a69e19c12..8cee1cf95e14 100644
--- a/src/components/Image/index.js
+++ b/src/components/Image/index.js
@@ -1,51 +1,35 @@
import lodashGet from 'lodash/get';
-import React, {useEffect, useMemo} from 'react';
-import {Image as RNImage} from 'react-native';
+import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import BaseImage from './BaseImage';
import {defaultProps, imagePropTypes} from './imagePropTypes';
import RESIZE_MODES from './resizeModes';
-function Image(props) {
- const {source: propsSource, isAuthTokenRequired, onLoad, session} = props;
- /**
- * Check if the image source is a URL - if so the `encryptedAuthToken` is appended
- * to the source.
- */
+function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) {
+ // Update the source to include the auth token if required
const source = useMemo(() => {
- if (isAuthTokenRequired) {
- // There is currently a `react-native-web` bug preventing the authToken being passed
- // in the headers of the image request so the authToken is added as a query param.
- // On native the authToken IS passed in the image request headers
- const authToken = lodashGet(session, 'encryptedAuthToken', null);
- return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`};
+ if (typeof lodashGet(propsSource, 'uri') === 'number') {
+ return propsSource.uri;
}
+ if (typeof propsSource !== 'number' && isAuthTokenRequired) {
+ const authToken = lodashGet(session, 'encryptedAuthToken');
+ return {
+ ...propsSource,
+ headers: {
+ [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken,
+ },
+ };
+ }
+
return propsSource;
// The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [propsSource, isAuthTokenRequired]);
- /**
- * The natural image dimensions are retrieved using the updated source
- * and as a result the `onLoad` event needs to be manually invoked to return these dimensions
- */
- useEffect(() => {
- // If an onLoad callback was specified then manually call it and pass
- // the natural image dimensions to match the native API
- if (onLoad == null) {
- return;
- }
- RNImage.getSize(source.uri, (width, height) => {
- onLoad({nativeEvent: {width, height}});
- });
- }, [onLoad, source]);
-
- // Omit the props which the underlying RNImage won't use
- const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']);
-
return (
- {
- const {width, height} = evt.nativeEvent;
- dimensionsCache.set(source.uri, {width, height});
- if (props.onLoad) {
- props.onLoad(evt);
- }
- }}
- />
- );
-}
-
-Image.propTypes = imagePropTypes;
-Image.defaultProps = defaultProps;
-Image.displayName = 'Image';
-const ImageWithOnyx = withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
-})(Image);
-ImageWithOnyx.resizeMode = RESIZE_MODES;
-ImageWithOnyx.resolveDimensions = resolveDimensions;
-
-export default ImageWithOnyx;
diff --git a/src/components/KYCWall/BaseKYCWall.js b/src/components/KYCWall/BaseKYCWall.js
index 9ba342ddda90..e229a860dd9f 100644
--- a/src/components/KYCWall/BaseKYCWall.js
+++ b/src/components/KYCWall/BaseKYCWall.js
@@ -1,5 +1,5 @@
import lodashGet from 'lodash/get';
-import React from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Dimensions} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -17,99 +17,121 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {defaultProps, propTypes} from './kycWallPropTypes';
+// This sets the Horizontal anchor position offset for POPOVER MENU.
+const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20;
+
// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow
// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it
// to render the AddPaymentMethodMenu in the correct location.
-class KYCWall extends React.Component {
- constructor(props) {
- super(props);
-
- this.continue = this.continue.bind(this);
- this.setMenuPosition = this.setMenuPosition.bind(this);
- this.selectPaymentMethod = this.selectPaymentMethod.bind(this);
- this.anchorRef = React.createRef(null);
-
- this.state = {
- shouldShowAddPaymentMenu: false,
- anchorPositionVertical: 0,
- anchorPositionHorizontal: 0,
- transferBalanceButton: null,
- };
- }
-
- componentDidMount() {
- PaymentMethods.kycWallRef.current = this;
- if (this.props.shouldListenForResize) {
- this.dimensionsSubscription = Dimensions.addEventListener('change', this.setMenuPosition);
- }
- }
-
- componentWillUnmount() {
- if (this.props.shouldListenForResize && this.dimensionsSubscription) {
- this.dimensionsSubscription.remove();
- }
- PaymentMethods.kycWallRef.current = null;
- }
-
- setMenuPosition() {
- if (!this.state.transferBalanceButton) {
- return;
- }
- const buttonPosition = getClickedTargetLocation(this.state.transferBalanceButton);
- const position = this.getAnchorPosition(buttonPosition);
- this.setPositionAddPaymentMenu(position);
- }
+function KYCWall({
+ addBankAccountRoute,
+ addDebitCardRoute,
+ anchorAlignment,
+ bankAccountList,
+ chatReportID,
+ children,
+ enablePaymentsRoute,
+ fundList,
+ iouReport,
+ onSelectPaymentMethod,
+ onSuccessfulKYC,
+ reimbursementAccount,
+ shouldIncludeDebitCard,
+ shouldListenForResize,
+ source,
+ userWallet,
+ walletTerms,
+}) {
+ const anchorRef = useRef(null);
+ const transferBalanceButtonRef = useRef(null);
+
+ const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
+ const [anchorPosition, setAnchorPosition] = useState({
+ anchorPositionVertical: 0,
+ anchorPositionHorizontal: 0,
+ });
/**
* @param {DOMRect} domRect
* @returns {Object}
*/
- getAnchorPosition(domRect) {
- if (this.props.anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
+ const getAnchorPosition = useCallback(
+ (domRect) => {
+ if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
+ return {
+ anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING,
+ anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET,
+ };
+ }
+
return {
- anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING,
- anchorPositionHorizontal: domRect.left + 20,
+ anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING,
+ anchorPositionHorizontal: domRect.left,
};
- }
-
- return {
- anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING,
- anchorPositionHorizontal: domRect.left,
- };
- }
+ },
+ [anchorAlignment.vertical],
+ );
/**
* Set position of the transfer payment menu
*
* @param {Object} position
*/
- setPositionAddPaymentMenu(position) {
- this.setState({
- anchorPositionVertical: position.anchorPositionVertical,
- anchorPositionHorizontal: position.anchorPositionHorizontal,
+ const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}) => {
+ setAnchorPosition({
+ anchorPositionVertical,
+ anchorPositionHorizontal,
});
- }
+ };
+
+ const setMenuPosition = useCallback(() => {
+ if (!transferBalanceButtonRef.current) {
+ return;
+ }
+ const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
+ const position = getAnchorPosition(buttonPosition);
+
+ setPositionAddPaymentMenu(position);
+ }, [getAnchorPosition]);
+
+ useEffect(() => {
+ let dimensionsSubscription = null;
+
+ PaymentMethods.kycWallRef.current = this;
+
+ if (shouldListenForResize) {
+ dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition);
+ }
+
+ return () => {
+ if (shouldListenForResize && dimensionsSubscription) {
+ dimensionsSubscription.remove();
+ }
+
+ PaymentMethods.kycWallRef.current = null;
+ };
+ }, [chatReportID, setMenuPosition, shouldListenForResize]);
/**
* @param {String} paymentMethod
*/
- selectPaymentMethod(paymentMethod) {
- this.props.onSelectPaymentMethod(paymentMethod);
+ const selectPaymentMethod = (paymentMethod) => {
+ onSelectPaymentMethod(paymentMethod);
if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
- Navigation.navigate(this.props.addBankAccountRoute);
+ Navigation.navigate(addBankAccountRoute);
} else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
- Navigation.navigate(this.props.addDebitCardRoute);
+ Navigation.navigate(addDebitCardRoute);
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
- if (ReportUtils.isIOUReport(this.props.iouReport)) {
- const policyID = Policy.createWorkspaceFromIOUPayment(this.props.iouReport);
+ if (ReportUtils.isIOUReport(iouReport)) {
+ const policyID = Policy.createWorkspaceFromIOUPayment(iouReport);
// Navigate to the bank account set up flow for this specific policy
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID));
return;
}
- Navigation.navigate(this.props.addBankAccountRoute);
+ Navigation.navigate(addBankAccountRoute);
}
- }
+ };
/**
* Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method.
@@ -119,83 +141,86 @@ class KYCWall extends React.Component {
* @param {Event} event
* @param {String} iouPaymentType
*/
- continue(event, iouPaymentType) {
- const currentSource = lodashGet(this.props.walletTerms, 'source', this.props.source);
+ const continueAction = (event, iouPaymentType) => {
+ const currentSource = lodashGet(walletTerms, 'source', source);
/**
* Set the source, so we can tailor the process according to how we got here.
* We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold.
*/
- Wallet.setKYCWallSource(this.props.source, this.props.chatReportID);
+ Wallet.setKYCWallSource(source, chatReportID);
+
+ if (shouldShowAddPaymentMenu) {
+ setShouldShowAddPaymentMenu(false);
- if (this.state.shouldShowAddPaymentMenu) {
- this.setState({shouldShowAddPaymentMenu: false});
return;
}
// Use event target as fallback if anchorRef is null for safety
- const targetElement = this.anchorRef.current || event.nativeEvent.target;
- this.setState({transferBalanceButton: targetElement});
- const isExpenseReport = ReportUtils.isExpenseReport(this.props.iouReport);
- const paymentCardList = this.props.fundList || {};
+ const targetElement = anchorRef.current || event.nativeEvent.target;
+
+ transferBalanceButtonRef.current = targetElement;
+ const isExpenseReport = ReportUtils.isExpenseReport(iouReport);
+ const paymentCardList = fundList || {};
// Check to see if user has a valid payment method on file and display the add payment popover if they don't
if (
- (isExpenseReport && lodashGet(this.props.reimbursementAccount, 'achData.state', '') !== CONST.BANK_ACCOUNT.STATE.OPEN) ||
- (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, this.props.bankAccountList, this.props.shouldIncludeDebitCard))
+ (isExpenseReport && lodashGet(reimbursementAccount, 'achData.state', '') !== CONST.BANK_ACCOUNT.STATE.OPEN) ||
+ (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard))
) {
Log.info('[KYC Wallet] User does not have valid payment method');
- if (!this.props.shouldIncludeDebitCard) {
- this.selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT);
+ if (!shouldIncludeDebitCard) {
+ selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT);
return;
}
+
const clickedElementLocation = getClickedTargetLocation(targetElement);
- const position = this.getAnchorPosition(clickedElementLocation);
- this.setPositionAddPaymentMenu(position);
- this.setState({
- shouldShowAddPaymentMenu: true,
- });
+ const position = getAnchorPosition(clickedElementLocation);
+
+ setPositionAddPaymentMenu(position);
+ setShouldShowAddPaymentMenu(true);
+
return;
}
+
if (!isExpenseReport) {
// Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks
- const hasActivatedWallet = this.props.userWallet.tierName && _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], this.props.userWallet.tierName);
+ const hasActivatedWallet = userWallet.tierName && _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName);
if (!hasActivatedWallet) {
Log.info('[KYC Wallet] User does not have active wallet');
- Navigation.navigate(this.props.enablePaymentsRoute);
+ Navigation.navigate(enablePaymentsRoute);
return;
}
}
Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');
- this.props.onSuccessfulKYC(iouPaymentType, currentSource);
- }
-
- render() {
- return (
- <>
- this.setState({shouldShowAddPaymentMenu: false})}
- anchorRef={this.anchorRef}
- anchorPosition={{
- vertical: this.state.anchorPositionVertical,
- horizontal: this.state.anchorPositionHorizontal,
- }}
- anchorAlignment={this.props.anchorAlignment}
- onItemSelected={(item) => {
- this.setState({shouldShowAddPaymentMenu: false});
- this.selectPaymentMethod(item);
- }}
- />
- {this.props.children(this.continue, this.anchorRef)}
- >
- );
- }
+ onSuccessfulKYC(iouPaymentType, currentSource);
+ };
+
+ return (
+ <>
+ setShouldShowAddPaymentMenu(false)}
+ anchorRef={anchorRef}
+ anchorPosition={{
+ vertical: anchorPosition.anchorPositionVertical,
+ horizontal: anchorPosition.anchorPositionHorizontal,
+ }}
+ anchorAlignment={anchorAlignment}
+ onItemSelected={(item) => {
+ setShouldShowAddPaymentMenu(false);
+ selectPaymentMethod(item);
+ }}
+ />
+ {children(continueAction, anchorRef)}
+ >
+ );
}
KYCWall.propTypes = propTypes;
KYCWall.defaultProps = defaultProps;
+KYCWall.displayName = 'BaseKYCWall';
export default withOnyx({
userWallet: {
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index 3b2de574ba17..1765c85cdd48 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -201,7 +201,7 @@ function OptionRowLHN(props) {
styles.alignItemsCenter,
styles.justifyContentBetween,
styles.sidebarLink,
- styles.sidebarLinkInner,
+ styles.sidebarLinkInnerLHN,
StyleUtils.getBackgroundColorStyle(theme.sidebar),
props.isFocused ? styles.sidebarLinkActive : null,
(hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle || styles.sidebarLinkHover : null,
diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js
deleted file mode 100644
index 83723541b16f..000000000000
--- a/src/components/LocalePicker.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import compose from '@libs/compose';
-import useTheme from '@styles/themes/useTheme';
-import useThemeStyles from '@styles/useThemeStyles';
-import * as App from '@userActions/App';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import Picker from './Picker';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-
-const propTypes = {
- /** Indicates which locale the user currently has selected */
- preferredLocale: PropTypes.string,
-
- /** Indicates size of a picker component and whether to render the label or not */
- size: PropTypes.oneOf(['normal', 'small']),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- preferredLocale: CONST.LOCALES.DEFAULT,
- size: 'normal',
-};
-
-function LocalePicker(props) {
- const theme = useTheme();
- const styles = useThemeStyles();
- const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({
- value: language,
- label: props.translate(`languagePage.languages.${language}.label`),
- keyForList: language,
- isSelected: props.preferredLocale === language,
- }));
- return (
- {
- if (locale === props.preferredLocale) {
- return;
- }
-
- App.setLocale(locale);
- }}
- items={localesToLanguages}
- size={props.size}
- value={props.preferredLocale}
- containerStyles={props.size === 'small' ? [styles.pickerContainerSmall] : []}
- backgroundColor={theme.signInPage}
- />
- );
-}
-
-LocalePicker.defaultProps = defaultProps;
-LocalePicker.propTypes = propTypes;
-LocalePicker.displayName = 'LocalePicker';
-
-export default compose(
- withLocalize,
- withOnyx({
- preferredLocale: {
- key: ONYXKEYS.NVP_PREFERRED_LOCALE,
- },
- }),
-)(LocalePicker);
diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx
new file mode 100644
index 000000000000..c04b0131744f
--- /dev/null
+++ b/src/components/LocalePicker.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import {OnyxEntry, withOnyx} from 'react-native-onyx';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
+import * as App from '@userActions/App';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Locale} from '@src/types/onyx';
+import Picker from './Picker';
+import type {PickerSize} from './Picker/types';
+
+type LocalePickerOnyxProps = {
+ /** Indicates which locale the user currently has selected */
+ preferredLocale: OnyxEntry;
+};
+
+type LocalePickerProps = LocalePickerOnyxProps & {
+ /** Indicates size of a picker component and whether to render the label or not */
+ size?: PickerSize;
+};
+
+function LocalePicker({preferredLocale = CONST.LOCALES.DEFAULT, size = 'normal'}: LocalePickerProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const localesToLanguages = CONST.LANGUAGES.map((language) => ({
+ value: language,
+ label: translate(`languagePage.languages.${language}.label`),
+ keyForList: language,
+ isSelected: preferredLocale === language,
+ }));
+
+ return (
+ {
+ if (locale === preferredLocale) {
+ return;
+ }
+
+ App.setLocale(locale);
+ }}
+ items={localesToLanguages}
+ size={size}
+ value={preferredLocale}
+ containerStyles={size === 'small' ? styles.pickerContainerSmall : {}}
+ backgroundColor={theme.signInPage}
+ />
+ );
+}
+
+LocalePicker.displayName = 'LocalePicker';
+
+export default withOnyx({
+ preferredLocale: {
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ },
+})(LocalePicker);
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index 653ce910da9f..f5c498b24893 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -1,6 +1,7 @@
import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
import ReactNativeModal from 'react-native-modal';
+import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import usePrevious from '@hooks/usePrevious';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -133,14 +134,14 @@ function BaseModal(
} = useMemo(
() =>
getModalStyles(
+ theme,
+ styles,
type,
{
windowWidth,
windowHeight,
isSmallScreenWidth,
},
- theme,
- styles,
popoverAnchorPosition,
innerContainerStyle,
outerStyle,
@@ -207,7 +208,7 @@ function BaseModal(
style={[styles.defaultModalContainer, modalContainerStyle, modalPaddingStyles, !isVisible && styles.pointerEventsNone]}
ref={ref}
>
- {children}
+ {children}
);
diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx
index 179b9bc0f5a1..efff279324ac 100644
--- a/src/components/MultipleAvatars.tsx
+++ b/src/components/MultipleAvatars.tsx
@@ -193,11 +193,11 @@ function MultipleAvatars({
(this.list = el)}
- optionHoveredStyle={this.props.optionHoveredStyle || this.props.themeStyles.hoveredComponentBG}
+ optionHoveredStyle={optionHoveredStyle}
onSelectRow={this.props.onSelectRow ? this.selectRow : undefined}
sections={this.state.sections}
focusedIndex={this.state.focusedIndex}
diff --git a/src/components/Picker/BasePicker.js b/src/components/Picker/BasePicker.js
deleted file mode 100644
index 084cabc02234..000000000000
--- a/src/components/Picker/BasePicker.js
+++ /dev/null
@@ -1,311 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
-import {View} from 'react-native';
-import RNPickerSelect from 'react-native-picker-select';
-import _ from 'underscore';
-import FormHelpMessage from '@components/FormHelpMessage';
-import Icon from '@components/Icon';
-import * as Expensicons from '@components/Icon/Expensicons';
-import refPropTypes from '@components/refPropTypes';
-import {ScrollContext} from '@components/ScrollViewWithContext';
-import Text from '@components/Text';
-import useTheme from '@styles/themes/useTheme';
-import useThemeStyles from '@styles/useThemeStyles';
-
-const propTypes = {
- /** A forwarded ref */
- forwardedRef: refPropTypes,
-
- /** BasePicker label */
- label: PropTypes.string,
-
- /** Should the picker appear disabled? */
- isDisabled: PropTypes.bool,
-
- /** Input value */
- value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- /** The items to display in the list of selections */
- items: PropTypes.arrayOf(
- PropTypes.shape({
- /** The value of the item that is being selected */
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
-
- /** The text to display for the item */
- label: PropTypes.string.isRequired,
- }),
- ).isRequired,
-
- /** Something to show as the placeholder before something is selected */
- placeholder: PropTypes.shape({
- /** The value of the placeholder item, usually an empty string */
- value: PropTypes.string,
-
- /** The text to be displayed as the placeholder */
- label: PropTypes.string,
- }),
-
- /** Error text to display */
- errorText: PropTypes.string,
-
- /** Customize the BasePicker container */
- // eslint-disable-next-line react/forbid-prop-types
- containerStyles: PropTypes.arrayOf(PropTypes.object),
-
- /** Customize the BasePicker background color */
- backgroundColor: PropTypes.string,
-
- /** The ID used to uniquely identify the input in a Form */
- inputID: PropTypes.string,
-
- /** Saves a draft of the input value when used in a form */
- // eslint-disable-next-line react/no-unused-prop-types
- shouldSaveDraft: PropTypes.bool,
-
- /** A callback method that is called when the value changes and it receives the selected value as an argument */
- onInputChange: PropTypes.func.isRequired,
-
- /** Size of a picker component */
- size: PropTypes.oneOf(['normal', 'small']),
-
- /** An icon to display with the picker */
- icon: PropTypes.func,
-
- /** Whether we should forward the focus/blur calls to the inner picker * */
- shouldFocusPicker: PropTypes.bool,
-
- /** Callback called when click or tap out of BasePicker */
- onBlur: PropTypes.func,
-
- /** Additional events passed to the core BasePicker for specific platforms such as web */
- additionalPickerEvents: PropTypes.func,
-
- /** Hint text that appears below the picker */
- hintText: PropTypes.string,
-};
-
-const defaultProps = {
- forwardedRef: undefined,
- label: '',
- isDisabled: false,
- errorText: '',
- hintText: '',
- containerStyles: [],
- backgroundColor: undefined,
- inputID: undefined,
- shouldSaveDraft: false,
- value: undefined,
- placeholder: {},
- size: 'normal',
- icon: undefined,
- shouldFocusPicker: false,
- onBlur: () => {},
- additionalPickerEvents: () => {},
-};
-
-function BasePicker(props) {
- const styles = useThemeStyles();
- const theme = useTheme();
-
- const [isHighlighted, setIsHighlighted] = useState(false);
-
- // reference to the root View
- const root = useRef(null);
-
- // reference to @react-native-picker/picker
- const picker = useRef(null);
-
- // Windows will reuse the text color of the select for each one of the options
- // so we might need to color accordingly so it doesn't blend with the background.
- const placeholder = _.isEmpty(props.placeholder)
- ? {}
- : {
- ...props.placeholder,
- color: theme.pickerOptionsTextColor,
- };
-
- useEffect(() => {
- if (props.value || !props.items || props.items.length !== 1 || !props.onInputChange) {
- return;
- }
-
- // When there is only 1 element in the selector, we do the user a favor and automatically select it for them
- // so they don't have to spend extra time selecting the only possible value.
- props.onInputChange(props.items[0].value, 0);
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.items]);
-
- const context = useContext(ScrollContext);
-
- /**
- * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onInputChange
- * We are overriding this behavior to make BasePicker work with Form
- * @param {String} value
- * @param {Number} index
- */
- const onInputChange = (value, index) => {
- if (props.inputID) {
- props.onInputChange(value);
- return;
- }
-
- props.onInputChange(value, index);
- };
-
- const enableHighlight = () => {
- setIsHighlighted(true);
- };
-
- const disableHighlight = () => {
- setIsHighlighted(false);
- };
-
- const {icon, size} = props;
-
- const iconToRender = useMemo(() => {
- if (icon) {
- return () => icon(size);
- }
-
- // eslint-disable-next-line react/display-name
- return () => (
-
- );
- }, [icon, size, styles]);
-
- useImperativeHandle(props.forwardedRef, () => ({
- /**
- * Focuses the picker (if configured to do so)
- *
- * This method is used by Form
- */
- focus() {
- if (!props.shouldFocusPicker) {
- return;
- }
-
- // Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the
- // same task when we scrolled to it left that element in a glitched state, where the dropdown list can't
- // be opened until the element gets re-focused
- _.defer(() => {
- picker.current.focus();
- });
- },
-
- /**
- * Like measure(), but measures the view relative to an ancestor
- *
- * This method is used by Form when scrolling to the input
- *
- * @param {Object} relativeToNativeComponentRef - reference to an ancestor
- * @param {function(x: number, y: number, width: number, height: number): void} onSuccess - callback called on success
- * @param {function(): void} onFail - callback called on failure
- */
- measureLayout(relativeToNativeComponentRef, onSuccess, onFail) {
- if (!root.current) {
- return;
- }
-
- root.current.measureLayout(relativeToNativeComponentRef, onSuccess, onFail);
- },
- }));
-
- const hasError = !_.isEmpty(props.errorText);
-
- if (props.isDisabled) {
- return (
-
- {Boolean(props.label) && (
-
- {props.label}
-
- )}
- {props.value}
- {Boolean(props.hintText) && {props.hintText}}
-
- );
- }
-
- return (
- <>
-
- {props.label && {props.label}}
- ({...item, color: theme.pickerOptionsTextColor}))}
- style={size === 'normal' ? styles.picker(props.isDisabled, props.backgroundColor) : styles.pickerSmall(props.backgroundColor)}
- useNativeAndroidPickerStyle={false}
- placeholder={placeholder}
- value={props.value}
- Icon={iconToRender}
- disabled={props.isDisabled}
- fixAndroidTouchableBug
- onOpen={enableHighlight}
- onClose={disableHighlight}
- textInputProps={{
- allowFontScaling: false,
- }}
- pickerProps={{
- ref: picker,
- tabIndex: -1,
- onFocus: enableHighlight,
- onBlur: () => {
- disableHighlight();
- props.onBlur();
- },
- ...props.additionalPickerEvents(enableHighlight, (value, index) => {
- onInputChange(value, index);
- disableHighlight();
- }),
- }}
- scrollViewRef={context && context.scrollViewRef}
- scrollViewContentOffsetY={context && context.contentOffsetY}
- />
-
-
- {Boolean(props.hintText) && {props.hintText}}
- >
- );
-}
-
-BasePicker.propTypes = propTypes;
-BasePicker.defaultProps = defaultProps;
-BasePicker.displayName = 'BasePicker';
-
-const BasePickerWithRef = React.forwardRef((props, ref) => (
-
-));
-
-BasePickerWithRef.displayName = 'BasePickerWithRef';
-
-export default BasePickerWithRef;
diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx
new file mode 100644
index 000000000000..dfb2d6332da5
--- /dev/null
+++ b/src/components/Picker/BasePicker.tsx
@@ -0,0 +1,206 @@
+import lodashDefer from 'lodash/defer';
+import React, {ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import {ScrollView, View} from 'react-native';
+import RNPickerSelect from 'react-native-picker-select';
+import FormHelpMessage from '@components/FormHelpMessage';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import Text from '@components/Text';
+import useScrollContext from '@hooks/useScrollContext';
+import useTheme from '@styles/themes/useTheme';
+import useThemeStyles from '@styles/useThemeStyles';
+import type {BasePickerHandle, BasePickerProps} from './types';
+
+type IconToRender = () => ReactElement;
+
+function BasePicker(
+ {
+ items,
+ backgroundColor,
+ inputID,
+ value,
+ onInputChange,
+ icon,
+ label = '',
+ isDisabled = false,
+ errorText = '',
+ hintText = '',
+ containerStyles,
+ placeholder = {},
+ size = 'normal',
+ shouldFocusPicker = false,
+ onBlur = () => {},
+ additionalPickerEvents = () => {},
+ }: BasePickerProps,
+ ref: ForwardedRef,
+) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+
+ const [isHighlighted, setIsHighlighted] = useState(false);
+
+ // reference to the root View
+ const root = useRef(null);
+
+ // reference to @react-native-picker/picker
+ const picker = useRef(null);
+
+ // Windows will reuse the text color of the select for each one of the options
+ // so we might need to color accordingly so it doesn't blend with the background.
+ const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: theme.pickerOptionsTextColor} : {};
+
+ useEffect(() => {
+ if (!!value || !items || items.length !== 1 || !onInputChange) {
+ return;
+ }
+
+ // When there is only 1 element in the selector, we do the user a favor and automatically select it for them
+ // so they don't have to spend extra time selecting the only possible value.
+ onInputChange(items[0].value, 0);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [items]);
+
+ const context = useScrollContext();
+
+ /**
+ * Forms use inputID to set values. But BasePicker passes an index as the second parameter to onValueChange
+ * We are overriding this behavior to make BasePicker work with Form
+ */
+ const onValueChange = (inputValue: TPickerValue, index: number) => {
+ if (inputID) {
+ onInputChange(inputValue);
+ return;
+ }
+
+ onInputChange(inputValue, index);
+ };
+
+ const enableHighlight = () => {
+ setIsHighlighted(true);
+ };
+
+ const disableHighlight = () => {
+ setIsHighlighted(false);
+ };
+
+ const iconToRender = useMemo((): IconToRender => {
+ if (icon) {
+ return () => icon(size);
+ }
+
+ // eslint-disable-next-line react/display-name
+ return () => (
+
+ );
+ }, [icon, size, styles]);
+
+ useImperativeHandle(ref, () => ({
+ /**
+ * Focuses the picker (if configured to do so)
+ *
+ * This method is used by Form
+ */
+ focus() {
+ if (!shouldFocusPicker) {
+ return;
+ }
+
+ // Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the
+ // same task when we scrolled to it left that element in a glitched state, where the dropdown list can't
+ // be opened until the element gets re-focused
+ lodashDefer(() => {
+ picker.current?.focus();
+ });
+ },
+
+ /**
+ * Like measure(), but measures the view relative to an ancestor
+ *
+ * This method is used by Form when scrolling to the input
+ *
+ * @param relativeToNativeComponentRef - reference to an ancestor
+ * @param onSuccess - callback called on success
+ * @param onFail - callback called on failure
+ */
+ measureLayout(relativeToNativeComponentRef, onSuccess, onFail) {
+ if (!root.current) {
+ return;
+ }
+
+ root.current.measureLayout(relativeToNativeComponentRef, onSuccess, onFail);
+ },
+ }));
+
+ const hasError = !!errorText;
+
+ if (isDisabled) {
+ return (
+
+ {!!label && (
+
+ {label}
+
+ )}
+ {value as ReactNode}
+ {!!hintText && {hintText}}
+
+ );
+ }
+
+ return (
+ <>
+
+ {label && {label}}
+ ({...item, color: theme.pickerOptionsTextColor}))}
+ style={size === 'normal' ? styles.picker(isDisabled, backgroundColor) : styles.pickerSmall(backgroundColor)}
+ useNativeAndroidPickerStyle={false}
+ placeholder={pickerPlaceholder}
+ value={value}
+ Icon={iconToRender}
+ disabled={isDisabled}
+ fixAndroidTouchableBug
+ onOpen={enableHighlight}
+ onClose={disableHighlight}
+ textInputProps={{
+ allowFontScaling: false,
+ }}
+ pickerProps={{
+ ref: picker,
+ tabIndex: -1,
+ onFocus: enableHighlight,
+ onBlur: () => {
+ disableHighlight();
+ onBlur();
+ },
+ ...additionalPickerEvents(enableHighlight, (inputValue, index) => {
+ onValueChange(inputValue, index);
+ disableHighlight();
+ }),
+ }}
+ scrollViewRef={context?.scrollViewRef as RefObject}
+ scrollViewContentOffsetY={context?.contentOffsetY}
+ />
+
+
+ {!!hintText && {hintText}}
+ >
+ );
+}
+
+BasePicker.displayName = 'BasePicker';
+
+export default forwardRef(BasePicker);
diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js
deleted file mode 100644
index 8e49a42e8932..000000000000
--- a/src/components/Picker/index.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import React, {forwardRef} from 'react';
-import BasePicker from './BasePicker';
-
-const additionalPickerEvents = (onMouseDown, onChange) => ({
- onMouseDown,
- onChange: (e) => {
- if (e.target.selectedIndex === undefined) {
- return;
- }
- const index = e.target.selectedIndex;
- const value = e.target.options[index].value;
- onChange(value, index);
- },
-});
-
-const BasePickerWithRef = forwardRef((props, ref) => (
-
-));
-
-BasePickerWithRef.displayName = 'BasePickerWithRef';
-
-export default BasePickerWithRef;
diff --git a/src/components/Picker/index.native.js b/src/components/Picker/index.native.js
deleted file mode 100644
index f441609fd4d0..000000000000
--- a/src/components/Picker/index.native.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React, {forwardRef} from 'react';
-import BasePicker from './BasePicker';
-
-const BasePickerWithRef = forwardRef((props, ref) => (
-
-));
-
-BasePickerWithRef.displayName = 'BasePickerWithRef';
-
-export default BasePickerWithRef;
diff --git a/src/components/Picker/index.native.tsx b/src/components/Picker/index.native.tsx
new file mode 100644
index 000000000000..7373f5a6f280
--- /dev/null
+++ b/src/components/Picker/index.native.tsx
@@ -0,0 +1,16 @@
+import React, {ForwardedRef, forwardRef} from 'react';
+import BasePicker from './BasePicker';
+import {BasePickerHandle, BasePickerProps} from './types';
+
+function Picker(props: BasePickerProps, ref: ForwardedRef) {
+ return (
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...props}
+ key={props.inputID}
+ ref={ref}
+ />
+ );
+}
+
+export default forwardRef(Picker);
diff --git a/src/components/Picker/index.tsx b/src/components/Picker/index.tsx
new file mode 100644
index 000000000000..18184b130bba
--- /dev/null
+++ b/src/components/Picker/index.tsx
@@ -0,0 +1,34 @@
+import React, {ForwardedRef, forwardRef} from 'react';
+import BasePicker from './BasePicker';
+import type {AdditionalPickerEvents, BasePickerHandle, BasePickerProps, OnChange, OnMouseDown} from './types';
+
+function Picker(props: BasePickerProps, ref: ForwardedRef) {
+ const additionalPickerEvents = (onMouseDown: OnMouseDown, onChange: OnChange): AdditionalPickerEvents => ({
+ onMouseDown,
+ onChange: (e) => {
+ if (e.target.selectedIndex === undefined) {
+ return;
+ }
+ const index = e.target.selectedIndex;
+ const value = e.target.options[index].value;
+ onChange(value as TPickerValue, index);
+ },
+ });
+
+ return (
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...props}
+ // Forward the ref to Picker, as we implement imperative methods there
+ ref={ref}
+ // On the Web, focusing the inner picker improves the accessibility,
+ // but doesn't open the picker (which we don't want), like it does on
+ // Native.
+ shouldFocusPicker
+ key={props.inputID}
+ additionalPickerEvents={additionalPickerEvents}
+ />
+ );
+}
+
+export default forwardRef(Picker);
diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts
new file mode 100644
index 000000000000..58eed0371893
--- /dev/null
+++ b/src/components/Picker/types.ts
@@ -0,0 +1,97 @@
+import {ChangeEvent, Component, ReactElement} from 'react';
+import {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native';
+
+type MeasureLayoutOnFailCallback = () => void;
+
+type RelativeToNativeComponentRef = (Component & Readonly) | number;
+
+type BasePickerHandle = {
+ focus: () => void;
+ measureLayout: (relativeToNativeComponentRef: RelativeToNativeComponentRef, onSuccess: MeasureLayoutOnSuccessCallback, onFail: MeasureLayoutOnFailCallback) => void;
+};
+
+type OnMouseDown = () => void;
+
+type OnChange = (value: TPickerValue, index: number) => void;
+
+type AdditionalPickerEvents = {
+ onMouseDown?: OnMouseDown;
+ onChange?: (event: ChangeEvent) => void;
+};
+
+type AdditionalPickerEventsCallback = (onMouseDown: OnMouseDown, onChange: OnChange) => AdditionalPickerEvents;
+
+type DefaultPickerEventsCallback = () => void;
+
+type PickerSize = 'normal' | 'small';
+
+type PickerItem = {
+ /** The value of the item that is being selected */
+ value: TPickerValue;
+
+ /** The text to display for the item */
+ label: string;
+};
+
+type PickerPlaceholder = {
+ /** The value of the placeholder item, usually an empty string */
+ value?: string;
+
+ /** The text to be displayed as the placeholder */
+ label?: string;
+};
+
+type BasePickerProps = {
+ /** BasePicker label */
+ label?: string | null;
+
+ /** Should the picker appear disabled? */
+ isDisabled?: boolean;
+
+ /** Input value */
+ value?: TPickerValue | null;
+
+ /** The items to display in the list of selections */
+ items: Array>;
+
+ /** Something to show as the placeholder before something is selected */
+ placeholder?: PickerPlaceholder;
+
+ /** Error text to display */
+ errorText?: string;
+
+ /** Customize the BasePicker container */
+ containerStyles?: StyleProp;
+
+ /** Customize the BasePicker background color */
+ backgroundColor?: string;
+
+ /** The ID used to uniquely identify the input in a Form */
+ inputID?: string;
+
+ /** Saves a draft of the input value when used in a form */
+ shouldSaveDraft?: boolean;
+
+ /** A callback method that is called when the value changes and it receives the selected value as an argument */
+ onInputChange: (value: TPickerValue, index?: number) => void;
+
+ /** Size of a picker component */
+ size?: PickerSize;
+
+ /** An icon to display with the picker */
+ icon?: (size: PickerSize) => ReactElement;
+
+ /** Whether we should forward the focus/blur calls to the inner picker * */
+ shouldFocusPicker?: boolean;
+
+ /** Callback called when click or tap out of BasePicker */
+ onBlur?: () => void;
+
+ /** Additional events passed to the core BasePicker for specific platforms such as web */
+ additionalPickerEvents?: AdditionalPickerEventsCallback | DefaultPickerEventsCallback;
+
+ /** Hint text that appears below the picker */
+ hintText?: string;
+};
+
+export type {BasePickerHandle, BasePickerProps, AdditionalPickerEventsCallback, PickerSize, AdditionalPickerEvents, OnMouseDown, OnChange};
diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js
index 1c785d4b363f..6572a55ed889 100644
--- a/src/components/PopoverWithoutOverlay/index.js
+++ b/src/components/PopoverWithoutOverlay/index.js
@@ -1,5 +1,6 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
+import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import {defaultProps, propTypes} from '@components/Popover/popoverPropTypes';
import {PopoverContext} from '@components/PopoverProvider';
import withWindowDimensions from '@components/withWindowDimensions';
@@ -16,14 +17,14 @@ function Popover(props) {
const {onOpen, close} = React.useContext(PopoverContext);
const insets = useSafeAreaInsets();
const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles(
+ theme,
+ styles,
'popover',
{
windowWidth: props.windowWidth,
windowHeight: props.windowHeight,
isSmallScreenWidth: false,
},
- theme,
- styles,
props.anchorPosition,
props.innerContainerStyle,
props.outerStyle,
@@ -114,7 +115,7 @@ function Popover(props) {
}}
ref={props.forwardedRef}
>
- {props.children}
+ {props.children}
);
diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
index 692733a7fe0c..c0bb531db007 100644
--- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
+++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
@@ -8,7 +8,7 @@ import KeyboardShortcut from '@libs/KeyboardShortcut';
import * as StyleUtils from '@styles/StyleUtils';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
-import PressableProps from './types';
+import PressableProps, {PressableRef} from './types';
function GenericPressable(
{
@@ -34,7 +34,7 @@ function GenericPressable(
accessible = true,
...rest
}: PressableProps,
- ref: ForwardedRef,
+ ref: PressableRef,
) {
const styles = useThemeStyles();
const {isExecuting, singleExecution} = useSingleExecution();
@@ -124,7 +124,7 @@ function GenericPressable(
}
onPress={!isDisabled ? singleExecution(onPressHandler) : undefined}
onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined}
onKeyDown={!isDisabled ? onKeyDown : undefined}
diff --git a/src/components/Pressable/GenericPressable/index.native.tsx b/src/components/Pressable/GenericPressable/index.native.tsx
index 5bed0f488063..968ee6063a95 100644
--- a/src/components/Pressable/GenericPressable/index.native.tsx
+++ b/src/components/Pressable/GenericPressable/index.native.tsx
@@ -1,9 +1,8 @@
-import React, {ForwardedRef, forwardRef} from 'react';
-import {View} from 'react-native';
+import React, {forwardRef} from 'react';
import GenericPressable from './BaseGenericPressable';
-import PressableProps from './types';
+import PressableProps, {PressableRef} from './types';
-function NativeGenericPressable(props: PressableProps, ref: ForwardedRef) {
+function NativeGenericPressable(props: PressableProps, ref: PressableRef) {
return (
) {
+function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: PressableRef) {
const accessible = props.accessible ?? props.accessible === undefined ? true : props.accessible;
return (
@@ -14,7 +14,7 @@ function WebGenericPressable({focusable = true, ...props}: PressableProps, ref:
// change native accessibility props to web accessibility props
focusable={focusable}
tabIndex={props.tabIndex ?? (!accessible || !focusable) ? -1 : 0}
- role={props.accessibilityRole as Role}
+ role={(props.accessibilityRole ?? props.role) as Role}
id={props.nativeID}
aria-label={props.accessibilityLabel}
aria-labelledby={props.accessibilityLabelledBy}
diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts
index cfe35641bffa..bff5f651ac9f 100644
--- a/src/components/Pressable/GenericPressable/types.ts
+++ b/src/components/Pressable/GenericPressable/types.ts
@@ -1,5 +1,5 @@
-import {ElementRef, RefObject} from 'react';
-import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, ViewStyle} from 'react-native';
+import {ElementRef, ForwardedRef, RefObject} from 'react';
+import {GestureResponderEvent, HostComponent, PressableStateCallbackType, PressableProps as RNPressableProps, StyleProp, View, ViewStyle} from 'react-native';
import {ValueOf} from 'type-fest';
import {Shortcut} from '@libs/KeyboardShortcut';
import CONST from '@src/CONST';
@@ -138,4 +138,7 @@ type PressableProps = RNPressableProps &
noDragArea?: boolean;
};
+type PressableRef = ForwardedRef;
+
export default PressableProps;
+export type {PressableRef};
diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx
index 914b9dc674c3..a80272ee05cf 100644
--- a/src/components/Pressable/PressableWithDelayToggle.tsx
+++ b/src/components/Pressable/PressableWithDelayToggle.tsx
@@ -1,6 +1,6 @@
/* eslint-disable react-native-a11y/has-valid-accessibility-descriptors */
-import React, {ForwardedRef, forwardRef} from 'react';
-import {Text as RNText, StyleProp, TextStyle, View, ViewStyle} from 'react-native';
+import React, {forwardRef} from 'react';
+import {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {SvgProps} from 'react-native-svg';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -12,7 +12,7 @@ import * as StyleUtils from '@styles/StyleUtils';
import useTheme from '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';
import variables from '@styles/variables';
-import PressableProps from './GenericPressable/types';
+import PressableProps, {PressableRef} from './GenericPressable/types';
import PressableWithoutFeedback from './PressableWithoutFeedback';
type PressableWithDelayToggleProps = PressableProps & {
@@ -65,10 +65,10 @@ function PressableWithDelayToggle(
iconStyles,
icon,
}: PressableWithDelayToggleProps,
- ref: ForwardedRef,
+ ref: PressableRef,
) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const [isActive, temporarilyDisableInteractions] = useThrottledButtonState();
const updatePressState = () => {
diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx
index 5d7f7c110ea7..a4c439c4441c 100644
--- a/src/components/Pressable/PressableWithFeedback.tsx
+++ b/src/components/Pressable/PressableWithFeedback.tsx
@@ -1,10 +1,10 @@
-import React, {ForwardedRef, forwardRef, useState} from 'react';
-import {StyleProp, View, ViewStyle} from 'react-native';
+import React, {forwardRef, useState} from 'react';
+import {StyleProp, ViewStyle} from 'react-native';
import {AnimatedStyle} from 'react-native-reanimated';
import OpacityView from '@components/OpacityView';
import variables from '@styles/variables';
import GenericPressable from './GenericPressable';
-import PressableProps from './GenericPressable/types';
+import PressableProps, {PressableRef} from './GenericPressable/types';
type PressableWithFeedbackProps = PressableProps & {
/** Style for the wrapper view */
@@ -37,7 +37,7 @@ function PressableWithFeedback(
hoverDimmingValue = variables.hoverDimValue,
...rest
}: PressableWithFeedbackProps,
- ref: ForwardedRef,
+ ref: PressableRef,
) {
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);
@@ -88,3 +88,4 @@ function PressableWithFeedback(
PressableWithFeedback.displayName = 'PressableWithFeedback';
export default forwardRef(PressableWithFeedback);
+export type {PressableWithFeedbackProps};
diff --git a/src/components/Pressable/PressableWithoutFeedback.tsx b/src/components/Pressable/PressableWithoutFeedback.tsx
index c3b780e63cfd..fd9d695cc2ed 100644
--- a/src/components/Pressable/PressableWithoutFeedback.tsx
+++ b/src/components/Pressable/PressableWithoutFeedback.tsx
@@ -1,11 +1,10 @@
-import React, {ForwardedRef} from 'react';
-import {View} from 'react-native';
+import React from 'react';
import GenericPressable from './GenericPressable';
-import PressableProps from './GenericPressable/types';
+import PressableProps, {PressableRef} from './GenericPressable/types';
function PressableWithoutFeedback(
{pressStyle, hoverStyle, focusStyle, disabledStyle, screenReaderActiveStyle, shouldUseHapticsOnPress, shouldUseHapticsOnLongPress, ...rest}: PressableProps,
- ref: ForwardedRef,
+ ref: PressableRef,
) {
return (
{
- e.preventDefault();
- props.onSecondaryInteraction(e);
- };
-
- return (
-
- {props.children}
-
- );
-}
-
-PressableWithSecondaryInteraction.propTypes = pressableWithSecondaryInteractionPropTypes.propTypes;
-PressableWithSecondaryInteraction.defaultProps = pressableWithSecondaryInteractionPropTypes.defaultProps;
-PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction';
-
-const PressableWithSecondaryInteractionWithRef = forwardRef((props, ref) => (
-
-));
-
-PressableWithSecondaryInteractionWithRef.displayName = 'PressableWithSecondaryInteractionWithRef';
-
-export default PressableWithSecondaryInteractionWithRef;
diff --git a/src/components/PressableWithSecondaryInteraction/index.native.tsx b/src/components/PressableWithSecondaryInteraction/index.native.tsx
new file mode 100644
index 000000000000..f3cef029aa65
--- /dev/null
+++ b/src/components/PressableWithSecondaryInteraction/index.native.tsx
@@ -0,0 +1,61 @@
+import React, {forwardRef} from 'react';
+import {GestureResponderEvent, TextProps} from 'react-native';
+import {PressableRef} from '@components/Pressable/GenericPressable/types';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import Text from '@components/Text';
+import PressableWithSecondaryInteractionProps from './types';
+
+/** This is a special Pressable that calls onSecondaryInteraction when LongPressed. */
+function PressableWithSecondaryInteraction(
+ {
+ children,
+ onSecondaryInteraction,
+ inline = false,
+ needsOffscreenAlphaCompositing = false,
+ suppressHighlighting = false,
+ activeOpacity = 1,
+ preventDefaultContextMenu,
+ withoutFocusOnSecondaryInteraction,
+ enableLongPressWithHover,
+ ...rest
+ }: PressableWithSecondaryInteractionProps,
+ ref: PressableRef,
+) {
+ const executeSecondaryInteraction = (event: GestureResponderEvent) => {
+ event.preventDefault();
+ onSecondaryInteraction?.(event);
+ };
+
+ // Use Text node for inline mode to prevent content overflow.
+ if (inline) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction';
+
+export default forwardRef(PressableWithSecondaryInteraction);
diff --git a/src/components/PressableWithSecondaryInteraction/index.js b/src/components/PressableWithSecondaryInteraction/index.tsx
similarity index 55%
rename from src/components/PressableWithSecondaryInteraction/index.js
rename to src/components/PressableWithSecondaryInteraction/index.tsx
index c56774638d6a..88aad38ad5a9 100644
--- a/src/components/PressableWithSecondaryInteraction/index.js
+++ b/src/components/PressableWithSecondaryInteraction/index.tsx
@@ -1,44 +1,39 @@
import React, {forwardRef, useEffect, useRef} from 'react';
-import _ from 'underscore';
+import {GestureResponderEvent} from 'react-native';
+import {PressableRef} from '@components/Pressable/GenericPressable/types';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as StyleUtils from '@styles/StyleUtils';
import useThemeStyles from '@styles/useThemeStyles';
-import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
-
-/**
- * This is a special Pressable that calls onSecondaryInteraction when LongPressed, or right-clicked.
- */
-
-function PressableWithSecondaryInteraction({
- children,
- inline,
- style,
- enableLongPressWithHover,
- withoutFocusOnSecondaryInteraction,
- preventDefaultContextMenu,
- onSecondaryInteraction,
- onPressIn,
- onPress,
- onPressOut,
- activeOpacity,
- forwardedRef,
- ...rest
-}) {
+import PressableWithSecondaryInteractionProps from './types';
+
+/** This is a special Pressable that calls onSecondaryInteraction when LongPressed, or right-clicked. */
+function PressableWithSecondaryInteraction(
+ {
+ children,
+ inline = false,
+ style,
+ enableLongPressWithHover = false,
+ withoutFocusOnSecondaryInteraction = false,
+ needsOffscreenAlphaCompositing = false,
+ preventDefaultContextMenu = true,
+ onSecondaryInteraction,
+ activeOpacity = 1,
+ ...rest
+ }: PressableWithSecondaryInteractionProps,
+ ref: PressableRef,
+) {
const styles = useThemeStyles();
- const pressableRef = useRef(null);
+ const pressableRef = useRef(null);
- /**
- * @param {Event} e - the secondary interaction event
- */
- const executeSecondaryInteraction = (e) => {
+ const executeSecondaryInteraction = (event: GestureResponderEvent) => {
if (DeviceCapabilities.hasHoverSupport() && !enableLongPressWithHover) {
return;
}
if (withoutFocusOnSecondaryInteraction && pressableRef.current) {
pressableRef.current.blur();
}
- onSecondaryInteraction(e);
+ onSecondaryInteraction?.(event);
};
useEffect(() => {
@@ -46,32 +41,32 @@ function PressableWithSecondaryInteraction({
return;
}
- if (forwardedRef) {
- if (_.isFunction(forwardedRef)) {
- forwardedRef(pressableRef.current);
- } else if (_.isObject(forwardedRef)) {
+ if (ref) {
+ if (typeof ref === 'function') {
+ ref(pressableRef.current);
+ } else if (typeof ref === 'object') {
// eslint-disable-next-line no-param-reassign
- forwardedRef.current = pressableRef.current;
+ ref.current = pressableRef.current;
}
}
const element = pressableRef.current;
/**
- * @param {contextmenu} e - A right-click MouseEvent.
+ * @param event - A right-click MouseEvent.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
*/
- const executeSecondaryInteractionOnContextMenu = (e) => {
+ const executeSecondaryInteractionOnContextMenu = (event: MouseEvent) => {
if (!onSecondaryInteraction) {
return;
}
- e.stopPropagation();
+ event.stopPropagation();
if (preventDefaultContextMenu) {
- e.preventDefault();
+ event.preventDefault();
}
- onSecondaryInteraction(e);
+ onSecondaryInteraction(event);
/**
* This component prevents the tapped element from capturing focus.
@@ -90,42 +85,28 @@ function PressableWithSecondaryInteraction({
return () => {
element.removeEventListener('contextmenu', executeSecondaryInteractionOnContextMenu);
};
- }, [forwardedRef, onSecondaryInteraction, preventDefaultContextMenu, withoutFocusOnSecondaryInteraction]);
+ }, [ref, onSecondaryInteraction, preventDefaultContextMenu, withoutFocusOnSecondaryInteraction]);
- const defaultPressableProps = _.omit(rest, ['onLongPress']);
const inlineStyle = inline ? styles.dInline : {};
// On Web, Text does not support LongPress events thus manage inline mode with styling instead of using Text.
return (
[StyleUtils.parseStyleFromFunction(style, state), inlineStyle]}
+ needsOffscreenAlphaCompositing={needsOffscreenAlphaCompositing}
>
{children}
);
}
-PressableWithSecondaryInteraction.propTypes = pressableWithSecondaryInteractionPropTypes.propTypes;
-PressableWithSecondaryInteraction.defaultProps = pressableWithSecondaryInteractionPropTypes.defaultProps;
PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction';
-const PressableWithSecondaryInteractionWithRef = forwardRef((props, ref) => (
-
-));
-
-PressableWithSecondaryInteractionWithRef.displayName = 'PressableWithSecondaryInteractionWithRef';
-
-export default PressableWithSecondaryInteractionWithRef;
+export default forwardRef(PressableWithSecondaryInteraction);
diff --git a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js b/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js
deleted file mode 100644
index 935b8ece5933..000000000000
--- a/src/components/PressableWithSecondaryInteraction/pressableWithSecondaryInteractionPropTypes.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import PropTypes from 'prop-types';
-import refPropTypes from '@components/refPropTypes';
-import stylePropTypes from '@styles/stylePropTypes';
-
-const propTypes = {
- /** The function that should be called when this pressable is pressed */
- onPress: PropTypes.func,
-
- /** The function that should be called when this pressable is pressedIn */
- onPressIn: PropTypes.func,
-
- /** The function that should be called when this pressable is pressedOut */
- onPressOut: PropTypes.func,
-
- /**
- * The function that should be called when this pressable is LongPressed or right-clicked.
- *
- * This function should be stable, preferably wrapped in a `useCallback` so that it does not
- * cause several re-renders.
- */
- onSecondaryInteraction: PropTypes.func,
-
- /** The children which should be contained in this wrapper component. */
- children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
-
- /** The ref to the search input (may be null on small screen widths) */
- forwardedRef: refPropTypes,
-
- /** Prevent the default ContextMenu on web/Desktop */
- preventDefaultContextMenu: PropTypes.bool,
-
- /** Use Text instead of Pressable to create inline layout.
- * It has few limitations in comparison to Pressable.
- *
- * - No support for delayLongPress.
- * - No support for pressIn and pressOut events.
- * - No support for opacity
- *
- * Note: Web uses styling instead of Text due to no support of LongPress. Thus above pointers are not valid for web.
- */
- inline: PropTypes.bool,
-
- /** Disable focus trap for the element on secondary interaction */
- withoutFocusOnSecondaryInteraction: PropTypes.bool,
-
- /** Opacity to reduce to when active */
- activeOpacity: PropTypes.number,
-
- /** Used to apply styles to the Pressable */
- style: stylePropTypes,
-
- /** Whether the view needs to be rendered offscreen (for Android only) */
- needsOffscreenAlphaCompositing: PropTypes.bool,
-
- /** Whether the text has a gray highlights on press down (for IOS only) */
- suppressHighlighting: PropTypes.bool,
-};
-
-const defaultProps = {
- forwardedRef: () => {},
- onPressIn: () => {},
- onPressOut: () => {},
- preventDefaultContextMenu: true,
- inline: false,
- withoutFocusOnSecondaryInteraction: false,
- activeOpacity: 1,
- enableLongPressWithHover: false,
- needsOffscreenAlphaCompositing: false,
- suppressHighlighting: false,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/PressableWithSecondaryInteraction/types.ts b/src/components/PressableWithSecondaryInteraction/types.ts
new file mode 100644
index 000000000000..cf286afcb63a
--- /dev/null
+++ b/src/components/PressableWithSecondaryInteraction/types.ts
@@ -0,0 +1,54 @@
+import {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
+import {PressableWithFeedbackProps} from '@components/Pressable/PressableWithFeedback';
+import ChildrenProps from '@src/types/utils/ChildrenProps';
+
+type PressableWithSecondaryInteractionProps = PressableWithFeedbackProps &
+ ChildrenProps & {
+ /** The function that should be called when this pressable is pressed */
+ onPress: (event?: GestureResponderEvent) => void;
+
+ /** The function that should be called when this pressable is pressedIn */
+ onPressIn?: (event?: GestureResponderEvent) => void;
+
+ /** The function that should be called when this pressable is pressedOut */
+ onPressOut?: (event?: GestureResponderEvent) => void;
+
+ /**
+ * The function that should be called when this pressable is LongPressed or right-clicked.
+ *
+ * This function should be stable, preferably wrapped in a `useCallback` so that it does not
+ * cause several re-renders.
+ */
+ onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void;
+
+ /** Prevent the default ContextMenu on web/Desktop */
+ preventDefaultContextMenu?: boolean;
+
+ /** Use Text instead of Pressable to create inline layout.
+ * It has few limitations in comparison to Pressable.
+ *
+ * - No support for delayLongPress.
+ * - No support for pressIn and pressOut events.
+ * - No support for opacity
+ *
+ * Note: Web uses styling instead of Text due to no support of LongPress. Thus above pointers are not valid for web.
+ */
+ inline?: boolean;
+
+ /** Disable focus trap for the element on secondary interaction */
+ withoutFocusOnSecondaryInteraction?: boolean;
+
+ /** Opacity to reduce to when active */
+ activeOpacity?: number;
+
+ /** Used to apply styles to the Pressable */
+ style?: StyleProp;
+
+ /** Whether the long press with hover behavior is enabled */
+ enableLongPressWithHover?: boolean;
+
+ /** Whether the text has a gray highlights on press down (for IOS only) */
+ suppressHighlighting?: boolean;
+ };
+
+export default PressableWithSecondaryInteractionProps;
diff --git a/src/components/Reactions/AddReactionBubble.js b/src/components/Reactions/AddReactionBubble.js
index 55c2c5bb04e0..78c1f5407d64 100644
--- a/src/components/Reactions/AddReactionBubble.js
+++ b/src/components/Reactions/AddReactionBubble.js
@@ -55,8 +55,8 @@ const defaultProps = {
};
function AddReactionBubble(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const ref = useRef();
useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []);
diff --git a/src/components/Reactions/EmojiReactionBubble.js b/src/components/Reactions/EmojiReactionBubble.js
index 2487f9d0c73c..73538a032e38 100644
--- a/src/components/Reactions/EmojiReactionBubble.js
+++ b/src/components/Reactions/EmojiReactionBubble.js
@@ -55,8 +55,8 @@ const defaultProps = {
};
function EmojiReactionBubble(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
return (
[
@@ -72,7 +72,6 @@ function EmojiReactionBubble(props) {
props.onPress();
}}
- onLongPress={props.onReactionListOpen}
onSecondaryInteraction={props.onReactionListOpen}
ref={props.forwardedRef}
enableLongPressWithHover={props.isSmallScreenWidth}
diff --git a/src/components/Reactions/MiniQuickEmojiReactions.js b/src/components/Reactions/MiniQuickEmojiReactions.js
index 2226573a196e..7795f77d5d53 100644
--- a/src/components/Reactions/MiniQuickEmojiReactions.js
+++ b/src/components/Reactions/MiniQuickEmojiReactions.js
@@ -56,8 +56,8 @@ const defaultProps = {
* @returns {JSX.Element}
*/
function MiniQuickEmojiReactions(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const ref = useRef();
const openEmojiPicker = () => {
diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js
index 6181e799ab2a..f0eed3ac2f02 100644
--- a/src/components/ReportActionItem/ReportActionItemImage.js
+++ b/src/components/ReportActionItem/ReportActionItemImage.js
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import _ from 'underscore';
-import AttachmentModal from '@components/AttachmentModal';
import EReceiptThumbnail from '@components/EReceiptThumbnail';
import Image from '@components/Image';
import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
@@ -10,10 +9,12 @@ import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import ThumbnailImage from '@components/ThumbnailImage';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
+import Navigation from '@libs/Navigation/Navigation';
import * as TransactionUtils from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
const propTypes = {
/** thumbnail URI for the image */
@@ -46,11 +47,11 @@ const defaultProps = {
*/
function ReportActionItemImage({thumbnail, image, enablePreviewModal, transaction, isLocalFile}) {
+ const styles = useThemeStyles();
const {translate} = useLocalize();
const imageSource = tryResolveUrlFromApiRoot(image || '');
const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || '');
const isEReceipt = !_.isEmpty(transaction) && TransactionUtils.hasEReceipt(transaction);
- const styles = useThemeStyles();
let receiptImageComponent;
@@ -82,25 +83,17 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal, transactio
return (
{({report}) => (
- {
+ const route = ROUTES.REPORT_ATTACHMENTS.getRoute(report.reportID, imageSource);
+ Navigation.navigate(route);
+ }}
+ role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+ accessibilityLabel={translate('accessibilityHints.viewAttachment')}
>
- {({show}) => (
-
- {receiptImageComponent}
-
- )}
-
+ {receiptImageComponent}
+
)}
);
diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js
index 5790d04845be..abc65b513ab9 100644
--- a/src/components/ReportActionItem/TaskPreview.js
+++ b/src/components/ReportActionItem/TaskPreview.js
@@ -76,8 +76,8 @@ const defaultProps = {
};
function TaskPreview(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
// The reportAction might not contain details regarding the taskReport
// Only the direct parent reportAction will contain details about the taskReport
diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js
index de0c1774b74f..3d3ed9315f6e 100644
--- a/src/components/ReportActionItem/TaskView.js
+++ b/src/components/ReportActionItem/TaskView.js
@@ -46,8 +46,8 @@ const propTypes = {
};
function TaskView(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
useEffect(() => {
Task.setTaskReport({...props.report});
}, [props.report]);
diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js
index bd7a24d15f1f..4a1d60a869ad 100644
--- a/src/components/RoomHeaderAvatars.js
+++ b/src/components/RoomHeaderAvatars.js
@@ -33,7 +33,6 @@ function RoomHeaderAvatars(props) {
@@ -78,7 +77,6 @@ function RoomHeaderAvatars(props) {
diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js
index 16bf9ba0761c..6af67c51ffaf 100644
--- a/src/components/ScreenWrapper/index.js
+++ b/src/components/ScreenWrapper/index.js
@@ -51,7 +51,7 @@ const ScreenWrapper = React.forwardRef(
const navigation = useNavigation();
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined;
- const minHeight = shouldEnableMinHeight ? initialHeight : undefined;
+ const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined;
const isKeyboardShown = lodashGet(keyboardState, 'isKeyboardShown', false);
const isKeyboardShownRef = useRef();
diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx
index 6122d5508a38..7c75ae2f71b2 100644
--- a/src/components/ScrollViewWithContext.tsx
+++ b/src/components/ScrollViewWithContext.tsx
@@ -63,3 +63,4 @@ ScrollViewWithContextWithRef.displayName = 'ScrollViewWithContextWithRef';
export default React.forwardRef(ScrollViewWithContextWithRef);
export {ScrollContext};
+export type {ScrollContextValue};
diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js
index 3a58b63954bd..f1c189842c28 100644
--- a/src/components/SelectionList/BaseListItem.js
+++ b/src/components/SelectionList/BaseListItem.js
@@ -24,6 +24,7 @@ function BaseListItem({
canSelectMultiple = false,
onSelectRow,
onDismissError = () => {},
+ keyForList,
}) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -47,6 +48,7 @@ function BaseListItem({
hoverStyle={styles.hoveredComponentBG}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined}
+ testID={keyForList}
>
{
- const focusedOption = flattenedSections.allOptions[focusedIndex];
+ const selectFocusedOption = (e) => {
+ const focusedItemKey = lodashGet(e, ['target', 'attributes', 'data-testid', 'value']);
+ const focusedOption = focusedItemKey ? _.find(flattenedSections.allOptions, (option) => option.keyForList === focusedItemKey) : flattenedSections.allOptions[focusedIndex];
if (!focusedOption || focusedOption.isDisabled) {
return;
@@ -310,6 +311,7 @@ function BaseSelectionList({
onSelectRow={() => selectRow(item, true)}
onDismissError={onDismissError}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
+ keyForList={item.keyForList}
/>
);
};
diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js
index 83601dbc27d8..602a326d6c48 100644
--- a/src/components/TabSelector/TabSelector.js
+++ b/src/components/TabSelector/TabSelector.js
@@ -70,8 +70,8 @@ const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => {
function TabSelector({state, navigation, onTabPress, position}) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]);
const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs);
diff --git a/src/components/TaskHeaderActionButton.js b/src/components/TaskHeaderActionButton.js
deleted file mode 100644
index 09ca427b8e56..000000000000
--- a/src/components/TaskHeaderActionButton.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import compose from '@libs/compose';
-import * as ReportUtils from '@libs/ReportUtils';
-import reportPropTypes from '@pages/reportPropTypes';
-import useThemeStyles from '@styles/useThemeStyles';
-import * as Session from '@userActions/Session';
-import * as Task from '@userActions/Task';
-import ONYXKEYS from '@src/ONYXKEYS';
-import Button from './Button';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-
-const propTypes = {
- /** The report currently being looked at */
- report: reportPropTypes.isRequired,
-
- /** Current user session */
- session: PropTypes.shape({
- accountID: PropTypes.number,
- }),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- session: {
- accountID: 0,
- },
-};
-
-function TaskHeaderActionButton(props) {
- const styles = useThemeStyles();
- return (
-
-
- );
-}
-
-TaskHeaderActionButton.propTypes = propTypes;
-TaskHeaderActionButton.defaultProps = defaultProps;
-TaskHeaderActionButton.displayName = 'TaskHeaderActionButton';
-
-export default compose(
- withLocalize,
- withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
- }),
-)(TaskHeaderActionButton);
diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx
new file mode 100644
index 000000000000..5f8461d8e324
--- /dev/null
+++ b/src/components/TaskHeaderActionButton.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import {View} from 'react-native';
+import {OnyxEntry, withOnyx} from 'react-native-onyx';
+import useLocalize from '@hooks/useLocalize';
+import * as ReportUtils from '@libs/ReportUtils';
+import useThemeStyles from '@styles/useThemeStyles';
+import * as Session from '@userActions/Session';
+import * as Task from '@userActions/Task';
+import ONYXKEYS from '@src/ONYXKEYS';
+import * as OnyxTypes from '@src/types/onyx';
+import Button from './Button';
+
+type TaskHeaderActionButtonOnyxProps = {
+ /** Current user session */
+ session: OnyxEntry;
+};
+
+type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & {
+ /** The report currently being looked at */
+ report: OnyxTypes.Report;
+};
+
+function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ return (
+
+
+ );
+}
+
+TaskHeaderActionButton.displayName = 'TaskHeaderActionButton';
+
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(TaskHeaderActionButton);
diff --git a/src/components/Text.tsx b/src/components/Text.tsx
index 58a5cf300699..96a6f535877a 100644
--- a/src/components/Text.tsx
+++ b/src/components/Text.tsx
@@ -12,8 +12,10 @@ type TextProps = RNTextProps & {
/** The size of the text */
fontSize?: number;
+
/** The alignment of the text */
textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify';
+
/** Any children to display */
children: React.ReactNode;
diff --git a/src/components/TextLink.js b/src/components/TextLink.js
index ab59f6f8b121..46c074eb79e6 100644
--- a/src/components/TextLink.js
+++ b/src/components/TextLink.js
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'underscore';
+import useEnvironment from '@hooks/useEnvironment';
import stylePropTypes from '@styles/stylePropTypes';
import useThemeStyles from '@styles/useThemeStyles';
import * as Link from '@userActions/Link';
@@ -37,6 +38,7 @@ const defaultProps = {
};
function TextLink(props) {
+ const {environmentURL} = useEnvironment();
const styles = useThemeStyles();
const rest = _.omit(props, _.keys(propTypes));
const additionalStyles = _.isArray(props.style) ? props.style : [props.style];
@@ -51,7 +53,7 @@ function TextLink(props) {
return;
}
- Link.openExternalLink(props.href);
+ Link.openLink(props.href, environmentURL);
};
/**
diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js
index c3898a4fe983..d92457238675 100644
--- a/src/components/Tooltip/TooltipRenderedOnPageBody.js
+++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js
@@ -115,7 +115,7 @@ function TooltipRenderedOnPageBody({
tooltipContentWidth: contentMeasuredWidth,
tooltipWrapperHeight: wrapperMeasuredHeight,
theme,
- themeStyles: styles,
+ styles,
shiftHorizontal,
shiftVertical,
}),
diff --git a/src/components/ZeroWidthView/index.native.js b/src/components/ZeroWidthView/index.native.tsx
similarity index 100%
rename from src/components/ZeroWidthView/index.native.js
rename to src/components/ZeroWidthView/index.native.tsx
diff --git a/src/components/ZeroWidthView/index.js b/src/components/ZeroWidthView/index.tsx
similarity index 66%
rename from src/components/ZeroWidthView/index.js
rename to src/components/ZeroWidthView/index.tsx
index 58b2dfa039ef..73cd349360c6 100644
--- a/src/components/ZeroWidthView/index.js
+++ b/src/components/ZeroWidthView/index.tsx
@@ -1,23 +1,17 @@
-import PropTypes from 'prop-types';
import React from 'react';
import Text from '@components/Text';
import * as Browser from '@libs/Browser';
import * as EmojiUtils from '@libs/EmojiUtils';
-const propTypes = {
+type ZeroWidthViewProps = {
/** If this is the Concierge chat, we'll open the modal for requesting a setup call instead of showing popover menu */
- text: PropTypes.string,
+ text?: string;
/** URL to the assigned guide's appointment booking calendar */
- displayAsGroup: PropTypes.bool,
+ displayAsGroup?: boolean;
};
-const defaultProps = {
- text: '',
- displayAsGroup: false,
-};
-
-function ZeroWidthView({text, displayAsGroup}) {
+function ZeroWidthView({text = '', displayAsGroup = false}: ZeroWidthViewProps) {
const firstLetterIsEmoji = EmojiUtils.isFirstLetterEmoji(text);
if (firstLetterIsEmoji && !displayAsGroup && !Browser.isMobile()) {
return ;
@@ -25,8 +19,6 @@ function ZeroWidthView({text, displayAsGroup}) {
return null;
}
-ZeroWidthView.propTypes = propTypes;
-ZeroWidthView.defaultProps = defaultProps;
ZeroWidthView.displayName = 'ZeroWidthView';
export default ZeroWidthView;
diff --git a/src/components/withTheme.tsx b/src/components/withTheme.tsx
index 451292f1a66f..532ff6e5c375 100644
--- a/src/components/withTheme.tsx
+++ b/src/components/withTheme.tsx
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, {ComponentType, ForwardedRef, forwardRef, ReactElement, RefAttributes} from 'react';
import getComponentDisplayName from '@libs/getComponentDisplayName';
-import {ThemeColors} from '@styles/themes/types';
+import {type ThemeColors} from '@styles/themes/types';
import useTheme from '@styles/themes/useTheme';
const withThemePropTypes = {
diff --git a/src/components/withThemeStyles.tsx b/src/components/withThemeStyles.tsx
index 8ea55c5fbef8..bdd5e50fe8e3 100644
--- a/src/components/withThemeStyles.tsx
+++ b/src/components/withThemeStyles.tsx
@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import React, {ComponentType, ForwardedRef, forwardRef, ReactElement, RefAttributes} from 'react';
import getComponentDisplayName from '@libs/getComponentDisplayName';
-import styles from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
import useThemeStyles from '@styles/useThemeStyles';
const withThemeStylesPropTypes = {
themeStyles: PropTypes.object.isRequired,
};
-type ThemeStylesProps = {themeStyles: typeof styles};
+type ThemeStylesProps = {themeStyles: ThemeStyles};
export default function withThemeStyles(
WrappedComponent: ComponentType>,
diff --git a/src/hooks/useScrollContext.ts b/src/hooks/useScrollContext.ts
new file mode 100644
index 000000000000..711c8326bdff
--- /dev/null
+++ b/src/hooks/useScrollContext.ts
@@ -0,0 +1,6 @@
+import {useContext} from 'react';
+import {ScrollContext, ScrollContextValue} from '@components/ScrollViewWithContext';
+
+export default function useScrollContext(): ScrollContextValue {
+ return useContext(ScrollContext);
+}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 57c217d85bae..917a144a8069 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1133,6 +1133,10 @@ export default {
year: 'Year',
selectYear: 'Please select a year',
},
+ focusModeUpdateModal: {
+ title: 'Welcome to #focus mode!',
+ prompt: "Read chats will be hidden, unless they have a green dot, which means there's an action you need to take on them. You can change this in your account settings ",
+ },
notFound: {
chatYouLookingForCannotBeFound: 'The chat you are looking for cannot be found.',
getMeOutOfHere: 'Get me out of here',
@@ -1543,6 +1547,12 @@ export default {
invitePeople: 'Invite new members',
genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.',
pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
+ user: 'user',
+ users: 'users',
+ invited: 'invited',
+ removed: 'removed',
+ to: 'to',
+ from: 'from',
},
inviteMessage: {
inviteMessageTitle: 'Add message',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 4dc149834fb5..653381425175 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1133,6 +1133,10 @@ export default {
year: 'Año',
selectYear: 'Por favor, selecciona un año',
},
+ focusModeUpdateModal: {
+ title: '¡Bienvenido al modo #concentración!',
+ prompt: 'Los mensajes leídos se ocultarán, a menos que tengan un punto verde, lo que significa que tienes que tomar una acción en ellos. Puedes cambiar esto en la configuración de tu cuenta ',
+ },
notFound: {
chatYouLookingForCannotBeFound: 'El chat que estás buscando no se pudo encontrar.',
getMeOutOfHere: 'Sácame de aquí',
@@ -1565,6 +1569,12 @@ export default {
invitePeople: 'Invitar nuevos miembros',
genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..',
pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
+ user: 'usuario',
+ users: 'usuarios',
+ invited: 'invitó',
+ removed: 'eliminó',
+ to: 'a',
+ from: 'de',
},
inviteMessage: {
inviteMessageTitle: 'Añadir un mensaje',
diff --git a/src/libs/ComposerUtils/updateNumberOfLines/types.ts b/src/libs/ComposerUtils/updateNumberOfLines/types.ts
index cef7d7ef6a80..5e109afe8959 100644
--- a/src/libs/ComposerUtils/updateNumberOfLines/types.ts
+++ b/src/libs/ComposerUtils/updateNumberOfLines/types.ts
@@ -1,7 +1,7 @@
import {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native';
import ComposerProps from '@libs/ComposerUtils/types';
-import themeStyles from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
-type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent, styles: typeof themeStyles) => void;
+type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent, styles: ThemeStyles) => void;
export default UpdateNumberOfLines;
diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts
index 488ff0d9b98a..77c34ebdc576 100644
--- a/src/libs/Localize/index.ts
+++ b/src/libs/Localize/index.ts
@@ -1,6 +1,7 @@
import * as RNLocalize from 'react-native-localize';
import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
+import {MessageElementBase, MessageTextElement} from '@libs/MessageElement';
import Config from '@src/CONFIG';
import CONST from '@src/CONST';
import translations from '@src/languages/translations';
@@ -121,15 +122,48 @@ function translateIfPhraseKey(message: MaybePhraseKey): string {
}
}
+function getPreferredListFormat(): Intl.ListFormat {
+ if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) {
+ init();
+ }
+
+ return CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()];
+}
+
/**
* Format an array into a string with comma and "and" ("a dog, a cat and a chicken")
*/
-function arrayToString(anArray: string[]) {
- if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) {
- init();
+function formatList(components: string[]) {
+ const listFormat = getPreferredListFormat();
+ return listFormat.format(components);
+}
+
+function formatMessageElementList(elements: readonly E[]): ReadonlyArray {
+ const listFormat = getPreferredListFormat();
+ const parts = listFormat.formatToParts(elements.map((e) => e.content));
+ const resultElements: Array = [];
+
+ let nextElementIndex = 0;
+ for (const part of parts) {
+ if (part.type === 'element') {
+ /**
+ * The standard guarantees that all input elements will be present in the constructed parts, each exactly
+ * once, and without any modifications: https://tc39.es/ecma402/#sec-createpartsfromlist
+ */
+ const element = elements[nextElementIndex++];
+
+ resultElements.push(element);
+ } else {
+ const literalElement: MessageTextElement = {
+ kind: 'text',
+ content: part.value,
+ };
+
+ resultElements.push(literalElement);
+ }
}
- const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()];
- return listFormat.format(anArray);
+
+ return resultElements;
}
/**
@@ -139,5 +173,5 @@ function getDevicePreferredLocale(): string {
return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT;
}
-export {translate, translateLocal, translateIfPhraseKey, arrayToString, getDevicePreferredLocale};
+export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale};
export type {PhraseParameters, Phrase, MaybePhraseKey};
diff --git a/src/libs/MessageElement.ts b/src/libs/MessageElement.ts
new file mode 100644
index 000000000000..584d7e1e289a
--- /dev/null
+++ b/src/libs/MessageElement.ts
@@ -0,0 +1,11 @@
+type MessageElementBase = {
+ readonly kind: string;
+ readonly content: string;
+};
+
+type MessageTextElement = {
+ readonly kind: 'text';
+ readonly content: string;
+} & MessageElementBase;
+
+export type {MessageElementBase, MessageTextElement};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
similarity index 83%
rename from src/libs/Navigation/AppNavigator/AuthScreens.js
rename to src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 4c610bc12099..a78b38728136 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -1,9 +1,6 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {memo, useEffect, useRef} from 'react';
import {View} from 'react-native';
-import Onyx, {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import Onyx, {OnyxEntry, withOnyx} from 'react-native-onyx';
import useWindowDimensions from '@hooks/useWindowDimensions';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import getCurrentUrl from '@libs/Navigation/currentUrl';
@@ -12,6 +9,7 @@ import NetworkConnection from '@libs/NetworkConnection';
import * as Pusher from '@libs/Pusher/pusher';
import PusherConnectionManager from '@libs/PusherConnectionManager';
import * as SessionUtils from '@libs/SessionUtils';
+import type {AuthScreensParamList} from '@navigation/types';
import DemoSetupPage from '@pages/DemoSetupPage';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import DesktopSignInRedirectPage from '@pages/signin/DesktopSignInRedirectPage';
@@ -20,6 +18,7 @@ import * as App from '@userActions/App';
import * as Download from '@userActions/Download';
import * as Modal from '@userActions/Modal';
import * as PersonalDetails from '@userActions/PersonalDetails';
+import * as PriorityMode from '@userActions/PriorityMode';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import Timing from '@userActions/Timing';
@@ -30,32 +29,52 @@ import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
+import * as OnyxTypes from '@src/types/onyx';
+import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
import createCustomStackNavigator from './createCustomStackNavigator';
import defaultScreenOptions from './defaultScreenOptions';
import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions';
import CentralPaneNavigator from './Navigators/CentralPaneNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
-const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default;
-const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default;
-const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default;
-const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default;
-const loadConciergePage = () => require('../../../pages/ConciergePage').default;
+type AuthScreensProps = {
+ /** Session of currently logged in user */
+ session: OnyxEntry;
+
+ /** The report ID of the last opened public room as anonymous user */
+ lastOpenedPublicRoomID: OnyxEntry;
+
+ /** Opt-in experimental mode that prevents certain Onyx keys from persisting to disk */
+ isUsingMemoryOnlyKeys: OnyxEntry;
+
+ /** The last Onyx update ID was applied to the client */
+ lastUpdateIDAppliedToClient: OnyxEntry;
-let timezone;
-let currentAccountID;
-let isLoadingApp;
+ /** Information about any currently running demos */
+ demoInfo: OnyxEntry;
+};
+
+const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default as React.ComponentType;
+const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default as React.ComponentType;
+const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default as React.ComponentType;
+const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default as React.ComponentType;
+const loadConciergePage = () => require('../../../pages/ConciergePage').default as React.ComponentType;
+
+let timezone: Timezone | null;
+let currentAccountID = -1;
+let isLoadingApp = false;
Onyx.connect({
key: ONYXKEYS.SESSION,
- callback: (val) => {
+ callback: (value) => {
// When signed out, val hasn't accountID
- if (!_.has(val, 'accountID')) {
+ if (!(value && 'accountID' in value)) {
timezone = null;
return;
}
- currentAccountID = val.accountID;
+ currentAccountID = value.accountID ?? -1;
+
if (Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
// This means sign in in RHP was successful, so we can dismiss the modal and subscribe to user events
Navigation.dismissModal();
@@ -66,17 +85,17 @@ Onyx.connect({
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- callback: (val) => {
- if (!val || timezone) {
+ callback: (value) => {
+ if (!value || timezone) {
return;
}
- timezone = lodashGet(val, [currentAccountID, 'timezone'], {});
- const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ timezone = value?.[currentAccountID]?.timezone ?? {};
+ const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone as SelectedTimezone;
// If the current timezone is different than the user's timezone, and their timezone is set to automatic
// then update their timezone.
- if (_.isObject(timezone) && timezone.automatic && timezone.selected !== currentTimezone) {
+ if (timezone?.automatic && timezone?.selected !== currentTimezone) {
timezone.selected = currentTimezone;
PersonalDetails.updateAutomaticTimezone({
automatic: true,
@@ -88,12 +107,12 @@ Onyx.connect({
Onyx.connect({
key: ONYXKEYS.IS_LOADING_APP,
- callback: (val) => {
- isLoadingApp = val;
+ callback: (value) => {
+ isLoadingApp = !!value;
},
});
-const RootStack = createCustomStackNavigator();
+const RootStack = createCustomStackNavigator();
// We want to delay the re-rendering for components(e.g. ReportActionCompose)
// that depends on modal visibility until Modal is completely closed and its focused
// When modal screen is focused, update modal visibility in Onyx
@@ -108,40 +127,7 @@ const modalScreenListeners = {
},
};
-const propTypes = {
- /** Session of currently logged in user */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
-
- /** The report ID of the last opened public room as anonymous user */
- lastOpenedPublicRoomID: PropTypes.string,
-
- /** Opt-in experimental mode that prevents certain Onyx keys from persisting to disk */
- isUsingMemoryOnlyKeys: PropTypes.bool,
-
- /** The last Onyx update ID was applied to the client */
- lastUpdateIDAppliedToClient: PropTypes.number,
-
- /** Information about any currently running demos */
- demoInfo: PropTypes.shape({
- money2020: PropTypes.shape({
- isBeginningDemo: PropTypes.bool,
- }),
- }),
-};
-
-const defaultProps = {
- isUsingMemoryOnlyKeys: false,
- session: {
- email: null,
- },
- lastOpenedPublicRoomID: null,
- lastUpdateIDAppliedToClient: null,
- demoInfo: {},
-};
-
-function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, session, lastOpenedPublicRoomID, demoInfo}) {
+function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoomID, demoInfo, isUsingMemoryOnlyKeys = false}: AuthScreensProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth, styles);
@@ -157,8 +143,8 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio
const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH;
const chatShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_CHAT;
const currentUrl = getCurrentUrl();
- const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(currentUrl, session.email);
- const shouldGetAllData = isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession() || isLoggingInAsNewUser;
+ const isLoggingInAsNewUser = !!session?.email && SessionUtils.isLoggingInAsNewUser(currentUrl, session.email);
+ const shouldGetAllData = !!isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession() || isLoggingInAsNewUser;
// Sign out the current user if we're transitioning with a different user
const isTransitioning = currentUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS);
if (isLoggingInAsNewUser && isTransitioning) {
@@ -194,12 +180,14 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio
App.reconnectApp(lastUpdateIDAppliedToClient);
}
+ PriorityMode.autoSwitchToFocusMode();
+
App.setUpPoliciesAndNavigate(session);
App.redirectThirdPartyDesktopSignIn();
// Check if we should be running any demos immediately after signing in.
- if (lodashGet(demoInfo, 'money2020.isBeginningDemo', false)) {
+ if (demoInfo?.money2020?.isBeginningDemo) {
Navigation.navigate(ROUTES.MONEY2020, CONST.NAVIGATION.TYPE.FORCED_UP);
}
if (lastOpenedPublicRoomID) {
@@ -262,15 +250,7 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio
return (
-
+
true);
-export default withOnyx({
+export default withOnyx({
session: {
key: ONYXKEYS.SESSION,
},
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
deleted file mode 100644
index be803e62a98b..000000000000
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ /dev/null
@@ -1,264 +0,0 @@
-import {CardStyleInterpolators, createStackNavigator} from '@react-navigation/stack';
-import React, {useMemo} from 'react';
-import _ from 'underscore';
-import useThemeStyles from '@styles/useThemeStyles';
-import SCREENS from '@src/SCREENS';
-
-/**
- * Create a modal stack navigator with an array of sub-screens.
- *
- * @param {Object} screens key/value pairs where the key is the name of the screen and the value is a functon that returns the lazy-loaded component
- * @returns {Function}
- */
-function createModalStackNavigator(screens) {
- const ModalStackNavigator = createStackNavigator();
-
- function ModalStack() {
- const styles = useThemeStyles();
-
- const defaultSubRouteOptions = useMemo(
- () => ({
- cardStyle: styles.navigationScreenCardStyle,
- headerShown: false,
- cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
- }),
- [styles],
- );
-
- return (
-
- {_.map(screens, (getComponent, name) => (
-
- ))}
-
- );
- }
-
- ModalStack.displayName = 'ModalStack';
-
- return ModalStack;
-}
-
-const MoneyRequestModalStackNavigator = createModalStackNavigator({
- Money_Request: () => require('../../../pages/iou/MoneyRequestSelectorPage').default,
- Money_Request_Amount: () => require('../../../pages/iou/steps/NewRequestAmountPage').default,
- Money_Request_Participants: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default,
- Money_Request_Confirmation: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default,
- Money_Request_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default,
- Money_Request_Date: () => require('../../../pages/iou/MoneyRequestDatePage').default,
- Money_Request_Description: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default,
- Money_Request_Category: () => require('../../../pages/iou/MoneyRequestCategoryPage').default,
- Money_Request_Tag: () => require('../../../pages/iou/MoneyRequestTagPage').default,
- Money_Request_Merchant: () => require('../../../pages/iou/MoneyRequestMerchantPage').default,
- IOU_Send_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default,
- IOU_Send_Add_Debit_Card: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default,
- IOU_Send_Enable_Payments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default,
- Money_Request_Waypoint: () => require('../../../pages/iou/NewDistanceRequestWaypointEditorPage').default,
- Money_Request_Edit_Waypoint: () => require('../../../pages/iou/MoneyRequestEditWaypointPage').default,
- Money_Request_Distance: () => require('../../../pages/iou/NewDistanceRequestPage').default,
- Money_Request_Receipt: () => require('../../../pages/EditRequestReceiptPage').default,
-});
-
-const SplitDetailsModalStackNavigator = createModalStackNavigator({
- SplitDetails_Root: () => require('../../../pages/iou/SplitBillDetailsPage').default,
- SplitDetails_Edit_Request: () => require('../../../pages/EditSplitBillPage').default,
- SplitDetails_Edit_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default,
-});
-
-const DetailsModalStackNavigator = createModalStackNavigator({
- Details_Root: () => require('../../../pages/DetailsPage').default,
-});
-
-const ProfileModalStackNavigator = createModalStackNavigator({
- Profile_Root: () => require('../../../pages/ProfilePage').default,
-});
-
-const ReportDetailsModalStackNavigator = createModalStackNavigator({
- Report_Details_Root: () => require('../../../pages/ReportDetailsPage').default,
- Report_Details_Share_Code: () => require('../../../pages/home/report/ReportDetailsShareCodePage').default,
-});
-
-const ReportSettingsModalStackNavigator = createModalStackNavigator({
- Report_Settings_Root: () => require('../../../pages/settings/Report/ReportSettingsPage').default,
- Report_Settings_Room_Name: () => require('../../../pages/settings/Report/RoomNamePage').default,
- Report_Settings_Notification_Preferences: () => require('../../../pages/settings/Report/NotificationPreferencePage').default,
- Report_Settings_Write_Capability: () => require('../../../pages/settings/Report/WriteCapabilityPage').default,
-});
-
-const TaskModalStackNavigator = createModalStackNavigator({
- Task_Title: () => require('../../../pages/tasks/TaskTitlePage').default,
- Task_Description: () => require('../../../pages/tasks/TaskDescriptionPage').default,
- Task_Assignee: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default,
-});
-
-const ReportWelcomeMessageModalStackNavigator = createModalStackNavigator({
- Report_WelcomeMessage_Root: () => require('../../../pages/ReportWelcomeMessagePage').default,
-});
-
-const ReportParticipantsModalStackNavigator = createModalStackNavigator({
- ReportParticipants_Root: () => require('../../../pages/ReportParticipantsPage').default,
-});
-
-const RoomMembersModalStackNavigator = createModalStackNavigator({
- RoomMembers_Root: () => require('../../../pages/RoomMembersPage').default,
-});
-
-const RoomInviteModalStackNavigator = createModalStackNavigator({
- RoomInvite_Root: () => require('../../../pages/RoomInvitePage').default,
-});
-
-const SearchModalStackNavigator = createModalStackNavigator({
- Search_Root: () => require('../../../pages/SearchPage').default,
-});
-
-const NewChatModalStackNavigator = createModalStackNavigator({
- NewChat_Root: () => require('../../../pages/NewChatSelectorPage').default,
-});
-
-const NewTaskModalStackNavigator = createModalStackNavigator({
- NewTask_Root: () => require('../../../pages/tasks/NewTaskPage').default,
- NewTask_TaskAssigneeSelector: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default,
- NewTask_TaskShareDestinationSelector: () => require('../../../pages/tasks/TaskShareDestinationSelectorModal').default,
- NewTask_Details: () => require('../../../pages/tasks/NewTaskDetailsPage').default,
- NewTask_Title: () => require('../../../pages/tasks/NewTaskTitlePage').default,
- NewTask_Description: () => require('../../../pages/tasks/NewTaskDescriptionPage').default,
-});
-
-const NewTeachersUniteNavigator = createModalStackNavigator({
- [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../pages/TeachersUnite/SaveTheWorldPage').default,
- I_Know_A_Teacher: () => require('../../../pages/TeachersUnite/KnowATeacherPage').default,
- Intro_School_Principal: () => require('../../../pages/TeachersUnite/ImTeacherPage').default,
- I_Am_A_Teacher: () => require('../../../pages/TeachersUnite/ImTeacherPage').default,
-});
-
-const SettingsModalStackNavigator = createModalStackNavigator({
- [SCREENS.SETTINGS.ROOT]: () => require('../../../pages/settings/InitialSettingsPage').default,
- Settings_Share_Code: () => require('../../../pages/ShareCodePage').default,
- [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../pages/workspace/WorkspacesListPage').default,
- Settings_Profile: () => require('../../../pages/settings/Profile/ProfilePage').default,
- Settings_Pronouns: () => require('../../../pages/settings/Profile/PronounsPage').default,
- Settings_Display_Name: () => require('../../../pages/settings/Profile/DisplayNamePage').default,
- Settings_Timezone: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default,
- Settings_Timezone_Select: () => require('../../../pages/settings/Profile/TimezoneSelectPage').default,
- Settings_PersonalDetails_Initial: () => require('../../../pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage').default,
- Settings_PersonalDetails_LegalName: () => require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default,
- Settings_PersonalDetails_DateOfBirth: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default,
- Settings_PersonalDetails_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default,
- Settings_PersonalDetails_Address_Country: () => require('../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default,
- Settings_ContactMethods: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default,
- Settings_ContactMethodDetails: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default,
- Settings_NewContactMethod: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default,
- [SCREENS.SETTINGS.PREFERENCES]: () => require('../../../pages/settings/Preferences/PreferencesPage').default,
- Settings_Preferences_PriorityMode: () => require('../../../pages/settings/Preferences/PriorityModePage').default,
- Settings_Preferences_Language: () => require('../../../pages/settings/Preferences/LanguagePage').default,
- // Will be uncommented as part of https://github.com/Expensify/App/issues/21670
- // Settings_Preferences_Theme: () => require('../../../pages/settings/Preferences/ThemePage').default,
- Settings_Close: () => require('../../../pages/settings/Security/CloseAccountPage').default,
- [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default,
- Settings_About: () => require('../../../pages/settings/AboutPage/AboutPage').default,
- Settings_App_Download_Links: () => require('../../../pages/settings/AppDownloadLinks').default,
- Settings_Lounge_Access: () => require('../../../pages/settings/Profile/LoungeAccessPage').default,
- Settings_Wallet: () => require('../../../pages/settings/Wallet/WalletPage').default,
- Settings_Wallet_Cards_Digital_Details_Update_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default,
- Settings_Wallet_DomainCard: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default,
- Settings_Wallet_ReportVirtualCardFraud: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default,
- Settings_Wallet_Card_Activate: () => require('../../../pages/settings/Wallet/ActivatePhysicalCardPage').default,
- [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.NAME]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardName').default,
- [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.PHONE]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardPhone').default,
- [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.ADDRESS]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardAddress').default,
- [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.CONFIRM]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardConfirm').default,
- Settings_Wallet_Transfer_Balance: () => require('../../../pages/settings/Wallet/TransferBalancePage').default,
- Settings_Wallet_Choose_Transfer_Account: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default,
- Settings_Wallet_EnablePayments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default,
- Settings_Add_Debit_Card: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default,
- Settings_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default,
- [SCREENS.SETTINGS.STATUS]: () => require('../../../pages/settings/Profile/CustomStatus/StatusPage').default,
- Settings_Status_Set: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default,
- Workspace_Initial: () => require('../../../pages/workspace/WorkspaceInitialPage').default,
- Workspace_Settings: () => require('../../../pages/workspace/WorkspaceSettingsPage').default,
- Workspace_Settings_Currency: () => require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default,
- Workspace_Card: () => require('../../../pages/workspace/card/WorkspaceCardPage').default,
- Workspace_Reimburse: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default,
- Workspace_RateAndUnit: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default,
- Workspace_Bills: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default,
- Workspace_Invoices: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default,
- Workspace_Travel: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default,
- Workspace_Members: () => require('../../../pages/workspace/WorkspaceMembersPage').default,
- Workspace_Invite: () => require('../../../pages/workspace/WorkspaceInvitePage').default,
- Workspace_Invite_Message: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default,
- ReimbursementAccount: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default,
- GetAssistance: () => require('../../../pages/GetAssistancePage').default,
- Settings_TwoFactorAuth: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default,
- Settings_ReportCardLostOrDamaged: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default,
- KeyboardShortcuts: () => require('../../../pages/KeyboardShortcutsPage').default,
-});
-
-const EnablePaymentsStackNavigator = createModalStackNavigator({
- EnablePayments_Root: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default,
-});
-
-const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator({
- AddPersonalBankAccount_Root: () => require('../../../pages/AddPersonalBankAccountPage').default,
-});
-
-const ReimbursementAccountModalStackNavigator = createModalStackNavigator({
- ReimbursementAccount_Root: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default,
-});
-
-const WalletStatementStackNavigator = createModalStackNavigator({
- WalletStatement_Root: () => require('../../../pages/wallet/WalletStatementPage').default,
-});
-
-const FlagCommentStackNavigator = createModalStackNavigator({
- FlagComment_Root: () => require('../../../pages/FlagCommentPage').default,
-});
-
-const EditRequestStackNavigator = createModalStackNavigator({
- EditRequest_Root: () => require('../../../pages/EditRequestPage').default,
- EditRequest_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default,
-});
-
-const PrivateNotesModalStackNavigator = createModalStackNavigator({
- PrivateNotes_View: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default,
- PrivateNotes_List: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default,
- PrivateNotes_Edit: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default,
-});
-
-const SignInModalStackNavigator = createModalStackNavigator({
- SignIn_Root: () => require('../../../pages/signin/SignInModal').default,
-});
-const ReferralModalStackNavigator = createModalStackNavigator({
- Referral_Details: () => require('../../../pages/ReferralDetailsPage').default,
-});
-
-export {
- MoneyRequestModalStackNavigator,
- SplitDetailsModalStackNavigator,
- DetailsModalStackNavigator,
- ProfileModalStackNavigator,
- ReportDetailsModalStackNavigator,
- TaskModalStackNavigator,
- ReportSettingsModalStackNavigator,
- ReportWelcomeMessageModalStackNavigator,
- ReportParticipantsModalStackNavigator,
- SearchModalStackNavigator,
- NewChatModalStackNavigator,
- NewTaskModalStackNavigator,
- SettingsModalStackNavigator,
- EnablePaymentsStackNavigator,
- AddPersonalBankAccountModalStackNavigator,
- ReimbursementAccountModalStackNavigator,
- WalletStatementStackNavigator,
- FlagCommentStackNavigator,
- EditRequestStackNavigator,
- PrivateNotesModalStackNavigator,
- NewTeachersUniteNavigator,
- SignInModalStackNavigator,
- RoomMembersModalStackNavigator,
- RoomInviteModalStackNavigator,
- ReferralModalStackNavigator,
-};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
new file mode 100644
index 000000000000..163423036362
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -0,0 +1,294 @@
+import {ParamListBase} from '@react-navigation/routers';
+import {CardStyleInterpolators, createStackNavigator, StackNavigationOptions} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
+import type {
+ AddPersonalBankAccountNavigatorParamList,
+ DetailsNavigatorParamList,
+ EditRequestNavigatorParamList,
+ EnablePaymentsNavigatorParamList,
+ FlagCommentNavigatorParamList,
+ MoneyRequestNavigatorParamList,
+ NewChatNavigatorParamList,
+ NewTaskNavigatorParamList,
+ ParticipantsNavigatorParamList,
+ PrivateNotesNavigatorParamList,
+ ProfileNavigatorParamList,
+ ReferralDetailsNavigatorParamList,
+ ReimbursementAccountNavigatorParamList,
+ ReportDetailsNavigatorParamList,
+ ReportSettingsNavigatorParamList,
+ ReportWelcomeMessageNavigatorParamList,
+ RoomInviteNavigatorParamList,
+ RoomMembersNavigatorParamList,
+ SearchNavigatorParamList,
+ SettingsNavigatorParamList,
+ SignInNavigatorParamList,
+ SplitDetailsNavigatorParamList,
+ TaskDetailsNavigatorParamList,
+ TeachersUniteNavigatorParamList,
+ WalletStatementNavigatorParamList,
+} from '@navigation/types';
+import useThemeStyles from '@styles/useThemeStyles';
+import SCREENS from '@src/SCREENS';
+import type {Screen} from '@src/SCREENS';
+
+type Screens = Partial React.ComponentType>>;
+
+/**
+ * Create a modal stack navigator with an array of sub-screens.
+ *
+ * @param screens key/value pairs where the key is the name of the screen and the value is a functon that returns the lazy-loaded component
+ */
+function createModalStackNavigator(screens: Screens): React.ComponentType {
+ const ModalStackNavigator = createStackNavigator();
+
+ function ModalStack() {
+ const styles = useThemeStyles();
+
+ const defaultSubRouteOptions = useMemo(
+ (): StackNavigationOptions => ({
+ cardStyle: styles.navigationScreenCardStyle,
+ headerShown: false,
+ cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
+ }),
+ [styles],
+ );
+
+ return (
+
+ {Object.keys(screens as Required).map((name) => (
+ )[name as Screen]}
+ />
+ ))}
+
+ );
+ }
+
+ ModalStack.displayName = 'ModalStack';
+
+ return ModalStack;
+}
+
+const MoneyRequestModalStackNavigator = createModalStackNavigator({
+ [SCREENS.MONEY_REQUEST.ROOT]: () => require('../../../pages/iou/MoneyRequestSelectorPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.AMOUNT]: () => require('../../../pages/iou/steps/NewRequestAmountPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.DESCRIPTION]: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.CATEGORY]: () => require('../../../pages/iou/MoneyRequestCategoryPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.TAG]: () => require('../../../pages/iou/MoneyRequestTagPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.MERCHANT]: () => require('../../../pages/iou/MoneyRequestMerchantPage').default as React.ComponentType,
+ [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType,
+ [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType,
+ [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/NewDistanceRequestWaypointEditorPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.EDIT_WAYPOINT]: () => require('../../../pages/iou/MoneyRequestEditWaypointPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.DISTANCE]: () => require('../../../pages/iou/NewDistanceRequestPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../pages/EditRequestReceiptPage').default as React.ComponentType,
+});
+
+const SplitDetailsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SPLIT_DETAILS.ROOT]: () => require('../../../pages/iou/SplitBillDetailsPage').default as React.ComponentType,
+ [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: () => require('../../../pages/EditSplitBillPage').default as React.ComponentType,
+ [SCREENS.SPLIT_DETAILS.EDIT_CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType,
+});
+
+const DetailsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.DETAILS_ROOT]: () => require('../../../pages/DetailsPage').default as React.ComponentType,
+});
+
+const ProfileModalStackNavigator = createModalStackNavigator({
+ [SCREENS.PROFILE_ROOT]: () => require('../../../pages/ProfilePage').default as React.ComponentType,
+});
+
+const ReportDetailsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_DETAILS.ROOT]: () => require('../../../pages/ReportDetailsPage').default as React.ComponentType,
+ [SCREENS.REPORT_DETAILS.SHARE_CODE]: () => require('../../../pages/home/report/ReportDetailsShareCodePage').default as React.ComponentType,
+});
+
+const ReportSettingsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../pages/settings/Report/ReportSettingsPage').default as React.ComponentType,
+ [SCREENS.REPORT_SETTINGS.ROOM_NAME]: () => require('../../../pages/settings/Report/RoomNamePage').default as React.ComponentType,
+ [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: () => require('../../../pages/settings/Report/NotificationPreferencePage').default as React.ComponentType,
+ [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: () => require('../../../pages/settings/Report/WriteCapabilityPage').default as React.ComponentType,
+});
+
+const TaskModalStackNavigator = createModalStackNavigator({
+ [SCREENS.TASK.TITLE]: () => require('../../../pages/tasks/TaskTitlePage').default as React.ComponentType,
+ [SCREENS.TASK.DESCRIPTION]: () => require('../../../pages/tasks/TaskDescriptionPage').default as React.ComponentType,
+ [SCREENS.TASK.ASSIGNEE]: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default as React.ComponentType,
+});
+
+const ReportWelcomeMessageModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_WELCOME_MESSAGE_ROOT]: () => require('../../../pages/ReportWelcomeMessagePage').default as React.ComponentType,
+});
+
+const ReportParticipantsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_PARTICIPANTS_ROOT]: () => require('../../../pages/ReportParticipantsPage').default as React.ComponentType,
+});
+
+const RoomMembersModalStackNavigator = createModalStackNavigator({
+ [SCREENS.ROOM_MEMBERS_ROOT]: () => require('../../../pages/RoomMembersPage').default as React.ComponentType,
+});
+
+const RoomInviteModalStackNavigator = createModalStackNavigator({
+ [SCREENS.ROOM_INVITE_ROOT]: () => require('../../../pages/RoomInvitePage').default as React.ComponentType,
+});
+
+const SearchModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SEARCH_ROOT]: () => require('../../../pages/SearchPage').default as React.ComponentType,
+});
+
+const NewChatModalStackNavigator = createModalStackNavigator({
+ [SCREENS.NEW_CHAT_ROOT]: () => require('../../../pages/NewChatSelectorPage').default as React.ComponentType,
+});
+
+const NewTaskModalStackNavigator = createModalStackNavigator({
+ [SCREENS.NEW_TASK.ROOT]: () => require('../../../pages/tasks/NewTaskPage').default as React.ComponentType,
+ [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default as React.ComponentType,
+ [SCREENS.NEW_TASK.TASK_SHARE_DESTINATION_SELECTOR]: () => require('../../../pages/tasks/TaskShareDestinationSelectorModal').default as React.ComponentType,
+ [SCREENS.NEW_TASK.DETAILS]: () => require('../../../pages/tasks/NewTaskDetailsPage').default as React.ComponentType,
+ [SCREENS.NEW_TASK.TITLE]: () => require('../../../pages/tasks/NewTaskTitlePage').default as React.ComponentType,
+ [SCREENS.NEW_TASK.DESCRIPTION]: () => require('../../../pages/tasks/NewTaskDescriptionPage').default as React.ComponentType,
+});
+
+const NewTeachersUniteNavigator = createModalStackNavigator({
+ [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../pages/TeachersUnite/SaveTheWorldPage').default as React.ComponentType,
+ [SCREENS.I_KNOW_A_TEACHER]: () => require('../../../pages/TeachersUnite/KnowATeacherPage').default as React.ComponentType,
+ [SCREENS.INTRO_SCHOOL_PRINCIPAL]: () => require('../../../pages/TeachersUnite/ImTeacherPage').default as React.ComponentType,
+ [SCREENS.I_AM_A_TEACHER]: () => require('../../../pages/TeachersUnite/ImTeacherPage').default as React.ComponentType,
+});
+
+const SettingsModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SETTINGS.ROOT]: () => require('../../../pages/settings/InitialSettingsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../pages/workspace/WorkspacesListPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PROFILE]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PRONOUNS]: () => require('../../../pages/settings/Profile/PronounsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.DISPLAY_NAME]: () => require('../../../pages/settings/Profile/DisplayNamePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.TIMEZONE]: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.TIMEZONE_SELECT]: () => require('../../../pages/settings/Profile/TimezoneSelectPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PERSONAL_DETAILS_INITIAL]: () => require('../../../pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PERSONAL_DETAILS_LEGAL_NAME]: () => require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PERSONAL_DETAILS_DATE_OF_BIRTH]: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PERSONAL_DETAILS_ADDRESS]: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PERSONAL_DETAILS_ADDRESS_COUNTRY]: () => require('../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.CONTACT_METHODS]: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.CONTACT_METHOD_DETAILS]: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.NEW_CONTACT_METHOD]: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PREFERENCES]: () => require('../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PREFERENCES_PRIORITY_MODE]: () => require('../../../pages/settings/Preferences/PriorityModePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PREFERENCES_LANGUAGE]: () => require('../../../pages/settings/Preferences/LanguagePage').default as React.ComponentType,
+ // Will be uncommented as part of https://github.com/Expensify/App/issues/21670
+ // [SCREENS.SETTINGS.PREFERENCES_THEME]: () => require('../../../pages/settings/Preferences/ThemePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.CLOSE]: () => require('../../../pages/settings/Security/CloseAccountPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.ABOUT]: () => require('../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: () => require('../../../pages/settings/AppDownloadLinks').default as React.ComponentType,
+ [SCREENS.SETTINGS.LOUNGE_ACCESS]: () => require('../../../pages/settings/Profile/LoungeAccessPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET]: () => require('../../../pages/settings/Wallet/WalletPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_DOMAIN_CARD]: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CARD_ACTIVATE]: () => require('../../../pages/settings/Wallet/ActivatePhysicalCardPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.NAME]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardName').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.PHONE]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardPhone').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.ADDRESS]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardAddress').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.CONFIRM]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardConfirm').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_TRANSFER_BALANCE]: () => require('../../../pages/settings/Wallet/TransferBalancePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.WALLET_ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.STATUS]: () => require('../../../pages/settings/Profile/CustomStatus/StatusPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.STATUS_SET]: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.INITIAL]: () => require('../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.SETTINGS]: () => require('../../../pages/workspace/WorkspaceSettingsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CARD]: () => require('../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.RATE_AND_UNIT]: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.BILLS]: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.INVOICES]: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.INVITE]: () => require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').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,
+ [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default as React.ComponentType,
+ [SCREENS.KEYBOARD_SHORTCUTS]: () => require('../../../pages/KeyboardShortcutsPage').default as React.ComponentType,
+});
+
+const EnablePaymentsStackNavigator = createModalStackNavigator({
+ [SCREENS.ENABLE_PAYMENTS_ROOT]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
+});
+
+const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator({
+ [SCREENS.ADD_PERSONAL_BANK_ACCOUNT_ROOT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType,
+});
+
+const ReimbursementAccountModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REIMBURSEMENT_ACCOUNT_ROOT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
+});
+
+const WalletStatementStackNavigator = createModalStackNavigator({
+ [SCREENS.WALLET_STATEMENT_ROOT]: () => require('../../../pages/wallet/WalletStatementPage').default as React.ComponentType,
+});
+
+const FlagCommentStackNavigator = createModalStackNavigator({
+ [SCREENS.FLAG_COMMENT_ROOT]: () => require('../../../pages/FlagCommentPage').default as React.ComponentType,
+});
+
+const EditRequestStackNavigator = createModalStackNavigator({
+ [SCREENS.EDIT_REQUEST.ROOT]: () => require('../../../pages/EditRequestPage').default as React.ComponentType,
+ [SCREENS.EDIT_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType,
+});
+
+const PrivateNotesModalStackNavigator = createModalStackNavigator({
+ [SCREENS.PRIVATE_NOTES.VIEW]: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default as React.ComponentType,
+ [SCREENS.PRIVATE_NOTES.LIST]: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default as React.ComponentType,
+ [SCREENS.PRIVATE_NOTES.EDIT]: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default as React.ComponentType,
+});
+
+const SignInModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SIGN_IN_ROOT]: () => require('../../../pages/signin/SignInModal').default as React.ComponentType,
+});
+
+const ReferralModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REFERRAL_DETAILS]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType,
+});
+
+export {
+ MoneyRequestModalStackNavigator,
+ SplitDetailsModalStackNavigator,
+ DetailsModalStackNavigator,
+ ProfileModalStackNavigator,
+ ReportDetailsModalStackNavigator,
+ TaskModalStackNavigator,
+ ReportSettingsModalStackNavigator,
+ ReportWelcomeMessageModalStackNavigator,
+ ReportParticipantsModalStackNavigator,
+ SearchModalStackNavigator,
+ NewChatModalStackNavigator,
+ NewTaskModalStackNavigator,
+ SettingsModalStackNavigator,
+ EnablePaymentsStackNavigator,
+ AddPersonalBankAccountModalStackNavigator,
+ ReimbursementAccountModalStackNavigator,
+ WalletStatementStackNavigator,
+ FlagCommentStackNavigator,
+ EditRequestStackNavigator,
+ PrivateNotesModalStackNavigator,
+ NewTeachersUniteNavigator,
+ SignInModalStackNavigator,
+ RoomMembersModalStackNavigator,
+ RoomInviteModalStackNavigator,
+ ReferralModalStackNavigator,
+};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
similarity index 89%
rename from src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js
rename to src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index d23b03c8c73e..228ea6bd3dce 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -2,10 +2,11 @@ import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper';
import getCurrentUrl from '@libs/Navigation/currentUrl';
+import type {CentralPaneNavigatorParamList} from '@navigation/types';
import useThemeStyles from '@styles/useThemeStyles';
import SCREENS from '@src/SCREENS';
-const Stack = createStackNavigator();
+const Stack = createStackNavigator();
const url = getCurrentUrl();
const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.tsx
similarity index 100%
rename from src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.js
rename to src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.tsx
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.tsx
similarity index 100%
rename from src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.js
rename to src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.tsx
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
similarity index 79%
rename from src/libs/Navigation/AppNavigator/Navigators/Overlay.js
rename to src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
index 44d996282617..31eb818b60dc 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
@@ -1,5 +1,4 @@
import {useCardAnimation} from '@react-navigation/stack';
-import PropTypes from 'prop-types';
import React from 'react';
import {Animated, View} from 'react-native';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
@@ -7,12 +6,12 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
-const propTypes = {
+type OverlayProps = {
/* Callback to close the modal */
- onPress: PropTypes.func.isRequired,
+ onPress: () => void;
};
-function Overlay(props) {
+function Overlay({onPress}: OverlayProps) {
const styles = useThemeStyles();
const {current} = useCardAnimation();
const {translate} = useLocalize();
@@ -20,20 +19,20 @@ function Overlay(props) {
return (
- {/* In the latest Electron version buttons can't be both clickable and draggable.
- That's why we added this workaround. Because of two Pressable components on the desktop app
+ {/* In the latest Electron version buttons can't be both clickable and draggable.
+ That's why we added this workaround. Because of two Pressable components on the desktop app
we have 30px draggable ba at the top and the rest of the dimmed area is clickable. On other devices,
everything behaves normally like one big pressable */}
;
-const propTypes = {
- /* Navigation functions provided by React Navigation */
- navigation: PropTypes.shape({
- goBack: PropTypes.func.isRequired,
- }).isRequired,
-};
+const Stack = createStackNavigator();
-function RightModalNavigator(props) {
+function RightModalNavigator({navigation}: RightModalNavigatorProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const screenOptions = useMemo(() => RHPScreenOptions(styles), [styles]);
return (
- {!isSmallScreenWidth && }
+ {!isSmallScreenWidth && }
();
function PublicScreens() {
return (
@@ -55,4 +56,5 @@ function PublicScreens() {
}
PublicScreens.displayName = 'PublicScreens';
+
export default PublicScreens;
diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js
deleted file mode 100644
index 02354b90591f..000000000000
--- a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import {CardStyleInterpolators} from '@react-navigation/stack';
-
-/**
- * RHP stack navigator screen options generator function
- * @function
- * @param {Object} styles - The styles object
- * @returns {Object} - The screen options object
- */
-const RHPScreenOptions = (styles) => ({
- headerShown: false,
- animationEnabled: true,
- gestureDirection: 'horizontal',
- cardStyle: styles.navigationScreenCardStyle,
- cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
-});
-
-export default RHPScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts b/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts
new file mode 100644
index 000000000000..6b56bb00cf56
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts
@@ -0,0 +1,17 @@
+import {CardStyleInterpolators, StackNavigationOptions} from '@react-navigation/stack';
+import styles from '@styles/styles';
+
+/**
+ * RHP stack navigator screen options generator function
+ * @param themeStyles - The styles object
+ * @returns The screen options object
+ */
+const RHPScreenOptions = (themeStyles: typeof styles): StackNavigationOptions => ({
+ headerShown: false,
+ animationEnabled: true,
+ gestureDirection: 'horizontal',
+ cardStyle: themeStyles.navigationScreenCardStyle,
+ cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
+});
+
+export default RHPScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js
deleted file mode 100644
index bb7acddb188c..000000000000
--- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import {useEffect} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import usePermissions from '@hooks/usePermissions';
-import * as ReportUtils from '@libs/ReportUtils';
-import reportPropTypes from '@pages/reportPropTypes';
-import * as App from '@userActions/App';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-const propTypes = {
- /** Available reports that would be displayed in this navigator */
- reports: PropTypes.objectOf(reportPropTypes),
-
- /** The policies which the user has access to */
- policies: PropTypes.objectOf(
- PropTypes.shape({
- /** The policy name */
- name: PropTypes.string,
-
- /** The type of the policy */
- type: PropTypes.string,
- }),
- ),
-
- isFirstTimeNewExpensifyUser: PropTypes.bool,
-
- /** Navigation route context info provided by react navigation */
- route: PropTypes.shape({
- /** Route specific parameters used on this screen */
- params: PropTypes.shape({
- /** If the admin room should be opened */
- openOnAdminRoom: PropTypes.bool,
-
- /** The ID of the report this screen should display */
- reportID: PropTypes.string,
- }),
- }).isRequired,
-
- /* Navigation functions provided by React Navigation */
- navigation: PropTypes.shape({
- setParams: PropTypes.func.isRequired,
- }).isRequired,
-};
-
-const defaultProps = {
- reports: {},
- policies: {},
- isFirstTimeNewExpensifyUser: false,
-};
-
-/**
- * Get the most recently accessed report for the user
- *
- * @param {Object} reports
- * @param {Boolean} ignoreDefaultRooms
- * @param {Object} policies
- * @param {Boolean} isFirstTimeNewExpensifyUser
- * @param {Boolean} openOnAdminRoom
- * @returns {Number}
- */
-const getLastAccessedReportID = (reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom) => {
- const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom);
- return lodashGet(lastReport, 'reportID');
-};
-
-// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params
-function ReportScreenIDSetter({route, reports, policies, isFirstTimeNewExpensifyUser, navigation}) {
- const {canUseDefaultRooms} = usePermissions();
-
- useEffect(() => {
- // Don't update if there is a reportID in the params already
- if (lodashGet(route, 'params.reportID', null)) {
- App.confirmReadyToOpenApp();
- return;
- }
-
- // If there is no reportID in route, try to find last accessed and use it for setParams
- const reportID = getLastAccessedReportID(reports, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, lodashGet(route, 'params.openOnAdminRoom', false));
-
- // It's possible that reports aren't fully loaded yet
- // in that case the reportID is undefined
- if (reportID) {
- navigation.setParams({reportID: String(reportID)});
- } else {
- App.confirmReadyToOpenApp();
- }
- }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser]);
-
- // The ReportScreen without the reportID set will display a skeleton
- // until the reportID is loaded and set in the route param
- return null;
-}
-
-ReportScreenIDSetter.propTypes = propTypes;
-ReportScreenIDSetter.defaultProps = defaultProps;
-ReportScreenIDSetter.displayName = 'ReportScreenIDSetter';
-
-export default withOnyx({
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- allowStaleData: true,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- allowStaleData: true,
- },
- isFirstTimeNewExpensifyUser: {
- key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
- initialValue: false,
- },
-})(ReportScreenIDSetter);
diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts
new file mode 100644
index 000000000000..8be512962981
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts
@@ -0,0 +1,80 @@
+import {useEffect} from 'react';
+import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx';
+import usePermissions from '@hooks/usePermissions';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as App from '@userActions/App';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy, Report} from '@src/types/onyx';
+import type {ReportScreenWrapperProps} from './ReportScreenWrapper';
+
+type ReportScreenIDSetterComponentProps = {
+ /** Available reports that would be displayed in this navigator */
+ reports: OnyxCollection;
+
+ /** The policies which the user has access to */
+ policies: OnyxCollection;
+
+ /** Whether user is a new user */
+ isFirstTimeNewExpensifyUser: OnyxEntry;
+};
+
+type ReportScreenIDSetterProps = ReportScreenIDSetterComponentProps & ReportScreenWrapperProps;
+
+/**
+ * Get the most recently accessed report for the user
+ */
+const getLastAccessedReportID = (
+ reports: OnyxCollection,
+ ignoreDefaultRooms: boolean,
+ policies: OnyxCollection,
+ isFirstTimeNewExpensifyUser: OnyxEntry,
+ openOnAdminRoom: boolean,
+): string | undefined => {
+ const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, !!isFirstTimeNewExpensifyUser, openOnAdminRoom);
+ return lastReport?.reportID;
+};
+
+// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params
+function ReportScreenIDSetter({route, reports, policies, navigation, isFirstTimeNewExpensifyUser = false}: ReportScreenIDSetterProps) {
+ const {canUseDefaultRooms} = usePermissions();
+
+ useEffect(() => {
+ // Don't update if there is a reportID in the params already
+ if (route?.params?.reportID) {
+ App.confirmReadyToOpenApp();
+ return;
+ }
+
+ // If there is no reportID in route, try to find last accessed and use it for setParams
+ const reportID = getLastAccessedReportID(reports, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, !!reports?.params?.openOnAdminRoom);
+
+ // It's possible that reports aren't fully loaded yet
+ // in that case the reportID is undefined
+ if (reportID) {
+ navigation.setParams({reportID: String(reportID)});
+ } else {
+ App.confirmReadyToOpenApp();
+ }
+ }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser]);
+
+ // The ReportScreen without the reportID set will display a skeleton
+ // until the reportID is loaded and set in the route param
+ return null;
+}
+
+ReportScreenIDSetter.displayName = 'ReportScreenIDSetter';
+
+export default withOnyx({
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ allowStaleData: true,
+ },
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ allowStaleData: true,
+ },
+ isFirstTimeNewExpensifyUser: {
+ key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
+ initialValue: false,
+ },
+})(ReportScreenIDSetter);
diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js
deleted file mode 100644
index 87a8a4abc687..000000000000
--- a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import ReportScreen from '@pages/home/ReportScreen';
-import ReportScreenIDSetter from './ReportScreenIDSetter';
-
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: PropTypes.shape({
- /** Route specific parameters used on this screen */
- params: PropTypes.shape({
- /** If the admin room should be opened */
- openOnAdminRoom: PropTypes.bool,
-
- /** The ID of the report this screen should display */
- reportID: PropTypes.string,
- }),
- }).isRequired,
-
- /* Navigation functions provided by React Navigation */
- navigation: PropTypes.shape({
- setParams: PropTypes.func.isRequired,
- }).isRequired,
-};
-
-const defaultProps = {};
-
-function ReportScreenWrapper(props) {
- // The ReportScreen without the reportID set will display a skeleton
- // until the reportID is loaded and set in the route param
- return (
- <>
-
-
- >
- );
-}
-
-ReportScreenWrapper.propTypes = propTypes;
-ReportScreenWrapper.defaultProps = defaultProps;
-ReportScreenWrapper.displayName = 'ReportScreenWrapper';
-
-export default ReportScreenWrapper;
diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx
new file mode 100644
index 000000000000..20922fd785ce
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx
@@ -0,0 +1,28 @@
+import {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import ReportScreen from '@pages/home/ReportScreen';
+import SCREENS from '@src/SCREENS';
+import ReportScreenIDSetter from './ReportScreenIDSetter';
+
+type ReportScreenWrapperProps = StackScreenProps;
+
+function ReportScreenWrapper({route, navigation}: ReportScreenWrapperProps) {
+ // The ReportScreen without the reportID set will display a skeleton
+ // until the reportID is loaded and set in the route param
+ return (
+ <>
+ {/* @ts-expect-error Error will be resolved after ReportScreen migration to TypeScript */}
+
+
+ >
+ );
+}
+
+ReportScreenWrapper.displayName = 'ReportScreenWrapper';
+
+export default ReportScreenWrapper;
+export type {ReportScreenWrapperProps};
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.js
deleted file mode 100644
index 5d3eb38d49dc..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import {StackRouter} from '@react-navigation/native';
-import lodashFindLast from 'lodash/findLast';
-import _ from 'underscore';
-import NAVIGATORS from '@src/NAVIGATORS';
-import SCREENS from '@src/SCREENS';
-
-/**
- * @param {Object} state - react-navigation state
- * @returns {Boolean}
- */
-const isAtLeastOneCentralPaneNavigatorInState = (state) => _.find(state.routes, (r) => r.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
-
-/**
- * @param {Object} state - react-navigation state
- * @returns {String}
- */
-const getTopMostReportIDFromRHP = (state) => {
- if (!state) {
- return '';
- }
- const topmostRightPane = lodashFindLast(state.routes, (route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
-
- if (topmostRightPane) {
- return getTopMostReportIDFromRHP(topmostRightPane.state);
- }
-
- const topmostRoute = lodashFindLast(state.routes);
-
- if (topmostRoute.state) {
- return getTopMostReportIDFromRHP(topmostRoute.state);
- }
-
- if (topmostRoute.params && topmostRoute.params.reportID) {
- return topmostRoute.params.reportID;
- }
-
- return '';
-};
-/**
- * Adds report route without any specific reportID to the state.
- * The report screen will self set proper reportID param based on the helper function findLastAccessedReport (look at ReportScreenWrapper for more info)
- *
- * @param {Object} state - react-navigation state
- */
-const addCentralPaneNavigatorRoute = (state) => {
- const reportID = getTopMostReportIDFromRHP(state);
- const centralPaneNavigatorRoute = {
- name: NAVIGATORS.CENTRAL_PANE_NAVIGATOR,
- state: {
- routes: [
- {
- name: SCREENS.REPORT,
- params: {
- reportID,
- },
- },
- ],
- },
- };
- state.routes.splice(1, 0, centralPaneNavigatorRoute);
- // eslint-disable-next-line no-param-reassign
- state.index = state.routes.length - 1;
-};
-
-function CustomRouter(options) {
- const stackRouter = StackRouter(options);
-
- return {
- ...stackRouter,
- getRehydratedState(partialState, {routeNames, routeParamList}) {
- // Make sure that there is at least one CentralPaneNavigator (ReportScreen by default) in the state if this is a wide layout
- if (!isAtLeastOneCentralPaneNavigatorInState(partialState) && !options.getIsSmallScreenWidth()) {
- // If we added a route we need to make sure that the state.stale is true to generate new key for this route
- // eslint-disable-next-line no-param-reassign
- partialState.stale = true;
- addCentralPaneNavigatorRoute(partialState);
- }
- const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList});
- return state;
- },
- };
-}
-
-export default CustomRouter;
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
new file mode 100644
index 000000000000..435ebc00362b
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts
@@ -0,0 +1,82 @@
+import {NavigationState, PartialState, RouterConfigOptions, StackNavigationState, StackRouter} from '@react-navigation/native';
+import {ParamListBase} from '@react-navigation/routers';
+import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import type {ResponsiveStackNavigatorRouterOptions} from './types';
+
+type State = NavigationState | PartialState;
+
+const isAtLeastOneCentralPaneNavigatorInState = (state: State): boolean => !!state.routes.find((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
+
+const getTopMostReportIDFromRHP = (state: State): string => {
+ if (!state) {
+ return '';
+ }
+
+ const topmostRightPane = state.routes.filter((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR).at(-1);
+
+ if (topmostRightPane?.state) {
+ return getTopMostReportIDFromRHP(topmostRightPane.state);
+ }
+
+ const topmostRoute = state.routes.at(-1);
+
+ if (topmostRoute?.state) {
+ return getTopMostReportIDFromRHP(topmostRoute.state);
+ }
+
+ if (topmostRoute?.params && 'reportID' in topmostRoute.params && typeof topmostRoute.params.reportID === 'string' && topmostRoute.params.reportID) {
+ return topmostRoute.params.reportID;
+ }
+
+ return '';
+};
+/**
+ * Adds report route without any specific reportID to the state.
+ * The report screen will self set proper reportID param based on the helper function findLastAccessedReport (look at ReportScreenWrapper for more info)
+ *
+ * @param state - react-navigation state
+ */
+const addCentralPaneNavigatorRoute = (state: State) => {
+ const reportID = getTopMostReportIDFromRHP(state);
+ const centralPaneNavigatorRoute = {
+ name: NAVIGATORS.CENTRAL_PANE_NAVIGATOR,
+ state: {
+ routes: [
+ {
+ name: SCREENS.REPORT,
+ params: {
+ reportID,
+ },
+ },
+ ],
+ },
+ };
+ state.routes.splice(1, 0, centralPaneNavigatorRoute);
+ // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style
+ (state.index as number) = state.routes.length - 1;
+};
+
+function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
+ const stackRouter = StackRouter(options);
+
+ return {
+ ...stackRouter,
+ getRehydratedState(partialState: StackNavigationState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState {
+ const isSmallScreenWidth = getIsSmallScreenWidth();
+ // Make sure that there is at least one CentralPaneNavigator (ReportScreen by default) in the state if this is a wide layout
+ if (!isAtLeastOneCentralPaneNavigatorInState(partialState) && !isSmallScreenWidth) {
+ // If we added a route we need to make sure that the state.stale is true to generate new key for this route
+
+ // eslint-disable-next-line no-param-reassign
+ (partialState.stale as boolean) = true;
+ addCentralPaneNavigatorRoute(partialState);
+ }
+ const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
+ return state;
+ },
+ };
+}
+
+export default CustomRouter;
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js
deleted file mode 100644
index ae36f4aff9ad..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native';
-import {StackView} from '@react-navigation/stack';
-import PropTypes from 'prop-types';
-import React, {useRef} from 'react';
-import useWindowDimensions from '@hooks/useWindowDimensions';
-import CustomRouter from './CustomRouter';
-
-const propTypes = {
- /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */
- isSmallScreenWidth: PropTypes.bool.isRequired,
-
- /* Children for the useNavigationBuilder hook */
- children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
-
- /* initialRouteName for this navigator */
- initialRouteName: PropTypes.oneOf([PropTypes.string, PropTypes.undefined]),
-
- /* Screen options defined for this navigator */
- // eslint-disable-next-line react/forbid-prop-types
- screenOptions: PropTypes.object,
-};
-
-const defaultProps = {
- initialRouteName: undefined,
- screenOptions: undefined,
-};
-
-function ResponsiveStackNavigator(props) {
- const {isSmallScreenWidth} = useWindowDimensions();
-
- const isSmallScreenWidthRef = useRef(isSmallScreenWidth);
-
- isSmallScreenWidthRef.current = isSmallScreenWidth;
-
- const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder(CustomRouter, {
- children: props.children,
- screenOptions: props.screenOptions,
- initialRouteName: props.initialRouteName,
- // Options for useNavigationBuilder won't update on prop change, so we need to pass a getter for the router to have the current state of isSmallScreenWidth.
- getIsSmallScreenWidth: () => isSmallScreenWidthRef.current,
- });
-
- return (
-
-
-
- );
-}
-
-ResponsiveStackNavigator.defaultProps = defaultProps;
-ResponsiveStackNavigator.propTypes = propTypes;
-ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator';
-
-export default createNavigatorFactory(ResponsiveStackNavigator);
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.tsx b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.tsx
new file mode 100644
index 000000000000..a55c74f3a479
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.tsx
@@ -0,0 +1,42 @@
+import {createNavigatorFactory, ParamListBase, StackActionHelpers, StackNavigationState, useNavigationBuilder} from '@react-navigation/native';
+import {StackNavigationEventMap, StackNavigationOptions, StackView} from '@react-navigation/stack';
+import React, {useRef} from 'react';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import CustomRouter from './CustomRouter';
+import type {ResponsiveStackNavigatorProps, ResponsiveStackNavigatorRouterOptions} from './types';
+
+function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+
+ const isSmallScreenWidthRef = useRef(isSmallScreenWidth);
+
+ isSmallScreenWidthRef.current = isSmallScreenWidth;
+
+ const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder<
+ StackNavigationState,
+ ResponsiveStackNavigatorRouterOptions,
+ StackActionHelpers,
+ StackNavigationOptions,
+ StackNavigationEventMap
+ >(CustomRouter, {
+ children: props.children,
+ screenOptions: props.screenOptions,
+ initialRouteName: props.initialRouteName,
+ });
+
+ return (
+
+
+
+ );
+}
+
+ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator';
+
+export default createNavigatorFactory, StackNavigationOptions, StackNavigationEventMap, typeof ResponsiveStackNavigator>(ResponsiveStackNavigator);
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
similarity index 52%
rename from src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js
rename to src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
index 8924b01e2acb..dd2e548064c4 100644
--- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx
@@ -1,33 +1,14 @@
-import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native';
-import {StackView} from '@react-navigation/stack';
-import PropTypes from 'prop-types';
+import {createNavigatorFactory, ParamListBase, StackActionHelpers, StackNavigationState, useNavigationBuilder} from '@react-navigation/native';
+import {StackNavigationEventMap, StackNavigationOptions, StackView} from '@react-navigation/stack';
import React, {useMemo, useRef} from 'react';
import useWindowDimensions from '@hooks/useWindowDimensions';
import NAVIGATORS from '@src/NAVIGATORS';
import CustomRouter from './CustomRouter';
+import type {ResponsiveStackNavigatorProps, ResponsiveStackNavigatorRouterOptions} from './types';
-const propTypes = {
- /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */
- isSmallScreenWidth: PropTypes.bool.isRequired,
-
- /* Children for the useNavigationBuilder hook */
- children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
-
- /* initialRouteName for this navigator */
- initialRouteName: PropTypes.oneOf([PropTypes.string, PropTypes.undefined]),
-
- /* Screen options defined for this navigator */
- // eslint-disable-next-line react/forbid-prop-types
- screenOptions: PropTypes.object,
-};
-
-const defaultProps = {
- initialRouteName: undefined,
- screenOptions: undefined,
-};
-
-function reduceReportRoutes(routes) {
- const result = [];
+type Routes = StackNavigationState['routes'];
+function reduceReportRoutes(routes: Routes): Routes {
+ const result: Routes = [];
let count = 0;
const reverseRoutes = [...routes].reverse();
@@ -46,19 +27,23 @@ function reduceReportRoutes(routes) {
return result.reverse();
}
-function ResponsiveStackNavigator(props) {
+function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) {
const {isSmallScreenWidth} = useWindowDimensions();
- const isSmallScreenWidthRef = useRef(isSmallScreenWidth);
+ const isSmallScreenWidthRef = useRef(isSmallScreenWidth);
isSmallScreenWidthRef.current = isSmallScreenWidth;
- const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder(CustomRouter, {
+ const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder<
+ StackNavigationState,
+ ResponsiveStackNavigatorRouterOptions,
+ StackActionHelpers,
+ StackNavigationOptions,
+ StackNavigationEventMap
+ >(CustomRouter, {
children: props.children,
screenOptions: props.screenOptions,
initialRouteName: props.initialRouteName,
- // Options for useNavigationBuilder won't update on prop change, so we need to pass a getter for the router to have the current state of isSmallScreenWidth.
- getIsSmallScreenWidth: () => isSmallScreenWidthRef.current,
});
const stateToRender = useMemo(() => {
@@ -84,8 +69,6 @@ function ResponsiveStackNavigator(props) {
);
}
-ResponsiveStackNavigator.defaultProps = defaultProps;
-ResponsiveStackNavigator.propTypes = propTypes;
ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator';
-export default createNavigatorFactory(ResponsiveStackNavigator);
+export default createNavigatorFactory, StackNavigationOptions, StackNavigationEventMap, typeof ResponsiveStackNavigator>(ResponsiveStackNavigator);
diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/types.ts
new file mode 100644
index 000000000000..707a0ff4498d
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/types.ts
@@ -0,0 +1,13 @@
+import {DefaultNavigatorOptions, ParamListBase, StackNavigationState, StackRouterOptions} from '@react-navigation/native';
+import {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack';
+
+type ResponsiveStackNavigatorConfig = {
+ isSmallScreenWidth: boolean;
+};
+
+type ResponsiveStackNavigatorRouterOptions = StackRouterOptions;
+
+type ResponsiveStackNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> &
+ ResponsiveStackNavigatorConfig;
+
+export type {ResponsiveStackNavigatorRouterOptions, ResponsiveStackNavigatorProps, ResponsiveStackNavigatorConfig};
diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions.js b/src/libs/Navigation/AppNavigator/defaultScreenOptions.ts
similarity index 58%
rename from src/libs/Navigation/AppNavigator/defaultScreenOptions.js
rename to src/libs/Navigation/AppNavigator/defaultScreenOptions.ts
index 3ccffb5f09ab..65a6bd052742 100644
--- a/src/libs/Navigation/AppNavigator/defaultScreenOptions.js
+++ b/src/libs/Navigation/AppNavigator/defaultScreenOptions.ts
@@ -1,4 +1,6 @@
-const defaultScreenOptions = {
+import {StackNavigationOptions} from '@react-navigation/stack';
+
+const defaultScreenOptions: StackNavigationOptions = {
cardStyle: {
overflow: 'visible',
flex: 1,
diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
similarity index 67%
rename from src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js
rename to src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
index 44fa7b6c0b09..08f18ce3ab9d 100644
--- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js
+++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
@@ -1,9 +1,13 @@
+import {StackCardInterpolationProps, StackNavigationOptions} from '@react-navigation/stack';
import getNavigationModalCardStyle from '@styles/getNavigationModalCardStyles';
+import styles from '@styles/styles';
import variables from '@styles/variables';
import CONFIG from '@src/CONFIG';
import modalCardStyleInterpolator from './modalCardStyleInterpolator';
-const commonScreenOptions = {
+type ScreenOptions = Record;
+
+const commonScreenOptions: StackNavigationOptions = {
headerShown: false,
gestureDirection: 'horizontal',
animationEnabled: true,
@@ -11,10 +15,10 @@ const commonScreenOptions = {
animationTypeForReplace: 'push',
};
-export default (isSmallScreenWidth, styles) => ({
+export default (isSmallScreenWidth: boolean, themeStyles: typeof styles): ScreenOptions => ({
rightModalNavigator: {
...commonScreenOptions,
- cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
presentation: 'transparentModal',
// We want pop in RHP since there are some flows that would work weird otherwise
@@ -32,7 +36,7 @@ export default (isSmallScreenWidth, styles) => ({
homeScreen: {
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
- cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
cardStyle: {
...getNavigationModalCardStyle(),
@@ -40,13 +44,13 @@ export default (isSmallScreenWidth, styles) => ({
// We need to translate the sidebar to not be covered by the StackNavigator so it can be clickable.
transform: [{translateX: isSmallScreenWidth ? 0 : -variables.sideBarWidth}],
- ...(isSmallScreenWidth ? {} : styles.borderRight),
+ ...(isSmallScreenWidth ? {} : themeStyles.borderRight),
},
},
- // eslint-disable-next-line rulesdir/no-negated-variables
+
fullScreen: {
...commonScreenOptions,
- cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
cardStyle: {
...getNavigationModalCardStyle(),
@@ -59,7 +63,7 @@ export default (isSmallScreenWidth, styles) => ({
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
animationEnabled: isSmallScreenWidth,
- cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
cardStyle: {
...getNavigationModalCardStyle(),
diff --git a/src/libs/Navigation/AppNavigator/index.js b/src/libs/Navigation/AppNavigator/index.tsx
similarity index 68%
rename from src/libs/Navigation/AppNavigator/index.js
rename to src/libs/Navigation/AppNavigator/index.tsx
index 0d03badf37bc..8d65f5166060 100644
--- a/src/libs/Navigation/AppNavigator/index.js
+++ b/src/libs/Navigation/AppNavigator/index.tsx
@@ -1,13 +1,12 @@
-import PropTypes from 'prop-types';
import React from 'react';
-const propTypes = {
+type AppNavigatorProps = {
/** If we have an authToken this is true */
- authenticated: PropTypes.bool.isRequired,
+ authenticated: boolean;
};
-function AppNavigator(props) {
- if (props.authenticated) {
+function AppNavigator({authenticated}: AppNavigatorProps) {
+ if (authenticated) {
const AuthScreens = require('./AuthScreens').default;
// These are the protected screens and only accessible when an authToken is present
@@ -17,6 +16,5 @@ function AppNavigator(props) {
return ;
}
-AppNavigator.propTypes = propTypes;
AppNavigator.displayName = 'AppNavigator';
export default AppNavigator;
diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
similarity index 69%
rename from src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
rename to src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
index 446d195fc466..f7e772148e79 100644
--- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js
+++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
@@ -1,8 +1,9 @@
+import type {StackCardInterpolatedStyle, StackCardInterpolationProps} from '@react-navigation/stack';
import {Animated} from 'react-native';
import getCardStyles from '@styles/cardStyles';
import variables from '@styles/variables';
-export default (isSmallScreenWidth, isFullScreenModal, {current: {progress}, inverted, layouts: {screen}}) => {
+export default (isSmallScreenWidth: boolean, isFullScreenModal: boolean, {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps): StackCardInterpolatedStyle => {
const translateX = Animated.multiply(
progress.interpolate({
inputRange: [0, 1],
diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts
index 1a4aa2d0cfb7..8be8dd1ecfae 100644
--- a/src/libs/Navigation/linkTo.ts
+++ b/src/libs/Navigation/linkTo.ts
@@ -89,7 +89,7 @@ export default function linkTo(navigation: NavigationContainerRef;
reportID: string;
};
- Money_Request_Receipt: {
+ [SCREENS.MONEY_REQUEST.RECEIPT]: {
iouType: string;
reportID: string;
};
};
type NewTaskNavigatorParamList = {
- NewTask_Root: undefined;
- NewTask_TaskAssigneeSelector: undefined;
- NewTask_TaskShareDestinationSelector: undefined;
- NewTask_Details: undefined;
- NewTask_Title: undefined;
- NewTask_Description: undefined;
+ [SCREENS.NEW_TASK.ROOT]: undefined;
+ [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: undefined;
+ [SCREENS.NEW_TASK.TASK_SHARE_DESTINATION_SELECTOR]: undefined;
+ [SCREENS.NEW_TASK.DETAILS]: undefined;
+ [SCREENS.NEW_TASK.TITLE]: undefined;
+ [SCREENS.NEW_TASK.DESCRIPTION]: undefined;
};
type TeachersUniteNavigatorParamList = {
[SCREENS.SAVE_THE_WORLD.ROOT]: undefined;
- I_Know_A_Teacher: undefined;
- Intro_School_Principal: undefined;
- I_Am_A_Teacher: undefined;
+ [SCREENS.I_KNOW_A_TEACHER]: undefined;
+ [SCREENS.INTRO_SCHOOL_PRINCIPAL]: undefined;
+ [SCREENS.I_AM_A_TEACHER]: undefined;
};
type TaskDetailsNavigatorParamList = {
- Task_Title: undefined;
- Task_Description: undefined;
- Task_Assignee: {
+ [SCREENS.TASK.TITLE]: undefined;
+ [SCREENS.TASK.DESCRIPTION]: undefined;
+ [SCREENS.TASK.ASSIGNEE]: {
reportID: string;
};
};
type EnablePaymentsNavigatorParamList = {
- EnablePayments_Root: undefined;
+ [SCREENS.ENABLE_PAYMENTS_ROOT]: undefined;
};
type SplitDetailsNavigatorParamList = {
- SplitDetails_Root: {
+ [SCREENS.SPLIT_DETAILS.ROOT]: {
reportActionID: string;
};
- SplitDetails_Edit_Request: undefined;
- SplitDetails_Edit_Currency: undefined;
+ [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: undefined;
+ [SCREENS.SPLIT_DETAILS.EDIT_CURRENCY]: undefined;
};
type AddPersonalBankAccountNavigatorParamList = {
- AddPersonalBankAccount_Root: undefined;
+ [SCREENS.ADD_PERSONAL_BANK_ACCOUNT_ROOT]: undefined;
+};
+
+type ReimbursementAccountNavigatorParamList = {
+ [SCREENS.REIMBURSEMENT_ACCOUNT_ROOT]: {
+ stepToOpen: string;
+ policyID: string;
+ };
};
type WalletStatementNavigatorParamList = {
- WalletStatement_Root: undefined;
+ [SCREENS.WALLET_STATEMENT_ROOT]: undefined;
};
type FlagCommentNavigatorParamList = {
- FlagComment_Root: {
+ [SCREENS.FLAG_COMMENT_ROOT]: {
reportID: string;
reportActionID: string;
};
};
type EditRequestNavigatorParamList = {
- EditRequest_Root: {
+ [SCREENS.EDIT_REQUEST.ROOT]: {
field: string;
threadReportID: string;
};
- EditRequest_Currency: undefined;
+ [SCREENS.EDIT_REQUEST.CURRENCY]: undefined;
};
type SignInNavigatorParamList = {
- SignIn_Root: undefined;
+ [SCREENS.SIGN_IN_ROOT]: undefined;
};
type ReferralDetailsNavigatorParamList = {
- Referral_Details: undefined;
+ [SCREENS.REFERRAL_DETAILS]: undefined;
};
type PrivateNotesNavigatorParamList = {
- PrivateNotes_View: {
+ [SCREENS.PRIVATE_NOTES.VIEW]: {
reportID: string;
accountID: string;
};
- PrivateNotes_List: {
+ [SCREENS.PRIVATE_NOTES.LIST]: {
reportID: string;
accountID: string;
};
- PrivateNotes_Edit: {
+ [SCREENS.PRIVATE_NOTES.EDIT]: {
reportID: string;
accountID: string;
};
};
type RightModalNavigatorParamList = {
- Settings: NavigatorScreenParams;
- NewChat: NavigatorScreenParams;
- Search: NavigatorScreenParams;
- Details: NavigatorScreenParams;
- Profile: NavigatorScreenParams;
- Report_Details: NavigatorScreenParams;
- Report_Settings: NavigatorScreenParams;
- Report_WelcomeMessage: NavigatorScreenParams;
- Participants: NavigatorScreenParams;
- RoomMembers: NavigatorScreenParams;
- RoomInvite: NavigatorScreenParams;
- MoneyRequest: NavigatorScreenParams;
- NewTask: NavigatorScreenParams;
- TeachersUnite: NavigatorScreenParams;
- Task_Details: NavigatorScreenParams;
- EnablePayments: NavigatorScreenParams;
- SplitDetails: NavigatorScreenParams;
- AddPersonalBankAccount: NavigatorScreenParams;
- Wallet_Statement: NavigatorScreenParams;
- Flag_Comment: NavigatorScreenParams;
- EditRequest: NavigatorScreenParams;
- SignIn: NavigatorScreenParams;
- Referral: NavigatorScreenParams;
- Private_Notes: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.SEARCH]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.REPORT_WELCOME_MESSAGE]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.ROOM_INVITE]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.ENABLE_PAYMENTS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.SPLIT_DETAILS]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.ADD_PERSONAL_BANK_ACCOUNT]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.WALLET_STATEMENT]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.FLAG_COMMENT]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.EDIT_REQUEST]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.SIGN_IN]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.REFERRAL]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: NavigatorScreenParams;
};
type PublicScreensParamList = {
@@ -390,6 +398,12 @@ type AuthScreensParamList = {
reportID: string;
source: string;
};
+ [CONST.DEMO_PAGES.SAASTR]: {
+ name: string;
+ };
+ [CONST.DEMO_PAGES.SBE]: {
+ name: string;
+ };
[SCREENS.NOT_FOUND]: undefined;
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined;
@@ -398,4 +412,40 @@ type AuthScreensParamList = {
type RootStackParamList = PublicScreensParamList & AuthScreensParamList;
-export type {NavigationRef, StackNavigationAction, CentralPaneNavigatorParamList, RootStackParamList, StateOrRoute, NavigationStateRoute, NavigationRoot};
+export type {
+ NavigationRef,
+ StackNavigationAction,
+ CentralPaneNavigatorParamList,
+ RootStackParamList,
+ StateOrRoute,
+ NavigationStateRoute,
+ NavigationRoot,
+ AuthScreensParamList,
+ RightModalNavigatorParamList,
+ PublicScreensParamList,
+ MoneyRequestNavigatorParamList,
+ SplitDetailsNavigatorParamList,
+ DetailsNavigatorParamList,
+ ProfileNavigatorParamList,
+ ReportDetailsNavigatorParamList,
+ ReportSettingsNavigatorParamList,
+ TaskDetailsNavigatorParamList,
+ ReportWelcomeMessageNavigatorParamList,
+ ParticipantsNavigatorParamList,
+ RoomMembersNavigatorParamList,
+ RoomInviteNavigatorParamList,
+ SearchNavigatorParamList,
+ NewChatNavigatorParamList,
+ NewTaskNavigatorParamList,
+ TeachersUniteNavigatorParamList,
+ SettingsNavigatorParamList,
+ EnablePaymentsNavigatorParamList,
+ AddPersonalBankAccountNavigatorParamList,
+ WalletStatementNavigatorParamList,
+ FlagCommentNavigatorParamList,
+ EditRequestNavigatorParamList,
+ PrivateNotesNavigatorParamList,
+ SignInNavigatorParamList,
+ ReferralDetailsNavigatorParamList,
+ ReimbursementAccountNavigatorParamList,
+};
diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts
index e150b8b650c1..f5c391aad09c 100644
--- a/src/libs/NetworkConnection.ts
+++ b/src/libs/NetworkConnection.ts
@@ -13,7 +13,7 @@ let hasPendingNetworkCheck = false;
// Holds all of the callbacks that need to be triggered when the network reconnects
let callbackID = 0;
-const reconnectionCallbacks: Record Promise> = {};
+const reconnectionCallbacks: Record void> = {};
/**
* Loop over all reconnection callbacks and fire each one
@@ -122,7 +122,7 @@ function listenForReconnect() {
* Register callback to fire when we reconnect
* @returns unsubscribe method
*/
-function onReconnect(callback: () => Promise): () => void {
+function onReconnect(callback: () => void): () => void {
const currentID = callbackID;
callbackID++;
reconnectionCallbacks[currentID] = callback;
diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts
similarity index 77%
rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js
rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts
index b36c0d0c7d18..5eef0b44a7d1 100644
--- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts
@@ -1,5 +1,6 @@
import Airship from '@ua/react-native-airship';
import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification';
+import ForegroundNotificationsModule from './types';
function configureForegroundNotifications() {
Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload)));
@@ -9,7 +10,9 @@ function disableForegroundNotifications() {
Airship.push.android.setForegroundDisplayPredicate(() => Promise.resolve(false));
}
-export default {
+const ForegroundNotifications: ForegroundNotificationsModule = {
configureForegroundNotifications,
disableForegroundNotifications,
};
+
+export default ForegroundNotifications;
diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts
similarity index 88%
rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js
rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts
index 0f0929951a90..e5e5665d1ea2 100644
--- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts
@@ -1,5 +1,6 @@
import Airship, {iOS} from '@ua/react-native-airship';
import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification';
+import ForegroundNotificationsModule from './types';
function configureForegroundNotifications() {
// Set our default iOS foreground presentation to be loud with a banner
@@ -20,7 +21,9 @@ function disableForegroundNotifications() {
Airship.push.iOS.setForegroundPresentationOptionsCallback(() => Promise.resolve([]));
}
-export default {
+const ForegroundNotifications: ForegroundNotificationsModule = {
configureForegroundNotifications,
disableForegroundNotifications,
};
+
+export default ForegroundNotifications;
diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts
similarity index 58%
rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.js
rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ts
index acb116f7bc43..25baa34099b6 100644
--- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.js
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts
@@ -1,7 +1,11 @@
+import ForegroundNotificationsModule from './types';
+
/**
* Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms.
*/
-export default {
+const ForegroundNotifications: ForegroundNotificationsModule = {
configureForegroundNotifications: () => {},
disableForegroundNotifications: () => {},
};
+
+export default ForegroundNotifications;
diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts
new file mode 100644
index 000000000000..f84934651259
--- /dev/null
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts
@@ -0,0 +1,6 @@
+type ForegroundNotificationsModule = {
+ configureForegroundNotifications: () => void;
+ disableForegroundNotifications: () => void;
+};
+
+export default ForegroundNotificationsModule;
diff --git a/src/libs/Notification/PushNotification/NotificationType.js b/src/libs/Notification/PushNotification/NotificationType.js
deleted file mode 100644
index 092a48fe7815..000000000000
--- a/src/libs/Notification/PushNotification/NotificationType.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
- * types of push notifications sent by our API.
- */
-export default {
- REPORT_COMMENT: 'reportComment',
-};
diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts
new file mode 100644
index 000000000000..91eec6895394
--- /dev/null
+++ b/src/libs/Notification/PushNotification/NotificationType.ts
@@ -0,0 +1,28 @@
+import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
+
+const NotificationType = {
+ REPORT_COMMENT: 'reportComment',
+} as const;
+
+type NotificationDataMap = {
+ [NotificationType.REPORT_COMMENT]: ReportCommentNotificationData;
+};
+
+type NotificationData = ReportCommentNotificationData;
+
+type ReportCommentNotificationData = {
+ title: string;
+ type: typeof NotificationType.REPORT_COMMENT;
+ reportID: number;
+ reportActionID: string;
+ shouldScrollToLastUnread?: boolean;
+ roomName?: string;
+ onyxData?: OnyxServerUpdate[];
+};
+
+/**
+ * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various
+ * types of push notifications sent by our API.
+ */
+export default NotificationType;
+export type {NotificationDataMap, NotificationData, ReportCommentNotificationData};
diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.js b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts
similarity index 86%
rename from src/libs/Notification/PushNotification/backgroundRefresh/index.android.js
rename to src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts
index 4502011b459e..2b3c6ebf21b4 100644
--- a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.js
+++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts
@@ -4,8 +4,9 @@ import Visibility from '@libs/Visibility';
import * as App from '@userActions/App';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import BackgroundRefresh from './types';
-function getLastOnyxUpdateID() {
+function getLastOnyxUpdateID(): Promise {
return new Promise((resolve) => {
const connectionID = Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
@@ -23,7 +24,7 @@ function getLastOnyxUpdateID() {
* We use this to refresh the app in the background after receiving a push notification (native only). Since the full app
* wakes on iOS and by extension runs reconnectApp already, this is a no-op on everything but Android.
*/
-export default function backgroundRefresh() {
+const backgroundRefresh: BackgroundRefresh = () => {
if (Visibility.isVisible()) {
return;
}
@@ -38,9 +39,11 @@ export default function backgroundRefresh() {
* See more here: https://reactnative.dev/docs/headless-js-android
*/
App.confirmReadyToOpenApp();
- App.reconnectApp(lastUpdateIDAppliedToClient);
+ App.reconnectApp(lastUpdateIDAppliedToClient ?? undefined);
})
.catch((error) => {
Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} [PushNotification] backgroundRefresh failed. This should never happen.`, {error});
});
-}
+};
+
+export default backgroundRefresh;
diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.js b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts
similarity index 69%
rename from src/libs/Notification/PushNotification/backgroundRefresh/index.js
rename to src/libs/Notification/PushNotification/backgroundRefresh/index.ts
index 657fb15ee429..c7f47a532d89 100644
--- a/src/libs/Notification/PushNotification/backgroundRefresh/index.js
+++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts
@@ -1,7 +1,11 @@
+import BackgroundRefresh from './types';
+
/**
* Runs our reconnectApp action if the app is in the background.
*
* We use this to refresh the app in the background after receiving a push notification (native only). Since the full app
* wakes on iOS and by extension runs reconnectApp already, this is a no-op on everything but Android.
*/
-export default function backgroundRefresh() {}
+const backgroundRefresh: BackgroundRefresh = () => {};
+
+export default backgroundRefresh;
diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/types.ts b/src/libs/Notification/PushNotification/backgroundRefresh/types.ts
new file mode 100644
index 000000000000..d3d1ee44a1fd
--- /dev/null
+++ b/src/libs/Notification/PushNotification/backgroundRefresh/types.ts
@@ -0,0 +1,3 @@
+type BackgroundRefresh = () => void;
+
+export default BackgroundRefresh;
diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.ts
similarity index 57%
rename from src/libs/Notification/PushNotification/index.native.js
rename to src/libs/Notification/PushNotification/index.native.ts
index 8513a88e46d3..7b2571eea368 100644
--- a/src/libs/Notification/PushNotification/index.native.js
+++ b/src/libs/Notification/PushNotification/index.native.ts
@@ -1,57 +1,59 @@
-import Airship, {EventType} from '@ua/react-native-airship';
-import lodashGet from 'lodash/get';
+import Airship, {EventType, PushPayload} from '@ua/react-native-airship';
import Onyx from 'react-native-onyx';
-import _ from 'underscore';
import Log from '@libs/Log';
-import * as PushNotification from '@userActions/PushNotification';
+import * as PushNotificationActions from '@userActions/PushNotification';
import ONYXKEYS from '@src/ONYXKEYS';
import ForegroundNotifications from './ForegroundNotifications';
-import NotificationType from './NotificationType';
+import NotificationType, {NotificationData} from './NotificationType';
+import PushNotificationType, {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types';
+
+type NotificationEventActionCallback = (data: NotificationData) => void;
+
+type NotificationEventActionMap = Partial>>;
let isUserOptedInToPushNotifications = false;
Onyx.connect({
key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED,
- callback: (val) => (isUserOptedInToPushNotifications = val),
+ callback: (value) => (isUserOptedInToPushNotifications = value ?? false),
});
-const notificationEventActionMap = {};
+const notificationEventActionMap: NotificationEventActionMap = {};
/**
* Handle a push notification event, and trigger and bound actions.
- *
- * @param {String} eventType
- * @param {Object} notification
*/
-function pushNotificationEventCallback(eventType, notification) {
- const actionMap = notificationEventActionMap[eventType] || {};
- let payload = lodashGet(notification, 'extras.payload');
+function pushNotificationEventCallback(eventType: EventType, notification: PushPayload) {
+ const actionMap = notificationEventActionMap[eventType] ?? {};
+ let payload = notification.extras.payload;
// On Android, some notification payloads are sent as a JSON string rather than an object
- if (_.isString(payload)) {
+ if (typeof payload === 'string') {
payload = JSON.parse(payload);
}
+ const data = payload as NotificationData;
+
Log.info(`[PushNotification] Callback triggered for ${eventType}`);
- if (!payload) {
+ if (!data) {
Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.');
return;
}
- if (!payload.type) {
+ if (!data.type) {
Log.warn('[PushNotification] No type value provided in payload, not executing any callback.');
return;
}
- const action = actionMap[payload.type];
+ const action = actionMap[data.type];
if (!action) {
Log.warn('[PushNotification] No callback set up: ', {
event: eventType,
- notificationType: payload.type,
+ notificationType: data.type,
});
return;
}
- action(payload);
+ action(data);
}
/**
@@ -65,7 +67,7 @@ function refreshNotificationOptInStatus() {
}
Log.info('[PushNotification] Push notification opt-in status changed.', false, {isOptedIn});
- PushNotification.setPushNotificationOptInStatus(isOptedIn);
+ PushNotificationActions.setPushNotificationOptInStatus(isOptedIn);
});
}
@@ -76,12 +78,12 @@ function refreshNotificationOptInStatus() {
* WARNING: Moving or changing this code could break Push Notification processing in non-obvious ways.
* DO NOT ALTER UNLESS YOU KNOW WHAT YOU'RE DOING. See this PR for details: https://github.com/Expensify/App/pull/3877
*/
-function init() {
+const init: Init = () => {
// Setup event listeners
Airship.addListener(EventType.PushReceived, (notification) => {
// By default, refresh notification opt-in status to true if we receive a notification
if (!isUserOptedInToPushNotifications) {
- PushNotification.setPushNotificationOptInStatus(true);
+ PushNotificationActions.setPushNotificationOptInStatus(true);
}
pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload);
@@ -97,47 +99,52 @@ function init() {
Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus);
ForegroundNotifications.configureForegroundNotifications();
-}
+};
/**
* Register this device for push notifications for the given notificationID.
- *
- * @param {String|Number} notificationID
*/
-function register(notificationID) {
- if (Airship.contact.getNamedUserId() === notificationID.toString()) {
- // No need to register again for this notificationID.
- return;
- }
-
- // Get permissions to display push notifications (prompts user on iOS, but not Android)
- Airship.push.enableUserNotifications().then((isEnabled) => {
- if (isEnabled) {
- return;
- }
-
- Log.info('[PushNotification] User has disabled visible push notifications for this app.');
- });
-
- // Register this device as a named user in AirshipAPI.
- // Regardless of the user's opt-in status, we still want to receive silent push notifications.
- Log.info(`[PushNotification] Subscribing to notifications`);
- Airship.contact.identify(notificationID.toString());
-
- // Refresh notification opt-in status NVP for the new user.
- refreshNotificationOptInStatus();
-}
+const register: Register = (notificationID) => {
+ Airship.contact
+ .getNamedUserId()
+ .then((userID) => {
+ if (userID === notificationID.toString()) {
+ // No need to register again for this notificationID.
+ return;
+ }
+
+ // Get permissions to display push notifications (prompts user on iOS, but not Android)
+ Airship.push.enableUserNotifications().then((isEnabled) => {
+ if (isEnabled) {
+ return;
+ }
+
+ Log.info('[PushNotification] User has disabled visible push notifications for this app.');
+ });
+
+ // Register this device as a named user in AirshipAPI.
+ // Regardless of the user's opt-in status, we still want to receive silent push notifications.
+ Log.info(`[PushNotification] Subscribing to notifications`);
+ Airship.contact.identify(notificationID.toString());
+
+ // Refresh notification opt-in status NVP for the new user.
+ refreshNotificationOptInStatus();
+ })
+ .catch((error) => {
+ Log.warn('[PushNotification] Failed to register for push notifications! Reason: ', error);
+ });
+};
/**
* Deregister this device from push notifications.
*/
-function deregister() {
+const deregister: Deregister = () => {
Log.info('[PushNotification] Unsubscribing from push notifications.');
Airship.contact.reset();
Airship.removeAllListeners(EventType.PushReceived);
Airship.removeAllListeners(EventType.NotificationResponse);
ForegroundNotifications.disableForegroundNotifications();
-}
+};
/**
* Bind a callback to a push notification of a given type.
@@ -148,45 +155,41 @@ function deregister() {
* if we attempt to bind two callbacks to the PushReceived event for reportComment notifications,
* the second will overwrite the first.
*
- * @param {String} notificationType
- * @param {Function} callback
- * @param {String} [triggerEvent] - The event that should trigger this callback. Should be one of UrbanAirship.EventType
+ * @param triggerEvent - The event that should trigger this callback. Should be one of UrbanAirship.EventType
*/
-function bind(notificationType, callback, triggerEvent) {
- if (!notificationEventActionMap[triggerEvent]) {
- notificationEventActionMap[triggerEvent] = {};
+function bind(notificationType: string, callback: NotificationEventActionCallback, triggerEvent: EventType) {
+ let actionMap = notificationEventActionMap[triggerEvent];
+
+ if (!actionMap) {
+ actionMap = {};
}
- notificationEventActionMap[triggerEvent][notificationType] = callback;
+
+ actionMap[notificationType] = callback;
+ notificationEventActionMap[triggerEvent] = actionMap;
}
/**
* Bind a callback to be executed when a push notification of a given type is received.
- *
- * @param {String} notificationType
- * @param {Function} callback
*/
-function onReceived(notificationType, callback) {
+const onReceived: OnReceived = (notificationType, callback) => {
bind(notificationType, callback, EventType.PushReceived);
-}
+};
/**
* Bind a callback to be executed when a push notification of a given type is tapped by the user.
- *
- * @param {String} notificationType
- * @param {Function} callback
*/
-function onSelected(notificationType, callback) {
+const onSelected: OnSelected = (notificationType, callback) => {
bind(notificationType, callback, EventType.NotificationResponse);
-}
+};
/**
* Clear all push notifications
*/
-function clearNotifications() {
+const clearNotifications: ClearNotifications = () => {
Airship.push.clearNotifications();
-}
+};
-export default {
+const PushNotification: PushNotificationType = {
init,
register,
deregister,
@@ -195,3 +198,5 @@ export default {
TYPE: NotificationType,
clearNotifications,
};
+
+export default PushNotification;
diff --git a/src/libs/Notification/PushNotification/index.js b/src/libs/Notification/PushNotification/index.ts
similarity index 71%
rename from src/libs/Notification/PushNotification/index.js
rename to src/libs/Notification/PushNotification/index.ts
index 88136ff5dc72..1e5499d1fe7d 100644
--- a/src/libs/Notification/PushNotification/index.js
+++ b/src/libs/Notification/PushNotification/index.ts
@@ -1,7 +1,8 @@
import NotificationType from './NotificationType';
+import PushNotificationType from './types';
// Push notifications are only supported on mobile, so we'll just noop here
-export default {
+const PushNotification: PushNotificationType = {
init: () => {},
register: () => {},
deregister: () => {},
@@ -10,3 +11,5 @@ export default {
TYPE: NotificationType,
clearNotifications: () => {},
};
+
+export default PushNotification;
diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.js b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts
similarity index 61%
rename from src/libs/Notification/PushNotification/shouldShowPushNotification.js
rename to src/libs/Notification/PushNotification/shouldShowPushNotification.ts
index f25d452a77d5..46f99fcc9271 100644
--- a/src/libs/Notification/PushNotification/shouldShowPushNotification.js
+++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts
@@ -1,30 +1,31 @@
-import _ from 'underscore';
+import {PushPayload} from '@ua/react-native-airship';
import Log from '@libs/Log';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
import * as Report from '@userActions/Report';
+import {NotificationData} from './NotificationType';
/**
* Returns whether the given Airship notification should be shown depending on the current state of the app
- * @param {PushPayload} pushPayload
- * @returns {Boolean}
*/
-export default function shouldShowPushNotification(pushPayload) {
+export default function shouldShowPushNotification(pushPayload: PushPayload): boolean {
Log.info('[PushNotification] push notification received', false, {pushPayload});
- let pushData = pushPayload.extras.payload;
+ let payload = pushPayload.extras.payload;
// The payload is string encoded on Android
- if (_.isString(pushData)) {
- pushData = JSON.parse(pushData);
+ if (typeof payload === 'string') {
+ payload = JSON.parse(payload);
}
- if (!pushData.reportID) {
+ const data = payload as NotificationData;
+
+ if (!data.reportID) {
Log.info('[PushNotification] Not a report action notification. Showing notification');
return true;
}
- const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(pushData.onyxData);
- const shouldShow = Report.shouldShowReportActionNotification(String(pushData.reportID), reportAction, true);
+ const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null);
+ const shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), reportAction, true);
Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`);
return shouldShow;
}
diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.js b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
similarity index 100%
rename from src/libs/Notification/PushNotification/subscribePushNotification/index.js
rename to src/libs/Notification/PushNotification/subscribePushNotification/index.ts
diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
similarity index 86%
rename from src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
rename to src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
index ede873f79c6e..547ecb1de5b2 100644
--- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js
+++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
@@ -12,7 +12,7 @@ import PushNotification from './index';
export default function subscribeToReportCommentPushNotifications() {
PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => {
Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID});
- Onyx.update(onyxData);
+ Onyx.update(onyxData ?? []);
backgroundRefresh();
});
@@ -33,9 +33,14 @@ export default function subscribeToReportCommentPushNotifications() {
}
Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID});
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID)));
} catch (error) {
- Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: error.message});
+ let errorMessage = String(error);
+ if (error instanceof Error) {
+ errorMessage = error.message;
+ }
+
+ Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage});
}
});
});
diff --git a/src/libs/Notification/PushNotification/types.ts b/src/libs/Notification/PushNotification/types.ts
new file mode 100644
index 000000000000..f72ee1af887a
--- /dev/null
+++ b/src/libs/Notification/PushNotification/types.ts
@@ -0,0 +1,22 @@
+import {ValueOf} from 'type-fest';
+import NotificationType, {NotificationDataMap} from './NotificationType';
+
+type Init = () => void;
+type Register = (notificationID: string | number) => void;
+type Deregister = () => void;
+type OnReceived = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void;
+type OnSelected = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void;
+type ClearNotifications = () => void;
+
+type PushNotification = {
+ init: Init;
+ register: Register;
+ deregister: Deregister;
+ onReceived: OnReceived;
+ onSelected: OnSelected;
+ TYPE: typeof NotificationType;
+ clearNotifications: ClearNotifications;
+};
+
+export default PushNotification;
+export type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register};
diff --git a/src/libs/PaymentUtils.ts b/src/libs/PaymentUtils.ts
index fc2bfb6d83fb..ee7a293f6ad1 100644
--- a/src/libs/PaymentUtils.ts
+++ b/src/libs/PaymentUtils.ts
@@ -1,5 +1,5 @@
import getBankIcon from '@components/Icon/BankIcons';
-import {ThemeStyles} from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
import CONST from '@src/CONST';
import BankAccount from '@src/types/onyx/BankAccount';
import Fund from '@src/types/onyx/Fund';
@@ -40,7 +40,7 @@ function getPaymentMethodDescription(accountType: AccountType, account: BankAcco
/**
* Get the PaymentMethods list
*/
-function formatPaymentMethods(bankAccountList: Record, fundList: Record, themeStyles: ThemeStyles): PaymentMethod[] {
+function formatPaymentMethods(bankAccountList: Record, fundList: Record, styles: ThemeStyles): PaymentMethod[] {
const combinedPaymentMethods: PaymentMethod[] = [];
Object.values(bankAccountList).forEach((bankAccount) => {
@@ -52,7 +52,7 @@ function formatPaymentMethods(bankAccountList: Record, fund
const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({
bankName: bankAccount?.accountData?.additionalData?.bankName,
isCard: false,
- themeStyles,
+ styles,
});
combinedPaymentMethods.push({
...bankAccount,
@@ -66,7 +66,7 @@ function formatPaymentMethods(bankAccountList: Record, fund
});
Object.values(fundList).forEach((card) => {
- const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({bankName: card?.accountData?.bank, isCard: true, themeStyles});
+ const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({bankName: card?.accountData?.bank, isCard: true, styles});
combinedPaymentMethods.push({
...card,
description: getPaymentMethodDescription(card?.accountType, card.accountData),
diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js
index 560480dcec9d..8a4151391453 100644
--- a/src/libs/PersonalDetailsUtils.js
+++ b/src/libs/PersonalDetailsUtils.js
@@ -197,6 +197,18 @@ function getFormattedAddress(privatePersonalDetails) {
return formattedAddress.trim().replace(/,$/, '');
}
+/**
+ * @param {Object} personalDetail - details object
+ * @returns {String | undefined} - The effective display name
+ */
+function getEffectiveDisplayName(personalDetail) {
+ if (personalDetail) {
+ return LocalePhoneNumber.formatPhoneNumber(personalDetail.login) || personalDetail.displayName;
+ }
+
+ return undefined;
+}
+
export {
getDisplayNameOrDefault,
getPersonalDetailsByIDs,
@@ -206,4 +218,5 @@ export {
getFormattedAddress,
getFormattedStreet,
getStreetLines,
+ getEffectiveDisplayName,
};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index f58021e17064..ff36a2ac3401 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -5,14 +5,17 @@ import OnyxUtils from 'react-native-onyx/lib/utils';
import {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import {ActionName} from '@src/types/onyx/OriginalMessage';
+import {ActionName, ChangeLog} from '@src/types/onyx/OriginalMessage';
import Report from '@src/types/onyx/Report';
-import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction';
+import ReportAction, {Message, ReportActions} from '@src/types/onyx/ReportAction';
import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject';
import * as CollectionUtils from './CollectionUtils';
import * as Environment from './Environment/Environment';
import isReportMessageAttachment from './isReportMessageAttachment';
+import * as Localize from './Localize';
import Log from './Log';
+import {MessageElementBase, MessageTextElement} from './MessageElement';
+import * as PersonalDetailsUtils from './PersonalDetailsUtils';
type LastVisibleMessage = {
lastMessageTranslationKey?: string;
@@ -20,6 +23,19 @@ type LastVisibleMessage = {
lastMessageHtml?: string;
};
+type MemberChangeMessageUserMentionElement = {
+ readonly kind: 'userMention';
+ readonly accountID: number;
+} & MessageElementBase;
+
+type MemberChangeMessageRoomReferenceElement = {
+ readonly kind: 'roomReference';
+ readonly roomName: string;
+ readonly roomID: number;
+} & MessageElementBase;
+
+type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement;
+
const allReports: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
@@ -100,7 +116,7 @@ function isReimbursementQueuedAction(reportAction: OnyxEntry) {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED;
}
-function isChannelLogMemberAction(reportAction: OnyxEntry) {
+function isMemberChangeAction(reportAction: OnyxEntry) {
return (
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ||
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM ||
@@ -109,6 +125,10 @@ function isChannelLogMemberAction(reportAction: OnyxEntry) {
);
}
+function isInviteMemberAction(reportAction: OnyxEntry) {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM;
+}
+
function isReimbursementDeQueuedAction(reportAction: OnyxEntry): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED;
}
@@ -639,6 +659,89 @@ function isNotifiableReportAction(reportAction: OnyxEntry): boolea
return actions.includes(reportAction.actionName);
}
+function getMemberChangeMessageElements(reportAction: OnyxEntry): readonly MemberChangeMessageElement[] {
+ const isInviteAction = isInviteMemberAction(reportAction);
+
+ // Currently, we only render messages when members are invited
+ const verb = isInviteAction ? Localize.translateLocal('workspace.invite.invited') : Localize.translateLocal('workspace.invite.removed');
+
+ const originalMessage = reportAction?.originalMessage as ChangeLog;
+ const targetAccountIDs: number[] = originalMessage?.targetAccountIDs ?? [];
+ const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs(targetAccountIDs, 0);
+
+ const mentionElements = targetAccountIDs.map((accountID): MemberChangeMessageUserMentionElement => {
+ const personalDetail = personalDetails.find((personal) => personal.accountID === accountID);
+ const handleText = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail) ?? Localize.translateLocal('common.hidden');
+
+ return {
+ kind: 'userMention',
+ content: `@${handleText}`,
+ accountID,
+ };
+ });
+
+ const buildRoomElements = (): readonly MemberChangeMessageElement[] => {
+ const roomName = originalMessage?.roomName;
+
+ if (roomName) {
+ const preposition = isInviteAction ? ` ${Localize.translateLocal('workspace.invite.to')} ` : ` ${Localize.translateLocal('workspace.invite.from')} `;
+
+ if (originalMessage.reportID) {
+ return [
+ {
+ kind: 'text',
+ content: preposition,
+ },
+ {
+ kind: 'roomReference',
+ roomName,
+ roomID: originalMessage.reportID,
+ content: roomName,
+ },
+ ];
+ }
+ }
+
+ return [];
+ };
+
+ return [
+ {
+ kind: 'text',
+ content: `${verb} `,
+ },
+ ...Localize.formatMessageElementList(mentionElements),
+ ...buildRoomElements(),
+ ];
+}
+
+function getMemberChangeMessageFragment(reportAction: OnyxEntry): Message {
+ const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction);
+ const html = messageElements
+ .map((messageElement) => {
+ switch (messageElement.kind) {
+ case 'userMention':
+ return ``;
+ case 'roomReference':
+ return `${messageElement.roomName}`;
+ default:
+ return messageElement.content;
+ }
+ })
+ .join('');
+
+ return {
+ html: `${html}`,
+ text: reportAction?.message ? reportAction?.message[0].text : '',
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ };
+}
+
+function getMemberChangeMessagePlainText(reportAction: OnyxEntry): string {
+ const messageElements = getMemberChangeMessageElements(reportAction);
+ return messageElements.map((element) => element.content).join('');
+}
+
/**
* Helper method to determine if the provided accountID has made a request on the specified report.
*
@@ -701,7 +804,9 @@ export {
shouldReportActionBeVisibleAsLastAction,
hasRequestFromCurrentAccount,
getFirstVisibleReportActionID,
- isChannelLogMemberAction,
+ isMemberChangeAction,
+ getMemberChangeMessageFragment,
+ getMemberChangeMessagePlainText,
isReimbursementDeQueuedAction,
};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index c444187cbd10..b50b4611a249 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -6,6 +6,7 @@ import lodashEscape from 'lodash/escape';
import lodashFindLastIndex from 'lodash/findLastIndex';
import lodashIntersection from 'lodash/intersection';
import lodashIsEqual from 'lodash/isEqual';
+import React from 'react';
import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import {SvgProps} from 'react-native-svg';
import {ValueOf} from 'type-fest';
@@ -17,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
-import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage';
+import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage';
import {Message, ReportActions} from '@src/types/onyx/ReportAction';
import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction';
import DeepValueOf from '@src/types/utils/DeepValueOf';
@@ -4173,44 +4174,6 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry)
});
}
-/**
- * Return room channel log display message
- */
-function getChannelLogMemberMessage(reportAction: OnyxEntry): string {
- const verb =
- reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
- ? 'invited'
- : 'removed';
-
- const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => {
- const personalDetail = allPersonalDetails?.accountID;
- const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden');
- return `@${displayNameOrLogin}`;
- });
-
- const lastMention = mentions?.pop();
- let message = '';
-
- if (mentions?.length === 0) {
- message = `${verb} ${lastMention}`;
- } else if (mentions?.length === 1) {
- message = `${verb} ${mentions?.[0]} and ${lastMention}`;
- } else {
- message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`;
- }
-
- const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? '';
- if (roomName) {
- const preposition =
- reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
- ? ' to'
- : ' from';
- message += `${preposition} ${roomName}`;
- }
-
- return message;
-}
-
/**
* Checks if a report is a group chat.
*
@@ -4251,6 +4214,23 @@ function shouldDisableWelcomeMessage(report: OnyxEntry, policy: OnyxEntr
return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy);
}
+function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean {
+ if (event.key.length > 1) {
+ return false;
+ }
+
+ // If a key is pressed in combination with Meta, Control or Alt do not focus
+ if (event.ctrlKey || event.metaKey) {
+ return false;
+ }
+
+ if (event.code === 'Space') {
+ return false;
+ }
+
+ return true;
+}
+
/**
* Navigates to the appropriate screen based on the presence of a private note for the current user.
*/
@@ -4428,11 +4408,11 @@ export {
getReimbursementQueuedActionMessage,
getReimbursementDeQueuedActionMessage,
getPersonalDetailsForAccountID,
- getChannelLogMemberMessage,
getRoom,
shouldDisableWelcomeMessage,
navigateToPrivateNotes,
canEditWriteCapability,
+ shouldAutoFocusOnKeyPress,
};
export type {OptionData};
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index bace29e06d28..6e382e11b49b 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -375,17 +375,17 @@ function getOptionData(
const targetAccountIDs = lastAction?.originalMessage?.targetAccountIDs ?? [];
const verb =
lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
- ? 'invited'
- : 'removed';
- const users = targetAccountIDs.length > 1 ? 'users' : 'user';
+ ? Localize.translate(preferredLocale, 'workspace.invite.invited')
+ : Localize.translate(preferredLocale, 'workspace.invite.removed');
+ const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user');
result.alternateText = `${verb} ${targetAccountIDs.length} ${users}`;
const roomName = lastAction?.originalMessage?.roomName ?? '';
if (roomName) {
const preposition =
lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
- ? ' to'
- : ' from';
+ ? ` ${Localize.translate(preferredLocale, 'workspace.invite.to')}`
+ : ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`;
result.alternateText += `${preposition} ${roomName}`;
}
} else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js
index bfa0cd911177..ee686444abfd 100644
--- a/src/libs/UnreadIndicatorUpdater/index.js
+++ b/src/libs/UnreadIndicatorUpdater/index.js
@@ -1,38 +1,29 @@
-import {InteractionManager} from 'react-native';
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import * as ReportUtils from '@libs/ReportUtils';
-import CONST from '@src/CONST';
+import Navigation, {navigationRef} from '@navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
import updateUnread from './updateUnread/index';
-let previousUnreadCount = 0;
+let allReports = [];
+
+const triggerUnreadUpdate = () => {
+ const currentReportID = navigationRef.isReady() ? Navigation.getTopmostReportId() : '';
+
+ // We want to keep notification count consistent with what can be accessed from the LHN list
+ const unreadReports = _.filter(allReports, (report) => ReportUtils.isUnread(report) && ReportUtils.shouldReportBeInOptionList(report, currentReportID));
+ updateUnread(_.size(unreadReports));
+};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (reportsFromOnyx) => {
- if (!reportsFromOnyx) {
- return;
- }
-
- /**
- * We need to wait until after interactions have finished to update the unread count because otherwise
- * the unread count will be updated while the interactions/animations are in progress and we don't want
- * to put more work on the main thread.
- *
- * For eg. On web we are manipulating DOM and it makes it a better candidate to wait until after interactions
- * have finished.
- *
- * More info: https://reactnative.dev/docs/interactionmanager
- */
- InteractionManager.runAfterInteractions(() => {
- const unreadReports = _.filter(reportsFromOnyx, (report) => ReportUtils.isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN);
- const unreadReportsCount = _.size(unreadReports);
- if (previousUnreadCount !== unreadReportsCount) {
- previousUnreadCount = unreadReportsCount;
- updateUnread(unreadReportsCount);
- }
- });
+ allReports = reportsFromOnyx;
+ triggerUnreadUpdate();
},
});
+
+navigationRef.addListener('state', () => {
+ triggerUnreadUpdate();
+});
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 388020bc0d6d..0adacac4035a 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -30,17 +30,6 @@ function validateCardNumber(value: string): boolean {
return sum % 10 === 0;
}
-/**
- * Validating that this is a valid address (PO boxes are not allowed)
- */
-function isValidAddress(value: string): boolean {
- if (!CONST.REGEX.ANY_VALUE.test(value)) {
- return false;
- }
-
- return !CONST.REGEX.PO_BOX.test(value);
-}
-
/**
* Validate date fields
*/
@@ -204,40 +193,6 @@ function isValidWebsite(url: string): boolean {
return new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase;
}
-function validateIdentity(identity: Record): Record {
- const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob'];
- const errors: Record = {};
-
- // Check that all required fields are filled
- requiredFields.forEach((fieldName) => {
- if (isRequiredFulfilled(identity[fieldName])) {
- return;
- }
- errors[fieldName] = true;
- });
-
- if (!isValidAddress(identity.street)) {
- errors.street = true;
- }
-
- if (!isValidZipCode(identity.zipCode)) {
- errors.zipCode = true;
- }
-
- // dob field has multiple validations/errors, we are handling it temporarily like this.
- if (!isValidDate(identity.dob) || !meetsMaximumAgeRequirement(identity.dob)) {
- errors.dob = true;
- } else if (!meetsMinimumAgeRequirement(identity.dob)) {
- errors.dobAge = true;
- }
-
- if (!isValidSSNLastFour(identity.ssnLast4)) {
- errors.ssnLast4 = true;
- }
-
- return errors;
-}
-
function isValidUSPhone(phoneNumber = '', isCountryCodeOptional?: boolean): boolean {
const phone = phoneNumber || '';
const regionCode = isCountryCodeOptional ? CONST.COUNTRY.US : undefined;
@@ -304,6 +259,51 @@ function isValidPersonName(value: string) {
return /^[^\d^!#$%*=<>;{}"]+$/.test(value);
}
+/**
+ * Validating that this is a valid address (PO boxes are not allowed)
+ */
+function isValidAddress(value: string): boolean {
+ if (!isValidLegalName(value)) {
+ return false;
+ }
+
+ return !CONST.REGEX.PO_BOX.test(value);
+}
+
+function validateIdentity(identity: Record): Record {
+ const requiredFields = ['firstName', 'lastName', 'street', 'city', 'zipCode', 'state', 'ssnLast4', 'dob'];
+ const errors: Record = {};
+
+ // Check that all required fields are filled
+ requiredFields.forEach((fieldName) => {
+ if (isRequiredFulfilled(identity[fieldName])) {
+ return;
+ }
+ errors[fieldName] = true;
+ });
+
+ if (!isValidAddress(identity.street)) {
+ errors.street = true;
+ }
+
+ if (!isValidZipCode(identity.zipCode)) {
+ errors.zipCode = true;
+ }
+
+ // dob field has multiple validations/errors, we are handling it temporarily like this.
+ if (!isValidDate(identity.dob) || !meetsMaximumAgeRequirement(identity.dob)) {
+ errors.dob = true;
+ } else if (!meetsMinimumAgeRequirement(identity.dob)) {
+ errors.dobAge = true;
+ }
+
+ if (!isValidSSNLastFour(identity.ssnLast4)) {
+ errors.ssnLast4 = true;
+ }
+
+ return errors;
+}
+
/**
* Checks if the provided string includes any of the provided reserved words
*/
@@ -384,7 +384,6 @@ export {
meetsMinimumAgeRequirement,
meetsMaximumAgeRequirement,
getAgeRequirementError,
- isValidAddress,
isValidDate,
isValidPastDate,
isValidSecurityCode,
@@ -396,7 +395,6 @@ export {
getFieldRequiredErrors,
isValidUSPhone,
isValidWebsite,
- validateIdentity,
isValidTwoFactorCode,
isNumericWithSpecialChars,
isValidRoutingNumber,
@@ -409,6 +407,8 @@ export {
isValidValidateCode,
isValidDisplayName,
isValidLegalName,
+ isValidAddress,
+ validateIdentity,
doesContainReservedWord,
isNumeric,
isValidAccountRoute,
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 4de8f1c1f171..ec43d4358134 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -1,7 +1,7 @@
// Issue - https://github.com/Expensify/App/issues/26719
import Str from 'expensify-common/lib/str';
import {AppState, AppStateStatus} from 'react-native';
-import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx';
+import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import {ValueOf} from 'type-fest';
import * as API from '@libs/API';
import * as Browser from '@libs/Browser';
@@ -228,7 +228,7 @@ function openApp() {
* Fetches data when the app reconnects to the network
* @param [updateIDFrom] the ID of the Onyx update that we want to start fetching from
*/
-function reconnectApp(updateIDFrom = 0) {
+function reconnectApp(updateIDFrom: OnyxEntry = 0) {
console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom}`);
getPolicyParamsForOpenOrReconnect().then((policyParams) => {
type ReconnectParams = {
@@ -384,7 +384,7 @@ function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, p
* pass it in as a parameter. withOnyx guarantees that the value has been read
* from Onyx because it will not render the AuthScreens until that point.
*/
-function setUpPoliciesAndNavigate(session: OnyxTypes.Session) {
+function setUpPoliciesAndNavigate(session: OnyxEntry) {
const currentUrl = getCurrentUrl();
if (!session || !currentUrl || !currentUrl.includes('exitTo')) {
return;
diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts
index d741ced6dc08..e6d33fec118d 100644
--- a/src/libs/actions/Link.ts
+++ b/src/libs/actions/Link.ts
@@ -2,8 +2,12 @@ import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import asyncOpenURL from '@libs/asyncOpenURL';
import * as Environment from '@libs/Environment/Environment';
+import Navigation from '@libs/Navigation/Navigation';
import * as Url from '@libs/Url';
+import CONFIG from '@src/CONFIG';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES, {Route} from '@src/ROUTES';
let isNetworkOffline = false;
Onyx.connect({
@@ -56,4 +60,58 @@ function openOldDotLink(url: string) {
(oldDotURL) => oldDotURL,
);
}
-export {buildOldDotURL, openOldDotLink, openExternalLink};
+
+function getInternalNewExpensifyPath(href: string) {
+ const attrPath = Url.getPathFromURL(href);
+ return (Url.hasSameExpensifyOrigin(href, CONST.NEW_EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONST.STAGING_NEW_EXPENSIFY_URL) || href.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) &&
+ !CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => path === attrPath)
+ ? attrPath
+ : '';
+}
+
+function getInternalExpensifyPath(href: string) {
+ const attrPath = Url.getPathFromURL(href);
+ const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.STAGING_API_ROOT);
+ if (!hasExpensifyOrigin || attrPath.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL_PATHNAME) || attrPath.startsWith(CONFIG.EXPENSIFY.DEVPORTAL_URL_PATHNAME)) {
+ return '';
+ }
+
+ return attrPath;
+}
+
+function openLink(href: string, environmentURL: string, isAttachment = false) {
+ const hasSameOrigin = Url.hasSameExpensifyOrigin(href, environmentURL);
+ const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.STAGING_API_ROOT);
+ const internalNewExpensifyPath = getInternalNewExpensifyPath(href);
+ const internalExpensifyPath = getInternalExpensifyPath(href);
+
+ // There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
+ // https://www.expensify.com.dev/newdotreport?reportID=3429600449838908 and they have a target="_blank" attribute. This is so that when a user is on OldDot,
+ // clicking on the link will open the chat in NewDot. However, when a user is in NewDot and clicks on the concierge link, the link needs to be handled differently.
+ // Normally, the link would be sent to Link.openOldDotLink() and opened in a new tab, and that's jarring to the user. Since the intention is to link to a specific NewDot chat,
+ // the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab.
+ if (hasExpensifyOrigin && href.indexOf('newdotreport?reportID=') > -1) {
+ const reportID = href.split('newdotreport?reportID=').pop();
+ const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID ?? '');
+ Navigation.navigate(reportRoute);
+ return;
+ }
+
+ // If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
+ // instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
+ if (internalNewExpensifyPath && hasSameOrigin) {
+ Navigation.navigate(internalNewExpensifyPath as Route);
+ return;
+ }
+
+ // If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in.
+ // As attachments also use expensify.com we don't want it working the same as links.
+ if (internalExpensifyPath && !isAttachment) {
+ openOldDotLink(internalExpensifyPath);
+ return;
+ }
+
+ openExternalLink(href);
+}
+
+export {buildOldDotURL, openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath};
diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts
index 02f0b49fe3d2..b3d1e3e23a24 100644
--- a/src/libs/actions/PaymentMethods.ts
+++ b/src/libs/actions/PaymentMethods.ts
@@ -11,7 +11,7 @@ import PaymentMethod from '@src/types/onyx/PaymentMethod';
import {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer';
type KYCWallRef = {
- continue?: () => void;
+ continueAction?: () => void;
};
/**
@@ -23,14 +23,14 @@ const kycWallRef = createRef();
* When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set.
*/
function continueSetup(fallbackRoute = ROUTES.HOME) {
- if (!kycWallRef.current?.continue) {
+ if (!kycWallRef.current?.continueAction) {
Navigation.goBack(fallbackRoute);
return;
}
// Close the screen (Add Debit Card, Add Bank Account, or Enable Payments) on success and continue with setup
Navigation.goBack(fallbackRoute);
- kycWallRef.current.continue();
+ kycWallRef.current.continueAction();
}
function openWalletPage() {
diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts
new file mode 100644
index 000000000000..d528c31d7453
--- /dev/null
+++ b/src/libs/actions/PriorityMode.ts
@@ -0,0 +1,138 @@
+import debounce from 'lodash/debounce';
+import Onyx, {OnyxCollection} from 'react-native-onyx';
+import * as CollectionUtils from '@libs/CollectionUtils';
+import Log from '@libs/Log';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {Report} from '@src/types/onyx';
+
+/**
+ * This actions file is used to automatically switch a user into #focus mode when they exceed a certain number of reports. We do this primarily for performance reasons.
+ * Similar to the "Welcome action" we must wait for a number of things to happen when the user signs in or refreshes the page:
+ *
+ * - NVP that tracks whether they have already been switched over. We only do this once.
+ * - Priority mode NVP (that dictates the ordering/filtering logic of the LHN)
+ * - Reports to load (in ReconnectApp or OpenApp). As we check the count of the reports to determine whether the user is eligible to be automatically switched.
+ */
+
+let resolveIsReadyPromise: (args?: unknown[]) => void;
+let isReadyPromise = new Promise((resolve) => {
+ resolveIsReadyPromise = resolve;
+});
+
+let currentUserAccountID: number | undefined | null;
+Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (val) => {
+ currentUserAccountID = val?.accountID;
+ },
+});
+
+/**
+ * Debounce the prompt to promote focus mode as many reports updates could happen in a short burst
+ */
+// eslint-disable-next-line @typescript-eslint/no-use-before-define
+const autoSwitchToFocusMode = debounce(tryFocusModeUpdate, 300, {leading: true});
+
+let allReports: OnyxCollection | undefined;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ callback: (report, key) => {
+ if (!key || !report) {
+ return;
+ }
+
+ if (!allReports) {
+ allReports = {};
+ }
+
+ const reportID = CollectionUtils.extractCollectionItemID(key);
+
+ allReports[reportID] = report;
+
+ // Each time a new report is added we will check to see if the user should be switched
+ autoSwitchToFocusMode();
+ },
+});
+
+let isLoadingReportData = true;
+Onyx.connect({
+ key: ONYXKEYS.IS_LOADING_REPORT_DATA,
+ initWithStoredValues: false,
+ callback: (value) => {
+ isLoadingReportData = value ?? false;
+
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ checkRequiredData();
+ },
+});
+
+let isInFocusMode: boolean | undefined;
+Onyx.connect({
+ key: ONYXKEYS.NVP_PRIORITY_MODE,
+ callback: (priorityMode) => {
+ isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD;
+
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ checkRequiredData();
+ },
+});
+
+let hasTriedFocusMode: boolean | undefined | null;
+Onyx.connect({
+ key: ONYXKEYS.NVP_TRY_FOCUS_MODE,
+ callback: (val) => {
+ hasTriedFocusMode = val;
+
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ checkRequiredData();
+ },
+});
+
+function resetHasReadRequiredDataFromStorage() {
+ // Create a new promise and a new resolve function
+ isReadyPromise = new Promise((resolve) => {
+ resolveIsReadyPromise = resolve;
+ });
+ isLoadingReportData = true;
+ allReports = {};
+}
+
+function checkRequiredData() {
+ if (allReports === undefined || hasTriedFocusMode === undefined || isInFocusMode === undefined || isLoadingReportData) {
+ return;
+ }
+
+ resolveIsReadyPromise();
+}
+
+function tryFocusModeUpdate() {
+ isReadyPromise.then(() => {
+ // User is signed out so do not try to switch them
+ if (!currentUserAccountID) {
+ return;
+ }
+
+ // Check to see if the user is using #focus mode, has tried it before, or we have already switched them over automatically.
+ if ((isInFocusMode ?? false) || hasTriedFocusMode) {
+ Log.info('Not switching user to optimized focus mode.', false, {isInFocusMode, hasTriedFocusMode});
+ return;
+ }
+
+ const reportCount = Object.keys(allReports ?? {}).length;
+ if (reportCount < CONST.REPORT.MAX_COUNT_BEFORE_FOCUS_UPDATE) {
+ Log.info('Not switching user to optimized focus mode as they do not have enough reports', false, {reportCount});
+ return;
+ }
+
+ Log.info('Switching user to optimized focus mode', false, {reportCount, hasTriedFocusMode, isInFocusMode});
+
+ // Record that we automatically switched them so we don't ask again.
+ hasTriedFocusMode = true;
+
+ // Setting this triggers a modal to open and notify the user.
+ Onyx.set(ONYXKEYS.FOCUS_MODE_NOTIFICATION, true);
+ });
+}
+
+export {resetHasReadRequiredDataFromStorage, autoSwitchToFocusMode};
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 1233bcd5d707..6161fd2066ff 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1743,7 +1743,7 @@ function setIsComposerFullSize(reportID, isComposerFullSize) {
/**
* @param {String} reportID
- * @param {Object} action the associated report action (optional)
+ * @param {Object|null} action the associated report action (optional)
* @param {Boolean} isRemote whether or not this notification is a remote push notification
* @returns {Boolean}
*/
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 559f60a21c98..82b51651cacc 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -15,6 +15,7 @@ import * as ReportUtils from '@libs/ReportUtils';
import Timers from '@libs/Timers';
import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import * as Device from '@userActions/Device';
+import * as PriorityMode from '@userActions/PriorityMode';
import redirectToSignIn from '@userActions/SignInRedirect';
import Timing from '@userActions/Timing';
import * as Welcome from '@userActions/Welcome';
@@ -589,6 +590,7 @@ function cleanupSession() {
Pusher.disconnect();
Timers.clearAll();
Welcome.resetReadyCheck();
+ PriorityMode.resetHasReadRequiredDataFromStorage();
}
function clearAccountMessages() {
diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js
index ad6fd7be10dd..1bd1cb0b763b 100644
--- a/src/libs/actions/User.js
+++ b/src/libs/actions/User.js
@@ -583,8 +583,10 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis) {
/**
* Sync user chat priority mode with Onyx and Server
* @param {String} mode
+ * @param {boolean} [automatic] if we changed the mode automatically
*/
-function updateChatPriorityMode(mode) {
+function updateChatPriorityMode(mode, automatic = false) {
+ const autoSwitchedToFocusMode = mode === CONST.PRIORITY_MODE.GSD && automatic;
const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -592,14 +594,31 @@ function updateChatPriorityMode(mode) {
value: mode,
},
];
+
+ if (autoSwitchedToFocusMode) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_TRY_FOCUS_MODE,
+ value: true,
+ });
+ }
+
API.write(
'UpdateChatPriorityMode',
{
value: mode,
+ automatic,
},
{optimisticData},
);
- Navigation.goBack(ROUTES.SETTINGS_PREFERENCES);
+
+ if (!autoSwitchedToFocusMode) {
+ Navigation.goBack(ROUTES.SETTINGS_PREFERENCES);
+ }
+}
+
+function clearFocusModeNotification() {
+ Onyx.set(ONYXKEYS.FOCUS_MODE_NOTIFICATION, false);
}
/**
@@ -843,6 +862,7 @@ function clearDraftCustomStatus() {
}
export {
+ clearFocusModeNotification,
closeAccount,
resendValidateCode,
requestContactMethodValidateCode,
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 618571ddf400..09cc1222310f 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -1,6 +1,7 @@
import {Alert, Linking, Platform} from 'react-native';
import DateUtils from '@libs/DateUtils';
import * as Localize from '@libs/Localize';
+import Log from '@libs/Log';
import CONST from '@src/CONST';
import type {ReadFileAsync, SplitExtensionFromFileName} from './types';
@@ -75,13 +76,22 @@ function showCameraPermissionsAlert() {
}
/**
- * Generate a random file name with timestamp and file extension
+ * Extracts a filename from a given URL and sanitizes it for file system usage.
+ *
+ * This function takes a URL as input and performs the following operations:
+ * 1. Extracts the last segment of the URL, which could be a file name, a path segment,
+ * or a query string parameter.
+ * 2. Decodes the extracted segment from URL encoding to a plain string for better readability.
+ * 3. Replaces any characters in the decoded string that are illegal in file names
+ * with underscores.
*/
-function getAttachmentName(url: string): string {
- if (!url) {
- return '';
+function getFileName(url: string): string {
+ const fileName = url.split(/[#?/]/).pop() ?? '';
+ if (!fileName) {
+ Log.warn('[FileUtils] Could not get attachment name', {url});
}
- return `${DateUtils.getDBTime()}.${url.split(/[#?]/)[0].split('.').pop()?.trim()}`;
+
+ return decodeURIComponent(fileName).replace(CONST.REGEX.ILLEGAL_FILENAME_CHARACTERS, '_');
}
function isImage(fileName: string): boolean {
@@ -231,7 +241,7 @@ export {
showPermissionErrorAlert,
showCameraPermissionsAlert,
splitExtensionFromFileName,
- getAttachmentName,
+ getFileName,
getFileType,
cleanFileName,
appendTimeToFileName,
diff --git a/src/libs/fileDownload/index.android.ts b/src/libs/fileDownload/index.android.ts
index 41c7cb29550a..8496c1cb6cf5 100644
--- a/src/libs/fileDownload/index.android.ts
+++ b/src/libs/fileDownload/index.android.ts
@@ -38,7 +38,7 @@ function handleDownload(url: string, fileName: string): Promise {
// Android files will download to Download directory
const path = dirs.DownloadDir;
- const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url);
+ const attachmentName = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url));
const isLocalFile = url.startsWith('file://');
diff --git a/src/libs/fileDownload/index.ios.ts b/src/libs/fileDownload/index.ios.ts
index fdc4a78e0b9b..7672b4b14926 100644
--- a/src/libs/fileDownload/index.ios.ts
+++ b/src/libs/fileDownload/index.ios.ts
@@ -73,7 +73,7 @@ const fileDownload: FileDownload = (fileUrl, fileName) =>
new Promise((resolve) => {
let fileDownloadPromise;
const fileType = FileUtils.getFileType(fileUrl);
- const attachmentName = FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(fileUrl);
+ const attachmentName = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(fileUrl));
switch (fileType) {
case CONST.ATTACHMENT_FILE_TYPE.IMAGE:
diff --git a/src/libs/fileDownload/index.ts b/src/libs/fileDownload/index.ts
index ef36647e549d..4f43b215925f 100644
--- a/src/libs/fileDownload/index.ts
+++ b/src/libs/fileDownload/index.ts
@@ -29,10 +29,7 @@ const fileDownload: FileDownload = (url, fileName) => {
// adding href to anchor
link.href = href;
link.style.display = 'none';
- link.setAttribute(
- 'download',
- FileUtils.appendTimeToFileName(fileName) || FileUtils.getAttachmentName(url), // generating the file name
- );
+ link.download = FileUtils.appendTimeToFileName(fileName || FileUtils.getFileName(url));
// Append to html link element page
document.body.appendChild(link);
diff --git a/src/libs/getIsSmallScreenWidth.ts b/src/libs/getIsSmallScreenWidth.ts
new file mode 100644
index 000000000000..6fba45ea1319
--- /dev/null
+++ b/src/libs/getIsSmallScreenWidth.ts
@@ -0,0 +1,6 @@
+import {Dimensions} from 'react-native';
+import variables from '@styles/variables';
+
+export default function getIsSmallScreenWidth(windowWidth = Dimensions.get('window').width) {
+ return windowWidth <= variables.mobileResponsiveWidthBreakpoint;
+}
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js
index 66345107dbb1..6d3f0198bbfe 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -134,7 +134,6 @@ function DetailsPage(props) {
{({show}) => (
diff --git a/src/pages/ErrorPage/NotFoundPage.js b/src/pages/ErrorPage/NotFoundPage.js
index aac2e6d613f9..e10ec32732ea 100644
--- a/src/pages/ErrorPage/NotFoundPage.js
+++ b/src/pages/ErrorPage/NotFoundPage.js
@@ -13,12 +13,12 @@ const defaultProps = {
};
// eslint-disable-next-line rulesdir/no-negated-variables
-function NotFoundPage({onBackButtonPress}) {
+function NotFoundPage(props) {
return (
);
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index 97ec3f99da3c..ece75b7f6918 100755
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -159,7 +159,6 @@ function ProfilePage(props) {
diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js
index f1d62eef89ae..d7d622d309d6 100644
--- a/src/pages/ReimbursementAccount/CompanyStep.js
+++ b/src/pages/ReimbursementAccount/CompanyStep.js
@@ -88,6 +88,10 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul
];
const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields);
+ if (values.companyName && !ValidationUtils.isValidLegalName(values.companyName)) {
+ errors.companyName = 'bankAccount.error.companyName';
+ }
+
if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) {
errors.addressStreet = 'bankAccount.error.addressStreet';
}
@@ -96,6 +100,10 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul
errors.addressZipCode = 'bankAccount.error.zipCode';
}
+ if (values.addressCity && !ValidationUtils.isValidLegalName(values.addressCity)) {
+ errors.addressCity = 'bankAccount.error.addressCity';
+ }
+
if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) {
errors.companyPhone = 'bankAccount.error.phoneNumber';
}
diff --git a/src/pages/ReimbursementAccount/EnableStep.js b/src/pages/ReimbursementAccount/EnableStep.js
index 13ab2794ea09..e7bdcf4082d2 100644
--- a/src/pages/ReimbursementAccount/EnableStep.js
+++ b/src/pages/ReimbursementAccount/EnableStep.js
@@ -50,7 +50,7 @@ function EnableStep(props) {
const styles = useThemeStyles();
const isUsingExpensifyCard = props.user.isUsingExpensifyCard;
const achData = lodashGet(props.reimbursementAccount, 'achData') || {};
- const {icon, iconSize} = getBankIcon({bankName: achData.bankName, themeStyles: styles});
+ const {icon, iconSize} = getBankIcon({bankName: achData.bankName, styles});
const formattedBankAccountNumber = achData.accountNumber ? `${props.translate('paymentMethodList.accountLastFour')} ${achData.accountNumber.slice(-4)}` : '';
const bankName = achData.addressName;
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index 4f35926c5957..6c645bc87486 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -281,8 +281,8 @@ export default [
} else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction);
Clipboard.setString(displayMessage);
- } else if (ReportActionsUtils.isChannelLogMemberAction(reportAction)) {
- const logMessage = ReportUtils.getChannelLogMemberMessage(reportAction);
+ } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) {
+ const logMessage = ReportActionsUtils.getMemberChangeMessagePlainText(reportAction);
Clipboard.setString(logMessage);
} else if (content) {
const parser = new ExpensiMark();
diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.js b/src/pages/home/report/ReactionList/HeaderReactionList.js
index 59367395119c..2d4e6bfbb35a 100644
--- a/src/pages/home/report/ReactionList/HeaderReactionList.js
+++ b/src/pages/home/report/ReactionList/HeaderReactionList.js
@@ -27,8 +27,8 @@ const defaultProps = {
};
function HeaderReactionList(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
return (
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js
index ea48f9cc931e..a3fdc7f574f6 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js
@@ -427,18 +427,7 @@ function ComposerWithSuggestions({
return;
}
- // If the key pressed is non-character keys like Enter, Shift, ... do not focus
- if (e.key.length > 1) {
- return;
- }
-
- // If a key is pressed in combination with Meta, Control or Alt do not focus
- if (e.metaKey || e.ctrlKey || e.altKey) {
- return;
- }
-
- // If the space key is pressed, do not focus
- if (e.code === 'Space') {
+ if (!ReportUtils.shouldAutoFocusOnKeyPress(e)) {
return;
}
diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js
index 2265530f29a1..46e0438f250a 100644
--- a/src/pages/home/report/ReportActionItemMessage.js
+++ b/src/pages/home/report/ReportActionItemMessage.js
@@ -8,6 +8,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
+import TextCommentFragment from './comment/TextCommentFragment';
import ReportActionItemFragment from './ReportActionItemFragment';
import reportActionPropTypes from './reportActionPropTypes';
@@ -40,6 +41,20 @@ function ReportActionItemMessage(props) {
const styles = useThemeStyles();
const fragments = _.compact(props.action.previousMessage || props.action.message);
const isIOUReport = ReportActionsUtils.isMoneyRequestAction(props.action);
+ if (ReportActionsUtils.isMemberChangeAction(props.action)) {
+ const fragment = ReportActionsUtils.getMemberChangeMessageFragment(props.action);
+
+ return (
+
+ );
+ }
+
let iouMessage;
if (isIOUReport) {
const iouReportID = lodashGet(props.action, 'originalMessage.IOUReportID');
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index 39bce4bf067a..b13d57ad2976 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -79,8 +79,8 @@ const showWorkspaceDetails = (reportID) => {
};
function ReportActionItemSingle(props) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID;
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.js
index d956e10e1384..60605311b1ab 100644
--- a/src/pages/settings/Wallet/PaymentMethodList.js
+++ b/src/pages/settings/Wallet/PaymentMethodList.js
@@ -207,8 +207,8 @@ function PaymentMethodList({
shouldEnableScroll,
style,
}) {
- const styles = useThemeStyles();
const theme = useTheme();
+ const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -224,7 +224,7 @@ function PaymentMethodList({
return _.map(assignedCards, (card) => {
const isExpensifyCard = CardUtils.isExpensifyCard(card.cardID);
- const icon = getBankIcon({bankName: card.bank, isCard: true, themeStyles: styles});
+ const icon = getBankIcon({bankName: card.bank, isCard: true, styles});
// In the case a user has been assigned multiple physical Expensify Cards under one domain, display the Card with PAN
const expensifyCardDescription = numberPhysicalExpensifyCards > 1 ? CardUtils.getCardDescription(card.cardID) : translate('walletPage.expensifyCard');
diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts
index 4cb62b930632..64ef54a8ab84 100644
--- a/src/styles/StyleUtils.ts
+++ b/src/styles/StyleUtils.ts
@@ -8,8 +8,8 @@ import CONST from '@src/CONST';
import {Transaction} from '@src/types/onyx';
import colors from './colors';
import fontFamily from './fontFamily';
-import {ThemeStyles} from './styles';
-import {ThemeColors} from './themes/types';
+import {type ThemeStyles} from './styles';
+import {type ThemeColors} from './themes/types';
import cursor from './utilities/cursor';
import positioning from './utilities/positioning';
import spacing from './utilities/spacing';
@@ -69,11 +69,11 @@ type ModalPaddingStylesParams = {
};
type AvatarBorderStyleParams = {
+ theme: ThemeColors;
isHovered: boolean;
isPressed: boolean;
isInReportAction: boolean;
shouldUseCardBackground: boolean;
- theme: ThemeColors;
};
type GetBaseAutoCompleteSuggestionContainerStyleParams = {
@@ -857,7 +857,7 @@ function fade(fadeAnimation: Animated.Value): Animated.WithAnimatedValue obj;
+// eslint-disable-next-line @typescript-eslint/naming-convention
+const addOutlineWidth: AddOutlineWidth = (_theme, obj) => obj;
export default addOutlineWidth;
diff --git a/src/styles/addOutlineWidth/index.ts b/src/styles/addOutlineWidth/index.ts
index 7063d610cf11..c1ac1af52a8b 100644
--- a/src/styles/addOutlineWidth/index.ts
+++ b/src/styles/addOutlineWidth/index.ts
@@ -7,11 +7,11 @@ import AddOutlineWidth from './types';
/**
* Adds the addOutlineWidth property to an object to be used when styling
*/
-const addOutlineWidth: AddOutlineWidth = (obj, theme, val, hasError = false) => ({
+const addOutlineWidth: AddOutlineWidth = (theme, obj, val, hasError = false) => ({
...obj,
outlineWidth: val,
outlineStyle: val ? 'auto' : 'none',
- boxShadow: val !== 0 ? `0px 0px 0px ${val}px ${hasError ? theme?.danger : theme?.borderFocus}` : 'none',
+ boxShadow: val !== 0 ? `0px 0px 0px ${val}px ${hasError ? theme.danger : theme.borderFocus}` : 'none',
});
export default addOutlineWidth;
diff --git a/src/styles/addOutlineWidth/types.ts b/src/styles/addOutlineWidth/types.ts
index 35b6bc97a631..91e3f7409259 100644
--- a/src/styles/addOutlineWidth/types.ts
+++ b/src/styles/addOutlineWidth/types.ts
@@ -1,6 +1,6 @@
import {TextStyle} from 'react-native';
-import {ThemeColors} from '@styles/themes/types';
+import {type ThemeColors} from '@styles/themes/types';
-type AddOutlineWidth = (obj: TextStyle, theme?: ThemeColors, val?: number, hasError?: boolean) => TextStyle;
+type AddOutlineWidth = (theme: ThemeColors, obj: TextStyle, val?: number, hasError?: boolean) => TextStyle;
export default AddOutlineWidth;
diff --git a/src/styles/containerComposeStyles/types.ts b/src/styles/containerComposeStyles/types.ts
index 0b2305f52b56..9930e0230016 100644
--- a/src/styles/containerComposeStyles/types.ts
+++ b/src/styles/containerComposeStyles/types.ts
@@ -1,6 +1,6 @@
import {ViewStyle} from 'react-native';
-import themeStyles from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
-type ContainerComposeStyles = (styles: typeof themeStyles) => ViewStyle[];
+type ContainerComposeStyles = (styles: ThemeStyles) => ViewStyle[];
export default ContainerComposeStyles;
diff --git a/src/styles/getContextMenuItemStyles/types.ts b/src/styles/getContextMenuItemStyles/types.ts
index 6a101fdae413..376fb8d46d7c 100644
--- a/src/styles/getContextMenuItemStyles/types.ts
+++ b/src/styles/getContextMenuItemStyles/types.ts
@@ -1,6 +1,6 @@
import {ViewStyle} from 'react-native';
-import {ThemeStyles} from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
-type GetContextMenuItemStyle = (themeStyles: ThemeStyles, windowWidth?: number) => ViewStyle[];
+type GetContextMenuItemStyle = (styles: ThemeStyles, windowWidth?: number) => ViewStyle[];
export default GetContextMenuItemStyle;
diff --git a/src/styles/getModalStyles.ts b/src/styles/getModalStyles.ts
index 1c28de3bb5e2..b11b350e4e9d 100644
--- a/src/styles/getModalStyles.ts
+++ b/src/styles/getModalStyles.ts
@@ -2,8 +2,8 @@ import {ViewStyle} from 'react-native';
import {ModalProps} from 'react-native-modal';
import {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
-import {ThemeStyles} from './styles';
-import {ThemeColors} from './themes/types';
+import {type ThemeStyles} from './styles';
+import {type ThemeColors} from './themes/types';
import variables from './variables';
function getCenteredModalStyles(styles: ThemeStyles, windowWidth: number, isSmallScreenWidth: boolean, isFullScreenWhenSmall = false): ViewStyle {
@@ -37,10 +37,10 @@ type GetModalStyles = {
};
export default function getModalStyles(
- type: ModalType | undefined,
- windowDimensions: WindowDimensions,
theme: ThemeColors,
styles: ThemeStyles,
+ type: ModalType | undefined,
+ windowDimensions: WindowDimensions,
popoverAnchorPosition: ViewStyle = {},
innerContainerStyle: ViewStyle = {},
outerStyle: ViewStyle = {},
diff --git a/src/styles/getNavigationModalCardStyles/types.ts b/src/styles/getNavigationModalCardStyles/types.ts
index 877981dd4dd2..e0dba07dc908 100644
--- a/src/styles/getNavigationModalCardStyles/types.ts
+++ b/src/styles/getNavigationModalCardStyles/types.ts
@@ -1,7 +1,5 @@
import {ViewStyle} from 'react-native';
-type GetNavigationModalCardStylesParams = {isSmallScreenWidth: number};
-
-type GetNavigationModalCardStyles = (params: GetNavigationModalCardStylesParams) => ViewStyle;
+type GetNavigationModalCardStyles = () => ViewStyle;
export default GetNavigationModalCardStyles;
diff --git a/src/styles/getReportActionContextMenuStyles.ts b/src/styles/getReportActionContextMenuStyles.ts
index 781d0cc11549..86dd14cb8446 100644
--- a/src/styles/getReportActionContextMenuStyles.ts
+++ b/src/styles/getReportActionContextMenuStyles.ts
@@ -1,6 +1,6 @@
import {ViewStyle} from 'react-native';
-import {ThemeStyles} from './styles';
-import {ThemeColors} from './themes/types';
+import {type ThemeStyles} from './styles';
+import {type ThemeColors} from './themes/types';
import variables from './variables';
const getDefaultWrapperStyle = (theme: ThemeColors): ViewStyle => ({
diff --git a/src/styles/getTooltipStyles.ts b/src/styles/getTooltipStyles.ts
index c47659c4f80c..1adfa1969ab9 100644
--- a/src/styles/getTooltipStyles.ts
+++ b/src/styles/getTooltipStyles.ts
@@ -1,8 +1,8 @@
import {TextStyle, View, ViewStyle} from 'react-native';
import fontFamily from './fontFamily';
import roundToNearestMultipleOfFour from './roundToNearestMultipleOfFour';
-import {ThemeStyles} from './styles';
-import {ThemeColors} from './themes/types';
+import {type ThemeStyles} from './styles';
+import {type ThemeColors} from './themes/types';
import positioning from './utilities/positioning';
import spacing from './utilities/spacing';
import variables from './variables';
@@ -113,7 +113,7 @@ type TooltipParams = {
tooltipContentWidth: number;
tooltipWrapperHeight: number;
theme: ThemeColors;
- themeStyles: ThemeStyles;
+ styles: ThemeStyles;
manualShiftHorizontal?: number;
manualShiftVertical?: number;
};
@@ -151,7 +151,7 @@ export default function getTooltipStyles({
tooltipContentWidth,
tooltipWrapperHeight,
theme,
- themeStyles,
+ styles,
manualShiftHorizontal = 0,
manualShiftVertical = 0,
}: TooltipParams): TooltipStyles {
@@ -245,7 +245,7 @@ export default function getTooltipStyles({
// at the center of the hovered component.
pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2);
- pointerAdditionalStyle = shouldShowBelow ? themeStyles.flipUpsideDown : {};
+ pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {};
}
return {
@@ -268,8 +268,8 @@ export default function getTooltipStyles({
left: rootWrapperLeft,
// We are adding this to prevent the tooltip text from being selected and copied on CTRL + A.
- ...themeStyles.userSelectNone,
- ...themeStyles.pointerEventsNone,
+ ...styles.userSelectNone,
+ ...styles.pointerEventsNone,
},
textStyle: {
color: theme.textReversed,
diff --git a/src/styles/optionRowStyles/types.ts b/src/styles/optionRowStyles/types.ts
index 68eb91975f8e..3425d7812be2 100644
--- a/src/styles/optionRowStyles/types.ts
+++ b/src/styles/optionRowStyles/types.ts
@@ -1,6 +1,6 @@
import {ViewStyle} from 'react-native';
-import {ThemeStyles} from '@styles/styles';
+import {type ThemeStyles} from '@styles/styles';
-type CompactContentContainerStyles = (themeStyles: ThemeStyles) => ViewStyle;
+type CompactContentContainerStyles = (styles: ThemeStyles) => ViewStyle;
export default CompactContentContainerStyles;
diff --git a/src/styles/styles.ts b/src/styles/styles.ts
index 983f1ba82caa..b88119beae74 100644
--- a/src/styles/styles.ts
+++ b/src/styles/styles.ts
@@ -21,7 +21,7 @@ import pointerEventsAuto from './pointerEventsAuto';
import pointerEventsBoxNone from './pointerEventsBoxNone';
import pointerEventsNone from './pointerEventsNone';
import defaultTheme from './themes/default';
-import {ThemeColors} from './themes/types';
+import {type ThemeColors} from './themes/types';
import borders from './utilities/borders';
import cursor from './utilities/cursor';
import display from './utilities/display';
@@ -586,7 +586,7 @@ const styles = (theme: ThemeColors) =>
buttonDivider: {
height: variables.dropDownButtonDividerHeight,
borderWidth: 0.7,
- borderColor: theme.text,
+ borderColor: theme.textLight,
},
noBorderRadius: {
@@ -1016,7 +1016,7 @@ const styles = (theme: ThemeColors) =>
flexDirection: 'row',
},
- textInputDesktop: addOutlineWidth({}, theme, 0),
+ textInputDesktop: addOutlineWidth(theme, {}, 0),
textInputIconContainer: {
paddingHorizontal: 11,
@@ -1133,7 +1133,7 @@ const styles = (theme: ThemeColors) =>
color: theme.icon,
},
- noOutline: addOutlineWidth({}, theme, 0),
+ noOutline: addOutlineWidth(theme, {}, 0),
textLabelSupporting: {
fontFamily: fontFamily.EXP_NEUE,
@@ -1150,7 +1150,7 @@ const styles = (theme: ThemeColors) =>
textReceiptUpload: {
...headlineFont,
fontSize: variables.fontSizeXLarge,
- color: theme.textLight,
+ color: theme.text,
textAlign: 'center',
},
@@ -1158,7 +1158,7 @@ const styles = (theme: ThemeColors) =>
fontFamily: fontFamily.EXP_NEUE,
lineHeight: variables.lineHeightLarge,
textAlign: 'center',
- color: theme.textLight,
+ color: theme.text,
},
furtherDetailsText: {
@@ -1492,6 +1492,8 @@ const styles = (theme: ThemeColors) =>
flexDirection: 'row',
paddingLeft: 8,
paddingRight: 8,
+ marginHorizontal: 12,
+ borderRadius: variables.componentBorderRadiusNormal,
},
sidebarLinkText: {
@@ -1779,6 +1781,7 @@ const styles = (theme: ThemeColors) =>
// Be extremely careful when editing the compose styles, as it is easy to introduce regressions.
// Make sure you run the following tests against any changes: #12669
textInputCompose: addOutlineWidth(
+ theme,
{
backgroundColor: theme.componentBG,
borderColor: theme.border,
@@ -1799,7 +1802,6 @@ const styles = (theme: ThemeColors) =>
alignSelf: 'center',
verticalAlign: 'middle',
},
- theme,
0,
),
@@ -2619,6 +2621,7 @@ const styles = (theme: ThemeColors) =>
},
iouAmountTextInput: addOutlineWidth(
+ theme,
{
...headlineFont,
fontSize: variables.iouAmountTextSize,
@@ -2626,7 +2629,6 @@ const styles = (theme: ThemeColors) =>
padding: 0,
lineHeight: undefined,
},
- theme,
0,
),
@@ -3353,7 +3355,7 @@ const styles = (theme: ThemeColors) =>
fontFamily: fontFamily.EXP_NEUE,
fontSize: variables.fontSizeXLarge,
lineHeight: variables.lineHeightXXLarge,
- color: theme.text,
+ color: theme.textColorfulBackground,
},
eReceiptWaypointTitle: {
@@ -3608,7 +3610,7 @@ const styles = (theme: ThemeColors) =>
marginLeft: 8,
fontFamily: isSelected ? fontFamily.EXP_NEUE_BOLD : fontFamily.EXP_NEUE,
fontWeight: isSelected ? fontWeightBold : '400',
- color: isSelected ? theme.textLight : theme.textSupporting,
+ color: isSelected ? theme.text : theme.textSupporting,
} satisfies TextStyle),
tabBackground: (hovered: boolean, isFocused: boolean, background: string) => ({
diff --git a/src/styles/themes/ThemeContext.ts b/src/styles/themes/ThemeContext.ts
index 8c57cc9c7e9f..3c969c7393c5 100644
--- a/src/styles/themes/ThemeContext.ts
+++ b/src/styles/themes/ThemeContext.ts
@@ -1,6 +1,6 @@
import React from 'react';
import darkTheme from './default';
-import {ThemeColors} from './types';
+import {type ThemeColors} from './types';
const ThemeContext = React.createContext(darkTheme);
diff --git a/src/styles/themes/Themes.ts b/src/styles/themes/Themes.ts
index a87407790502..5de65c7316b4 100644
--- a/src/styles/themes/Themes.ts
+++ b/src/styles/themes/Themes.ts
@@ -1,7 +1,7 @@
import CONST from '@src/CONST';
import darkTheme from './default';
import lightTheme from './light';
-import {ThemeColors, ThemePreferenceWithoutSystem} from './types';
+import {type ThemeColors, ThemePreferenceWithoutSystem} from './types';
const Themes = {
[CONST.THEME.LIGHT]: lightTheme,
diff --git a/src/styles/themes/default.ts b/src/styles/themes/default.ts
index 519e818054e0..59de9b7269a8 100644
--- a/src/styles/themes/default.ts
+++ b/src/styles/themes/default.ts
@@ -1,7 +1,7 @@
import colors from '@styles/colors';
import CONST from '@src/CONST';
import SCREENS from '@src/SCREENS';
-import {ThemeColors} from './types';
+import {type ThemeColors} from './types';
const darkTheme = {
// Figma keys
diff --git a/src/styles/themes/light.ts b/src/styles/themes/light.ts
index 2f6eda2f70dc..1541c0e723d7 100644
--- a/src/styles/themes/light.ts
+++ b/src/styles/themes/light.ts
@@ -1,7 +1,7 @@
import colors from '@styles/colors';
import CONST from '@src/CONST';
import SCREENS from '@src/SCREENS';
-import {ThemeColors} from './types';
+import {type ThemeColors} from './types';
const lightTheme = {
// Figma keys
diff --git a/src/styles/themes/useTheme.ts b/src/styles/themes/useTheme.ts
index 8bb4fe73c106..adeaffbcae8a 100644
--- a/src/styles/themes/useTheme.ts
+++ b/src/styles/themes/useTheme.ts
@@ -1,6 +1,6 @@
import {useContext} from 'react';
import ThemeContext from './ThemeContext';
-import {ThemeColors} from './types';
+import {type ThemeColors} from './types';
function useTheme(): ThemeColors {
const theme = useContext(ThemeContext);
diff --git a/src/types/onyx/DemoInfo.ts b/src/types/onyx/DemoInfo.ts
new file mode 100644
index 000000000000..dcd7efc44d8d
--- /dev/null
+++ b/src/types/onyx/DemoInfo.ts
@@ -0,0 +1,8 @@
+type DemoInfo = {
+ money2020: {
+ /** If the beginning demo should be shown */
+ isBeginningDemo?: boolean;
+ };
+};
+
+export default DemoInfo;
diff --git a/src/types/onyx/Locale.ts b/src/types/onyx/Locale.ts
new file mode 100644
index 000000000000..1a5124684995
--- /dev/null
+++ b/src/types/onyx/Locale.ts
@@ -0,0 +1,6 @@
+import {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+
+type Locale = ValueOf;
+
+export default Locale;
diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts
index 50b1503b90bd..843d3ae86e46 100644
--- a/src/types/onyx/OnyxUpdatesFromServer.ts
+++ b/src/types/onyx/OnyxUpdatesFromServer.ts
@@ -2,9 +2,11 @@ import {OnyxUpdate} from 'react-native-onyx';
import Request from './Request';
import Response from './Response';
+type OnyxServerUpdate = OnyxUpdate & {shouldNotify?: boolean};
+
type OnyxUpdateEvent = {
eventType: string;
- data: OnyxUpdate[];
+ data: OnyxServerUpdate[];
};
type OnyxUpdatesFromServer = {
@@ -16,4 +18,4 @@ type OnyxUpdatesFromServer = {
updates?: OnyxUpdateEvent[];
};
-export type {OnyxUpdatesFromServer, OnyxUpdateEvent};
+export type {OnyxUpdatesFromServer, OnyxUpdateEvent, OnyxServerUpdate};
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index f76fbd5ffd7d..72ea275e3ba3 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -140,6 +140,7 @@ type ChronosOOOTimestamp = {
type ChangeLog = {
targetAccountIDs?: number[];
roomName?: string;
+ reportID?: number;
};
type ChronosOOOEvent = {
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index b49599913543..a077bf1a3281 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -94,6 +94,9 @@ type Report = {
/** The report type */
type?: string;
+ /** If the admin room should be opened */
+ openOnAdminRoom?: boolean;
+
/** The report visibility */
visibility?: ValueOf;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 4d4f45442d55..dcaa4ee3d623 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -7,11 +7,13 @@ import Card from './Card';
import Credentials from './Credentials';
import Currency from './Currency';
import CustomStatusDraft from './CustomStatusDraft';
+import DemoInfo from './DemoInfo';
import Download from './Download';
import Form, {AddDebitCardForm, DateOfBirthForm} from './Form';
import FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import Fund from './Fund';
import IOU from './IOU';
+import Locale from './Locale';
import Login from './Login';
import MapboxAccessToken from './MapboxAccessToken';
import Modal from './Modal';
@@ -65,11 +67,13 @@ export type {
Currency,
CustomStatusDraft,
DateOfBirthForm,
+ DemoInfo,
Download,
Form,
FrequentlyUsedEmoji,
Fund,
IOU,
+ Locale,
Login,
MapboxAccessToken,
Modal,
diff --git a/tests/unit/LocalizeTests.js b/tests/unit/LocalizeTests.js
index 4c89d587fc06..7693a0a4a88d 100644
--- a/tests/unit/LocalizeTests.js
+++ b/tests/unit/LocalizeTests.js
@@ -15,7 +15,7 @@ describe('localize', () => {
afterEach(() => Onyx.clear());
- describe('arrayToString', () => {
+ describe('formatList', () => {
test.each([
[
[],
@@ -52,9 +52,9 @@ describe('localize', () => {
[CONST.LOCALES.ES]: 'rory, vit e ionatan',
},
],
- ])('arrayToSpokenList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => {
- expect(Localize.arrayToString(input)).toBe(expectedOutput);
- return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.arrayToString(input)).toBe(expectedOutputES));
+ ])('formatList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => {
+ expect(Localize.formatList(input)).toBe(expectedOutput);
+ return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.formatList(input)).toBe(expectedOutputES));
});
});
});
diff --git a/web/index.html b/web/index.html
index a7630b1a2cdc..967873fe586c 100644
--- a/web/index.html
+++ b/web/index.html
@@ -31,9 +31,6 @@
#root > div > div {
height: 100% !important;
}
- :root {
- color-scheme: dark !important;
- }
* {
touch-action: pan-x pan-y;
}
@@ -74,6 +71,17 @@
}
/* Customizes the background and text colors for autofill inputs in Chrome */
+ /* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature.
+ We should rely on the chrome-autofilled property being added to the input when users use auto-fill */
+ input[chrome-autofilled],
+ input[chrome-autofilled]:hover,
+ input[chrome-autofilled]:focus,
+ textarea[chrome-autofilled],
+ textarea[chrome-autofilled]:hover,
+ textarea[chrome-autofilled]:focus,
+ select[chrome-autofilled],
+ select[chrome-autofilled]:hover,
+ select[chrome-autofilled]:focus,
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
@@ -89,7 +97,7 @@
}
/* Prevent autofill from overlapping with the input label in Chrome */
- div:has(input:-webkit-autofill) > label {
+ div:has(input:-webkit-autofill, input[chrome-autofilled]) > label {
transform: translateY(var(--active-label-translate-y)) scale(var(--active-label-scale)) !important;
transition: transform var(--label-transition-duration);
}