diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3ced6a4444a3..2a5fd87db3e4 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001046106
- versionName "1.4.61-6"
+ versionCode 1001046203
+ versionName "1.4.62-3"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/assets/images/new-expensify-adhoc.svg b/assets/images/new-expensify-adhoc.svg
index f2603555fc38..b3dd92fbbaae 100644
--- a/assets/images/new-expensify-adhoc.svg
+++ b/assets/images/new-expensify-adhoc.svg
@@ -1 +1,31 @@
-
\ No newline at end of file
+
+
+
diff --git a/assets/images/new-expensify-dev.svg b/assets/images/new-expensify-dev.svg
index 9c11ed02433c..316da6b5aa4d 100644
--- a/assets/images/new-expensify-dev.svg
+++ b/assets/images/new-expensify-dev.svg
@@ -1 +1,27 @@
-
\ No newline at end of file
+
+
+
diff --git a/assets/images/new-expensify-stg.svg b/assets/images/new-expensify-stg.svg
index f151d7c4c130..1a1994c7a9fd 100644
--- a/assets/images/new-expensify-stg.svg
+++ b/assets/images/new-expensify-stg.svg
@@ -1 +1,35 @@
-
\ No newline at end of file
+
+
+
diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts
index b0e301ef3a6c..7cafafca9973 100644
--- a/config/webpack/webpack.common.ts
+++ b/config/webpack/webpack.common.ts
@@ -30,7 +30,7 @@ const includeModules = [
].join('|');
const environmentToLogoSuffixMap: Record = {
- production: '',
+ production: '-dark',
staging: '-stg',
dev: '-dev',
adhoc: '-adhoc',
diff --git a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md
index f2a2efca1f5f..e7dcf5404c34 100644
--- a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md
+++ b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md
@@ -24,4 +24,7 @@ C+ are contributors who are experienced at working with Expensify and have gaine
## How to join?
-Email contributors@expensify.com and include "C+ Team Application" in the subject line if you’re interested in joining. Please include your GitHub username and a link to the PRs you've authored that have been merged. ie. `https://github.com/Expensify/App/pulls?q=is%3Apr+author%3Aparasharrajat+is%3Amerged`
+Email contributors@expensify.com and include "C+ Team Application" in the subject line if you’re interested in joining. Please include:
+1. Your GitHub username.
+2. A link to the PRs you've authored that have been merged. ie. `https://github.com/Expensify/App/pulls?q=is%3Apr+is%3Amerged+author%3Aparasharrajat`.
+3. Links to three GitHub issues that were particularly challenging and best demonstrate your skill level.
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-AUD.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-AUD.md
new file mode 100644
index 000000000000..e274cb3d5b60
--- /dev/null
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-AUD.md
@@ -0,0 +1,21 @@
+---
+title: Deposit Accounts (AUD)
+description: Expensify allows you to add a personal bank account to receive reimbursements for your expenses. We never take money out of this account — it is only a place for us to deposit funds from your employer. This article covers deposit accounts for Australian banks.
+---
+
+## How-to add your Australian personal deposit account information
+1. Confirm with your Policy Admin that they’ve set up Global Reimbursment
+2. Set your default policy (by selecting the correct policy after clicking on your profile picture) before adding your deposit account.
+3. Go to **Settings > Account > Payments** and click **Add Deposit-Only Bank Account**
+{:width="100%"}
+
+4. Enter your BSB, account number and name. If your screen looks different than the image below, that means your company hasn't enabled reimbursements through Expensify. Please contact your administrator and ask them to enable reimbursements.
+
+{:width="100%"}
+
+# How-to delete a bank account
+Bank accounts are easy to delete! Simply click the red **Delete** button in the bank account under **Settings > Account > Payments**.
+
+{:width="100%"}
+
+You can complete this process on a computer or on the mobile app.
diff --git a/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md b/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md
deleted file mode 100644
index 8c5ead911da4..000000000000
--- a/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md
+++ /dev/null
@@ -1,51 +0,0 @@
----
-title: Add a Business Bank Account
-description: This article provides insight on setting up and using an Australian Business Bank account in Expensify.
----
-
-# How to add an Australian business bank account (for admins)
-A withdrawal account is the business bank account that you want to use to pay your employee reimbursements.
-
-_Your policy currency must be set to AUD and reimbursement setting set to Indirect to continue. If your main policy is used for something other than AUD, then you will need to create a new one and set that policy to AUD._
-
-To set this up, you’ll run through the following steps:
-
-1. Go to **Settings > Your Account > Payments** and click **Add Verified Bank Account**
-{:width="100%"}
-
-2. Enter the required information to connect to your business bank account. If you don't know your Bank User ID/Direct Entry ID/APCA Number, please contact your bank and they will be able to provide this.
-{:width="100%"}
-
-3. Link the withdrawal account to your policy by heading to **Settings > Policies > Group > [Policy name] > Reimbursement**
-4. Click **Direct reimbursement**
-5. Set the default withdrawal account for processing reimbursements
-6. Tell your employees to add their deposit accounts and start reimbursing.
-
-# How to delete a bank account
-If you’re no longer using a bank account you previously connected to Expensify, you can delete it by doing the following:
-
-1. Navigate to Settings > Accounts > Payments
-2. Click **Delete**
-{:width="100%"}
-
-You can complete this process either via the web app (on a computer), or via the mobile app.
-
-# Deep Dive
-## Bank-specific batch payment support
-
-If you are new to using Batch Payments in Australia, to reimburse your staff or process payroll, you may want to check out these bank-specific instructions for how to upload your .aba file:
-
-- ANZ Bank - [Import a file for payroll payments](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/)
-- CommBank - [Importing and using Direct Entry (EFT) files](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf)
-- Westpac - [Importing Payment Files](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/)
-- NAB - [Quick Reference Guide - Upload a payment file](https://www.nab.com.au/business/online-banking/nab-connect/help)
-- Bendigo Bank - [Bulk payments user guide](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf)
-- Bank of Queensland - [Payments file upload facility FAQ](https://www.boq.com.au/help-and-support/online-banking/ob-faqs-and-support/faq-pfuf)
-
-**Note:** Some financial institutions require an ABA file to include a *self-balancing transaction*. If you are unsure, please check with your bank to ensure whether to tick this option or not, as selecting an incorrect option will result in the ABA file not working with your bank's internet banking platform.
-
-## Enable Global Reimbursement
-
-If you have employees in other countries outside of Australia, you can now reimburse them directly using Global Reimbursement.
-
-To do this, you’ll first need to delete any existing Australian business bank accounts. Then, you’ll want to follow the instructions to enable Global Reimbursements
diff --git a/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md
deleted file mode 100644
index 0bc5cb0ad955..000000000000
--- a/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-USD.md
+++ /dev/null
@@ -1,77 +0,0 @@
----
-title: Deposit Accounts - USD
-description: How to add a deposit account to receive payments for yourself or your business (US)
----
-# Overview
-
-There are two types of deposit-only accounts:
-
-1. If you're an employee seeking reimbursement for expenses you’ve incurred, you’ll add a **Personal deposit-only bank account**.
-2. If you're a vendor seeking payment for goods or services, you’ll add a **Business deposit-only account**.
-
-# How to connect a personal deposit-only bank account
-
-**Connect a personal deposit-only bank account if you are:**
-
-- An employee based in the US who gets reimbursed by their employer
-- An employee based in Australia who gets reimbursed by their company via batch payments
-- An international (non-US) employee whose US-based employers send international reimbursements
-
-**To establish the connection to a personal bank account, follow these steps:**
-
-1. Navigate to your **Settings > Account > Payments** and click the **Add Deposit-Only Bank Account** button.
-2. Click **Log into your bank** button and click **Continue** on the Plaid connection pop-up window.
-3. Search for your bank account in the list of banks and follow the prompts to sign-in to your bank account.
-4. Enter your bank login credentials when prompted.
- - If your bank doesn't appear, click the 'x' in the upper right corner of the Plaid pop-up window and click **Connect Manually**.
- - Enter your account information, then click **Save & Continue**.
-
-You should be all set! You’ll receive reimbursement for your expense reports directly to this bank account.
-
-# How to connect a business deposit-only bank account
-
-**Connect a business deposit-only bank account if you are:**
-
-- A US-based vendor who wants to be paid directly for bills sent to customers/clients
-- A US-based vendor who want to pay invoices directly via Expensify
-
-**To establish the connection to a business bank account, follow these steps:**
-
-1. Navigate to your **Settings > Account > Payments and click the Add Deposit-Only Bank Account** button.
-2. Click **Log into your bank** button and click **Continue** on the Plaid connection pop-up window.
-3. Search for your bank account in the list of banks and follow the prompts to sign-in to your bank account.
-4. Enter your bank login credentials when prompted.
- - If your bank doesn't appear, click the 'x' in the upper right corner of the Plaid pop-up window and click **Connect Manually**.
- - Enter your account information, then click **Save & Continue**.
-5. If you see the option to “Switch to Business” after entering the account owner information, click that link.
-6. Enter your Company Name and FEIN or TIN information.
-7. Enter your company’s website formatted as https://www.domain.com.
-
-You should be all set! The bank account will display as a deposit-only business account, and you’ll be paid directly for any invoices you submit for payment.
-
-# How to delete a deposit-only bank account
-
-**To delete a deposit-only bank account, do the following:**
-
-1. Navigate to **Settings > Account > Payments > Bank Accounts**
-2. Click the **Delete** next to the bank account you want to remove
-
-{% include faq-begin.md %}
-
-## **What happens if my bank requires an additional security check before adding it to a third-party?**
-
-If your bank account has 2FA enabled or another security step, you should be prompted to complete this when adding the account. If not, and you encounter an error, you can always select the option to “Connect Manually”. Either way, please double check that you are entering the correct bank account details to ensure successful payments.
-
-## **What if I also want to pay employees with my business bank account?**
-
-If you’ve added a business deposit-only account and also wish to also pay employees, vendors, or utilize the Expensify Card with this bank account, select “Verify” on the listed bank account. This will take you through the additional verification steps to use this account to issue payments.
-
-## **I connected my deposit-only bank account – Why haven’t I received my reimbursement?**
-
-There are a few reasons a reimbursement may be unsuccessful. The first step is to review the estimated deposit date on the report. If it’s after that date and you still haven’t seen the funds, it could have been unsuccessful because:
- - The incorrect account was added. If you believe you may have entered the wrong account, please reach out to Concierge and provide the Report ID for the missing reimbursement.
- - Your account wasn’t set up for Direct Deposit/ACH. You may want to contact your bank to confirm.
-
-If you aren’t sure, please reach out to Concierge and we can assist!
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md
deleted file mode 100644
index 5a27f58cf2e8..000000000000
--- a/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-title: Invite Members
-description: Learn how add your employees to submit expenses in Expensify
----
-# Overview
-
-To invite your employees to Expensify, simply add them as members to your Workspace.
-
-# How to Invite members to Expensify
-
-## Inviting Members Manually
-
-Navigate to **Settings > Workspace > Group > *Workspace Name* > People** - then click **Invite** and enter the invitee's email address.
-
-Indicate whether you want them to be an Employee, Admin, or Auditor on the Workspace.
-
-If you are utilizing the Advanced Approval feature and the invitee is an approver, you can use the "Approves to" field to specify to whom they approve and forward reports for additional approval.
-
-## Inviting Members to a Workspace in Bulk
-
-Navigate to **Settings > Workspaces > Group > *Workspace Name* > People** - then click Invite and enter all of the email addresses separated by comma. Indicate whether you want them to be an Employee, Admin, or Auditor on the Workspace.
-
-If you are utilizing the Advanced Approval feature, you can specify who each member should submit their expense reports to and who an approver should send approved reports to for the next step in the approval process. If someone is the final approver, you can leave this field blank.
-
-Another convenient method is to employ the spreadsheet bulk upload option for inviting members to a Workspace. This proves particularly helpful when initially configuring your system or when dealing with numerous member updates. Simply click the "Import from Spreadsheet" button and upload a file in formats such as .csv, .txt, .xls, or .xlsx to streamline the process.
-
-After uploading the spreadsheet, we'll display a window where you can choose which columns to import and what they correspond to. These are the fields:
-- Email
-- Role
-- Custom Field 1
-- Custom Field 2
-- Submits To
-- Approves To
-- Approval Limit
-- Over Limit Forward To
-
-Click the **Import** button and you're done. We will import the new members with the optional settings and update any already existing ones.
-
-## Inviting Members with a Shareable Workspace Joining Link
-
-You have the ability to invite your colleagues to join your Expensify Workspace by sharing a unique Workspace Joining Link. You can use this link as many times as necessary to invite multiple members through various communication methods such as internal emails, chats, text messages, and more.
-
-To find your unique link, simply go to **Settings > Workspace > Group > *Workspace Name* > People**.
-
-## Allowing Members to Automatically Join Your Workspace
-
-You can streamline the process of inviting colleagues to your Workspace by enabling the Pre-approve switch located below your Workspace Joining Link. This allows teammates to automatically become part of your Workspace as soon as they create an Expensify account using their work email address.
-
-Here's how it works: If a colleague signs up with a work email address that matches the email domain of a company Workspace owner (e.g., if the Workspace owner's email is admin@expensify.com and the colleague signs up with employee@expensify.com), they will be able to join your Workspace seamlessly without requiring a manual invitation. When new members join the Workspace, they will be set up to submit their expense reports to the Workspace owner by default.
-
-To enable this feature, go to **Settings > Workspace > Group > *Workspace Name* > People**.
-
-
-{% include faq-begin.md %}
-## Who can invite members to Expensify
-Any Workspace Admin can add members to a Group Workspace using any of the above methods.
-
-## How can I customize an invite message?
-Under **Settings > Workspace > Group > *Workspace Name* > People > Invite** you can enter a custom message you'd like members to receive in their invitation email.
-
-## How can I invite members via the API?
-If you would like to integrate an open API HR software, you can use our [Advanced Employee Updater API](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/) to invite members to your Workspace.
-
-{% include faq-end.md %}
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 8e9e6350a326..fac32e6e4e57 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -77,6 +77,13 @@ https://help.expensify.com/articles/expensify-classic/workspace-and-domain-setti
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Reimbursement,https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing-Reports
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Tags
https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles,https://help.expensify.com/articles/expensify-classic/workspaces/Change-member-workspace-roles
+https://help.expensify.com/articles/new-expensify/account-settings/Preferences,https://help.expensify.com/articles/new-expensify/settings/Preferences
+https://help.expensify.com/articles/new-expensify/account-settings/Profile,https://help.expensify.com/articles/new-expensify/settings/Profile
+https://help.expensify.com/articles/new-expensify/account-settings/Security,https://help.expensify.com/articles/new-expensify/settings/Security
+https://help.expensify.com/articles/new-expensify/bank-accounts/Connect-a-Bank-Account,https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account
+https://help.expensify.com/articles/new-expensify/payments/Distance-Requests,https://help.expensify.com/articles/new-expensify/expenses/Distance-Requests
+https://help.expensify.com/articles/expensify-classic/expenses/Referral-Program,https://help.expensify.com/articles/new-expensify/expenses/Referral-Program
+https://help.expensify.com/articles/new-expensify/payments/Request-Money,https://help.expensify.com/articles/new-expensify/expenses/Request-Money
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/tax-tracking,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Apply-Tax
https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles.html,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Owner,https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 8e1b8746afd0..f7cff60e90fd 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.61
+ 1.4.62
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.61.6
+ 1.4.62.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 1dccb441b241..f7e41253a922 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.61
+ 1.4.62
CFBundleSignature
????
CFBundleVersion
- 1.4.61.6
+ 1.4.62.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index d3050fa055fb..a215a2d9f43b 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.61
+ 1.4.62
CFBundleVersion
- 1.4.61.6
+ 1.4.62.3
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2f53f00918bf..231ce0248d5e 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1836,7 +1836,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.38):
+ - RNLiveMarkdown (0.1.47):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1854,9 +1854,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.38)
+ - RNLiveMarkdown/common (= 0.1.47)
- Yoga
- - RNLiveMarkdown/common (0.1.38):
+ - RNLiveMarkdown/common (0.1.47):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2565,7 +2565,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: 9d974f060d0bd857f7d96fac0e9a1539363baa5e
+ RNLiveMarkdown: f172c7199283dc9d21bccf7e21ea10741fd19e1d
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d
RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729
@@ -2582,7 +2582,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf
- Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
+ Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70
PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d
diff --git a/package-lock.json b/package-lock.json
index 4ad32ba9e720..615f94ad6092 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "1.4.61-6",
+ "version": "1.4.62-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.61-6",
+ "version": "1.4.62-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "github:Expensify/react-native-live-markdown#f762be6fa832419dbbecb8a0cf64bf7dce18545b",
+ "@expensify/react-native-live-markdown": "0.1.47",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -38,7 +38,7 @@
"@react-native-picker/picker": "2.6.1",
"@react-navigation/material-top-tabs": "^6.6.3",
"@react-navigation/native": "6.1.12",
- "@react-navigation/stack": "6.3.16",
+ "@react-navigation/stack": "6.3.29",
"@react-ng/bounds-observer": "^0.2.1",
"@rnmapbox/maps": "10.1.11",
"@shopify/flash-list": "1.6.3",
@@ -3570,13 +3570,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.38",
- "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#f762be6fa832419dbbecb8a0cf64bf7dce18545b",
- "integrity": "sha512-m8+t3y1AtpvFAt3GAwRCiGwcOhUagOTCvwJ87kMGO5q/SKB2GCBHYMQ0QZaHw2QvAzRE6v2kCdqItX5DY+4MPQ==",
- "license": "MIT",
- "workspaces": [
- "example"
- ],
+ "version": "0.1.47",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.47.tgz",
+ "integrity": "sha512-zUfwgg6qq47MnGuynamDpdHSlBYwVKFV4Zc/2wlVzFcBndQOjOyFu04Ns8YDB4Gl80LyGvfAuBT/sU+kvmMU6g==",
"engines": {
"node": ">= 18.0.0"
},
@@ -9216,8 +9212,9 @@
}
},
"node_modules/@react-navigation/elements": {
- "version": "1.3.24",
- "license": "MIT",
+ "version": "1.3.30",
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.30.tgz",
+ "integrity": "sha512-plhc8UvCZs0UkV+sI+3bisIyn78wz9O/BiWZXpounu72k/R/Sj5PuZYFJ1fi6psvriUveMCGh4LeZckAZu2qiQ==",
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
@@ -9262,10 +9259,11 @@
}
},
"node_modules/@react-navigation/stack": {
- "version": "6.3.16",
- "license": "MIT",
+ "version": "6.3.29",
+ "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.29.tgz",
+ "integrity": "sha512-tzlGkoRgB6P7vgw7rHuWo3TL7Gzu6xh5LMf+zSdCuEiKp/qASzxYfnTEr9tOLbVs/gf+qeukEDheCSAJKVpBXw==",
"dependencies": {
- "@react-navigation/elements": "^1.3.17",
+ "@react-navigation/elements": "^1.3.30",
"color": "^4.2.3",
"warn-once": "^0.1.0"
},
diff --git a/package.json b/package.json
index 719d8f49d77d..e089d874b71a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.61-6",
+ "version": "1.4.62-3",
"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.",
@@ -64,7 +64,7 @@
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "github:Expensify/react-native-live-markdown#f762be6fa832419dbbecb8a0cf64bf7dce18545b",
+ "@expensify/react-native-live-markdown": "0.1.47",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -89,7 +89,7 @@
"@react-native-picker/picker": "2.6.1",
"@react-navigation/material-top-tabs": "^6.6.3",
"@react-navigation/native": "6.1.12",
- "@react-navigation/stack": "6.3.16",
+ "@react-navigation/stack": "6.3.29",
"@react-ng/bounds-observer": "^0.2.1",
"@rnmapbox/maps": "10.1.11",
"@shopify/flash-list": "1.6.3",
diff --git a/patches/@react-navigation+stack+6.3.16+003+fixKeyboardFlicker.patch b/patches/@react-navigation+stack+6.3.16+003+fixKeyboardFlicker.patch
deleted file mode 100644
index 2b2819f098d2..000000000000
--- a/patches/@react-navigation+stack+6.3.16+003+fixKeyboardFlicker.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx b/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx
-index 913218e..ebc2f93 100755
---- a/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx
-+++ b/node_modules/@react-navigation/stack/src/views/Stack/Card.tsx
-@@ -517,7 +517,7 @@ export default class Card extends React.Component {
- // Make sure that this view isn't removed. If this view is removed, our style with animated value won't apply
- collapsable={false}
- />
--
-+
- {overlayEnabled ? (
-
- {overlay({ style: overlayStyle })}
diff --git a/patches/@react-navigation+stack+6.3.16+001+initial.patch b/patches/@react-navigation+stack+6.3.29+001+initial.patch
similarity index 100%
rename from patches/@react-navigation+stack+6.3.16+001+initial.patch
rename to patches/@react-navigation+stack+6.3.29+001+initial.patch
diff --git a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch
similarity index 100%
rename from patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch
rename to patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch
diff --git a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch
new file mode 100644
index 000000000000..b5810c903873
--- /dev/null
+++ b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch
@@ -0,0 +1,12 @@
+diff --git a/node_modules/react-native-quick-sqlite/android/build.gradle b/node_modules/react-native-quick-sqlite/android/build.gradle
+index 323d34e..c2d0c44 100644
+--- a/node_modules/react-native-quick-sqlite/android/build.gradle
++++ b/node_modules/react-native-quick-sqlite/android/build.gradle
+@@ -90,7 +90,6 @@ android {
+ externalNativeBuild {
+ cmake {
+ cppFlags "-O2", "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID"
+- abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
+ arguments '-DANDROID_STL=c++_shared',
+ "-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}",
+ "-DSQLITE_FLAGS='${SQLITE_FLAGS ? SQLITE_FLAGS : ''}'"
diff --git a/src/CONST.ts b/src/CONST.ts
index b07b622cec05..39c3d9d3109b 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -3,6 +3,7 @@ import dateAdd from 'date-fns/add';
import dateSubtract from 'date-fns/sub';
import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
+import type {ValueOf} from 'type-fest';
import * as Url from './libs/Url';
import SCREENS from './SCREENS';
@@ -1487,6 +1488,15 @@ const CONST = {
'callMeByMyName',
],
+ // Map updated pronouns key to deprecated pronouns
+ DEPRECATED_PRONOUNS_LIST: {
+ heHimHis: 'He/him',
+ sheHerHers: 'She/her',
+ theyThemTheirs: 'They/them',
+ zeHirHirs: 'Ze/hir',
+ callMeByMyName: 'Call me by my name',
+ },
+
POLICY: {
TYPE: {
FREE: 'free',
@@ -4325,7 +4335,8 @@ const CONST = {
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
+type IOUType = ValueOf;
-export type {Country};
+export type {Country, IOUType};
export default CONST;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 506f969b9ad1..1eafd9d898ec 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -1,4 +1,3 @@
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type * as FormTypes from './types/form';
@@ -337,9 +336,10 @@ const ONYXKEYS = {
SECURITY_GROUP: 'securityGroup_',
TRANSACTION: 'transactions_',
TRANSACTION_VIOLATIONS: 'transactionViolations_',
+ TRANSACTION_DRAFT: 'transactionsDraft_',
// Holds temporary transactions used during the creation and edit flow
- TRANSACTION_DRAFT: 'transactionsDraft_',
+ TRANSACTION_BACKUP: 'transactionsBackup_',
SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_',
PRIVATE_NOTES_DRAFT: 'privateNotesDraft_',
NEXT_STEP: 'reportNextStep_',
@@ -437,8 +437,8 @@ const ONYXKEYS = {
REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft',
REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount',
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
- PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccountForm',
- PERSONAL_BANK_ACCOUNT_FORM_DRAFT: 'personalBankAccountFormDraft',
+ PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount',
+ PERSONAL_BANK_ACCOUNT_FORM_DRAFT: 'personalBankAccountDraft',
EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm',
EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft',
EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm',
@@ -539,6 +539,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup;
[ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction;
+ [ONYXKEYS.COLLECTION.TRANSACTION_BACKUP]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations;
[ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
@@ -657,7 +658,6 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;
type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
-type OnyxValue = TOnyxKey extends keyof OnyxCollectionValuesMapping ? OnyxCollection : OnyxEntry;
type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
@@ -665,4 +665,4 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing:
type AssertOnyxKeys = AssertTypesEqual;
export default ONYXKEYS;
-export type {OnyxValues, OnyxKey, OnyxCollectionKey, OnyxValue, OnyxValueKey, OnyxFormKey, OnyxFormValuesMapping, OnyxFormDraftKey, OnyxCollectionValuesMapping};
+export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxValueKey, OnyxValues};
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 31c2af8f4e58..7fee9b5497ce 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1,6 +1,7 @@
-import type {IsEqual, ValueOf} from 'type-fest';
+import type {ValueOf} from 'type-fest';
import type CONST from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
+import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
// This is a file containing constants for all the routes we want to be able to go to
@@ -368,16 +369,16 @@ const ROUTES = {
getUrlWithBackToParam(`${action}/${iouType}/scan/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_TAG: {
- route: ':action/:iouType/tag/:tagIndex/:transactionID/:reportID/:reportActionID?',
+ route: ':action/:iouType/tag/:orderWeight/:transactionID/:reportID/:reportActionID?',
getRoute: (
action: ValueOf,
iouType: ValueOf,
- tagIndex: number,
+ orderWeight: number,
transactionID: string,
reportID: string,
backTo = '',
reportActionID?: string,
- ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${tagIndex}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
+ ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_WAYPOINT: {
route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
@@ -730,20 +731,18 @@ export default ROUTES;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExtractRouteName = TRoute extends {getRoute: (...args: any[]) => infer TRouteName} ? TRouteName : TRoute;
-type AllRoutes = {
+/**
+ * Represents all routes in the app as a union of literal strings.
+ */
+type Route = {
[K in keyof typeof ROUTES]: ExtractRouteName<(typeof ROUTES)[K]>;
}[keyof typeof ROUTES];
-type RouteIsPlainString = IsEqual;
+type RoutesValidationError = 'Error: One or more routes defined within `ROUTES` have not correctly used `as const` in their `getRoute` function return value.';
-/**
- * Represents all routes in the app as a union of literal strings.
- *
- * If this type resolves to `never`, it implies that one or more routes defined within `ROUTES` have not correctly used
- * `as const` in their `getRoute` function return value.
- */
-type Route = RouteIsPlainString extends true ? never : AllRoutes;
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+type RouteIsPlainString = AssertTypesNotEqual;
type HybridAppRoute = (typeof HYBRID_APP_ROUTES)[keyof typeof HYBRID_APP_ROUTES];
-export type {Route, HybridAppRoute, AllRoutes};
+export type {Route, HybridAppRoute};
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index e0ad50a75645..35638a0b604e 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -5,7 +5,7 @@ import RNFetchBlob from 'react-native-blob-util';
import RNDocumentPicker from 'react-native-document-picker';
import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker';
import {launchImageLibrary} from 'react-native-image-picker';
-import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker';
+import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker';
import ImageSize from 'react-native-image-size';
import type {FileObject, ImagePickerResponse as FileResponse} from '@components/AttachmentModal';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -41,11 +41,12 @@ type Item = {
* See https://github.com/react-native-image-picker/react-native-image-picker/#options
* for ImagePicker configuration options
*/
-const imagePickerOptions = {
+const imagePickerOptions: Partial = {
includeBase64: false,
saveToPhotos: false,
selectionLimit: 1,
includeExtra: false,
+ assetRepresentationMode: 'current',
};
/**
diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx
index 6691c068eb3a..6cea253d5957 100644
--- a/src/components/Composer/index.native.tsx
+++ b/src/components/Composer/index.native.tsx
@@ -27,6 +27,7 @@ function Composer(
// user can read new chats without the keyboard in the way of the view.
// On Android the selection prop is required on the TextInput but this prop has issues on IOS
selection,
+ value,
...props
}: ComposerProps,
ref: ForwardedRef,
@@ -34,7 +35,7 @@ function Composer(
const textInput = useRef(null);
const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const theme = useTheme();
- const markdownStyle = useMarkdownStyle();
+ const markdownStyle = useMarkdownStyle(value);
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -73,6 +74,7 @@ function Composer(
autoComplete="off"
placeholderTextColor={theme.placeholderText}
ref={setTextInputRef}
+ value={value}
onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)}
rejectResponderTermination={false}
smartInsertDelete={false}
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 69cc6b208652..23d24a5ae5dd 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -81,7 +81,7 @@ function Composer(
) {
const theme = useTheme();
const styles = useThemeStyles();
- const markdownStyle = useMarkdownStyle();
+ const markdownStyle = useMarkdownStyle(value);
const StyleUtils = useStyleUtils();
const {windowWidth} = useWindowDimensions();
const textRef = useRef(null);
diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx
index 35fa4d02f5e0..2f09b27f3067 100644
--- a/src/components/FixedFooter.tsx
+++ b/src/components/FixedFooter.tsx
@@ -2,6 +2,8 @@ import type {ReactNode} from 'react';
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import useKeyboardState from '@hooks/useKeyboardState';
+import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
type FixedFooterProps = {
@@ -13,8 +15,17 @@ type FixedFooterProps = {
};
function FixedFooter({style, children}: FixedFooterProps) {
+ const {isKeyboardShown} = useKeyboardState();
+ const insets = useSafeAreaInsets();
const styles = useThemeStyles();
- return {children};
+
+ if (!children) {
+ return null;
+ }
+
+ const shouldAddBottomPadding = isKeyboardShown || !insets.bottom;
+
+ return {children};
}
FixedFooter.displayName = 'FixedFooter';
diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx
index 27db5687a925..8182ee487a80 100644
--- a/src/components/FormAlertWithSubmitButton.tsx
+++ b/src/components/FormAlertWithSubmitButton.tsx
@@ -79,7 +79,7 @@ function FormAlertWithSubmitButton({
return (
;
+ return ;
}
return (
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index d39e40179a9f..cadad07b6585 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -26,7 +26,7 @@ import * as TransactionUtils from '@libs/TransactionUtils';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {AllRoutes} from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
@@ -122,7 +122,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
isReadOnly?: boolean;
/** Depending on expense report or personal IOU report, respective bank account route */
- bankAccountRoute?: AllRoutes;
+ bankAccountRoute?: Route;
/** The policyID of the request */
policyID?: string;
@@ -223,13 +223,12 @@ function MoneyRequestConfirmationList({
const theme = useTheme();
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
- const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();
+ const {canUseViolations} = usePermissions();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
- const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill);
const isSplitWithScan = isSplitBill && isScanRequest;
@@ -719,7 +718,7 @@ function MoneyRequestConfirmationList({
)}
{isDistanceRequest && (
)}
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx
similarity index 70%
rename from src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
rename to src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx
index eec6cce0a1a3..21815f00253b 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx
@@ -1,21 +1,21 @@
import {useIsFocused} from '@react-navigation/native';
import {format} from 'date-fns';
import Str from 'expensify-common/lib/str';
-import {isUndefined} from 'lodash';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import {View} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
+import type {DefaultMileageRate} from '@libs/DistanceRequestUtils';
import * as IOUUtils from '@libs/IOUUtils';
import Log from '@libs/Log';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
@@ -27,247 +27,207 @@ import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportUtils from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import * as TransactionUtils from '@libs/TransactionUtils';
-import {policyPropTypes} from '@pages/workspace/withPolicy';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {Participant} from '@src/types/onyx/IOU';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import Button from './Button';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
-import categoryPropTypes from './categoryPropTypes';
+import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import ConfirmedRoute from './ConfirmedRoute';
import ConfirmModal from './ConfirmModal';
import FormHelpMessage from './FormHelpMessage';
import * as Expensicons from './Icon/Expensicons';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
-import optionPropTypes from './optionPropTypes';
import OptionsSelector from './OptionsSelector';
import PDFThumbnail from './PDFThumbnail';
import ReceiptEmptyState from './ReceiptEmptyState';
import ReceiptImage from './ReceiptImage';
import SettlementButton from './SettlementButton';
import Switch from './Switch';
-import tagPropTypes from './tagPropTypes';
import Text from './Text';
-import transactionPropTypes from './transactionPropTypes';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails';
-const propTypes = {
+type MoneyRequestConfirmationListOnyxProps = {
+ /** Collection of categories attached to a policy */
+ policyCategories: OnyxEntry;
+
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+
+ /** The policy of the report */
+ policy: OnyxEntry;
+
+ /** The session of the logged in user */
+ session: OnyxEntry;
+
+ /** Unit and rate used for if the money request is a distance request */
+ mileageRate: OnyxEntry;
+};
+
+type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & {
/** Callback to inform parent modal of success */
- onConfirm: PropTypes.func,
+ onConfirm?: (selectedParticipants: Participant[]) => void;
/** Callback to parent modal to send money */
- onSendMoney: PropTypes.func,
+ onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void;
/** Callback to inform a participant is selected */
- onSelectParticipant: PropTypes.func,
+ onSelectParticipant?: (option: Participant) => void;
/** Should we request a single or multiple participant selection from user */
- hasMultipleParticipants: PropTypes.bool.isRequired,
+ hasMultipleParticipants: boolean;
/** IOU amount */
- iouAmount: PropTypes.number.isRequired,
+ iouAmount: number;
/** IOU comment */
- iouComment: PropTypes.string,
+ iouComment?: string;
/** IOU currency */
- iouCurrencyCode: PropTypes.string,
+ iouCurrencyCode?: string;
/** IOU type */
- iouType: PropTypes.string,
+ iouType?: ValueOf;
/** IOU date */
- iouCreated: PropTypes.string,
+ iouCreated?: string;
/** IOU merchant */
- iouMerchant: PropTypes.string,
+ iouMerchant?: string;
- /** IOU category */
- iouCategory: PropTypes.string,
+ /** IOU Category */
+ iouCategory?: string;
/** IOU isBillable */
- iouIsBillable: PropTypes.bool,
+ iouIsBillable?: boolean;
/** Callback to toggle the billable state */
- onToggleBillable: PropTypes.func,
+ onToggleBillable?: (isOn: boolean) => void;
/** Selected participants from MoneyRequestModal with login / accountID */
- selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired,
+ selectedParticipants: Participant[];
/** Payee of the money request with login */
- payeePersonalDetails: optionPropTypes,
+ payeePersonalDetails?: OnyxTypes.PersonalDetails;
/** Can the participants be modified or not */
- canModifyParticipants: PropTypes.bool,
+ canModifyParticipants?: boolean;
/** Should the list be read only, and not editable? */
- isReadOnly: PropTypes.bool,
-
- /** Whether the money request is a scan request */
- isScanRequest: PropTypes.bool,
+ isReadOnly?: boolean;
/** Depending on expense report or personal IOU report, respective bank account route */
- bankAccountRoute: PropTypes.string,
-
- ...withCurrentUserPersonalDetailsPropTypes,
-
- /** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
+ bankAccountRoute?: Route;
/** The policyID of the request */
- policyID: PropTypes.string,
+ policyID?: string;
/** The reportID of the request */
- reportID: PropTypes.string,
+ reportID?: string;
/** File path of the receipt */
- receiptPath: PropTypes.string,
+ receiptPath?: string;
/** File name of the receipt */
- receiptFilename: PropTypes.string,
+ receiptFilename?: string;
/** List styles for OptionsSelector */
- listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
- /** ID of the transaction that represents the money request */
- transactionID: PropTypes.string,
-
- /** Unit and rate used for if the money request is a distance request */
- mileageRate: PropTypes.shape({
- /** Unit used to represent distance */
- unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]),
-
- /** Rate used to calculate the distance request amount */
- rate: PropTypes.number,
+ listStyles?: StyleProp;
- /** The currency of the rate */
- currency: PropTypes.string,
- }),
+ /** Transaction that represents the money request */
+ transaction?: OnyxEntry;
/** Whether the money request is a distance request */
- isDistanceRequest: PropTypes.bool,
+ isDistanceRequest?: boolean;
+
+ /** Whether the money request is a scan request */
+ isScanRequest?: boolean;
/** Whether we're editing a split bill */
- isEditingSplitBill: PropTypes.bool,
+ isEditingSplitBill?: boolean;
/** Whether we should show the amount, date, and merchant fields. */
- shouldShowSmartScanFields: PropTypes.bool,
+ shouldShowSmartScanFields?: boolean;
/** A flag for verifying that the current report is a sub-report of a workspace chat */
- isPolicyExpenseChat: PropTypes.bool,
+ isPolicyExpenseChat?: boolean;
- /* Onyx Props */
- /** Collection of categories attached to a policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
-
- /** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
-
- /* Onyx Props */
- /** The policy of the report */
- policy: policyPropTypes.policy,
+ /** Whether smart scan failed */
+ hasSmartScanFailed?: boolean;
- /** Transaction that represents the money request */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- onConfirm: () => {},
- onSendMoney: () => {},
- onSelectParticipant: () => {},
- iouType: CONST.IOU.TYPE.REQUEST,
- iouCategory: '',
- iouIsBillable: false,
- onToggleBillable: () => {},
- payeePersonalDetails: null,
- canModifyParticipants: false,
- isReadOnly: false,
- bankAccountRoute: '',
- session: {
- email: null,
- },
- policyID: '',
- reportID: '',
- ...withCurrentUserPersonalDetailsDefaultProps,
- receiptPath: '',
- receiptFilename: '',
- listStyles: [],
- policy: {},
- policyCategories: {},
- policyTags: {},
- transactionID: '',
- transaction: {},
- mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'},
- isDistanceRequest: false,
- shouldShowSmartScanFields: true,
- isPolicyExpenseChat: false,
+ reportActionID?: string;
};
-const getTaxAmount = (transaction, defaultTaxValue) => {
- const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || '';
- return TransactionUtils.calculateTaxAmount(percentage, transaction.amount);
+const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || '';
+ return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0);
};
function MoneyTemporaryForRefactorRequestConfirmationList({
- bankAccountRoute,
- canModifyParticipants,
- currentUserPersonalDetails,
- hasMultipleParticipants,
- hasSmartScanFailed,
+ transaction = null,
+ onSendMoney,
+ onConfirm,
+ onSelectParticipant,
+ iouType = CONST.IOU.TYPE.REQUEST,
+ isScanRequest = false,
iouAmount,
- iouCategory,
- iouComment,
- iouCreated,
+ policyCategories,
+ mileageRate,
+ isDistanceRequest = false,
+ policy,
+ isPolicyExpenseChat = false,
+ iouCategory = '',
+ shouldShowSmartScanFields = true,
+ isEditingSplitBill,
+ policyTags,
iouCurrencyCode,
- iouIsBillable,
iouMerchant,
- iouType,
- isDistanceRequest,
- isEditingSplitBill,
- isPolicyExpenseChat,
- isReadOnly,
- isScanRequest,
+ hasMultipleParticipants,
+ selectedParticipants: pickedParticipants,
+ payeePersonalDetails,
+ canModifyParticipants = false,
+ session,
+ isReadOnly = false,
+ bankAccountRoute = '',
+ policyID = '',
+ reportID = '',
+ receiptPath = '',
+ iouComment,
+ receiptFilename = '',
listStyles,
- mileageRate,
- onConfirm,
- onSelectParticipant,
- onSendMoney,
+ iouCreated,
+ iouIsBillable = false,
onToggleBillable,
- payeePersonalDetails,
- policy,
- policyCategories,
- policyID,
- policyTags,
- receiptFilename,
- receiptPath,
+ hasSmartScanFailed,
reportActionID,
- reportID,
- selectedParticipants: pickedParticipants,
- session: {accountID},
- shouldShowSmartScanFields,
- transaction,
-}) {
+}: MoneyRequestConfirmationListProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
- const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {canUseViolations} = usePermissions();
const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE;
- const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit);
- const {unit, rate, currency} = mileageRate;
- const distance = lodashGet(transaction, 'routes.route0.distance', 0);
+ const {unit, rate, currency} = mileageRate ?? {
+ unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
+ rate: 0,
+ currency: 'USD',
+ };
+ const distance = transaction?.routes?.route0.distance ?? 0;
const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0;
- const taxRates = lodashGet(policy, 'taxRates', {});
+ const taxRates = policy?.taxRates;
// A flag for showing the categories field
- const shouldShowCategories = isPolicyExpenseChat && (iouCategory || OptionsListUtils.hasEnabledOptions(_.values(policyCategories)));
+ const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {})));
// A flag and a toggler for showing the rest of the form fields
const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false);
@@ -287,21 +247,20 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy);
// A flag for showing the billable field
- const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true);
+ const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
const hasRoute = TransactionUtils.hasRoute(transaction);
const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate);
const formattedAmount = isDistanceRequestWithPendingRoute
? ''
: CurrencyUtils.convertToDisplayString(
- shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount,
+ shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount,
isDistanceRequest ? currency : iouCurrencyCode,
);
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode);
-
- const taxRateTitle = TransactionUtils.getDefaultTaxName(taxRates, transaction);
+ const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode);
+ const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : '';
- const previousTransactionAmount = usePrevious(transaction.amount);
+ const previousTransactionAmount = usePrevious(transaction?.amount);
const isFocused = useIsFocused();
const [formError, setFormError] = useState('');
@@ -314,21 +273,21 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const navigateBack = () => {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID));
};
- const shouldDisplayFieldError = useMemo(() => {
+ const shouldDisplayFieldError: boolean = useMemo(() => {
if (!isEditingSplitBill) {
return false;
}
- return (hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
+ return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction));
}, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant;
- const isCategoryRequired = canUseViolations && lodashGet(policy, 'requiresCategory', false);
+ const isCategoryRequired = canUseViolations && !!policy?.requiresCategory;
useEffect(() => {
if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) {
@@ -364,30 +323,28 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
- const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate);
- IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency);
+ const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0);
+ IOU.setMoneyRequestAmount_temporaryForRefactor(transaction?.transactionID ?? '', amount, currency ?? '');
}, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]);
// Calculate and set tax amount in transaction draft
useEffect(() => {
- const taxAmount = getTaxAmount(transaction, taxRates.defaultValue);
+ const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString();
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
- if (transaction.taxAmount && previousTransactionAmount === transaction.amount) {
- return IOU.setMoneyRequestTaxAmount(transaction.transactionID, transaction.taxAmount, true);
+ if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) {
+ return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true);
}
- IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true);
- }, [taxRates.defaultValue, transaction, previousTransactionAmount]);
+ IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true);
+ }, [taxRates?.defaultValue, transaction, previousTransactionAmount]);
/**
* Returns the participants with amount
- * @param {Array} participants
- * @returns {Array}
*/
const getParticipantsWithAmount = useCallback(
- (participantsList) => {
- const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode);
+ (participantsList: Participant[]) => {
+ const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? '');
return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '');
},
[iouAmount, iouCurrencyCode],
@@ -398,7 +355,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
setDidConfirm(false);
}
- const splitOrRequestOptions = useMemo(() => {
+ const splitOrRequestOptions: Array> = useMemo(() => {
let text;
if (isTypeTrackExpense) {
text = translate('iou.trackExpense');
@@ -421,8 +378,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
];
}, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]);
- const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]);
- const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]);
+ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]);
+ const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]);
const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants);
useEffect(() => {
userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants;
@@ -431,19 +388,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const optionSelectorSections = useMemo(() => {
const sections = [];
- const unselectedParticipants = _.filter(pickedParticipants, (participant) => !participant.selected);
+ const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected);
if (hasMultipleParticipants) {
const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants);
- let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants);
+ let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])];
- if (!userCanModifyParticipants.current) {
- formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({
+ if (!canModifyParticipants) {
+ formattedParticipantsList = formattedParticipantsList.map((participant) => ({
...participant,
- isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
}
- const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode, true);
+ const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true);
const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(
personalDetailsOfPayee,
iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '',
@@ -463,9 +420,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
},
);
} else {
- const formattedSelectedParticipants = _.map(selectedParticipants, (participant) => ({
+ const formattedSelectedParticipants = selectedParticipants.map((participant) => ({
...participant,
- isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1),
}));
sections.push({
title: translate('common.to'),
@@ -484,7 +441,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
personalDetailsOfPayee,
translate,
shouldDisablePaidBySection,
- userCanModifyParticipants,
+ canModifyParticipants,
]);
const selectedOptions = useMemo(() => {
@@ -504,56 +461,54 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page.
In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending.
*/
- IOU.setMoneyRequestPendingFields(transaction.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
+ IOU.setMoneyRequestPendingFields(transaction?.transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
- const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit);
- IOU.setMoneyRequestMerchant(transaction.transactionID, distanceMerchant, true);
+ const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit);
+ IOU.setMoneyRequestMerchant(transaction?.transactionID ?? '', distanceMerchant, true);
}, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]);
// Auto select the category if there is only one enabled category and it is required
useEffect(() => {
- const enabledCategories = _.filter(policyCategories, (category) => category.enabled);
+ const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled);
if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
return;
}
- IOU.setMoneyRequestCategory(transaction.transactionID, enabledCategories[0].name);
+ IOU.setMoneyRequestCategory(transaction?.transactionID ?? '', enabledCategories[0].name);
}, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]);
// Auto select the tag if there is only one enabled tag and it is required
useEffect(() => {
let updatedTagsString = TransactionUtils.getTag(transaction);
policyTagLists.forEach((tagList, index) => {
- const enabledTags = _.filter(tagList.tags, (tag) => tag.enabled);
- const isTagListRequired = isUndefined(tagList.required) ? false : tagList.required && canUseViolations;
+ const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled);
+ const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations;
if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) {
return;
}
updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index);
});
if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) {
- IOU.setMoneyRequestTag(transaction.transactionID, updatedTagsString);
+ IOU.setMoneyRequestTag(transaction?.transactionID ?? '', updatedTagsString);
}
}, [policyTagLists, transaction, policyTags, canUseViolations]);
/**
- * @param {Object} option
*/
const selectParticipant = useCallback(
- (option) => {
+ (option: Participant) => {
// Return early if selected option is currently logged in user.
- if (option.accountID === accountID) {
+ if (option.accountID === session?.accountID) {
return;
}
- onSelectParticipant(option);
+ onSelectParticipant?.(option);
},
- [accountID, onSelectParticipant],
+ [session?.accountID, onSelectParticipant],
);
/**
* Navigate to report details or profile of selected user
- * @param {Object} option
*/
- const navigateToReportOrUserDetail = (option) => {
+ const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => {
const activeRoute = Navigation.getActiveRouteWithoutParams();
if (option.isSelfDM) {
@@ -572,11 +527,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
* @param {String} paymentMethod
*/
const confirm = useCallback(
- (paymentMethod) => {
- if (_.isEmpty(selectedParticipants)) {
+ (paymentMethod: PaymentMethodType | undefined) => {
+ if (selectedParticipants.length === 0) {
return;
}
- if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction))) {
+ if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) {
setMerchantError(true);
return;
}
@@ -589,7 +544,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
setDidConfirm(true);
Log.info(`[IOU] Sending money via: ${paymentMethod}`);
- onSendMoney(paymentMethod);
+ onSendMoney?.(paymentMethod);
} else {
// validate the amount for distance requests
const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode);
@@ -598,7 +553,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
- if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) {
+ if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) {
setDidConfirmSplit(true);
setFormError('iou.error.genericSmartscanFailureMessage');
return;
@@ -606,7 +561,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
playSound(SOUNDS.DONE);
setDidConfirm(true);
- onConfirm(selectedParticipants);
+ onConfirm?.(selectedParticipants);
}
},
[
@@ -660,7 +615,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
success
pressOnEnter
isDisabled={shouldDisableButton}
- onPress={(_event, value) => confirm(value)}
+ onPress={(event, value) => confirm(value as PaymentMethodType)}
options={splitOrRequestOptions}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
enterKeyEventListenerPriority={1}
@@ -669,13 +624,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return (
<>
- {!_.isEmpty(formError) && (
+ {!!formError && (
)}
+
{button}
>
);
@@ -697,18 +653,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
if (isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT));
+ Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT));
return;
}
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
);
}}
style={[styles.moneyRequestMenuItem, styles.mt2]}
titleStyle={styles.moneyRequestConfirmationAmount}
disabled={didConfirm}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
- error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''}
/>
),
shouldShow: shouldShowSmartScanFields,
@@ -724,7 +680,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
description={translate('common.description')}
onPress={() => {
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
);
}}
style={[styles.moneyRequestMenuItem]}
@@ -741,18 +703,24 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
item: (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- disabled={didConfirm || !canEditDistance}
+ disabled={didConfirm}
interactive={!isReadOnly}
/>
),
@@ -770,12 +738,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
titleStyle={styles.flex1}
onPress={() => {
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
);
}}
disabled={didConfirm}
interactive={!isReadOnly}
- brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={merchantError ? translate('common.error.fieldRequired') : ''}
rightLabel={isMerchantRequired ? translate('common.required') : ''}
/>
@@ -788,18 +762,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
{
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()),
);
}}
disabled={didConfirm}
interactive={!isReadOnly}
- brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''}
/>
),
@@ -816,7 +791,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
numberOfLinesTitle={2}
onPress={() =>
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
style={[styles.moneyRequestMenuItem]}
@@ -829,8 +810,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
shouldShow: shouldShowCategories,
isSupplementary: !isCategoryRequired,
},
- ..._.map(policyTagLists, ({name, required}, index) => {
- const isTagRequired = isUndefined(required) ? false : canUseViolations && required;
+ ...policyTagLists.map(({name, required}, index) => {
+ const isTagRequired = required === undefined ? false : canUseViolations && required;
return {
item: (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
disabled={didConfirm}
@@ -885,7 +872,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
{
item: (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
)
}
disabled={didConfirm}
@@ -910,7 +903,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
onToggleBillable?.(isOn)}
/>
),
@@ -919,15 +912,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
},
];
- const primaryFields = _.map(
- _.filter(classifiedFields, (classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary),
- (primaryField) => primaryField.item,
- );
+ const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item);
- const supplementaryFields = _.map(
- _.filter(classifiedFields, (classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary),
- (supplementaryField) => supplementaryField.item,
- );
+ const supplementaryFields = classifiedFields
+ .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary)
+ .map((supplementaryField) => supplementaryField.item);
const {
image: receiptImage,
@@ -935,13 +924,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
isThumbnail,
fileExtension,
isLocalFile,
- } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {};
+ } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI);
const receiptThumbnailContent = useMemo(
() =>
isLocalFile && Str.isPDF(receiptFilename) ? (
),
@@ -963,6 +954,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
);
return (
+ // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q)
{isDistanceRequest && (
-
+
)}
- {receiptImage || receiptThumbnail
- ? receiptThumbnailContent
- : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
- PolicyUtils.isPaidGroupPolicy(policy) &&
- !isDistanceRequest &&
- iouType === CONST.IOU.TYPE.REQUEST && (
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
- )
- }
- />
- )}
+ {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ receiptImage || receiptThumbnail
+ ? receiptThumbnailContent
+ : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
+ PolicyUtils.isPaidGroupPolicy(policy) &&
+ !isDistanceRequest &&
+ iouType === CONST.IOU.TYPE.REQUEST && (
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType,
+ transaction?.transactionID ?? '',
+ reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
+ )
+ }
+ />
+ )
+ }
{primaryFields}
{!shouldShowAllFields && (
@@ -1030,28 +1031,23 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
);
}
-MoneyTemporaryForRefactorRequestConfirmationList.propTypes = propTypes;
-MoneyTemporaryForRefactorRequestConfirmationList.defaultProps = defaultProps;
MoneyTemporaryForRefactorRequestConfirmationList.displayName = 'MoneyTemporaryForRefactorRequestConfirmationList';
-export default compose(
- withCurrentUserPersonalDetails,
- withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
- policyCategories: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
- },
- policyTags: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
- },
- mileageRate: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- selector: DistanceRequestUtils.getDefaultMileageRate,
- },
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
- }),
-)(MoneyTemporaryForRefactorRequestConfirmationList);
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ policyCategories: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ },
+ policyTags: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`,
+ },
+ mileageRate: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ selector: DistanceRequestUtils.getDefaultMileageRate,
+ },
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
+})(MoneyTemporaryForRefactorRequestConfirmationList);
diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx
index 0bc9130ea4a8..af16b7300e1a 100644
--- a/src/components/OnyxProvider.tsx
+++ b/src/components/OnyxProvider.tsx
@@ -16,6 +16,7 @@ const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = crea
const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS);
const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION);
+const [, AccountProvider, , useAccount] = createOnyxContext(ONYXKEYS.ACCOUNT);
type OnyxProviderProps = {
/** Rendered child component */
@@ -37,6 +38,7 @@ function OnyxProvider(props: OnyxProviderProps) {
FrequentlyUsedEmojisProvider,
PreferredEmojiSkinToneProvider,
SessionProvider,
+ AccountProvider,
]}
>
{props.children}
@@ -69,4 +71,5 @@ export {
useBlockedFromConcierge,
useReportActionsDrafts,
useSession,
+ useAccount,
};
diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx
index 43c5906d4900..a83eeda5a419 100644
--- a/src/components/OptionListContextProvider.tsx
+++ b/src/components/OptionListContextProvider.tsx
@@ -1,6 +1,7 @@
import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection} from 'react-native-onyx';
+import usePrevious from '@hooks/usePrevious';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import type {OptionList} from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -42,8 +43,13 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp
reports: [],
personalDetails: [],
});
+
const personalDetails = usePersonalDetails();
+ const prevReports = usePrevious(reports);
+ /**
+ * This effect is used to update the options list when a report is updated.
+ */
useEffect(() => {
// there is no need to update the options if the options are not initialized
if (!areOptionsInitialized.current) {
@@ -71,6 +77,31 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reports]);
+ /**
+ * This effect is used to add a new report option to the list of options when a new report is added to the collection.
+ */
+ useEffect(() => {
+ if (!areOptionsInitialized.current || !reports) {
+ return;
+ }
+ const missingReportId = Object.keys(reports).find((key) => prevReports && !(key in prevReports));
+ const report = missingReportId ? reports[missingReportId] : null;
+ if (!missingReportId || !report) {
+ return;
+ }
+
+ const reportOption = OptionsListUtils.createOptionFromReport(report, personalDetails);
+ setOptions((prevOptions) => {
+ const newOptions = {...prevOptions};
+ newOptions.reports.push(reportOption);
+ return newOptions;
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [reports]);
+
+ /**
+ * This effect is used to update the options list when personal details change.
+ */
useEffect(() => {
// there is no need to update the options if the options are not initialized
if (!areOptionsInitialized.current) {
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx
index ff28a8f88849..99b5be1c8f53 100644
--- a/src/components/PDFView/index.tsx
+++ b/src/components/PDFView/index.tsx
@@ -29,7 +29,7 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, err
/**
* On small screens notify parent that the keyboard has opened or closed.
*
- * @param isKeyboardOpen True if keyboard is open
+ * @param isKBOpen True if keyboard is open
*/
const toggleKeyboardOnSmallScreens = useCallback(
(isKBOpen: boolean) => {
@@ -37,9 +37,9 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, err
return;
}
setIsKeyboardOpen(isKBOpen);
- onToggleKeyboard?.(isKeyboardOpen);
+ onToggleKeyboard?.(isKBOpen);
},
- [isKeyboardOpen, isSmallScreenWidth, onToggleKeyboard],
+ [isSmallScreenWidth, onToggleKeyboard],
);
/**
diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx
index 08892f11b021..f4aa2de090f7 100644
--- a/src/components/ReceiptImage.tsx
+++ b/src/components/ReceiptImage.tsx
@@ -46,7 +46,7 @@ type ReceiptImageProps = (
isEReceipt?: boolean;
isThumbnail?: boolean;
source: string;
- isPDFThumbnail: string;
+ isPDFThumbnail?: string;
}
) & {
/** Whether we should display the receipt with ThumbnailImage component */
diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
index c93b75bf11ad..0588f31a0a8c 100644
--- a/src/components/ReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -1,43 +1,49 @@
-import React from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import React, {useEffect} from 'react';
+import type {ViewStyle} from 'react-native';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
import Navigation from '@src/libs/Navigation/Navigation';
-import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type * as OnyxTypes from '@src/types/onyx';
import Icon from './Icon';
import {Close} from './Icon/Expensicons';
import {PressableWithoutFeedback} from './Pressable';
import Text from './Text';
import Tooltip from './Tooltip';
-type ReferralProgramCTAOnyxProps = {
- dismissedReferralBanners: OnyxEntry;
-};
-
-type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & {
+type ReferralProgramCTAProps = {
referralContentType:
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
+ style?: ViewStyle;
+ onDismiss?: () => void;
};
-function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: ReferralProgramCTAProps) {
+function ReferralProgramCTA({referralContentType, style, onDismiss}: ReferralProgramCTAProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
+ const {isDismissed, setAsDismissed} = useDismissedReferralBanners({referralContentType});
const handleDismissCallToAction = () => {
- User.dismissReferralBanner(referralContentType);
+ setAsDismissed();
+ onDismiss?.();
};
- if (!referralContentType || dismissedReferralBanners?.[referralContentType]) {
+ const shouldShowBanner = referralContentType && !isDismissed;
+
+ useEffect(() => {
+ if (shouldShowBanner) {
+ return;
+ }
+ onDismiss?.();
+ }, [onDismiss, shouldShowBanner]);
+
+ if (!shouldShowBanner) {
return null;
}
@@ -46,7 +52,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
onPress={() => {
Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType, Navigation.getActiveRouteWithoutParams()));
}}
- style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]}
+ style={[styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5, style]}
accessibilityLabel="referral"
role={CONST.ACCESSIBILITY_ROLE.BUTTON}
>
@@ -81,8 +87,4 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
);
}
-export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
- },
-})(ReferralProgramCTA);
+export default ReferralProgramCTA;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index dd34d0ca2540..f6c937b72653 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -408,7 +408,7 @@ function MoneyRequestView({
)}
{shouldShowTag &&
- policyTagLists.map(({name}, index) => (
+ policyTagLists.map(({name, orderWeight}, index) => (
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, index, transaction?.transactionID ?? '', report.reportID),
+ ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, orderWeight, transaction?.transactionID ?? '', report.reportID),
)
}
brickRoadIndicator={getErrorForField('tag', {tagListIndex: index, tagListName: name}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index b78e274371ca..e53823860ce0 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -1,7 +1,7 @@
import {useNavigation} from '@react-navigation/native';
import type {StackNavigationProp} from '@react-navigation/stack';
import type {ForwardedRef, ReactNode} from 'react';
-import React, {forwardRef, useEffect, useRef, useState} from 'react';
+import React, {createContext, forwardRef, useEffect, useMemo, useRef, useState} from 'react';
import type {DimensionValue, StyleProp, ViewStyle} from 'react-native';
import {Keyboard, PanResponder, View} from 'react-native';
import {PickerAvoidingView} from 'react-native-picker-select';
@@ -25,7 +25,7 @@ import SafeAreaConsumer from './SafeAreaConsumer';
import TestToolsModal from './TestToolsModal';
import withNavigationFallback from './withNavigationFallback';
-type ChildrenProps = {
+type ScreenWrapperChildrenProps = {
insets: EdgeInsets;
safeAreaPaddingBottomStyle?: {
paddingBottom?: DimensionValue;
@@ -35,7 +35,7 @@ type ChildrenProps = {
type ScreenWrapperProps = {
/** Returns a function as a child to pass insets to or a node to render without insets */
- children: ReactNode | React.FC;
+ children: ReactNode | React.FC;
/** A unique ID to find the screen wrapper in tests */
testID: string;
@@ -99,6 +99,8 @@ type ScreenWrapperProps = {
shouldShowOfflineIndicatorInWideScreen?: boolean;
};
+const ScreenWrapperStatusContext = createContext({didScreenTransitionEnd: false});
+
function ScreenWrapper(
{
shouldEnableMaxHeight = false,
@@ -201,6 +203,7 @@ function ScreenWrapper(
}, []);
const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileSafari());
+ const contextValue = useMemo(() => ({didScreenTransitionEnd}), [didScreenTransitionEnd]);
return (
@@ -251,16 +254,18 @@ function ScreenWrapper(
{isDevelopment && }
- {
- // If props.children is a function, call it to provide the insets to the children.
- typeof children === 'function'
- ? children({
- insets,
- safeAreaPaddingBottomStyle,
- didScreenTransitionEnd,
- })
- : children
- }
+
+ {
+ // If props.children is a function, call it to provide the insets to the children.
+ typeof children === 'function'
+ ? children({
+ insets,
+ safeAreaPaddingBottomStyle,
+ didScreenTransitionEnd,
+ })
+ : children
+ }
+
{isSmallScreenWidth && shouldShowOfflineIndicator && }
{!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && (
(
showConfirmButton = false,
shouldPreventDefaultFocusOnSelectRow = false,
containerStyle,
- isKeyboardShown = false,
disableKeyboardShortcuts = false,
children,
shouldStopPropagation = false,
@@ -88,6 +88,7 @@ function BaseSelectionList(
const isFocused = useIsFocused();
const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT);
const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true);
+ const {isKeyboardShown} = useKeyboardState();
const [itemsToHighlight, setItemsToHighlight] = useState | null>(null);
const itemFocusTimeoutRef = useRef(null);
const [currentPage, setCurrentPage] = useState(1);
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 38c5f03fcae6..af2ea3469408 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -284,8 +284,8 @@ type BaseSelectionListProps = Partial & {
/** Styles to apply to SelectionList container */
containerStyle?: StyleProp;
- /** Whether keyboard is visible on the screen */
- isKeyboardShown?: boolean;
+ /** Whether focus event should be delayed */
+ shouldDelayFocus?: boolean;
/** Component to display on the right side of each child */
rightHandSideComponent?: ((item: ListItem) => ReactElement | null) | ReactElement | null;
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx
index 684d5e416471..1693bafe323d 100644
--- a/src/components/Switch.tsx
+++ b/src/components/Switch.tsx
@@ -1,8 +1,11 @@
import React, {useEffect, useRef} from 'react';
import {Animated} from 'react-native';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useNativeDriver from '@libs/useNativeDriver';
import CONST from '@src/CONST';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
type SwitchProps = {
@@ -27,6 +30,7 @@ const OFFSET_X = {
function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) {
const styles = useThemeStyles();
const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF));
+ const theme = useTheme();
useEffect(() => {
Animated.timing(offsetX.current, {
@@ -49,7 +53,16 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) {
hoverDimmingValue={1}
pressDimmingValue={0.8}
>
-
+
+ {disabled && (
+
+ )}
+
);
}
diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx
index ff5768efaede..f968af4f6030 100644
--- a/src/components/TagPicker/index.tsx
+++ b/src/components/TagPicker/index.tsx
@@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import type * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx';
@@ -38,7 +39,7 @@ type TagPickerProps = TagPickerOnyxProps & {
tagListName: string;
/** Callback to submit the selected tag */
- onSubmit: () => void;
+ onSubmit: (selectedTag: Partial) => void;
/** Should show the selected option that is disabled? */
shouldShowDisabledAndSelectedOption?: boolean;
diff --git a/src/components/createOnyxContext.tsx b/src/components/createOnyxContext.tsx
index c19b8006c86c..7cf802c57951 100644
--- a/src/components/createOnyxContext.tsx
+++ b/src/components/createOnyxContext.tsx
@@ -1,9 +1,10 @@
import Str from 'expensify-common/lib/str';
import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, PropsWithoutRef, ReactNode, RefAttributes} from 'react';
import React, {createContext, forwardRef, useContext} from 'react';
+import type {OnyxValue} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import getComponentDisplayName from '@libs/getComponentDisplayName';
-import type {OnyxKey, OnyxValue} from '@src/ONYXKEYS';
+import type {OnyxKey} from '@src/ONYXKEYS';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
// Provider types
diff --git a/src/hooks/useDismissedReferralBanners.ts b/src/hooks/useDismissedReferralBanners.ts
new file mode 100644
index 000000000000..94ccd0a0b567
--- /dev/null
+++ b/src/hooks/useDismissedReferralBanners.ts
@@ -0,0 +1,29 @@
+import {useOnyx} from 'react-native-onyx';
+import * as User from '@userActions/User';
+import type CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type UseDismissedReferralBannersProps = {
+ referralContentType:
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
+};
+
+function useDismissedReferralBanners({referralContentType}: UseDismissedReferralBannersProps): {isDismissed: boolean; setAsDismissed: () => void} {
+ const [dismissedReferralBanners] = useOnyx(ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS);
+ const isDismissed = dismissedReferralBanners?.[referralContentType] ?? false;
+
+ const setAsDismissed = () => {
+ if (!referralContentType) {
+ return;
+ }
+ // Set the banner as dismissed
+ User.dismissReferralBanner(referralContentType);
+ };
+
+ return {isDismissed, setAsDismissed};
+}
+
+export default useDismissedReferralBanners;
diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts
index 72e2734a4744..21c8d02e9194 100644
--- a/src/hooks/useMarkdownStyle.ts
+++ b/src/hooks/useMarkdownStyle.ts
@@ -1,11 +1,13 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
import {useMemo} from 'react';
+import {containsOnlyEmojis} from '@libs/EmojiUtils';
import FontUtils from '@styles/utils/FontUtils';
import variables from '@styles/variables';
import useTheme from './useTheme';
-function useMarkdownStyle(): MarkdownStyle {
+function useMarkdownStyle(message: string | null = null): MarkdownStyle {
const theme = useTheme();
+ const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal;
const markdownStyle = useMemo(
() => ({
@@ -18,6 +20,9 @@ function useMarkdownStyle(): MarkdownStyle {
h1: {
fontSize: variables.fontSizeLarge,
},
+ emoji: {
+ fontSize: emojiFontSize,
+ },
blockquote: {
borderColor: theme.border,
borderWidth: 4,
@@ -45,7 +50,7 @@ function useMarkdownStyle(): MarkdownStyle {
backgroundColor: theme.mentionBG,
},
}),
- [theme],
+ [theme, emojiFontSize],
);
return markdownStyle;
diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts
index e60825b610e9..22200304fdd5 100644
--- a/src/hooks/usePermissions.ts
+++ b/src/hooks/usePermissions.ts
@@ -1,12 +1,13 @@
import {useContext, useMemo} from 'react';
import {BetasContext} from '@components/OnyxProvider';
import Permissions from '@libs/Permissions';
+import type {IOUType} from '@src/CONST';
type PermissionKey = keyof typeof Permissions;
type UsePermissions = Partial>;
let permissionKey: PermissionKey;
-export default function usePermissions(): UsePermissions {
+export default function usePermissions(iouType: IOUType | undefined = undefined): UsePermissions {
const betas = useContext(BetasContext);
return useMemo(() => {
const permissions: UsePermissions = {};
@@ -15,10 +16,10 @@ export default function usePermissions(): UsePermissions {
if (betas) {
const checkerFunction = Permissions[permissionKey];
- permissions[permissionKey] = checkerFunction(betas);
+ permissions[permissionKey] = checkerFunction(betas, iouType);
}
}
return permissions;
- }, [betas]);
+ }, [betas, iouType]);
}
diff --git a/src/hooks/useScreenWrapperTransitionStatus.ts b/src/hooks/useScreenWrapperTransitionStatus.ts
new file mode 100644
index 000000000000..b9e94abfc024
--- /dev/null
+++ b/src/hooks/useScreenWrapperTransitionStatus.ts
@@ -0,0 +1,17 @@
+import {useContext} from 'react';
+import {ScreenWrapperStatusContext} from '@components/ScreenWrapper';
+
+/**
+ * Hook to get the transition status of a screen inside a ScreenWrapper.
+ * Use this hook if you can't get the transition status from the ScreenWrapper itself. Usually when ScreenWrapper is used inside TopTabNavigator.
+ * @returns `didScreenTransitionEnd` flag to indicate if navigation transition ended.
+ */
+export default function useScreenWrapperTranstionStatus() {
+ const value = useContext(ScreenWrapperStatusContext);
+
+ if (value === undefined) {
+ throw new Error("Couldn't find values for screen ScreenWrapper transition status. Are you inside a screen in ScreenWrapper?");
+ }
+
+ return value;
+}
diff --git a/src/hooks/useViewportOffsetTop/index.ts b/src/hooks/useViewportOffsetTop/index.ts
index 56fb19187c4f..da2325a7e13f 100644
--- a/src/hooks/useViewportOffsetTop/index.ts
+++ b/src/hooks/useViewportOffsetTop/index.ts
@@ -1,4 +1,5 @@
-import {useEffect, useRef, useState} from 'react';
+import {useCallback, useEffect, useRef, useState} from 'react';
+import * as Browser from '@libs/Browser';
import addViewportResizeListener from '@libs/VisualViewport';
/**
@@ -6,17 +7,18 @@ import addViewportResizeListener from '@libs/VisualViewport';
*/
export default function useViewportOffsetTop(shouldAdjustScrollView = false): number {
const [viewportOffsetTop, setViewportOffsetTop] = useState(0);
- const initialHeight = useRef(window.visualViewport?.height ?? window.innerHeight).current;
const cachedDefaultOffsetTop = useRef(0);
- useEffect(() => {
- const updateOffsetTop = (event?: Event) => {
+
+ const updateOffsetTop = useCallback(
+ (event?: Event) => {
let targetOffsetTop = window.visualViewport?.offsetTop ?? 0;
if (event?.target instanceof VisualViewport) {
targetOffsetTop = event.target.offsetTop;
}
- if (shouldAdjustScrollView && window.visualViewport) {
- const adjustScrollY = Math.round(initialHeight - window.visualViewport.height);
+ if (Browser.isMobileSafari() && shouldAdjustScrollView && window.visualViewport) {
+ const clientHeight = document.body.clientHeight;
+ const adjustScrollY = Math.round(clientHeight - window.visualViewport.height);
if (cachedDefaultOffsetTop.current === 0) {
cachedDefaultOffsetTop.current = targetOffsetTop;
}
@@ -31,16 +33,17 @@ export default function useViewportOffsetTop(shouldAdjustScrollView = false): nu
} else {
setViewportOffsetTop(targetOffsetTop);
}
- };
- updateOffsetTop();
- return addViewportResizeListener(updateOffsetTop);
- }, [initialHeight, shouldAdjustScrollView]);
+ },
+ [shouldAdjustScrollView],
+ );
+
+ useEffect(() => addViewportResizeListener(updateOffsetTop), [updateOffsetTop]);
useEffect(() => {
if (!shouldAdjustScrollView) {
return;
}
- window.scrollTo({top: viewportOffsetTop});
+ window.scrollTo({top: viewportOffsetTop, behavior: 'instant'});
}, [shouldAdjustScrollView, viewportOffsetTop]);
return viewportOffsetTop;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 3b670f7b6ebc..dbdda0d35635 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -600,12 +600,12 @@ export default {
splitBill: 'Split Bill',
splitScan: 'Split Receipt',
splitDistance: 'Split Distance',
+ trackManual: 'Track Expense',
+ trackScan: 'Track Receipt',
+ trackDistance: 'Track Distance',
sendMoney: 'Send Money',
assignTask: 'Assign Task',
shortcut: 'Shortcut',
- trackManual: 'Track Manual',
- trackScan: 'Track Scan',
- trackDistance: 'Track Distance',
},
iou: {
amount: 'Amount',
@@ -670,7 +670,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`,
- adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager} cancelled the ${amount} payment.`,
+ adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}cancelled the ${amount} payment.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5027174b2922..e81efa07a58c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -599,9 +599,9 @@ export default {
sendMoney: 'Enviar Dinero',
assignTask: 'Assignar Tarea',
shortcut: 'Acceso Directo',
- trackManual: 'Seguimiento de Gastos',
- trackScan: 'Seguimiento de Recibo',
- trackDistance: 'Seguimiento de Distancia',
+ trackManual: 'Crear Gasto',
+ trackScan: 'Crear Recibo',
+ trackDistance: 'Crear Gasto por desplazamiento',
},
iou: {
amount: 'Importe',
@@ -666,7 +666,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `aprobó ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`,
- adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager} canceló el pago de ${amount}.`,
+ adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}canceló el pago de ${amount}.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
diff --git a/src/libs/API/parameters/ApproveMoneyRequestParams.ts b/src/libs/API/parameters/ApproveMoneyRequestParams.ts
index f35ff31702d6..fc6528047f22 100644
--- a/src/libs/API/parameters/ApproveMoneyRequestParams.ts
+++ b/src/libs/API/parameters/ApproveMoneyRequestParams.ts
@@ -1,6 +1,7 @@
type ApproveMoneyRequestParams = {
reportID: string;
approvedReportActionID: string;
+ full?: boolean;
};
export default ApproveMoneyRequestParams;
diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts
index 9c8d9761d888..c806aded144e 100644
--- a/src/libs/API/parameters/TrackExpenseParams.ts
+++ b/src/libs/API/parameters/TrackExpenseParams.ts
@@ -25,6 +25,7 @@ type TrackExpenseParams = {
gpsPoints?: string;
transactionThreadReportID: string;
createdReportActionIDForThread: string;
+ waypoints?: string;
};
export default TrackExpenseParams;
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index fc19ba60693c..7c814608dc08 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -81,6 +81,7 @@ const WRITE_COMMANDS = {
TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate',
ADD_COMMENT: 'AddComment',
ADD_ATTACHMENT: 'AddAttachment',
+ ADD_TEXT_AND_ATTACHMENT: 'AddTextAndAttachment',
CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid',
ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount',
RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup',
@@ -268,6 +269,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams;
[WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
+ [WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountParams;
[WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams;
[WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams;
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index b94c2c5fad4a..7b1960261182 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -13,6 +13,7 @@ import type {Report} from '@src/types/onyx';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import originalDismissModal from './dismissModal';
import originalDismissModalWithReport from './dismissModalWithReport';
+import originalDismissRHP from './dismissRHP';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
@@ -61,6 +62,11 @@ const dismissModal = (reportID?: string, ref = navigationRef) => {
originalDismissModalWithReport({reportID, ...report}, ref);
};
+// Re-exporting the dismissRHP here to fill in default value for navigationRef. The dismissRHP isn't defined in this file to avoid cyclic dependencies.
+const dismissRHP = (ref = navigationRef) => {
+ originalDismissRHP(ref);
+};
+
// Re-exporting the dismissModalWithReport here to fill in default value for navigationRef. The dismissModalWithReport isn't defined in this file to avoid cyclic dependencies.
// This method is needed because it allows to dismiss the modal and then open the report. Within this method is checked whether the report belongs to a specific workspace. Sometimes the report we want to check, hasn't been added to the Onyx yet.
// Then we can pass the report as a param without getting it from the Onyx.
@@ -363,6 +369,7 @@ export default {
setShouldPopAllStateOnUP,
navigate,
setParams,
+ dismissRHP,
dismissModal,
dismissModalWithReport,
isActiveRoute,
diff --git a/src/libs/Navigation/dismissRHP.ts b/src/libs/Navigation/dismissRHP.ts
new file mode 100644
index 000000000000..1c497a79600c
--- /dev/null
+++ b/src/libs/Navigation/dismissRHP.ts
@@ -0,0 +1,25 @@
+import type {NavigationContainerRef} from '@react-navigation/native';
+import {StackActions} from '@react-navigation/native';
+import NAVIGATORS from '@src/NAVIGATORS';
+import type {RootStackParamList} from './types';
+
+// This function is in a separate file than Navigation.ts to avoid cyclic dependency.
+
+/**
+ * Dismisses the RHP modal stack if there is any
+ *
+ * @param targetReportID - The reportID to navigate to after dismissing the modal
+ */
+function dismissRHP(navigationRef: NavigationContainerRef) {
+ if (!navigationRef.isReady()) {
+ return;
+ }
+
+ const state = navigationRef.getState();
+ const lastRoute = state.routes.at(-1);
+ if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ navigationRef.dispatch({...StackActions.pop(), target: state.key});
+ }
+}
+
+export default dismissRHP;
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 1f1e7ec9a459..f378d17dc0b0 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -396,6 +396,8 @@ type MoneyRequestNavigatorParamList = {
transactionID: string;
reportID: string;
backTo: Routes;
+ reportActionID: string;
+ orderWeight: string;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: {
action: ValueOf;
@@ -439,6 +441,11 @@ type MoneyRequestNavigatorParamList = {
iouType: string;
reportID: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: {
+ iouType: ValueOf;
+ transactionID: string;
+ reportID: string;
+ };
[SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: {
action: ValueOf;
iouType: ValueOf;
@@ -447,6 +454,14 @@ type MoneyRequestNavigatorParamList = {
pageIndex?: string;
backTo?: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_SCAN]: {
+ action: ValueOf;
+ iouType: ValueOf;
+ transactionID: string;
+ reportID: string;
+ pageIndex: number;
+ backTo: Routes;
+ };
};
type NewTaskNavigatorParamList = {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index f61f51cd5350..280ba825761f 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -39,6 +39,7 @@ import times from '@src/utils/times';
import Timing from './actions/Timing';
import * as CollectionUtils from './CollectionUtils';
import * as ErrorUtils from './ErrorUtils';
+import filterArrayByMatch from './filterArrayByMatch';
import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
@@ -179,7 +180,7 @@ type MemberForList = {
type SectionForSearchTerm = {
section: CategorySection;
};
-type GetOptions = {
+type Options = {
recentReports: ReportUtils.OptionData[];
personalDetails: ReportUtils.OptionData[];
userToInvite: ReportUtils.OptionData | null;
@@ -447,19 +448,20 @@ function getSearchText(
): string {
let searchTerms: string[] = [];
- for (const personalDetail of personalDetailList) {
- if (personalDetail.login) {
- // The regex below is used to remove dots only from the local part of the user email (local-part@domain)
- // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain)
- // More info https://github.com/Expensify/App/issues/8007
- searchTerms = searchTerms.concat([
- PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false),
- personalDetail.login,
- personalDetail.login.replace(/\.(?=[^\s@]*@)/g, ''),
- ]);
+ if (!isChatRoomOrPolicyExpenseChat) {
+ for (const personalDetail of personalDetailList) {
+ if (personalDetail.login) {
+ // The regex below is used to remove dots only from the local part of the user email (local-part@domain)
+ // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain)
+ // More info https://github.com/Expensify/App/issues/8007
+ searchTerms = searchTerms.concat([
+ PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false),
+ personalDetail.login,
+ personalDetail.login.replace(/\.(?=[^\s@]*@)/g, ''),
+ ]);
+ }
}
}
-
if (report) {
Array.prototype.push.apply(searchTerms, reportName.split(/[,\s]/));
@@ -609,7 +611,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
} else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report);
+ lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report, true);
} else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
} else if (ReportActionUtils.isPendingRemove(lastReportAction) && ReportActionUtils.isThreadParentMessage(lastReportAction, report?.reportID ?? '')) {
@@ -1497,6 +1499,35 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry {
+ if (!!option.isChatRoom || option.isArchivedRoom) {
+ return 3;
+ }
+ if (!option.login) {
+ return 2;
+ }
+ if (option.login.toLowerCase() !== searchValue?.toLowerCase()) {
+ return 1;
+ }
+
+ // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list
+ return 0;
+ },
+ ],
+ ['asc'],
+ );
+}
+
/**
* filter options based on specific conditions
*/
@@ -1539,7 +1570,7 @@ function getOptions(
policyReportFieldOptions = [],
recentlyUsedPolicyReportFieldOptions = [],
}: GetOptionsConfig,
-): GetOptions {
+): Options {
if (includeCategories) {
const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow);
@@ -1597,7 +1628,7 @@ function getOptions(
}
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue)));
- const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase();
+ const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase();
const topmostReportId = Navigation.getTopmostReportId() ?? '';
// Filter out all the reports that shouldn't be displayed
@@ -1847,26 +1878,7 @@ function getOptions(
// When sortByReportTypeInSearch is true, recentReports will be returned with all the reports including personalDetailsOptions in the correct Order.
recentReportOptions.push(...personalDetailsOptions);
personalDetailsOptions = [];
- recentReportOptions = lodashOrderBy(
- recentReportOptions,
- [
- (option) => {
- if (!!option.isChatRoom || option.isArchivedRoom) {
- return 3;
- }
- if (!option.login) {
- return 2;
- }
- if (option.login.toLowerCase() !== searchValue?.toLowerCase()) {
- return 1;
- }
-
- // When option.login is an exact match with the search value, returning 0 puts it at the top of the option list
- return 0;
- },
- ],
- ['asc'],
- );
+ recentReportOptions = orderOptions(recentReportOptions, searchValue);
}
return {
@@ -1883,7 +1895,7 @@ function getOptions(
/**
* Build the options for the Search view
*/
-function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions {
+function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options {
Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS);
Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS);
const optionList = getOptions(options, {
@@ -1908,7 +1920,7 @@ function getSearchOptions(options: OptionList, searchValue = '', betas: Beta[] =
return optionList;
}
-function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): GetOptions {
+function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] = []): Options {
return getOptions(options, {
betas,
searchInputValue: searchValue.trim(),
@@ -2085,7 +2097,7 @@ function getMemberInviteOptions(
searchValue = '',
excludeLogins: string[] = [],
includeSelectedOptions = false,
-): GetOptions {
+): Options {
return getOptions(
{reports: [], personalDetails},
{
@@ -2204,6 +2216,90 @@ function formatSectionsFromSearchTerm(
};
}
+/**
+ * Filters options based on the search input value
+ */
+function filterOptions(options: Options, searchInputValue: string): Options {
+ const searchValue = getSearchValueForPhoneOrEmail(searchInputValue);
+ const searchTerms = searchValue ? searchValue.split(' ') : [];
+
+ // The regex below is used to remove dots only from the local part of the user email (local-part@domain)
+ // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain)
+ const emailRegex = /\.(?=[^\s@]*@)/g;
+
+ const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => {
+ const keys: string[] = [];
+ const visibleChatMemberAccountIDs = item.participantsList ?? [];
+ if (allPersonalDetails) {
+ visibleChatMemberAccountIDs.forEach((participant) => {
+ const login = participant?.login;
+
+ if (participant?.displayName) {
+ keys.push(participant.displayName);
+ }
+
+ if (login) {
+ keys.push(login);
+ keys.push(login.replace(emailRegex, ''));
+ }
+ });
+ }
+
+ return keys;
+ };
+ const matchResults = searchTerms.reduceRight((items, term) => {
+ const recentReports = filterArrayByMatch(items.recentReports, term, (item) => {
+ let values: string[] = [];
+ if (item.text) {
+ values.push(item.text);
+ }
+
+ if (item.login) {
+ values.push(item.login);
+ values.push(item.login.replace(emailRegex, ''));
+ }
+
+ if (item.isThread) {
+ if (item.alternateText) {
+ values.push(item.alternateText);
+ }
+ } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) {
+ if (item.subtitle) {
+ values.push(item.subtitle);
+ }
+ }
+ values = values.concat(getParticipantsLoginsArray(item));
+
+ return uniqFast(values);
+ });
+ const personalDetails = filterArrayByMatch(items.personalDetails, term, (item) =>
+ uniqFast([item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(emailRegex, '') ?? '']),
+ );
+
+ return {
+ recentReports: recentReports ?? [],
+ personalDetails: personalDetails ?? [],
+ userToInvite: null,
+ currentUserOption: null,
+ categoryOptions: [],
+ tagOptions: [],
+ taxRatesOptions: [],
+ };
+ }, options);
+
+ const recentReports = matchResults.recentReports.concat(matchResults.personalDetails);
+
+ return {
+ personalDetails: [],
+ recentReports: orderOptions(recentReports, searchValue),
+ userToInvite: null,
+ currentUserOption: null,
+ categoryOptions: [],
+ tagOptions: [],
+ taxRatesOptions: [],
+ };
+}
+
export {
getAvatarsForAccountIDs,
isCurrentUser,
@@ -2236,10 +2332,11 @@ export {
formatSectionsFromSearchTerm,
transformedTaxRates,
getShareLogOptions,
+ filterOptions,
createOptionList,
createOptionFromReport,
getReportOption,
getTaxRatesSection,
};
-export type {MemberForList, CategorySection, CategoryTreeSection, GetOptions, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption};
+export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption};
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 1973e665b20f..105736faeba0 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -1,5 +1,6 @@
import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
+import type {IOUType} from '@src/CONST';
import type Beta from '@src/types/onyx/Beta';
function canUseAllBetas(betas: OnyxEntry): boolean {
@@ -26,8 +27,9 @@ function canUseTrackExpense(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas);
}
-function canUseP2PDistanceRequests(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas);
+function canUseP2PDistanceRequests(betas: OnyxEntry, iouType: IOUType | undefined): boolean {
+ // Allow using P2P distance request for TrackExpense outside of the beta, because that project doesn't want to be limited by the more cautious P2P distance beta
+ return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas) || iouType === CONST.IOU.TYPE.TRACK_EXPENSE;
}
function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean {
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 665830ca7167..1b9423c70ee7 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -181,19 +181,15 @@ function getSortedTagKeys(policyTagList: OnyxEntry): Array, tagIndex: number): string {
+function getTagListName(policyTagList: OnyxEntry, orderWeight: number): string {
if (isEmptyObject(policyTagList)) {
return '';
}
- const policyTagKeys = getSortedTagKeys(policyTagList ?? {});
- const policyTagKey = policyTagKeys[tagIndex] ?? '';
-
- return policyTagList?.[policyTagKey]?.name ?? '';
+ return Object.values(policyTagList).find((tag) => tag.orderWeight === orderWeight)?.name ?? '';
}
-
/**
* Gets all tag lists of a policy
*/
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index b09f58b969f0..69917ce35c6b 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -130,6 +130,10 @@ function isReportPreviewAction(reportAction: OnyxEntry): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
}
+function isReportActionSubmitted(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED;
+}
+
function isModifiedExpenseAction(reportAction: OnyxEntry | ReportAction | Record): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE;
}
@@ -216,7 +220,13 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty
/**
* Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
*/
-function getOneTransactionThreadReportID(reportActions: OnyxEntry | ReportAction[]): string | null {
+function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[]): string | null {
+ // If the report is not an IOU or Expense report, it shouldn't be treated as one-transaction report.
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE) {
+ return null;
+ }
+
const reportActionsArray = Object.values(reportActions ?? {});
if (!reportActionsArray.length) {
@@ -444,6 +454,18 @@ function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] |
return false;
}
+ if (isReportActionSubmitted(currentAction)) {
+ const currentActionAdminAccountID = currentAction.adminAccountID;
+
+ return currentActionAdminAccountID === previousAction.actorAccountID || currentActionAdminAccountID === previousAction.adminAccountID;
+ }
+
+ if (isReportActionSubmitted(previousAction)) {
+ return typeof previousAction.adminAccountID === 'number'
+ ? currentAction.actorAccountID === previousAction.adminAccountID
+ : currentAction.actorAccountID === previousAction.actorAccountID;
+ }
+
return currentAction.actorAccountID === previousAction.actorAccountID;
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 85f5c414dbe4..6197a29cd4a6 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -209,7 +209,19 @@ type OptimisticApprovedReportAction = Pick<
type OptimisticSubmittedReportAction = Pick<
ReportAction,
- 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
+ | 'actionName'
+ | 'actorAccountID'
+ | 'adminAccountID'
+ | 'automatic'
+ | 'avatar'
+ | 'isAttachment'
+ | 'originalMessage'
+ | 'message'
+ | 'person'
+ | 'reportActionID'
+ | 'shouldShow'
+ | 'created'
+ | 'pendingAction'
>;
type OptimisticHoldReportAction = Pick<
@@ -418,7 +430,7 @@ type OptionData = {
notificationPreference?: NotificationPreference | null;
isDisabled?: boolean | null;
name?: string | null;
- isSelfDM?: boolean | null;
+ isSelfDM?: boolean;
reportID?: string;
enabled?: boolean;
data?: Partial;
@@ -1305,7 +1317,7 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | stri
*/
function isOneTransactionReport(reportID: string): boolean {
const reportActions = reportActionsByReport?.[reportID] ?? ([] as ReportAction[]);
- return ReportActionsUtils.getOneTransactionThreadReportID(reportActions) !== null;
+ return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null;
}
/**
@@ -1313,7 +1325,7 @@ function isOneTransactionReport(reportID: string): boolean {
*/
function isOneTransactionThread(reportID: string, parentReportID: string): boolean {
const parentReportActions = reportActionsByReport?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]);
- const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportActions);
+ const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportID, parentReportActions);
return reportID === transactionThreadReportID;
}
@@ -1956,13 +1968,17 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry, report: OnyxEntry | EmptyObject): string {
+function getReimbursementDeQueuedActionMessage(
+ reportAction: OnyxEntry,
+ report: OnyxEntry | EmptyObject,
+ isLHNPreview = false,
+): string {
const originalMessage = reportAction?.originalMessage as ReimbursementDeQueuedMessage | undefined;
const amount = originalMessage?.amount;
const currency = originalMessage?.currency;
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency);
if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN) {
- const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false) : getDisplayNameForParticipant(report?.managerID) ?? '';
+ const payerOrApproverName = report?.managerID === currentUserAccountID || !isLHNPreview ? '' : getDisplayNameForParticipant(report?.managerID, true);
return Localize.translateLocal('iou.adminCanceledRequest', {manager: payerOrApproverName, amount: formattedAmount});
}
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? '';
@@ -3082,13 +3098,27 @@ function getPolicyDescriptionText(policy: OnyxEntry): string {
function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number): OptimisticReportAction {
const parser = new ExpensiMark();
const commentText = getParsedComment(text ?? '');
+ const isAttachmentOnly = file && !text;
+ const isTextOnly = text && !file;
+
+ let htmlForNewComment;
+ let textForNewComment;
+ if (isAttachmentOnly) {
+ htmlForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
+ textForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
+ } else if (isTextOnly) {
+ htmlForNewComment = commentText;
+ textForNewComment = parser.htmlToText(htmlForNewComment);
+ } else {
+ htmlForNewComment = `${commentText}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
+ textForNewComment = `${commentText}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
+ }
+
const isAttachment = !text && file !== undefined;
- const attachmentInfo = isAttachment ? file : {};
- const htmlForNewComment = isAttachment ? CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML : commentText;
+ const attachmentInfo = file ?? {};
const accountID = actorAccountID ?? currentUserAccountID;
// Remove HTML from text when applying optimistic offline comment
- const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment);
return {
commentText,
reportAction: {
@@ -3107,7 +3137,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject,
created: DateUtils.getDBTimeWithSkew(),
message: [
{
- translationKey: isAttachment ? CONST.TRANSLATION_KEYS.ATTACHMENT : '',
+ translationKey: isAttachmentOnly ? CONST.TRANSLATION_KEYS.ATTACHMENT : '',
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
html: htmlForNewComment,
text: textForNewComment,
@@ -3559,7 +3589,7 @@ function buildOptimisticMovedReportAction(fromPolicyID: string, toPolicyID: stri
* Builds an optimistic SUBMITTED report action with a randomly generated reportActionID.
*
*/
-function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string): OptimisticSubmittedReportAction {
+function buildOptimisticSubmittedReportAction(amount: number, currency: string, expenseReportID: string, adminAccountID: number | undefined): OptimisticSubmittedReportAction {
const originalMessage = {
amount,
currency,
@@ -3569,6 +3599,7 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
return {
actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED,
actorAccountID: currentUserAccountID,
+ adminAccountID,
automatic: false,
avatar: getCurrentUserAvatarOrDefault(),
isAttachment: false,
@@ -5014,7 +5045,7 @@ function canUserPerformWriteAction(report: OnyxEntry) {
function getOriginalReportID(reportID: string, reportAction: OnyxEntry): string | undefined {
const reportActions = reportActionsByReport?.[reportID];
const currentReportAction = reportActions?.[reportAction?.reportActionID ?? ''] ?? null;
- const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? ([] as ReportAction[]));
+ const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions ?? ([] as ReportAction[]));
if (transactionThreadReportID !== null) {
return Object.keys(currentReportAction ?? {}).length === 0 ? transactionThreadReportID : reportID;
}
@@ -5726,6 +5757,19 @@ function hasActionsWithErrors(reportID: string): boolean {
return Object.values(reportActions ?? {}).some((action) => !isEmptyObject(action.errors));
}
+function getReportActionActorAccountID(reportAction: OnyxEntry, iouReport: OnyxEntry | undefined): number | undefined {
+ switch (reportAction?.actionName) {
+ case CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW:
+ return iouReport ? iouReport.managerID : reportAction?.actorAccountID;
+
+ case CONST.REPORT.ACTIONS.TYPE.SUBMITTED:
+ return reportAction?.adminAccountID ?? reportAction?.actorAccountID;
+
+ default:
+ return reportAction?.actorAccountID;
+ }
+}
+
/**
* @returns the object to update `report.hasOutstandingChildRequest`
*/
@@ -5977,6 +6021,7 @@ export {
isGroupChat,
isTrackExpenseReport,
hasActionsWithErrors,
+ getReportActionActorAccountID,
getGroupChatName,
getOutstandingChildRequest,
};
diff --git a/src/libs/StringUtils.ts b/src/libs/StringUtils.ts
index 2fb918c7a233..94cd04046ccc 100644
--- a/src/libs/StringUtils.ts
+++ b/src/libs/StringUtils.ts
@@ -72,4 +72,21 @@ function normalizeCRLF(value?: string): string | undefined {
return value?.replace(/\r\n/g, '\n');
}
-export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF};
+/**
+ * Generates an acronym for a string.
+ * @param string the string for which to produce the acronym
+ * @returns the acronym
+ */
+function getAcronym(string: string): string {
+ let acronym = '';
+ const wordsInString = string.split(' ');
+ wordsInString.forEach((wordInString) => {
+ const splitByHyphenWords = wordInString.split('-');
+ splitByHyphenWords.forEach((splitByHyphenWord) => {
+ acronym += splitByHyphenWord.substring(0, 1);
+ });
+ });
+ return acronym;
+}
+
+export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF, getAcronym};
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index 234868f8322c..85896d50e095 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -113,7 +113,7 @@ function setPersonalBankAccountContinueKYCOnSuccess(onSuccessFallbackRoute: Rout
function clearPersonalBankAccount() {
clearPlaid();
- Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {});
+ Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, null);
Onyx.set(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, null);
clearPersonalBankAccountSetupType();
}
diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts
index 8207b78e8759..1fcf9bed6a55 100644
--- a/src/libs/actions/FormActions.ts
+++ b/src/libs/actions/FormActions.ts
@@ -1,6 +1,6 @@
+import type {NullishDeep, OnyxValue} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
-import type {NullishDeep} from 'react-native-onyx';
-import type {OnyxFormDraftKey, OnyxFormKey, OnyxValue} from '@src/ONYXKEYS';
+import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
function setIsLoading(formID: OnyxFormKey, isLoading: boolean) {
@@ -31,4 +31,4 @@ function clearDraftValues(formID: OnyxFormKey) {
Onyx.set(`${formID}Draft`, null);
}
-export {setDraftValues, setErrorFields, setErrors, clearErrors, clearErrorFields, setIsLoading, clearDraftValues};
+export {clearDraftValues, clearErrorFields, clearErrors, setDraftValues, setErrorFields, setErrors, setIsLoading};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 0382732e6f47..55dd2eb4fe39 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -319,7 +319,7 @@ function clearMoneyRequest(transactionID: string) {
/**
* Update money request-related pages IOU type params
*/
-function updateMoneyRequestTypeParams(routes: StackNavigationState['routes'] | NavigationPartialRoute[], newIouType: string, tab: string) {
+function updateMoneyRequestTypeParams(routes: StackNavigationState['routes'] | NavigationPartialRoute[], newIouType: string, tab?: string) {
routes.forEach((route) => {
const tabList = [CONST.TAB_REQUEST.DISTANCE, CONST.TAB_REQUEST.MANUAL, CONST.TAB_REQUEST.SCAN] as string[];
if (!route.name.startsWith('Money_Request_') && !tabList.includes(route.name)) {
@@ -1491,7 +1491,7 @@ function getTrackExpenseInformation(
/** Requests money based on a distance (e.g. mileage from a map) */
function createDistanceRequest(
- report: OnyxTypes.Report,
+ report: OnyxEntry,
participant: Participant,
comment: string,
created: string,
@@ -1508,8 +1508,8 @@ function createDistanceRequest(
) {
// If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function
const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
- const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report;
- const moneyRequestReportID = isMoneyRequestReport ? report.reportID : '';
+ const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report;
+ const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : '';
const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
const optimisticReceipt: Receipt = {
@@ -1569,7 +1569,7 @@ function createDistanceRequest(
};
API.write(WRITE_COMMANDS.CREATE_DISTANCE_REQUEST, parameters, onyxData);
- Navigation.dismissModal(isMoneyRequestReport ? report.reportID : chatReport.reportID);
+ Navigation.dismissModal(isMoneyRequestReport ? report?.reportID : chatReport.reportID);
Report.notifyNewAction(chatReport.reportID, userAccountID);
}
@@ -2375,6 +2375,7 @@ function trackExpense(
policyTagList?: OnyxEntry,
policyCategories?: OnyxEntry,
gpsPoints?: GPSPoint,
+ validWaypoints?: WaypointCollection,
) {
const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report;
@@ -2437,6 +2438,7 @@ function trackExpense(
gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined,
transactionThreadReportID,
createdReportActionIDForThread,
+ waypoints: validWaypoints ? JSON.stringify(validWaypoints) : undefined,
};
API.write(WRITE_COMMANDS.TRACK_EXPENSE, parameters, onyxData);
@@ -4311,7 +4313,7 @@ function deleteTrackExpense(chatReportID: string, transactionID: string, reportA
* @param recipient - The user receiving the money
*/
function getSendMoneyParams(
- report: OnyxTypes.Report,
+ report: OnyxEntry | EmptyObject,
amount: number,
currency: string,
comment: string,
@@ -4330,11 +4332,8 @@ function getSendMoneyParams(
idempotencyKey: Str.guid(),
});
- let chatReport = report.reportID ? report : null;
+ let chatReport = !isEmptyObject(report) && report?.reportID ? report : ReportUtils.getChatByParticipants([recipientAccountID]);
let isNewChat = false;
- if (!chatReport) {
- chatReport = ReportUtils.getChatByParticipants([recipientAccountID]);
- }
if (!chatReport) {
chatReport = ReportUtils.buildOptimisticChatReport([recipientAccountID]);
isNewChat = true;
@@ -4766,7 +4765,7 @@ function getPayMoneyRequestParams(
* @param managerID - Account ID of the person sending the money
* @param recipient - The user receiving the money
*/
-function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) {
+function sendMoneyElsewhere(report: OnyxEntry, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) {
const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.ELSEWHERE, managerID, recipient);
API.write(WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, params, {optimisticData, successData, failureData});
@@ -4780,7 +4779,7 @@ function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency:
* @param managerID - Account ID of the person sending the money
* @param recipient - The user receiving the money
*/
-function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) {
+function sendMoneyWithWallet(report: OnyxEntry, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) {
const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.EXPENSIFY, managerID, recipient);
API.write(WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, params, {optimisticData, successData, failureData});
@@ -4959,6 +4958,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full
const parameters: ApproveMoneyRequestParams = {
reportID: expenseReport.reportID,
approvedReportActionID: optimisticApprovedReportAction.reportActionID,
+ full,
};
API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData});
@@ -4966,11 +4966,12 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full
function submitReport(expenseReport: OnyxTypes.Report) {
const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null;
- const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID);
const parentReport = ReportUtils.getReport(expenseReport.parentReportID);
const policy = getPolicy(expenseReport.policyID);
const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID;
const isSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy);
+ const adminAccountID = policy.role === CONST.POLICY.ROLE.ADMIN ? currentUserPersonalDetails.accountID : undefined;
+ const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID, adminAccountID);
const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, isSubmitAndClosePolicy ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.SUBMITTED);
const optimisticData: OnyxUpdate[] = !isSubmitAndClosePolicy
@@ -5274,9 +5275,9 @@ function replaceReceipt(transactionID: string, file: File, source: string) {
* @param transactionID of the transaction to set the participants of
* @param report attached to the transaction
*/
-function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report) {
+function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry) {
// If the report is iou or expense report, we should get the chat report to set participant for request money
- const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report;
+ const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report;
const currentUserAccountID = currentUserPersonalDetails.accountID;
const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport);
const participants: Participant[] =
diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts
index 60aff19223bc..8248b721c416 100644
--- a/src/libs/actions/PersonalDetails.ts
+++ b/src/libs/actions/PersonalDetails.ts
@@ -44,25 +44,25 @@ Onyx.connect({
});
function updatePronouns(pronouns: string) {
- if (currentUserAccountID) {
- const parameters: UpdatePronounsParams = {pronouns};
+ if (!currentUserAccountID) {
+ return;
+ }
- API.write(WRITE_COMMANDS.UPDATE_PRONOUNS, parameters, {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- value: {
- [currentUserAccountID]: {
- pronouns,
- },
+ const parameters: UpdatePronounsParams = {pronouns};
+
+ API.write(WRITE_COMMANDS.UPDATE_PRONOUNS, parameters, {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [currentUserAccountID]: {
+ pronouns,
},
},
- ],
- });
- }
-
- Navigation.goBack();
+ },
+ ],
+ });
}
function updateDisplayName(firstName: string, lastName: string) {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index d2f85362baf8..096125215a32 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -373,9 +373,9 @@ function addActions(reportID: string, text = '', file?: FileObject) {
let reportCommentText = '';
let reportCommentAction: OptimisticAddCommentReportAction | undefined;
let attachmentAction: OptimisticAddCommentReportAction | undefined;
- let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT;
+ let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT | typeof WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT;
- if (text) {
+ if (text && !file) {
const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text);
reportCommentAction = reportComment.reportAction;
reportCommentText = reportComment.commentText;
@@ -385,10 +385,18 @@ function addActions(reportID: string, text = '', file?: FileObject) {
// When we are adding an attachment we will call AddAttachment.
// It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only.
commandName = WRITE_COMMANDS.ADD_ATTACHMENT;
- const attachment = ReportUtils.buildOptimisticAddCommentReportAction('', file);
+ const attachment = ReportUtils.buildOptimisticAddCommentReportAction(text, file);
attachmentAction = attachment.reportAction;
}
+ if (text && file) {
+ // When there is both text and a file, the text for the report comment needs to be parsed)
+ reportCommentText = ReportUtils.getParsedComment(text ?? '');
+
+ // And the API command needs to go to the new API which supports combining both text and attachments in a single report action
+ commandName = WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT;
+ }
+
// Always prefer the file as the last action over text
const lastAction = attachmentAction ?? reportCommentAction;
const currentTime = DateUtils.getDBTimeWithSkew();
@@ -412,7 +420,9 @@ function addActions(reportID: string, text = '', file?: FileObject) {
// Optimistically add the new actions to the store before waiting to save them to the server
const optimisticReportActions: OnyxCollection = {};
- if (text && reportCommentAction?.reportActionID) {
+
+ // Only add the reportCommentAction when there is no file attachment. If there is both a file attachment and text, that will all be contained in the attachmentAction.
+ if (text && reportCommentAction?.reportActionID && !file) {
optimisticReportActions[reportCommentAction.reportActionID] = reportCommentAction;
}
if (file && attachmentAction?.reportActionID) {
diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts
index b1710aa72cbb..26219d72920e 100644
--- a/src/libs/actions/TransactionEdit.ts
+++ b/src/libs/actions/TransactionEdit.ts
@@ -16,24 +16,24 @@ function createBackupTransaction(transaction: OnyxEntry) {
};
// Use set so that it will always fully overwrite any backup transaction that could have existed before
- Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction);
+ Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`, newTransaction);
}
/**
* Removes a transaction from Onyx that was only used temporary in the edit flow
*/
function removeBackupTransaction(transactionID: string) {
- Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null);
+ Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`, null);
}
-function restoreOriginalTransactionFromBackup(transactionID: string) {
+function restoreOriginalTransactionFromBackup(transactionID: string, isDraft: boolean) {
const connectionID = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`,
callback: (backupTransaction) => {
Onyx.disconnect(connectionID);
// Use set to completely overwrite the original transaction
- Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, backupTransaction);
+ Onyx.set(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, backupTransaction);
removeBackupTransaction(transactionID);
},
});
diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts
index f8c92351d36c..b59a656d0ac2 100644
--- a/src/libs/fileDownload/types.ts
+++ b/src/libs/fileDownload/types.ts
@@ -8,7 +8,7 @@ type GetImageResolution = (url: File | Asset) => Promise;
type ExtensionAndFileName = {fileName: string; fileExtension: string};
type SplitExtensionFromFileName = (fileName: string) => ExtensionAndFileName;
-type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void, fileType?: string) => Promise;
+type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure?: (error?: unknown) => void, fileType?: string) => Promise;
type AttachmentDetails = {
previewSourceURL: null | string;
diff --git a/src/libs/filterArrayByMatch.ts b/src/libs/filterArrayByMatch.ts
new file mode 100644
index 000000000000..3abf82b6afab
--- /dev/null
+++ b/src/libs/filterArrayByMatch.ts
@@ -0,0 +1,117 @@
+/**
+ * This file is a slim version of match-sorter library (https://github.com/kentcdodds/match-sorter) adjusted to the needs.
+ Use `threshold` option with one of the rankings defined below to control the strictness of the match.
+*/
+import type {ValueOf} from 'type-fest';
+import StringUtils from './StringUtils';
+
+const MATCH_RANK = {
+ CASE_SENSITIVE_EQUAL: 7,
+ EQUAL: 6,
+ STARTS_WITH: 5,
+ WORD_STARTS_WITH: 4,
+ CONTAINS: 3,
+ ACRONYM: 2,
+ MATCHES: 1,
+ NO_MATCH: 0,
+} as const;
+
+type Ranking = ValueOf;
+
+/**
+ * Gives a rankings score based on how well the two strings match.
+ * @param testString - the string to test against
+ * @param stringToRank - the string to rank
+ * @returns the ranking for how well stringToRank matches testString
+ */
+function getMatchRanking(testString: string, stringToRank: string): Ranking {
+ // too long
+ if (stringToRank.length > testString.length) {
+ return MATCH_RANK.NO_MATCH;
+ }
+
+ // case sensitive equals
+ if (testString === stringToRank) {
+ return MATCH_RANK.CASE_SENSITIVE_EQUAL;
+ }
+
+ // Lower casing before further comparison
+ const lowercaseTestString = testString.toLowerCase();
+ const lowercaseStringToRank = stringToRank.toLowerCase();
+
+ // case insensitive equals
+ if (lowercaseTestString === lowercaseStringToRank) {
+ return MATCH_RANK.EQUAL;
+ }
+
+ // starts with
+ if (lowercaseTestString.startsWith(lowercaseStringToRank)) {
+ return MATCH_RANK.STARTS_WITH;
+ }
+
+ // word starts with
+ if (lowercaseTestString.includes(` ${lowercaseStringToRank}`)) {
+ return MATCH_RANK.WORD_STARTS_WITH;
+ }
+
+ // contains
+ if (lowercaseTestString.includes(lowercaseStringToRank)) {
+ return MATCH_RANK.CONTAINS;
+ }
+ if (lowercaseStringToRank.length === 1) {
+ return MATCH_RANK.NO_MATCH;
+ }
+
+ // acronym
+ if (StringUtils.getAcronym(lowercaseTestString).includes(lowercaseStringToRank)) {
+ return MATCH_RANK.ACRONYM;
+ }
+
+ // will return a number between rankings.MATCHES and rankings.MATCHES + 1 depending on how close of a match it is.
+ let matchingInOrderCharCount = 0;
+ let charNumber = 0;
+ for (const char of stringToRank) {
+ charNumber = lowercaseTestString.indexOf(char, charNumber) + 1;
+ if (!charNumber) {
+ return MATCH_RANK.NO_MATCH;
+ }
+ matchingInOrderCharCount++;
+ }
+
+ // Calculate ranking based on character sequence and spread
+ const spread = charNumber - lowercaseTestString.indexOf(stringToRank[0]);
+ const spreadPercentage = 1 / spread;
+ const inOrderPercentage = matchingInOrderCharCount / stringToRank.length;
+ const ranking = MATCH_RANK.MATCHES + inOrderPercentage * spreadPercentage;
+
+ return ranking as Ranking;
+}
+
+/**
+ * Takes an array of items and a value and returns a new array with the items that match the given value
+ * @param items - the items to filter
+ * @param searchValue - the value to use for ranking
+ * @param extractRankableValuesFromItem - an array of functions
+ * @returns the new filtered array
+ */
+function filterArrayByMatch(items: readonly T[], searchValue: string, extractRankableValuesFromItem: (item: T) => string[]): T[] {
+ const filteredItems = [];
+ for (const item of items) {
+ const valuesToRank = extractRankableValuesFromItem(item);
+ let itemRank: Ranking = MATCH_RANK.NO_MATCH;
+ for (const value of valuesToRank) {
+ const rank = getMatchRanking(value, searchValue);
+ if (rank > itemRank) {
+ itemRank = rank;
+ }
+ }
+
+ if (itemRank >= MATCH_RANK.MATCHES + 1) {
+ filteredItems.push(item);
+ }
+ }
+ return filteredItems;
+}
+
+export default filterArrayByMatch;
+export {MATCH_RANK};
diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts
index d827d9936fd1..412a8e00f052 100644
--- a/src/libs/migrateOnyx.ts
+++ b/src/libs/migrateOnyx.ts
@@ -2,6 +2,7 @@ import Log from './Log';
import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID';
import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID';
import NVPMigration from './migrations/NVPMigration';
+import PronounsMigration from './migrations/PronounsMigration';
import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts';
import RenameReceiptFilename from './migrations/RenameReceiptFilename';
import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection';
@@ -19,6 +20,7 @@ export default function () {
TransactionBackupsToCollection,
RemoveEmptyReportActionsDrafts,
NVPMigration,
+ PronounsMigration,
];
// Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the
diff --git a/src/libs/migrations/PronounsMigration.ts b/src/libs/migrations/PronounsMigration.ts
new file mode 100644
index 000000000000..5fe911ae7f98
--- /dev/null
+++ b/src/libs/migrations/PronounsMigration.ts
@@ -0,0 +1,53 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import * as PersonalDetails from '@userActions/PersonalDetails';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {PersonalDetails as TPersonalDetails} from '@src/types/onyx';
+
+function getCurrentUserAccountIDFromOnyx(): Promise {
+ return new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (val) => {
+ Onyx.disconnect(connectionID);
+ return resolve(val?.accountID ?? -1);
+ },
+ });
+ });
+}
+
+function getCurrentUserPersonalDetailsFromOnyx(currentUserAccountID: number): Promise> {
+ return new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ callback: (val) => {
+ Onyx.disconnect(connectionID);
+ return resolve(val?.[currentUserAccountID] ?? null);
+ },
+ });
+ });
+}
+
+/**
+ * This migration updates deprecated pronouns with new predefined ones.
+ */
+export default function (): Promise {
+ return getCurrentUserAccountIDFromOnyx()
+ .then(getCurrentUserPersonalDetailsFromOnyx)
+ .then((currentUserPersonalDetails: OnyxEntry) => {
+ if (!currentUserPersonalDetails) {
+ return;
+ }
+
+ const pronouns = currentUserPersonalDetails.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? '';
+ if (!pronouns || (CONST.PRONOUNS_LIST as readonly string[]).includes(pronouns)) {
+ return;
+ }
+
+ // Find the updated pronouns key replaceable for the deprecated value.
+ const pronounsKey = Object.entries(CONST.DEPRECATED_PRONOUNS_LIST).find((deprecated) => deprecated[1] === pronouns)?.[0] ?? '';
+ // If couldn't find the updated pronouns, reset it to require user's manual update.
+ PersonalDetails.updatePronouns(pronounsKey ? `${CONST.PRONOUNS.PREFIX}${pronounsKey}` : '');
+ });
+}
diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx
index bb6230fbb6a4..5cd0f3ef8026 100644
--- a/src/pages/AddPersonalBankAccountPage.tsx
+++ b/src/pages/AddPersonalBankAccountPage.tsx
@@ -102,6 +102,7 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona
AddPersonalBankAccountPage.displayName = 'AddPersonalBankAccountPage';
export default withOnyx({
+ // @ts-expect-error: ONYXKEYS.PERSONAL_BANK_ACCOUNT is conflicting with ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM
personalBankAccount: {
key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
},
diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx
index c07ad9aba587..314f5da988fd 100644
--- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx
+++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx
@@ -119,6 +119,7 @@ export default withOnyx void;
+
+ shouldForceFullScreen?: boolean;
};
// eslint-disable-next-line rulesdir/no-negated-variables
-function NotFoundPage({onBackButtonPress}: NotFoundPageProps) {
+function NotFoundPage({onBackButtonPress, shouldForceFullScreen}: NotFoundPageProps) {
return (
);
diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx
index 751813d1d3cf..97f14fd5d0a4 100755
--- a/src/pages/NewChatPage.tsx
+++ b/src/pages/NewChatPage.tsx
@@ -1,7 +1,6 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import OfflineIndicator from '@components/OfflineIndicator';
import {useOptionsList} from '@components/OptionListContextProvider';
@@ -25,32 +24,21 @@ import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type * as OnyxTypes from '@src/types/onyx';
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
-type NewChatPageWithOnyxProps = {
- /** New group chat draft data */
- newGroupDraft: OnyxEntry;
-
- /** All of the personal details for everyone */
- personalDetails: OnyxEntry;
-
- betas: OnyxEntry;
-
- /** An object that holds data about which referral banners have been dismissed */
- dismissedReferralBanners: OnyxEntry;
-
- /** Whether we are searching for reports in the server */
- isSearchingForReports: OnyxEntry;
-};
-
-type NewChatPageProps = NewChatPageWithOnyxProps & {
+type NewChatPageProps = {
isGroupChat?: boolean;
};
const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE);
-function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports, dismissedReferralBanners, newGroupDraft}: NewChatPageProps) {
+function NewChatPage({isGroupChat}: NewChatPageProps) {
+ const [dismissedReferralBanners] = useOnyx(ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS);
+ const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT);
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false});
+
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -311,21 +299,4 @@ function NewChatPage({betas, isGroupChat, personalDetails, isSearchingForReports
NewChatPage.displayName = 'NewChatPage';
-export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
- },
- newGroupDraft: {
- key: ONYXKEYS.NEW_GROUP_CHAT_DRAFT,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- betas: {
- key: ONYXKEYS.BETAS,
- },
- isSearchingForReports: {
- key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
- initWithStoredValues: false,
- },
-})(NewChatPage);
+export default NewChatPage;
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 270b72b93da8..6ad3860132d2 100755
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -1,9 +1,9 @@
import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import React, {useEffect} from 'react';
+import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import AutoUpdateTime from '@components/AutoUpdateTime';
import Avatar from '@components/Avatar';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -36,31 +36,11 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {PersonalDetails, PersonalDetailsList, PersonalDetailsMetadata, Report, Session} from '@src/types/onyx';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {PersonalDetails, Report} from '@src/types/onyx';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-type ProfilePageOnyxProps = {
- /** The personal details of the person who is logged in */
- personalDetails: OnyxEntry;
-
- /** Loading status of the personal details */
- personalDetailsMetadata: OnyxEntry>;
-
- /** The report currently being looked at */
- report: OnyxEntry;
-
- /** The list of all reports
- * ONYXKEYS.COLLECTION.REPORT is needed for report key function
- */
- // eslint-disable-next-line react/no-unused-prop-types
- reports: OnyxCollection;
-
- /** Session info for the currently logged in user. */
- session: OnyxEntry;
-};
-
-type ProfilePageProps = ProfilePageOnyxProps & StackScreenProps;
+type ProfilePageProps = StackScreenProps;
/**
* Gets the phone number to display for SMS logins
@@ -77,7 +57,38 @@ const getPhoneNumber = ({login = '', displayName = ''}: PersonalDetails | EmptyO
return login ? Str.removeSMSDomain(login) : '';
};
-function ProfilePage({personalDetails, personalDetailsMetadata, route, session, report}: ProfilePageProps) {
+/**
+ * This function narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering
+ * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI.
+ */
+const chatReportSelector = (report: OnyxEntry): OnyxEntry =>
+ report && {
+ reportID: report.reportID,
+ participantAccountIDs: report.participantAccountIDs,
+ parentReportID: report.parentReportID,
+ parentReportActionID: report.parentReportActionID,
+ type: report.type,
+ chatType: report.chatType,
+ isPolicyExpenseChat: report.isPolicyExpenseChat,
+ };
+
+function ProfilePage({route}: ProfilePageProps) {
+ const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: chatReportSelector});
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
+ const [personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_METADATA);
+ const [session] = useOnyx(ONYXKEYS.SESSION);
+
+ const reportKey = useMemo(() => {
+ const accountID = Number(route.params?.accountID ?? 0);
+ const reportID = ReportUtils.getChatByParticipants([accountID], reports)?.reportID ?? '';
+
+ if ((Boolean(session) && Number(session?.accountID) === accountID) || SessionActions.isAnonymousUser() || !reportID) {
+ return `${ONYXKEYS.COLLECTION.REPORT}0` as const;
+ }
+ return `${ONYXKEYS.COLLECTION.REPORT}${reportID}` as const;
+ }, [reports, route.params?.accountID, session]);
+ const [report] = useOnyx(reportKey);
+
const styles = useThemeStyles();
const {translate, formatPhoneNumber} = useLocalize();
const accountID = Number(route.params?.accountID ?? 0);
@@ -247,44 +258,4 @@ function ProfilePage({personalDetails, personalDetailsMetadata, route, session,
ProfilePage.displayName = 'ProfilePage';
-/**
- * This function narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering
- * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI.
- */
-const chatReportSelector = (report: OnyxEntry): Report =>
- (report && {
- reportID: report.reportID,
- participantAccountIDs: report.participantAccountIDs,
- parentReportID: report.parentReportID,
- parentReportActionID: report.parentReportActionID,
- type: report.type,
- chatType: report.chatType,
- isPolicyExpenseChat: report.isPolicyExpenseChat,
- }) as Report;
-
-export default withOnyx({
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- selector: chatReportSelector,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- personalDetailsMetadata: {
- key: ONYXKEYS.PERSONAL_DETAILS_METADATA,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- report: {
- key: ({route, session, reports}) => {
- const accountID = Number(route.params?.accountID ?? 0);
- const reportID = ReportUtils.getChatByParticipants([accountID], reports)?.reportID ?? '';
-
- if ((Boolean(session) && Number(session?.accountID) === accountID) || SessionActions.isAnonymousUser() || !reportID) {
- return `${ONYXKEYS.COLLECTION.REPORT}0`;
- }
- return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`;
- },
- },
-})(ProfilePage);
+export default ProfilePage;
diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx
index 49e53381e040..ab5bd10317be 100644
--- a/src/pages/RoomInvitePage.tsx
+++ b/src/pages/RoomInvitePage.tsx
@@ -192,6 +192,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) {
-
-
+
);
}
diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx
index c072bfd56913..5576f64ba67a 100644
--- a/src/pages/SearchPage/index.tsx
+++ b/src/pages/SearchPage/index.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
+import isEmpty from 'lodash/isEmpty';
import React, {useEffect, useMemo, useState} from 'react';
-import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -9,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
import useDebouncedState from '@hooks/useDebouncedState';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -48,7 +49,7 @@ const setPerformanceTimersEnd = () => {
Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
};
-const SearchPageFooterInstance = ;
+const SerachPageFooterInstance = ;
function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) {
const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false);
@@ -72,24 +73,45 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps)
Report.searchInServer(debouncedSearchValue.trim());
}, [debouncedSearchValue]);
- const {
- recentReports,
- personalDetails: localPersonalDetails,
- userToInvite,
- headerMessage,
- } = useMemo(() => {
- if (!areOptionsInitialized) {
+ const searchOptions = useMemo(() => {
+ if (!areOptionsInitialized || !isScreenTransitionEnd) {
return {
recentReports: [],
personalDetails: [],
userToInvite: null,
+ currentUserOption: null,
+ categoryOptions: [],
+ tagOptions: [],
+ taxRatesOptions: [],
headerMessage: '',
};
}
- const optionList = OptionsListUtils.getSearchOptions(options, debouncedSearchValue.trim(), betas ?? []);
- const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), debouncedSearchValue);
+ const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []);
+ const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), '');
return {...optionList, headerMessage: header};
- }, [areOptionsInitialized, options, debouncedSearchValue, betas]);
+ }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]);
+
+ const filteredOptions = useMemo(() => {
+ if (debouncedSearchValue.trim() === '') {
+ return {
+ recentReports: [],
+ personalDetails: [],
+ userToInvite: null,
+ headerMessage: '',
+ };
+ }
+
+ const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue);
+ const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, false, debouncedSearchValue);
+ return {
+ recentReports: newOptions.recentReports,
+ personalDetails: newOptions.personalDetails,
+ userToInvite: null,
+ headerMessage: header,
+ };
+ }, [debouncedSearchValue, searchOptions]);
+
+ const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions;
const sections = useMemo((): SearchPageSectionList => {
const newSections: SearchPageSectionList = [];
@@ -108,7 +130,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps)
});
}
- if (userToInvite) {
+ if (!isEmpty(userToInvite)) {
newSections.push({
data: [userToInvite],
shouldShow: true,
@@ -135,6 +157,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps)
setIsScreenTransitionEnd(true);
};
+ const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND});
+
return (
- {({safeAreaPaddingBottomStyle}) => (
- <>
-
-
-
- sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY}
- ListItem={UserListItem}
- textInputValue={searchValue}
- textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
- textInputHint={offlineMessage}
- onChangeText={setSearchValue}
- headerMessage={headerMessage}
- headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined}
- onLayout={setPerformanceTimersEnd}
- onSelectRow={selectReport}
- showLoadingPlaceholder={!areOptionsInitialized}
- footerContent={SearchPageFooterInstance}
- isLoadingNewOptions={isSearchingForReports ?? undefined}
- />
-
- >
- )}
+
+
+ sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY}
+ ListItem={UserListItem}
+ textInputValue={searchValue}
+ textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
+ textInputHint={offlineMessage}
+ onChangeText={setSearchValue}
+ headerMessage={headerMessage}
+ headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined}
+ onLayout={setPerformanceTimersEnd}
+ onSelectRow={selectReport}
+ showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd}
+ footerContent={!isDismissed && SerachPageFooterInstance}
+ isLoadingNewOptions={isSearchingForReports ?? undefined}
+ />
);
}
diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx
index 631d377e34cd..f1a439548f1b 100644
--- a/src/pages/WorkspaceSwitcherPage.tsx
+++ b/src/pages/WorkspaceSwitcherPage.tsx
@@ -106,9 +106,11 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
const {policyID} = option;
setActiveWorkspaceID(policyID);
- Navigation.goBack();
+
if (policyID !== activeWorkspaceID) {
Navigation.navigateWithSwitchPolicyID({policyID});
+ } else {
+ Navigation.goBack();
}
},
[activeWorkspaceID, setActiveWorkspaceID],
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 902e38f4a181..b93daf4f097b 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -2,10 +2,10 @@ import {useIsFocused} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import lodashIsEqual from 'lodash/isEqual';
import React, {memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
-import {InteractionManager, View} from 'react-native';
import type {FlatList, ViewStyle} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {InteractionManager, View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
import type {LayoutChangeEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import Banner from '@components/Banner';
import BlockingView from '@components/BlockingViews/BlockingView';
@@ -18,8 +18,8 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
import ScreenWrapper from '@components/ScreenWrapper';
import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
-import withCurrentReportID from '@components/withCurrentReportID';
import type {CurrentReportIDContextValue} from '@components/withCurrentReportID';
+import withCurrentReportID from '@components/withCurrentReportID';
import useAppFocusEvent from '@hooks/useAppFocusEvent';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
@@ -27,7 +27,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Timing from '@libs/actions/Timing';
-import * as Browser from '@libs/Browser';
import Navigation from '@libs/Navigation/Navigation';
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
import Performance from '@libs/Performance';
@@ -48,8 +47,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import HeaderView from './HeaderView';
import ReportActionsView from './report/ReportActionsView';
import ReportFooter from './report/ReportFooter';
-import {ActionListContext, ReactionListContext} from './ReportScreenContext';
import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext';
+import {ActionListContext, ReactionListContext} from './ReportScreenContext';
type ReportScreenOnyxPropsWithoutParentReportAction = {
/** Get modal status */
@@ -254,8 +253,7 @@ function ReportScreen({
if (!sortedAllReportActions.length) {
return [];
}
- const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute);
- return currentRangeOfReportActions;
+ return ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute);
}, [reportActionIDFromRoute, sortedAllReportActions]);
// Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger.
@@ -275,7 +273,8 @@ function ReportScreen({
Performance.markStart(CONST.TIMING.CHAT_RENDER);
}
const [isComposerFocus, setIsComposerFocus] = useState(false);
- const viewportOffsetTop = useViewportOffsetTop(Browser.isMobileSafari() && isComposerFocus && !modal?.willAlertModalBecomeVisible);
+ const shouldAdjustScrollView = useMemo(() => isComposerFocus && !modal?.willAlertModalBecomeVisible, [isComposerFocus, modal]);
+ const viewportOffsetTop = useViewportOffsetTop(shouldAdjustScrollView);
const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report);
const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
@@ -333,7 +332,7 @@ function ReportScreen({
);
}
- const transactionThreadReportID = useMemo(() => ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? []), [reportActions]);
+ const transactionThreadReportID = useMemo(() => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? []), [report.reportID, reportActions]);
if (ReportUtils.isMoneyRequestReport(report)) {
headerView = (
+
{
return 'quickAction.splitScan';
case CONST.QUICK_ACTIONS.SPLIT_DISTANCE:
return 'quickAction.splitDistance';
- case CONST.QUICK_ACTIONS.SEND_MONEY:
- return 'quickAction.sendMoney';
- case CONST.QUICK_ACTIONS.ASSIGN_TASK:
- return 'quickAction.assignTask';
case CONST.QUICK_ACTIONS.TRACK_MANUAL:
return 'quickAction.trackManual';
case CONST.QUICK_ACTIONS.TRACK_SCAN:
return 'quickAction.trackScan';
case CONST.QUICK_ACTIONS.TRACK_DISTANCE:
return 'quickAction.trackDistance';
+ case CONST.QUICK_ACTIONS.SEND_MONEY:
+ return 'quickAction.sendMoney';
+ case CONST.QUICK_ACTIONS.ASSIGN_TASK:
+ return 'quickAction.assignTask';
default:
return '' as TranslationPaths;
}
diff --git a/src/pages/iou/ReceiptDropUI.js b/src/pages/iou/ReceiptDropUI.tsx
similarity index 78%
rename from src/pages/iou/ReceiptDropUI.js
rename to src/pages/iou/ReceiptDropUI.tsx
index 0f7226668a80..f1f25f80bc57 100644
--- a/src/pages/iou/ReceiptDropUI.js
+++ b/src/pages/iou/ReceiptDropUI.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import ReceiptUpload from '@assets/images/receipt-upload.svg';
@@ -9,19 +8,15 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-const propTypes = {
- /** Callback to execute when a file is dropped. */
- onDrop: PropTypes.func.isRequired,
+type ReceiptDropUIProps = {
+ /** Function to execute when an item is dropped in the drop zone. */
+ onDrop: (event: DragEvent) => void;
/** Pixels the receipt image should be shifted down to match the non-drag view UI */
- receiptImageTopPosition: PropTypes.number,
+ receiptImageTopPosition?: number;
};
-const defaultProps = {
- receiptImageTopPosition: 0,
-};
-
-function ReceiptDropUI({onDrop, receiptImageTopPosition}) {
+function ReceiptDropUI({onDrop, receiptImageTopPosition = 0}: ReceiptDropUIProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
return (
@@ -43,7 +38,5 @@ function ReceiptDropUI({onDrop, receiptImageTopPosition}) {
}
ReceiptDropUI.displayName = 'ReceiptDropUI';
-ReceiptDropUI.propTypes = propTypes;
-ReceiptDropUI.defaultProps = defaultProps;
export default ReceiptDropUI;
diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js
index cb078fac133c..e9057fef9226 100644
--- a/src/pages/iou/request/IOURequestStartPage.js
+++ b/src/pages/iou/request/IOURequestStartPage.js
@@ -82,7 +82,7 @@ function IOURequestStartPage({
};
const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction));
const previousIOURequestType = usePrevious(transactionRequestType.current);
- const {canUseP2PDistanceRequests} = usePermissions();
+ const {canUseP2PDistanceRequests} = usePermissions(iouType);
const isFromGlobalCreate = _.isEmpty(report.reportID);
useFocusEffect(
@@ -110,7 +110,8 @@ function IOURequestStartPage({
const isExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isExpenseReport = ReportUtils.isExpenseReport(report);
- const shouldDisplayDistanceRequest = iouType !== CONST.IOU.TYPE.TRACK_EXPENSE && (canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate);
+
+ const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate;
// Allow the user to create the request if we are creating the request in global menu or the report can create the request
const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType);
diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
index a05167d5cedf..3c65f0fa9a96 100644
--- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
+++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
@@ -1,7 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {memo, useCallback, useEffect, useMemo} from 'react';
-import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import Button from '@components/Button';
@@ -14,9 +13,11 @@ import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
import useDebouncedState from '@hooks/useDebouncedState';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePermissions from '@hooks/usePermissions';
+import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
@@ -28,9 +29,6 @@ const propTypes = {
/** Beta features list */
betas: PropTypes.arrayOf(PropTypes.string),
- /** An object that holds data about which referral banners have been dismissed */
- dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool),
-
/** Callback to request parent modal to go to next step, which should be split */
onFinish: PropTypes.func.isRequired,
@@ -48,45 +46,28 @@ const propTypes = {
}),
),
- /** Padding bottom style of safe area */
- safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
/** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)).isRequired,
/** The request type, ie. manual, scan, distance */
iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired,
-
- /** Whether the parent screen transition has ended */
- didScreenTransitionEnd: PropTypes.bool,
};
const defaultProps = {
participants: [],
- safeAreaPaddingBottomStyle: {},
betas: [],
- dismissedReferralBanners: {},
- didScreenTransitionEnd: false,
};
-function MoneyTemporaryForRefactorRequestParticipantsSelector({
- betas,
- participants,
- onFinish,
- onParticipantsAdded,
- safeAreaPaddingBottomStyle,
- iouType,
- iouRequestType,
- dismissedReferralBanners,
- didScreenTransitionEnd,
-}) {
+function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participants, onFinish, onParticipantsAdded, iouType, iouRequestType}) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
+ const {isDismissed} = useDismissedReferralBanners({referralContentType});
const {canUseP2PDistanceRequests} = usePermissions();
+ const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus();
const {options, areOptionsInitialized} = useOptionsList({
shouldInitialize: didScreenTransitionEnd,
});
@@ -106,7 +87,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
*/
const [sections, newChatOptions] = useMemo(() => {
const newSections = [];
- if (!areOptionsInitialized) {
+ if (!areOptionsInitialized || !didScreenTransitionEnd) {
return [newSections, {}];
}
const chatOptions = OptionsListUtils.getFilteredOptions(
@@ -185,6 +166,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
maxParticipantsReached,
personalDetails,
translate,
+ didScreenTransitionEnd,
]);
/**
@@ -267,7 +249,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
// the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants
const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat);
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
- const isAllowedToSplit = (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && iouType !== CONST.IOU.TYPE.SEND;
+
+ // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet
+ const isAllowedToSplit =
+ (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && (iouType !== CONST.IOU.TYPE.SEND || iouType !== CONST.IOU.TYPE.TRACK_EXPENSE);
const handleConfirmSelection = useCallback(
(keyEvent, option) => {
@@ -286,13 +271,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
[shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants],
);
- const footerContent = useMemo(
- () => (
-
- {!dismissedReferralBanners[referralContentType] && (
-
-
-
+ const footerContent = useMemo(() => {
+ if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) {
+ return;
+ }
+
+ return (
+ <>
+ {!isDismissed && (
+
)}
{shouldShowSplitBillErrorMessage && (
@@ -313,10 +303,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
isDisabled={shouldShowSplitBillErrorMessage}
/>
)}
-
- ),
- [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
- );
+ >
+ );
+ }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]);
const itemRightSideComponent = useCallback(
(item) => {
@@ -353,23 +342,21 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
);
return (
- 0 ? safeAreaPaddingBottomStyle : {}]}>
-
-
+
);
}
@@ -378,12 +365,6 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.defaultProps = defaultProps
MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector';
export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
- },
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
betas: {
key: ONYXKEYS.BETAS,
},
@@ -392,8 +373,6 @@ export default withOnyx({
MoneyTemporaryForRefactorRequestParticipantsSelector,
(prevProps, nextProps) =>
_.isEqual(prevProps.participants, nextProps.participants) &&
- prevProps.didScreenTransitionEnd === nextProps.didScreenTransitionEnd &&
- _.isEqual(prevProps.dismissedReferralBanners, nextProps.dismissedReferralBanners) &&
prevProps.iouRequestType === nextProps.iouRequestType &&
prevProps.iouType === nextProps.iouType &&
_.isEqual(prevProps.betas, nextProps.betas),
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index d20a576d279e..83f831708799 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -2,7 +2,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -30,6 +29,7 @@ import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Policy, PolicyCategories, PolicyTagList} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {Receipt} from '@src/types/onyx/Transaction';
import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
@@ -97,7 +97,7 @@ function IOURequestStepConfirmation({
transaction?.participants?.map((participant) => {
const participantAccountID = participant.accountID ?? 0;
return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant);
- }),
+ }) ?? [],
[transaction?.participants, personalDetails],
);
const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]);
@@ -228,6 +228,7 @@ function IOURequestStepConfirmation({
policyTags,
policyCategories,
gpsPoints,
+ Object.keys(transaction?.comment?.waypoints ?? {}).length ? TransactionUtils.getValidWaypoints(transaction.comment.waypoints, true) : undefined,
);
},
[report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, transactionTaxCode, transactionTaxAmount, policy, policyTags, policyCategories],
@@ -235,7 +236,7 @@ function IOURequestStepConfirmation({
const createDistanceRequest = useCallback(
(selectedParticipants: Participant[], trimmedComment: string) => {
- if (!report || !transaction) {
+ if (!transaction) {
return;
}
IOU.createDistanceRequest(
@@ -421,27 +422,25 @@ function IOURequestStepConfirmation({
/**
* Checks if user has a GOLD wallet then creates a paid IOU report on the fly
- *
- * @param {String} paymentMethodType
*/
const sendMoney = useCallback(
- (paymentMethodType: ValueOf) => {
+ (paymentMethod: PaymentMethodType | undefined) => {
const currency = transaction?.currency;
const trimmedComment = transaction?.comment?.comment ? transaction.comment.comment.trim() : '';
const participant = participants?.[0];
- if (!participant || !report || !transaction?.amount || !currency) {
+ if (!participant || !transaction?.amount || !currency) {
return;
}
- if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) {
+ if (paymentMethod === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) {
IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant);
return;
}
- if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
+ if (paymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant);
}
},
@@ -489,12 +488,11 @@ function IOURequestStepConfirmation({
/>
{isLoading && }
- {/* @ts-expect-error TODO: Remove this once MoneyRequestConfirmationList (https://github.com/Expensify/App/issues/36130) is migrated to TypeScript. */}
optionsSelectorRef.current && optionsSelectorRef.current.focus()}
shouldShowWrapper
testID={IOURequestStepCurrency.displayName}
+ includeSafeAreaPaddingBottom={false}
>
{({didScreenTransitionEnd}) => (
nonEmptyWaypointsCount >= 2 && _.size(validatedWaypoints) !== nonEmptyWaypointsCount, [nonEmptyWaypointsCount, validatedWaypoints]);
const atLeastTwoDifferentWaypointsError = useMemo(() => _.size(validatedWaypoints) < 2, [validatedWaypoints]);
const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const transactionWasSaved = useRef(false);
const isCreatingNewRequest = !(backTo || isEditing);
useEffect(() => {
@@ -112,6 +114,29 @@ function IOURequestStepDistance({
setShouldShowAtLeastTwoDifferentWaypointsError(false);
}, [atLeastTwoDifferentWaypointsError, duplicateWaypointsError, hasRouteError, isLoading, isLoadingRoute, nonEmptyWaypointsCount, transaction]);
+ // This effect runs when the component is mounted and unmounted. It's purpose is to be able to properly
+ // discard changes if the user cancels out of making any changes. This is accomplished by backing up the
+ // original transaction, letting the user modify the current transaction, and then if the user ever
+ // cancels out of the modal without saving changes, the original transaction is restored from the backup.
+ useEffect(() => {
+ if (isCreatingNewRequest) {
+ return () => {};
+ }
+
+ // On mount, create the backup transaction.
+ TransactionEdit.createBackupTransaction(transaction);
+
+ return () => {
+ // If the user cancels out of the modal without without saving changes, then the original transaction
+ // needs to be restored from the backup so that all changes are removed.
+ if (transactionWasSaved.current) {
+ return;
+ }
+ TransactionEdit.restoreOriginalTransactionFromBackup(lodashGet(transaction, 'transactionID', ''), action === CONST.IOU.ACTION.CREATE);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const navigateBack = () => {
Navigation.goBack(backTo);
};
@@ -191,6 +216,9 @@ function IOURequestStepDistance({
setShouldShowAtLeastTwoDifferentWaypointsError(true);
return;
}
+ if (!isCreatingNewRequest) {
+ transactionWasSaved.current = true;
+ }
if (isEditing) {
// If nothing was changed, simply go to transaction thread
// We compare only addresses because numbers are rounded while backup
@@ -213,6 +241,7 @@ function IOURequestStepDistance({
hasRouteError,
isLoadingRoute,
isLoading,
+ isCreatingNewRequest,
isEditing,
navigateToNextStep,
transactionBackup,
@@ -292,7 +321,10 @@ export default compose(
withFullTransactionOrNotFound,
withOnyx({
transactionBackup: {
- key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${props.transactionID}`,
+ key: ({route}) => {
+ const transactionID = lodashGet(route, 'params.transactionID', 0);
+ return `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`;
+ },
},
}),
)(IOURequestStepDistance);
diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.js b/src/pages/iou/request/step/IOURequestStepMerchant.tsx
similarity index 61%
rename from src/pages/iou/request/step/IOURequestStepMerchant.js
rename to src/pages/iou/request/step/IOURequestStepMerchant.tsx
index 98caea625981..901edfbab562 100644
--- a/src/pages/iou/request/step/IOURequestStepMerchant.js
+++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx
@@ -1,65 +1,46 @@
-import lodashGet from 'lodash/get';
-import lodashIsEmpty from 'lodash/isEmpty';
-import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import categoryPropTypes from '@components/categoryPropTypes';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
-import tagPropTypes from '@components/tagPropTypes';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import TextInput from '@components/TextInput';
-import transactionPropTypes from '@components/transactionPropTypes';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
-import reportPropTypes from '@pages/reportPropTypes';
-import {policyPropTypes} from '@pages/workspace/withPolicy';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm';
-import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
+import type * as OnyxTypes from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import StepScreenWrapper from './StepScreenWrapper';
+import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /** Onyx Props */
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- transaction: transactionPropTypes,
-
+type IOURequestStepMerchantOnyxProps = {
/** The draft transaction that holds data to be persisted on the current transaction */
- splitDraftTransaction: transactionPropTypes,
+ splitDraftTransaction: OnyxEntry;
/** The policy of the report */
- policy: policyPropTypes.policy,
+ policy: OnyxEntry;
/** Collection of categories attached to a policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
+ policyCategories: OnyxEntry;
/** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
-
- /** The report currently being looked at */
- report: reportPropTypes,
+ policyTags: OnyxEntry;
};
-const defaultProps = {
- transaction: {},
- splitDraftTransaction: {},
- policy: null,
- policyTags: null,
- policyCategories: null,
- report: {},
-};
+type IOURequestStepMerchantProps = IOURequestStepMerchantOnyxProps &
+ WithWritableReportOrNotFoundProps &
+ WithFullTransactionOrNotFoundProps;
function IOURequestStepMerchant({
route: {
@@ -71,7 +52,7 @@ function IOURequestStepMerchant({
policyTags,
policyCategories,
report,
-}) {
+}: IOURequestStepMerchantProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -79,22 +60,19 @@ function IOURequestStepMerchant({
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
const isEditingSplitBill = iouType === CONST.IOU.TYPE.SPLIT && isEditing;
- const {merchant} = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction);
+ const merchant = ReportUtils.getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant;
const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
- const isMerchantRequired = ReportUtils.isGroupPolicy(report) || _.some(transaction.participants, (participant) => Boolean(participant.isPolicyExpenseChat));
+
+ const isMerchantRequired = ReportUtils.isGroupPolicy(report) || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat));
const navigateBack = () => {
Navigation.goBack(backTo);
};
- /**
- * @param {Object} value
- * @param {String} value.moneyRequestMerchant
- */
const validate = useCallback(
- (value) => {
- const errors = {};
+ (value: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
- if (isMerchantRequired && _.isEmpty(value.moneyRequestMerchant)) {
+ if (isMerchantRequired && !value.moneyRequestMerchant) {
errors.moneyRequestMerchant = 'common.error.fieldRequired';
}
@@ -103,12 +81,8 @@ function IOURequestStepMerchant({
[isMerchantRequired],
);
- /**
- * @param {Object} value
- * @param {String} value.moneyRequestMerchant
- */
- const updateMerchant = (value) => {
- const newMerchant = value.moneyRequestMerchant.trim();
+ const updateMerchant = (value: FormOnyxValues) => {
+ const newMerchant = value.moneyRequestMerchant?.trim();
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
if (isEditingSplitBill) {
@@ -123,7 +97,7 @@ function IOURequestStepMerchant({
navigateBack();
return;
}
- IOU.setMoneyRequestMerchant(transactionID, newMerchant, !isEditing);
+ IOU.setMoneyRequestMerchant(transactionID, newMerchant ?? '', !isEditing);
if (isEditing) {
// When creating new money requests newMerchant can be blank so we fall back on PARTIAL_TRANSACTION_MERCHANT
IOU.updateMoneyRequestMerchant(transactionID, reportID, newMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, policy, policyTags, policyCategories);
@@ -164,28 +138,26 @@ function IOURequestStepMerchant({
);
}
-IOURequestStepMerchant.propTypes = propTypes;
-IOURequestStepMerchant.defaultProps = defaultProps;
IOURequestStepMerchant.displayName = 'IOURequestStepMerchant';
-export default compose(
- withWritableReportOrNotFound,
- withFullTransactionOrNotFound,
- withOnyx({
- splitDraftTransaction: {
- key: ({route}) => {
- const transactionID = lodashGet(route, 'params.transactionID', 0);
- return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
+export default withWritableReportOrNotFound(
+ withFullTransactionOrNotFound(
+ withOnyx({
+ splitDraftTransaction: {
+ key: ({route}) => {
+ const transactionID = route.params.transactionID ?? 0;
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
+ },
},
- },
- policy: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
- },
- policyCategories: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
- },
- policyTags: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
- },
- }),
-)(IOURequestStepMerchant);
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
+ },
+ policyCategories: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
+ },
+ policyTags: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
+ },
+ })(IOURequestStepMerchant),
+ ),
+);
diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
similarity index 74%
rename from src/pages/iou/request/step/IOURequestStepParticipants.js
rename to src/pages/iou/request/step/IOURequestStepParticipants.tsx
index 7ccbdb18ee03..cebb000b2121 100644
--- a/src/pages/iou/request/step/IOURequestStepParticipants.js
+++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
@@ -1,10 +1,9 @@
import {useNavigation} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
+import lodashIsEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
-import _ from 'underscore';
-import transactionPropTypes from '@components/transactionPropTypes';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import useLocalize from '@hooks/useLocalize';
-import compose from '@libs/compose';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -12,35 +11,39 @@ import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyTemporaryF
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
-import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
+import type SCREENS from '@src/SCREENS';
+import type {Transaction} from '@src/types/onyx';
+import type {Participant} from '@src/types/onyx/IOU';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /* Onyx Props */
+type IOURequestStepParticipantsOnyxProps = {
/** The transaction object being modified in Onyx */
- transaction: transactionPropTypes,
+ transaction: OnyxEntry;
};
-const defaultProps = {
- transaction: {},
-};
+type IOUValueType = ValueOf;
+
+type IOURequestStepParticipantsProps = IOURequestStepParticipantsOnyxProps &
+ WithWritableReportOrNotFoundProps &
+ WithFullTransactionOrNotFoundProps;
+
+type IOURef = IOUValueType | null;
function IOURequestStepParticipants({
route: {
params: {iouType, reportID, transactionID},
},
transaction,
- transaction: {participants = []},
-}) {
+}: IOURequestStepParticipantsProps) {
+ const participants = transaction?.participants;
const {translate} = useLocalize();
const navigation = useNavigation();
- const selectedReportID = useRef(reportID);
- const numberOfParticipants = useRef(participants.length);
+ const selectedReportID = useRef(reportID);
+ const numberOfParticipants = useRef(participants?.length ?? 0);
const iouRequestType = TransactionUtils.getRequestType(transaction);
const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT;
const headerTitle = useMemo(() => {
@@ -53,20 +56,24 @@ function IOURequestStepParticipants({
return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction));
}, [iouType, transaction, translate, isSplitRequest]);
- const receiptFilename = lodashGet(transaction, 'filename');
- const receiptPath = lodashGet(transaction, 'receipt.source');
- const receiptType = lodashGet(transaction, 'receipt.type');
- const newIouType = useRef();
+ const receiptFilename = transaction?.filename;
+ const receiptPath = transaction?.receipt?.source;
+ const receiptType = transaction?.receipt?.type;
+ const newIouType = useRef();
// When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow.
// This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then
// the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process.
useEffect(() => {
- IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, () => {}, iouRequestType, iouType, transactionID, reportID, receiptType);
+ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename ?? '', receiptPath ?? '', () => {}, iouRequestType, iouType, transactionID, reportID, receiptType ?? '');
}, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID]);
const updateRouteParams = useCallback(() => {
- IOU.updateMoneyRequestTypeParams(navigation.getState().routes, newIouType.current);
+ const navigationState = navigation.getState();
+ if (!navigationState || !newIouType.current) {
+ return;
+ }
+ IOU.updateMoneyRequestTypeParams(navigationState.routes, newIouType.current);
}, [navigation]);
useEffect(() => {
@@ -80,7 +87,7 @@ function IOURequestStepParticipants({
}, [participants, updateRouteParams]);
const addParticipant = useCallback(
- (val, selectedIouType) => {
+ (val: Participant[], selectedIouType: IOUValueType) => {
const isSplit = selectedIouType === CONST.IOU.TYPE.SPLIT;
// It's only possible to switch between REQUEST and SPLIT.
// We want to update the IOU type only if it's not updated yet to prevent unnecessary updates.
@@ -94,7 +101,7 @@ function IOURequestStepParticipants({
// If the Onyx participants has the same items as the selected participants (val), Onyx won't update it
// thus this component won't rerender, so we can immediately update the route params.
- if (newIouType.current && _.isEqual(participants, val)) {
+ if (newIouType.current && lodashIsEqual(participants, val)) {
updateRouteParams();
newIouType.current = null;
}
@@ -110,15 +117,15 @@ function IOURequestStepParticipants({
}
// When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step.
- selectedReportID.current = lodashGet(val, '[0].reportID', reportID);
+ selectedReportID.current = val[0]?.reportID ?? reportID;
},
[reportID, transactionID, iouType, participants, updateRouteParams],
);
const goToNextStep = useCallback(
- (selectedIouType) => {
+ (selectedIouType: IOUValueType) => {
const isSplit = selectedIouType === CONST.IOU.TYPE.SPLIT;
- let nextStepIOUType = CONST.IOU.TYPE.REQUEST;
+ let nextStepIOUType: IOUValueType = CONST.IOU.TYPE.REQUEST;
if (isSplit && iouType !== CONST.IOU.TYPE.REQUEST) {
nextStepIOUType = CONST.IOU.TYPE.SPLIT;
@@ -143,7 +150,7 @@ function IOURequestStepParticipants({
onBackButtonPress={navigateBack}
shouldShowWrapper
testID={IOURequestStepParticipants.displayName}
- includeSafeAreaPaddingBottom
+ includeSafeAreaPaddingBottom={false}
>
{({didScreenTransitionEnd}) => (
Promise) | undefined;
+ getCameraPermissionStatus: (() => Promise) | undefined;
+};
+
+export default CameraPermissionModule;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx
similarity index 65%
rename from src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js
rename to src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx
index 64fa291b2003..beeb8938e917 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx
@@ -1,15 +1,11 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import type {ForwardedRef} from 'react';
import {Camera} from 'react-native-vision-camera';
import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';
-
-const propTypes = {
- /* The index of the tab that contains this camera */
- cameraTabIndex: PropTypes.number.isRequired,
-};
+import type {NavigationAwareCameraNativeProps} from './types';
// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
-const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => {
+function NavigationAwareCamera({cameraTabIndex, ...props}: NavigationAwareCameraNativeProps, ref: ForwardedRef) {
const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex});
return (
@@ -21,9 +17,8 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref)
isActive={isCameraActive}
/>
);
-});
+}
-NavigationAwareCamera.propTypes = propTypes;
NavigationAwareCamera.displayName = 'NavigationAwareCamera';
-export default NavigationAwareCamera;
+export default React.forwardRef(NavigationAwareCamera);
diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.tsx
similarity index 60%
rename from src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js
rename to src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.tsx
index 37223915f4a2..2e5f1b2014b3 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.tsx
@@ -1,16 +1,13 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import type {ForwardedRef} from 'react';
import {View} from 'react-native';
+import type {Camera} from 'react-native-vision-camera';
import Webcam from 'react-webcam';
import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';
-
-const propTypes = {
- /** The index of the tab that contains this camera */
- cameraTabIndex: PropTypes.number.isRequired,
-};
+import type {NavigationAwareCameraProps} from './types';
// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused.
-const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => {
+function NavigationAwareCamera({torchOn, onTorchAvailability, cameraTabIndex, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) {
const shouldShowCamera = useTabNavigatorFocus({
tabIndex: cameraTabIndex,
});
@@ -21,17 +18,14 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref)
return (
}
/>
);
-});
+}
-NavigationAwareCamera.propTypes = propTypes;
NavigationAwareCamera.displayName = 'NavigationAwareCamera';
-export default NavigationAwareCamera;
+export default React.forwardRef(NavigationAwareCamera);
diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts
new file mode 100644
index 000000000000..0e6845792122
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts
@@ -0,0 +1,19 @@
+import type {CameraProps} from 'react-native-vision-camera';
+import type {WebcamProps} from 'react-webcam';
+
+type NavigationAwareCameraProps = WebcamProps & {
+ /** Flag to turn on/off the torch/flashlight - if available */
+ torchOn?: boolean;
+
+ /** The index of the tab that contains this camera */
+ onTorchAvailability?: (torchAvailable: boolean) => void;
+
+ /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */
+ cameraTabIndex: number;
+};
+
+type NavigationAwareCameraNativeProps = CameraProps & {
+ cameraTabIndex: number;
+};
+
+export type {NavigationAwareCameraProps, NavigationAwareCameraNativeProps};
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
similarity index 81%
rename from src/pages/iou/request/step/IOURequestStepScan/index.native.js
rename to src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index 83ca90e7330b..e084a3db7422 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -1,15 +1,16 @@
import {useFocusEffect} from '@react-navigation/core';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback, useRef, useState} from 'react';
import {ActivityIndicator, Alert, AppState, InteractionManager, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import {RESULTS} from 'react-native-permissions';
import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withSpring, withTiming} from 'react-native-reanimated';
+import type {Camera, PhotoFile, Point} from 'react-native-vision-camera';
import {useCameraDevice} from 'react-native-vision-camera';
import Hand from '@assets/images/hand.svg';
import Shutter from '@assets/images/shutter.svg';
+import type {FileObject} from '@components/AttachmentModal';
import AttachmentPicker from '@components/AttachmentPicker';
import Button from '@components/Button';
import Icon from '@components/Icon';
@@ -17,49 +18,31 @@ import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
-import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
-import IOURequestStepRoutePropTypes from '@pages/iou/request/step/IOURequestStepRoutePropTypes';
import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper';
import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound';
import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound';
-import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import * as CameraPermission from './CameraPermission';
+import type SCREENS from '@src/SCREENS';
+import type * as OnyxTypes from '@src/types/onyx';
+import CameraPermission from './CameraPermission';
import NavigationAwareCamera from './NavigationAwareCamera';
+import type IOURequestStepOnyxProps from './types';
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /* Onyx Props */
- /** The report that the transaction belongs to */
- report: reportPropTypes,
-
- /** Information about the logged in user's account */
- user: PropTypes.shape({
- /** Whether user muted all sounds in the application */
- isMutedAllSounds: PropTypes.bool,
- }),
-
- /** The transaction (or draft transaction) being changed */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- report: {},
- user: {},
- transaction: {},
-};
+type IOURequestStepScanProps = IOURequestStepOnyxProps &
+ WithWritableReportOrNotFoundProps & {
+ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
+ transaction: OnyxEntry;
+ };
function IOURequestStepScan({
report,
@@ -67,8 +50,8 @@ function IOURequestStepScan({
route: {
params: {action, iouType, reportID, transactionID, backTo},
},
- transaction: {isFromGlobalCreate},
-}) {
+ transaction,
+}: IOURequestStepScanProps) {
const theme = useTheme();
const styles = useThemeStyles();
const device = useCameraDevice('back', {
@@ -76,9 +59,9 @@ function IOURequestStepScan({
});
const hasFlash = device != null && device.hasFlash;
- const camera = useRef(null);
+ const camera = useRef(null);
const [flash, setFlash] = useState(false);
- const [cameraPermissionStatus, setCameraPermissionStatus] = useState(undefined);
+ const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null);
const [didCapturePhoto, setDidCapturePhoto] = useState(false);
const {translate} = useLocalize();
@@ -86,8 +69,8 @@ function IOURequestStepScan({
const askForPermissions = () => {
// There's no way we can check for the BLOCKED status without requesting the permission first
// https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670
- CameraPermission.requestCameraPermission()
- .then((status) => {
+ CameraPermission.requestCameraPermission?.()
+ .then((status: string) => {
setCameraPermissionStatus(status);
if (status === RESULTS.BLOCKED) {
@@ -108,7 +91,7 @@ function IOURequestStepScan({
transform: [{translateX: focusIndicatorPosition.value.x}, {translateY: focusIndicatorPosition.value.y}, {scale: focusIndicatorScale.value}],
}));
- const focusCamera = (point) => {
+ const focusCamera = (point: Point) => {
if (!camera.current) {
return;
}
@@ -122,8 +105,8 @@ function IOURequestStepScan({
};
const tapGesture = Gesture.Tap()
- .enabled(device && device.supportsFocus)
- .onStart((ev) => {
+ .enabled(device?.supportsFocus ?? false)
+ .onStart((ev: {x: number; y: number}) => {
const point = {x: ev.x, y: ev.y};
focusIndicatorOpacity.value = withSequence(withTiming(0.8, {duration: 250}), withDelay(1000, withTiming(0, {duration: 250})));
@@ -138,7 +121,7 @@ function IOURequestStepScan({
useCallback(() => {
setDidCapturePhoto(false);
const refreshCameraPermissionStatus = () => {
- CameraPermission.getCameraPermissionStatus()
+ CameraPermission?.getCameraPermissionStatus?.()
.then(setCameraPermissionStatus)
.catch(() => setCameraPermissionStatus(RESULTS.UNAVAILABLE));
};
@@ -163,19 +146,21 @@ function IOURequestStepScan({
}, []),
);
- const validateReceipt = (file) => {
- const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
- if (!CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase())) {
+ const validateReceipt = (file: FileObject) => {
+ const {fileExtension} = FileUtils.splitExtensionFromFileName(file?.name ?? '');
+ if (
+ !CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase() as (typeof CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS)[number])
+ ) {
Alert.alert(translate('attachmentPicker.wrongFileType'), translate('attachmentPicker.notAllowedExtension'));
return false;
}
- if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+ if ((file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
Alert.alert(translate('attachmentPicker.attachmentTooLarge'), translate('attachmentPicker.sizeExceeded'));
return false;
}
- if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+ if ((file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
Alert.alert(translate('attachmentPicker.attachmentTooSmall'), translate('attachmentPicker.sizeNotMet'));
return false;
}
@@ -193,7 +178,7 @@ function IOURequestStepScan({
}
// If the transaction was created from the global create, the person needs to select participants, so take them there.
- if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) {
+ if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) {
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID));
return;
}
@@ -201,22 +186,22 @@ function IOURequestStepScan({
// If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically
// be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step.
IOU.setMoneyRequestParticipantsFromReport(transactionID, report);
+
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
- }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]);
+ }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]);
const updateScanAndNavigate = useCallback(
- (file, source) => {
+ (file: FileObject, source: string) => {
Navigation.dismissModal();
- IOU.replaceReceipt(transactionID, file, source);
+ IOU.replaceReceipt(transactionID, file as File, source);
},
[transactionID],
);
/**
* Sets the Receipt objects and navigates the user to the next page
- * @param {Object} file
*/
- const setReceiptAndNavigate = (file) => {
+ const setReceiptAndNavigate = (file: FileObject) => {
if (!validateReceipt(file)) {
return;
}
@@ -224,10 +209,10 @@ function IOURequestStepScan({
// Store the receipt on the transaction object in Onyx
// On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file.
// So, let us also save the file type in receipt for later use during blob fetch
- IOU.setMoneyRequestReceipt(transactionID, file.uri, file.name, action !== CONST.IOU.ACTION.EDIT, file.type);
+ IOU.setMoneyRequestReceipt(transactionID, file?.uri ?? '', file.name ?? '', action !== CONST.IOU.ACTION.EDIT, file.type);
if (action === CONST.IOU.ACTION.EDIT) {
- updateScanAndNavigate(file, file.uri);
+ updateScanAndNavigate(file, file?.uri ?? '');
return;
}
@@ -246,19 +231,18 @@ function IOURequestStepScan({
if (!camera.current) {
showCameraAlert();
- return;
}
if (didCapturePhoto) {
return;
}
- return camera.current
- .takePhoto({
+ camera?.current
+ ?.takePhoto({
flash: flash && hasFlash ? 'on' : 'off',
- enableShutterSound: !user.isMutedAllSounds,
+ enableShutterSound: !user?.isMutedAllSounds,
})
- .then((photo) => {
+ .then((photo: PhotoFile) => {
// Store the receipt on the transaction object in Onyx
const source = `file://${photo.path}`;
IOU.setMoneyRequestReceipt(transactionID, source, photo.path, action !== CONST.IOU.ACTION.EDIT);
@@ -273,12 +257,12 @@ function IOURequestStepScan({
setDidCapturePhoto(true);
navigateToConfirmationStep();
})
- .catch((error) => {
+ .catch((error: string) => {
setDidCapturePhoto(false);
showCameraAlert();
Log.warn('Error taking photo', error);
});
- }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user.isMutedAllSounds, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]);
+ }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user?.isMutedAllSounds, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]);
// Wait for camera permission status to render
if (cameraPermissionStatus == null) {
@@ -331,6 +315,7 @@ function IOURequestStepScan({
)}
-
+
{({openPicker}) => (
({
+ user: {
+ key: ONYXKEYS.USER,
+ },
+})(IOURequestStepScan);
+
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScanOnyxProps);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepScanWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepScanWithWritableReportOrNotFound);
+
+export default IOURequestStepScanWithFullTransactionOrNotFound;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
similarity index 76%
rename from src/pages/iou/request/step/IOURequestStepScan/index.js
rename to src/pages/iou/request/step/IOURequestStepScan/index.tsx
index 056f68385dc4..b9c4f866d493 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -1,10 +1,11 @@
-import lodashGet from 'lodash/get';
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react';
import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
+import type Webcam from 'react-webcam';
import Hand from '@assets/images/hand.svg';
import ReceiptUpload from '@assets/images/receipt-upload.svg';
import Shutter from '@assets/images/shutter.svg';
+import type {FileObject} from '@components/AttachmentModal';
import AttachmentPicker from '@components/AttachmentPicker';
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
@@ -14,74 +15,65 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
-import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Browser from '@libs/Browser';
-import compose from '@libs/compose';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import Navigation from '@libs/Navigation/Navigation';
import ReceiptDropUI from '@pages/iou/ReceiptDropUI';
-import IOURequestStepRoutePropTypes from '@pages/iou/request/step/IOURequestStepRoutePropTypes';
import StepScreenDragAndDropWrapper from '@pages/iou/request/step/StepScreenDragAndDropWrapper';
import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound';
import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound';
-import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type * as OnyxTypes from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import NavigationAwareCamera from './NavigationAwareCamera';
+import type IOURequestStepOnyxProps from './types';
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /* Onyx Props */
- /** The report that the transaction belongs to */
- report: reportPropTypes,
-
- /** The transaction (or draft transaction) being changed */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- report: {},
- transaction: {},
-};
+type IOURequestStepScanProps = IOURequestStepOnyxProps &
+ WithWritableReportOrNotFoundProps & {
+ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
+ transaction: OnyxEntry;
+ };
function IOURequestStepScan({
report,
route: {
params: {action, iouType, reportID, transactionID, backTo},
},
- transaction: {isFromGlobalCreate},
-}) {
+ transaction,
+}: IOURequestStepScanProps) {
const theme = useTheme();
const styles = useThemeStyles();
// Grouping related states
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
- const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState('');
- const [attachmentInvalidReason, setAttachmentValidReason] = useState('');
+ const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState();
+ const [attachmentInvalidReason, setAttachmentValidReason] = useState();
const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0);
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const {isDraggingOver} = useContext(DragAndDropContext);
- const [cameraPermissionState, setCameraPermissionState] = useState('prompt');
+ const [cameraPermissionState, setCameraPermissionState] = useState('prompt');
const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false);
const [isTorchAvailable, setIsTorchAvailable] = useState(false);
- const cameraRef = useRef(null);
- const trackRef = useRef(null);
+ const cameraRef = useRef(null);
+ const trackRef = useRef(null);
const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false);
- const getScreenshotTimeoutRef = useRef(null);
+ const getScreenshotTimeoutRef = useRef(null);
- const [videoConstraints, setVideoConstraints] = useState(null);
+ const [videoConstraints, setVideoConstraints] = useState();
const tabIndex = 1;
const isTabActive = useTabNavigatorFocus({tabIndex});
@@ -90,23 +82,28 @@ function IOURequestStepScan({
* The last deviceId is of regular len camera.
*/
const requestCameraPermission = useCallback(() => {
- if (!_.isEmpty(videoConstraints) || !Browser.isMobile()) {
+ if (!isEmptyObject(videoConstraints) || !Browser.isMobile()) {
return;
}
const defaultConstraints = {facingMode: {exact: 'environment'}};
navigator.mediaDevices
+ // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010
.getUserMedia({video: {facingMode: {exact: 'environment'}, zoom: {ideal: 1}}})
.then((stream) => {
setCameraPermissionState('granted');
- _.forEach(stream.getTracks(), (track) => track.stop());
+ stream.getTracks().forEach((track) => track.stop());
// Only Safari 17+ supports zoom constraint
if (Browser.isMobileSafari() && stream.getTracks().length > 0) {
- const deviceId = _.chain(stream.getTracks())
- .map((track) => track.getSettings())
- .find((setting) => setting.zoom === 1)
- .get('deviceId')
- .value();
+ let deviceId;
+ for (const track of stream.getTracks()) {
+ const setting = track.getSettings();
+ // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010
+ if (setting.zoom === 1) {
+ deviceId = setting.deviceId;
+ break;
+ }
+ }
if (deviceId) {
setVideoConstraints({deviceId});
return;
@@ -117,12 +114,14 @@ function IOURequestStepScan({
return;
}
navigator.mediaDevices.enumerateDevices().then((devices) => {
- const lastBackDeviceId = _.chain(devices)
- .filter((item) => item.kind === 'videoinput')
- .last()
- .get('deviceId', '')
- .value();
-
+ let lastBackDeviceId = '';
+ for (let i = devices.length - 1; i >= 0; i--) {
+ const device = devices[i];
+ if (device.kind === 'videoinput') {
+ lastBackDeviceId = device.deviceId;
+ break;
+ }
+ }
if (!lastBackDeviceId) {
setVideoConstraints(defaultConstraints);
return;
@@ -142,7 +141,10 @@ function IOURequestStepScan({
return;
}
navigator.permissions
- .query({name: 'camera'})
+ .query({
+ // @ts-expect-error camera does exist in PermissionName
+ name: 'camera',
+ })
.then((permissionState) => {
setCameraPermissionState(permissionState.state);
if (permissionState.state === 'granted') {
@@ -165,29 +167,28 @@ function IOURequestStepScan({
/**
* Sets the upload receipt error modal content when an invalid receipt is uploaded
- * @param {*} isInvalid
- * @param {*} title
- * @param {*} reason
*/
- const setUploadReceiptError = (isInvalid, title, reason) => {
+ const setUploadReceiptError = (isInvalid: boolean, title: TranslationPaths, reason: TranslationPaths) => {
setIsAttachmentInvalid(isInvalid);
setAttachmentInvalidReasonTitle(title);
setAttachmentValidReason(reason);
};
- function validateReceipt(file) {
- const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
- if (!CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase())) {
+ function validateReceipt(file: FileObject) {
+ const {fileExtension} = FileUtils.splitExtensionFromFileName(file?.name ?? '');
+ if (
+ !CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase() as (typeof CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS)[number])
+ ) {
setUploadReceiptError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension');
return false;
}
- if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+ if ((file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooLarge', 'attachmentPicker.sizeExceeded');
return false;
}
- if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+ if ((file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooSmall', 'attachmentPicker.sizeNotMet');
return false;
}
@@ -206,7 +207,7 @@ function IOURequestStepScan({
}
// If the transaction was created from the global create, the person needs to select participants, so take them there.
- if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) {
+ if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) {
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID));
return;
}
@@ -215,11 +216,11 @@ function IOURequestStepScan({
// be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step.
IOU.setMoneyRequestParticipantsFromReport(transactionID, report);
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
- }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]);
+ }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]);
const updateScanAndNavigate = useCallback(
- (file, source) => {
- IOU.replaceReceipt(transactionID, file, source);
+ (file: FileObject, source: string) => {
+ IOU.replaceReceipt(transactionID, file as File, source);
Navigation.dismissModal();
},
[transactionID],
@@ -227,16 +228,16 @@ function IOURequestStepScan({
/**
* Sets the Receipt objects and navigates the user to the next page
- * @param {Object} file
*/
- const setReceiptAndNavigate = (file) => {
+ const setReceiptAndNavigate = (file: FileObject) => {
if (!validateReceipt(file)) {
return;
}
// Store the receipt on the transaction object in Onyx
- const source = URL.createObjectURL(file);
- IOU.setMoneyRequestReceipt(transactionID, source, file.name, action !== CONST.IOU.ACTION.EDIT);
+ const source = URL.createObjectURL(file as Blob);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ IOU.setMoneyRequestReceipt(transactionID, source, file.name || '', action !== CONST.IOU.ACTION.EDIT);
if (action === CONST.IOU.ACTION.EDIT) {
updateScanAndNavigate(file, source);
@@ -246,15 +247,16 @@ function IOURequestStepScan({
navigateToConfirmationStep();
};
- const setupCameraPermissionsAndCapabilities = (stream) => {
+ const setupCameraPermissionsAndCapabilities = (stream: MediaStream) => {
setCameraPermissionState('granted');
const [track] = stream.getVideoTracks();
const capabilities = track.getCapabilities();
- if (capabilities.torch) {
+
+ if ('torch' in capabilities && capabilities.torch) {
trackRef.current = track;
}
- setIsTorchAvailable(!!capabilities.torch);
+ setIsTorchAvailable('torch' in capabilities && !!capabilities.torch);
};
const getScreenshot = useCallback(() => {
@@ -266,7 +268,7 @@ function IOURequestStepScan({
const imageBase64 = cameraRef.current.getScreenshot();
const filename = `receipt_${Date.now()}.png`;
- const file = FileUtils.base64ToFile(imageBase64, filename);
+ const file = FileUtils.base64ToFile(imageBase64 ?? '', filename);
const source = URL.createObjectURL(file);
IOU.setMoneyRequestReceipt(transactionID, source, file.name, action !== CONST.IOU.ACTION.EDIT);
@@ -283,6 +285,7 @@ function IOURequestStepScan({
return;
}
trackRef.current.applyConstraints({
+ // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010
advanced: [{torch: false}],
});
}, []);
@@ -291,6 +294,7 @@ function IOURequestStepScan({
if (trackRef.current && isFlashLightOn) {
trackRef.current
.applyConstraints({
+ // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010
advanced: [{torch: true}],
})
.then(() => {
@@ -324,7 +328,7 @@ function IOURequestStepScan({
const mobileCameraView = () => (
<>
- {((cameraPermissionState === 'prompt' && !isQueriedPermissionState) || (cameraPermissionState === 'granted' && _.isEmpty(videoConstraints))) && (
+ {((cameraPermissionState === 'prompt' && !isQueriedPermissionState) || (cameraPermissionState === 'granted' && isEmptyObject(videoConstraints))) && (
{translate('receipt.takePhoto')}
{translate('receipt.cameraAccess')}
@@ -351,7 +355,7 @@ function IOURequestStepScan({
/>
)}
- {cameraPermissionState === 'granted' && !_.isEmpty(videoConstraints) && (
+ {cameraPermissionState === 'granted' && !isEmptyObject(videoConstraints) && (
setCameraPermissionState('denied')}
@@ -361,6 +365,11 @@ function IOURequestStepScan({
videoConstraints={videoConstraints}
forceScreenshotSourceSize
cameraTabIndex={tabIndex}
+ audio={false}
+ disablePictureInPicture={false}
+ imageSmoothing={false}
+ mirrored={false}
+ screenshotQuality={0}
/>
)}
@@ -417,7 +426,7 @@ function IOURequestStepScan({
const desktopUploadView = () => (
<>
- setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.top))}>
+ setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.y))}>
{
- const file = lodashGet(e, ['dataTransfer', 'files', 0]);
- setReceiptAndNavigate(file);
+ const file = e?.dataTransfer?.files[0];
+ if (file) {
+ setReceiptAndNavigate(file);
+ }
}}
receiptImageTopPosition={receiptImageTopPosition}
/>
@@ -489,8 +500,11 @@ function IOURequestStepScan({
);
}
-IOURequestStepScan.defaultProps = defaultProps;
-IOURequestStepScan.propTypes = propTypes;
IOURequestStepScan.displayName = 'IOURequestStepScan';
-export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepScan);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScan);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepScanWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepScanWithWritableReportOrNotFound);
+
+export default IOURequestStepScanWithFullTransactionOrNotFound;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/types.ts b/src/pages/iou/request/step/IOURequestStepScan/types.ts
new file mode 100644
index 000000000000..adf3e5c81748
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepScan/types.ts
@@ -0,0 +1,8 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import type * as OnyxTypes from '@src/types/onyx';
+
+type IOURequestStepOnyxProps = {
+ user: OnyxEntry;
+};
+
+export default IOURequestStepOnyxProps;
diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js
deleted file mode 100644
index 3693e1cf9449..000000000000
--- a/src/pages/iou/request/step/IOURequestStepTag.js
+++ /dev/null
@@ -1,199 +0,0 @@
-import lodashGet from 'lodash/get';
-import lodashIsEmpty from 'lodash/isEmpty';
-import PropTypes from 'prop-types';
-import React, {useMemo} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import categoryPropTypes from '@components/categoryPropTypes';
-import TagPicker from '@components/TagPicker';
-import tagPropTypes from '@components/tagPropTypes';
-import Text from '@components/Text';
-import transactionPropTypes from '@components/transactionPropTypes';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import * as IOUUtils from '@libs/IOUUtils';
-import Navigation from '@libs/Navigation/Navigation';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import {canEditMoneyRequest} from '@libs/ReportUtils';
-import * as TransactionUtils from '@libs/TransactionUtils';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
-import reportPropTypes from '@pages/reportPropTypes';
-import {policyPropTypes} from '@pages/workspace/withPolicy';
-import * as IOU from '@userActions/IOU';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
-import StepScreenWrapper from './StepScreenWrapper';
-import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
-import withWritableReportOrNotFound from './withWritableReportOrNotFound';
-
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /* Onyx props */
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- transaction: transactionPropTypes,
-
- /** The draft transaction that holds data to be persisted on the current transaction */
- splitDraftTransaction: transactionPropTypes,
-
- /** The report currently being used */
- report: reportPropTypes,
-
- /** The policy of the report */
- policy: policyPropTypes.policy,
-
- /** The category configuration of the report's policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
-
- /** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
-
- /** The actions from the parent report */
- reportActions: PropTypes.shape(reportActionPropTypes),
-
- /** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user accountID */
- accountID: PropTypes.number,
-
- /** Currently logged in user email */
- email: PropTypes.string,
- }).isRequired,
-};
-
-const defaultProps = {
- report: {},
- policy: null,
- policyTags: null,
- policyCategories: null,
- transaction: {},
- splitDraftTransaction: {},
- reportActions: {},
-};
-
-function IOURequestStepTag({
- policy,
- policyCategories,
- policyTags,
- report,
- route: {
- params: {action, tagIndex: rawTagIndex, transactionID, backTo, iouType, reportActionID},
- },
- transaction,
- splitDraftTransaction,
- reportActions,
- session,
-}) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- const tagListIndex = Number(rawTagIndex);
- const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex);
-
- const isEditing = action === CONST.IOU.ACTION.EDIT;
- const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
- const isEditingSplitBill = isEditing && isSplitBill;
- const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction;
- const transactionTag = TransactionUtils.getTag(currentTransaction);
- const tag = TransactionUtils.getTag(currentTransaction, tagListIndex);
- const reportAction = reportActions[report.parentReportActionID || reportActionID];
- const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction);
- const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
-
- const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
-
- // eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundPage = !shouldShowTag || (isEditing && (isSplitBill ? !canEditSplitBill : !canEditMoneyRequest(reportAction)));
-
- const navigateBack = () => {
- Navigation.goBack(backTo);
- };
-
- /**
- * @param {Object} selectedTag
- * @param {String} selectedTag.searchText
- */
- const updateTag = (selectedTag) => {
- const isSelectedTag = selectedTag.searchText === tag;
- const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagListIndex);
- if (isEditingSplitBill) {
- IOU.setDraftSplitTransaction(transactionID, {tag: updatedTag});
- navigateBack();
- return;
- }
- if (isEditing) {
- IOU.updateMoneyRequestTag(transactionID, report.reportID, updatedTag, policy, policyTags, policyCategories);
- Navigation.dismissModal();
- return;
- }
- IOU.setMoneyRequestTag(transactionID, updatedTag);
- navigateBack();
- };
-
- return (
-
- {translate('iou.tagSelection')}
-
-
- );
-}
-
-IOURequestStepTag.displayName = 'IOURequestStepTag';
-IOURequestStepTag.propTypes = propTypes;
-IOURequestStepTag.defaultProps = defaultProps;
-
-export default compose(
- withWritableReportOrNotFound,
- withFullTransactionOrNotFound,
- withOnyx({
- splitDraftTransaction: {
- key: ({route}) => {
- const transactionID = lodashGet(route, 'params.transactionID', 0);
- return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
- },
- },
- policy: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
- },
- policyCategories: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
- },
- policyTags: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
- },
- reportActions: {
- key: ({
- report,
- route: {
- params: {action, iouType},
- },
- }) => {
- let reportID = '0';
- if (action === CONST.IOU.ACTION.EDIT) {
- reportID = iouType === CONST.IOU.TYPE.SPLIT ? report.reportID : report.parentReportID;
- }
- return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`;
- },
- canEvict: false,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- }),
-)(IOURequestStepTag);
diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx
new file mode 100644
index 000000000000..a62720cbd13a
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepTag.tsx
@@ -0,0 +1,169 @@
+import React, {useMemo} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import TagPicker from '@components/TagPicker';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as IOUUtils from '@libs/IOUUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import {canEditMoneyRequest} from '@libs/ReportUtils';
+import * as TransactionUtils from '@libs/TransactionUtils';
+import * as IOU from '@userActions/IOU';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type * as OnyxTypes from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import StepScreenWrapper from './StepScreenWrapper';
+import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound';
+import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
+import withWritableReportOrNotFound from './withWritableReportOrNotFound';
+
+type IOURequestStepTagOnyxProps = {
+ /** The draft transaction that holds data to be persisted on the current transaction */
+ splitDraftTransaction: OnyxEntry;
+
+ /** The policy of the report */
+ policy: OnyxEntry;
+
+ /** The category configuration of the report's policy */
+ policyCategories: OnyxEntry;
+
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+
+ /** The actions from the parent report */
+ reportActions: OnyxEntry;
+
+ /** Session info for the currently logged in user. */
+ session: OnyxEntry;
+};
+
+type IOURequestStepTagProps = IOURequestStepTagOnyxProps &
+ WithWritableReportOrNotFoundProps &
+ WithFullTransactionOrNotFoundProps;
+
+function IOURequestStepTag({
+ policy,
+ policyCategories,
+ policyTags,
+ report,
+ route: {
+ params: {action, orderWeight: rawTagIndex, transactionID, backTo, iouType, reportActionID},
+ },
+ transaction,
+ splitDraftTransaction,
+ reportActions,
+ session,
+}: IOURequestStepTagProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const tagListIndex = Number(rawTagIndex);
+ const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex);
+
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
+ const isEditingSplitBill = isEditing && isSplitBill;
+ const currentTransaction = isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction;
+ const transactionTag = TransactionUtils.getTag(currentTransaction);
+ const tag = TransactionUtils.getTag(currentTransaction, tagListIndex);
+ const reportAction = reportActions?.[report?.parentReportActionID ?? reportActionID];
+ const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction);
+ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
+
+ const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
+
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ const shouldShowNotFoundPage = !shouldShowTag || (isEditing && (isSplitBill ? !canEditSplitBill : reportAction && !canEditMoneyRequest(reportAction)));
+
+ const navigateBack = () => {
+ Navigation.goBack(backTo);
+ };
+
+ const updateTag = (selectedTag: Partial) => {
+ const isSelectedTag = selectedTag.searchText === tag;
+ const searchText = selectedTag.searchText ?? '';
+ const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : searchText, tagListIndex);
+ if (isEditingSplitBill) {
+ IOU.setDraftSplitTransaction(transactionID, {tag: updatedTag});
+ navigateBack();
+ return;
+ }
+ if (isEditing) {
+ IOU.updateMoneyRequestTag(transactionID, report?.reportID ?? '0', updatedTag, policy, policyTags, policyCategories);
+ Navigation.dismissModal();
+ return;
+ }
+ IOU.setMoneyRequestTag(transactionID, updatedTag);
+ navigateBack();
+ };
+
+ return (
+
+ <>
+ {translate('iou.tagSelection')}
+
+ >
+
+ );
+}
+
+IOURequestStepTag.displayName = 'IOURequestStepTag';
+
+export default withWritableReportOrNotFound(
+ withFullTransactionOrNotFound(
+ withOnyx({
+ splitDraftTransaction: {
+ key: ({route}) => {
+ const transactionID = route.params.transactionID ?? 0;
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
+ },
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
+ },
+ policyCategories: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
+ },
+ policyTags: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
+ },
+ reportActions: {
+ key: ({
+ report,
+ route: {
+ params: {action, iouType},
+ },
+ }) => {
+ let reportID: string | undefined = '0';
+ if (action === CONST.IOU.ACTION.EDIT) {
+ reportID = iouType === CONST.IOU.TYPE.SPLIT ? report?.reportID : report?.parentReportID;
+ }
+ return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`;
+ },
+ canEvict: false,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ })(IOURequestStepTag),
+ ),
+);
diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx
index a88de35a5025..f7bef7671804 100644
--- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx
+++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx
@@ -41,8 +41,8 @@ function getTaxAmount(transaction: OnyxEntry, taxRates: TaxRatesWit
const transactionTaxAmount = TransactionUtils.getAmount(transaction);
const transactionTaxCode = transaction?.taxCode ?? '';
const defaultTaxValue = taxRates?.defaultValue;
- const editingTaxPercentage = (transactionTaxCode ? taxRates?.taxes[transactionTaxCode]?.value : taxRates?.defaultValue) ?? '';
const moneyRequestTaxPercentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? '';
+ const editingTaxPercentage = (transactionTaxCode ? taxRates?.taxes[transactionTaxCode]?.value : moneyRequestTaxPercentage) ?? '';
const taxPercentage = isEditing ? editingTaxPercentage : moneyRequestTaxPercentage;
return CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, transactionTaxAmount));
}
diff --git a/src/pages/iou/request/step/StepScreenWrapper.tsx b/src/pages/iou/request/step/StepScreenWrapper.tsx
index e64f2792d2e4..077711b3a919 100644
--- a/src/pages/iou/request/step/StepScreenWrapper.tsx
+++ b/src/pages/iou/request/step/StepScreenWrapper.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import type {PropsWithChildren} from 'react';
+import type {ReactNode} from 'react';
import {View} from 'react-native';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {ScreenWrapperChildrenProps} from '@components/ScreenWrapper';
import ScreenWrapper from '@components/ScreenWrapper';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -29,6 +30,9 @@ type StepScreenWrapperProps = {
/** Whether or not to include safe area padding */
includeSafeAreaPaddingBottom?: boolean;
+
+ /** Returns a function as a child to pass insets to or a node to render without insets */
+ children: ReactNode | React.FC;
};
function StepScreenWrapper({
@@ -40,11 +44,11 @@ function StepScreenWrapper({
shouldShowWrapper,
shouldShowNotFoundPage,
includeSafeAreaPaddingBottom,
-}: PropsWithChildren) {
+}: StepScreenWrapperProps) {
const styles = useThemeStyles();
if (!shouldShowWrapper) {
- return {children};
+ return {children as ReactNode};
}
return (
diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
index 9fee88b45d0c..e3aa1ed2431d 100644
--- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
+++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
@@ -21,9 +21,13 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT
| typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION
| typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT
+ | typeof SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS
+ | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT
+ | typeof SCREENS.MONEY_REQUEST.STEP_TAG
| typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION
| typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY
- | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE;
+ | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE
+ | typeof SCREENS.MONEY_REQUEST.STEP_SCAN;
type Route = RouteProp;
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
index 515f6f97f280..4cad980eb680 100644
--- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
+++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
@@ -23,7 +23,11 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY
| typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION
| typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE
- | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT;
+ | typeof SCREENS.MONEY_REQUEST.STEP_TAG
+ | typeof SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS
+ | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT
+ | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT
+ | typeof SCREENS.MONEY_REQUEST.STEP_SCAN;
type Route = RouteProp;
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index b5d11faac6c4..88d03727d6ca 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -3,14 +3,12 @@ import lodashGet from 'lodash/get';
import lodashSize from 'lodash/size';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
-import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import transactionPropTypes from '@components/transactionPropTypes';
import useInitialValue from '@hooks/useInitialValue';
import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
@@ -53,7 +51,6 @@ const defaultProps = {
};
function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) {
- const styles = useThemeStyles();
const {translate} = useLocalize();
const prevMoneyRequestId = useRef(iou.id);
const iouType = useInitialValue(() => lodashGet(route, 'params.iouType', ''));
@@ -128,8 +125,8 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) {
shouldEnableMaxHeight={DeviceCapabilities.canUseTouchScreen()}
testID={MoneyRequestParticipantsPage.displayName}
>
- {({safeAreaPaddingBottomStyle}) => (
-
+ {({didScreenTransitionEnd}) => (
+ <>
navigateToConfirmationStep(iouType)}
navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.TYPE.SPLIT)}
- safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
iouType={iouType}
isDistanceRequest={isDistanceRequest}
isScanRequest={isScanRequest}
+ didScreenTransitionEnd={didScreenTransitionEnd}
/>
-
+ >
)}
);
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 05ef5baa8432..dbde94b60e96 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -13,6 +13,8 @@ import ReferralProgramCTA from '@components/ReferralProgramCTA';
import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
+import useDebouncedState from '@hooks/useDebouncedState';
+import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePermissions from '@hooks/usePermissions';
@@ -36,9 +38,6 @@ const propTypes = {
/** Callback to add participants in MoneyRequestModal */
onAddParticipants: PropTypes.func.isRequired,
- /** An object that holds data about which referral banners have been dismissed */
- dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool),
-
/** Selected participants from MoneyRequestModal with login */
participants: PropTypes.arrayOf(
PropTypes.shape({
@@ -50,43 +49,32 @@ const propTypes = {
}),
),
- /** padding bottom style of safe area */
- safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
/** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string.isRequired,
/** Whether the money request is a distance request or not */
isDistanceRequest: PropTypes.bool,
+
+ /** Whether the screen transition has ended */
+ didScreenTransitionEnd: PropTypes.bool,
};
const defaultProps = {
- dismissedReferralBanners: {},
participants: [],
- safeAreaPaddingBottomStyle: {},
betas: [],
isDistanceRequest: false,
+ didScreenTransitionEnd: false,
};
-function MoneyRequestParticipantsSelector({
- betas,
- dismissedReferralBanners,
- participants,
- navigateToRequest,
- navigateToSplit,
- onAddParticipants,
- safeAreaPaddingBottomStyle,
- iouType,
- isDistanceRequest,
-}) {
+function MoneyRequestParticipantsSelector({betas, participants, navigateToRequest, navigateToSplit, onAddParticipants, iouType, isDistanceRequest, didScreenTransitionEnd}) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const [searchTerm, setSearchTerm] = useState('');
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
- const {canUseP2PDistanceRequests} = usePermissions();
- const {options, areOptionsInitialized} = useOptionsList();
+ const {options, areOptionsInitialized} = useOptionsList({shouldInitialize: didScreenTransitionEnd});
+ const {canUseP2PDistanceRequests} = usePermissions(iouType);
const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
@@ -98,7 +86,7 @@ function MoneyRequestParticipantsSelector({
options.reports,
options.personalDetails,
betas,
- searchTerm,
+ debouncedSearchTerm,
participants,
CONST.EXPENSIFY_EMAILS,
@@ -123,7 +111,7 @@ function MoneyRequestParticipantsSelector({
personalDetails: chatOptions.personalDetails,
userToInvite: chatOptions.userToInvite,
};
- }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]);
+ }, [options.reports, options.personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]);
/**
* Returns the sections needed for the OptionsSelector
@@ -134,7 +122,7 @@ function MoneyRequestParticipantsSelector({
const newSections = [];
const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(
- searchTerm,
+ debouncedSearchTerm,
participants,
newChatOptions.recentReports,
newChatOptions.personalDetails,
@@ -172,7 +160,7 @@ function MoneyRequestParticipantsSelector({
}
return newSections;
- }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, searchTerm, translate]);
+ }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, debouncedSearchTerm, translate]);
/**
* Adds a single participant to the request
@@ -247,11 +235,11 @@ function MoneyRequestParticipantsSelector({
OptionsListUtils.getHeaderMessage(
_.get(newChatOptions, 'personalDetails', []).length + _.get(newChatOptions, 'recentReports', []).length !== 0,
Boolean(newChatOptions.userToInvite),
- searchTerm.trim(),
+ debouncedSearchTerm.trim(),
maxParticipantsReached,
- _.some(participants, (participant) => participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase())),
+ _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())),
),
- [maxParticipantsReached, newChatOptions, participants, searchTerm],
+ [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm],
);
// Right now you can't split a request with a workspace and other additional participants
@@ -259,7 +247,9 @@ function MoneyRequestParticipantsSelector({
// the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants
const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat);
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
- const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND;
+
+ // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet
+ const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && (iouType !== CONST.IOU.TYPE.SEND || iouType !== CONST.IOU.TYPE.TRACK_EXPENSE);
const handleConfirmSelection = useCallback(
(keyEvent, option) => {
@@ -279,13 +269,19 @@ function MoneyRequestParticipantsSelector({
[shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length],
);
- const footerContent = useMemo(
- () => (
+ const {isDismissed} = useDismissedReferralBanners({referralContentType});
+
+ const footerContent = useMemo(() => {
+ if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) {
+ return null;
+ }
+ return (
- {!dismissedReferralBanners[referralContentType] && (
-
-
-
+ {!isDismissed && (
+
)}
{shouldShowSplitBillErrorMessage && (
@@ -307,9 +303,8 @@ function MoneyRequestParticipantsSelector({
/>
)}
- ),
- [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
- );
+ );
+ }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]);
const itemRightSideComponent = useCallback(
(item) => {
@@ -343,23 +338,21 @@ function MoneyRequestParticipantsSelector({
);
return (
- 0 ? safeAreaPaddingBottomStyle : {}]}>
-
-
+
);
}
@@ -368,9 +361,6 @@ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector
MoneyRequestParticipantsSelector.defaultProps = defaultProps;
export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
- },
betas: {
key: ONYXKEYS.BETAS,
},
diff --git a/src/pages/settings/Preferences/PreferencesPage.tsx b/src/pages/settings/Preferences/PreferencesPage.tsx
index 5849f323dc36..3d957af373ef 100755
--- a/src/pages/settings/Preferences/PreferencesPage.tsx
+++ b/src/pages/settings/Preferences/PreferencesPage.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import LottieAnimations from '@components/LottieAnimations';
@@ -20,22 +19,12 @@ import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {PreferredTheme, PriorityMode, User as UserType} from '@src/types/onyx';
-type PreferencesPageOnyxProps = {
- /** The chat priority mode */
- priorityMode: PriorityMode;
+function PreferencesPage() {
+ const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE);
+ const [user] = useOnyx(ONYXKEYS.USER);
+ const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
- /** The app's color theme */
- preferredTheme: PreferredTheme;
-
- /** The details about the user that is signed in */
- user: OnyxEntry;
-};
-
-type PreferencesPageProps = PreferencesPageOnyxProps;
-
-function PreferencesPage({priorityMode, preferredTheme, user}: PreferencesPageProps) {
const styles = useThemeStyles();
const {translate, preferredLocale} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -125,14 +114,4 @@ function PreferencesPage({priorityMode, preferredTheme, user}: PreferencesPagePr
PreferencesPage.displayName = 'PreferencesPage';
-export default withOnyx({
- priorityMode: {
- key: ONYXKEYS.NVP_PRIORITY_MODE,
- },
- user: {
- key: ONYXKEYS.USER,
- },
- preferredTheme: {
- key: ONYXKEYS.PREFERRED_THEME,
- },
-})(PreferencesPage);
+export default PreferencesPage;
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
index 2ba4fc33580b..8aaeb7151563 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -2,8 +2,8 @@ import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, Keyboard, View} from 'react-native';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ConfirmModal from '@components/ConfirmModal';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
@@ -29,34 +29,30 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {LoginList, Policy, SecurityGroup, Session as TSession} from '@src/types/onyx';
+import type {Policy} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import ValidateCodeForm from './ValidateCodeForm';
import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';
-type ContactMethodDetailsPageOnyxProps = {
- /** Login list for the user that is signed in */
- loginList: OnyxEntry;
+const policiesSelector = (policy: OnyxEntry): Pick => ({
+ id: policy?.id ?? '',
+ ownerAccountID: policy?.ownerAccountID,
+ owner: policy?.owner ?? '',
+});
- /** Current user session */
- session: OnyxEntry;
+type ContactMethodDetailsPageProps = StackScreenProps;
- /** User's security group IDs by domain */
- myDomainSecurityGroups: OnyxEntry>;
+function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) {
+ const [loginList, loginListResult] = useOnyx(ONYXKEYS.LOGIN_LIST);
+ const [session, sessionResult] = useOnyx(ONYXKEYS.SESSION);
+ const [myDomainSecurityGroups, myDomainSecurityGroupsResult] = useOnyx(ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS);
+ const [securityGroups, securityGroupsResult] = useOnyx(ONYXKEYS.COLLECTION.SECURITY_GROUP);
+ const [isLoadingReportData, isLoadingReportDataResult] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true});
+ const [policies, policiesResult] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policiesSelector});
- /** All of the user's security groups and their settings */
- securityGroups: OnyxCollection;
+ const isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult, policiesResult);
- /** Indicated whether the report data is loading */
- isLoadingReportData: OnyxEntry;
-
- /** The list of this user's policies */
- policies: OnyxCollection>;
-};
-
-type ContactMethodDetailsPageProps = ContactMethodDetailsPageOnyxProps & StackScreenProps;
-
-function ContactMethodDetailsPage({loginList, session, myDomainSecurityGroups, securityGroups, isLoadingReportData = true, route, policies}: ContactMethodDetailsPageProps) {
const {formatPhoneNumber, translate} = useLocalize();
const theme = useTheme();
const themeStyles = useThemeStyles();
@@ -172,7 +168,7 @@ function ContactMethodDetailsPage({loginList, session, myDomainSecurityGroups, s
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route);
}, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod]);
- if (isLoadingReportData && isEmptyObject(loginList)) {
+ if (isLoadingOnyxValues || (isLoadingReportData && isEmptyObject(loginList))) {
return ;
}
@@ -290,28 +286,4 @@ function ContactMethodDetailsPage({loginList, session, myDomainSecurityGroups, s
ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage';
-export default withOnyx({
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- myDomainSecurityGroups: {
- key: ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS,
- },
- securityGroups: {
- key: `${ONYXKEYS.COLLECTION.SECURITY_GROUP}`,
- },
- isLoadingReportData: {
- key: `${ONYXKEYS.IS_LOADING_REPORT_DATA}`,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- selector: (data) => ({
- id: data?.id ?? '',
- ownerAccountID: data?.ownerAccountID,
- owner: data?.owner ?? '',
- }),
- },
-})(ContactMethodDetailsPage);
+export default ContactMethodDetailsPage;
diff --git a/src/pages/settings/Profile/PronounsPage.tsx b/src/pages/settings/Profile/PronounsPage.tsx
index b8022f6a4079..3f4c7ebe6b66 100644
--- a/src/pages/settings/Profile/PronounsPage.tsx
+++ b/src/pages/settings/Profile/PronounsPage.tsx
@@ -70,6 +70,7 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp = true}: Pronoun
const updatePronouns = (selectedPronouns: PronounEntry) => {
PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : selectedPronouns?.value ?? '');
+ Navigation.goBack();
};
return (
diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx
index 5c8456366c6b..4e74ef3b4b20 100644
--- a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx
@@ -50,7 +50,12 @@ function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFound
}
if (shouldShowNotFoundPage) {
- return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />;
+ return (
+ Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))}
+ shouldForceFullScreen
+ />
+ );
}
return typeof props.children === 'function' ? props.children(props) : props.children;
diff --git a/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx
index 9b6047493561..7361fc77536b 100644
--- a/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx
@@ -50,7 +50,12 @@ function PaidPolicyAccessOrNotFoundComponent(props: PaidPolicyAccessOrNotFoundCo
}
if (shouldShowNotFoundPage) {
- return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />;
+ return (
+ Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))}
+ shouldForceFullScreen
+ />
+ );
}
return typeof props.children === 'function' ? props.children(props) : props.children;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 2e9094f565de..a6a131f5372c 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -247,6 +247,20 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
// We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace
(PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy));
+ // We are checking if the user can access the route.
+ // If user can't access the route, we are dismissing any modals that are open when the NotFound view is shown
+ const canAccessRoute = activeRoute && menuItems.some((item) => item.routeName === activeRoute);
+
+ useEffect(() => {
+ if (!shouldShowNotFoundPage && canAccessRoute) {
+ return;
+ }
+ // We are dismissing any modals that are open when the NotFound view is shown
+ Navigation.isNavigationReady().then(() => {
+ Navigation.dismissRHP();
+ });
+ }, [canAccessRoute, policy, shouldShowNotFoundPage]);
+
const policyAvatar = useMemo(() => {
if (!policy) {
return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
index a9d8860ae12a..e18b315b2dd0 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -81,6 +81,9 @@ function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsT
setWelcomeNote(parser.htmlToMarkdown(getDefaultWelcomeNote()));
return;
}
+ if (isEmptyObject(policy)) {
+ return;
+ }
Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx
index 3f95c3e02a5b..4a85e01d973a 100644
--- a/src/pages/workspace/WorkspaceInvitePage.tsx
+++ b/src/pages/workspace/WorkspaceInvitePage.tsx
@@ -1,7 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {SectionListData} from 'react-native';
-import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -64,7 +63,6 @@ function WorkspaceInvitePage({
invitedEmailsToAccountIDsDraft,
policy,
isLoadingReportData = true,
- didScreenTransitionEnd,
}: WorkspaceInvitePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -72,6 +70,7 @@ function WorkspaceInvitePage({
const [selectedOptions, setSelectedOptions] = useState([]);
const [personalDetails, setPersonalDetails] = useState([]);
const [usersToInvite, setUsersToInvite] = useState([]);
+ const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const openWorkspaceInvitePage = () => {
const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp);
Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs));
@@ -223,18 +222,16 @@ function WorkspaceInvitePage({
setSelectedOptions(newSelectedOptions);
};
- const validate = (): boolean => {
+ const inviteUser = useCallback(() => {
const errors: Errors = {};
if (selectedOptions.length <= 0) {
errors.noUserSelected = 'true';
}
Policy.setWorkspaceErrors(route.params.policyID, errors);
- return isEmptyObject(errors);
- };
+ const isValid = isEmptyObject(errors);
- const inviteUser = () => {
- if (!validate()) {
+ if (!isValid) {
return;
}
@@ -249,7 +246,7 @@ function WorkspaceInvitePage({
});
Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs);
Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID));
- };
+ }, [route.params.policyID, selectedOptions]);
const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]);
@@ -271,11 +268,29 @@ function WorkspaceInvitePage({
return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue);
}, [excludedUsers, translate, searchTerm, policyName, usersToInvite, personalDetails.length]);
+ const footerContent = useMemo(
+ () => (
+
+ ),
+ [inviteUser, policy?.alertMessage, selectedOptions.length, shouldShowAlertPrompt, styles, translate],
+ );
+
return (
setDidScreenTransitionEnd(true)}
>
-
-
-
);
diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx
index 09f8e9425c74..7cc8e63da2ee 100644
--- a/src/pages/workspace/WorkspaceJoinUserPage.tsx
+++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx
@@ -39,10 +39,10 @@ function WorkspaceJoinUserPage({route, policy}: WorkspaceJoinUserPageProps) {
}, []);
useEffect(() => {
- if (!policy || isUnmounted.current || isJoinLinkUsed) {
+ if (isUnmounted.current || isJoinLinkUsed) {
return;
}
- if (!isEmptyObject(policy)) {
+ if (!isEmptyObject(policy) && !policy?.isJoinRequestPending) {
Navigation.isNavigationReady().then(() => {
Navigation.goBack(undefined, false, true);
Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID ?? ''));
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 1a76eecb533f..dfaf50c0bcf6 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -7,16 +7,13 @@ import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Badge from '@components/Badge';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MessagesRow from '@components/MessagesRow';
-import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import TableListItem from '@components/SelectionList/TableListItem';
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
@@ -47,6 +44,7 @@ import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
+import WorkspacePageWithSections from './WorkspacePageWithSections';
type WorkspaceMembersPageOnyxProps = {
/** Personal details of all users */
@@ -75,16 +73,7 @@ function invertObject(object: Record): Record {
type MemberOption = Omit & {accountID: number};
-function WorkspaceMembersPage({
- policyMembers,
- personalDetails,
- invitedEmailsToAccountIDsDraft,
- route,
- policy,
- session,
- currentUserPersonalDetails,
- isLoadingReportData = true,
-}: WorkspaceMembersPageProps) {
+function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails}: WorkspaceMembersPageProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedEmployees, setSelectedEmployees] = useState([]);
@@ -544,71 +533,63 @@ function WorkspaceMembersPage({
};
return (
-
-
- {
- Navigation.goBack();
- }}
- shouldShowBackButton={isSmallScreenWidth}
- guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
- >
- {!isSmallScreenWidth && getHeaderButtons()}
-
- {isSmallScreenWidth && {getHeaderButtons()}}
- setRemoveMembersConfirmModalVisible(false)}
- prompt={translate('workspace.people.removeMembersPrompt')}
- confirmText={translate('common.remove')}
- cancelText={translate('common.cancel')}
- onModalHide={() => {
- InteractionManager.runAfterInteractions(() => {
- if (!textInputRef.current) {
- return;
- }
- textInputRef.current.focus();
- });
- }}
- />
-
- toggleUser(item.accountID)}
- onSelectAll={() => toggleAllUsers(data)}
- onDismissError={dismissError}
- showLoadingPlaceholder={isLoading}
- showScrollIndicator
- shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
- textInputRef={textInputRef}
- customListHeader={getCustomListHeader()}
- listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ {() => (
+ <>
+ {isSmallScreenWidth && {getHeaderButtons()}}
+ setRemoveMembersConfirmModalVisible(false)}
+ prompt={translate('workspace.people.removeMembersPrompt')}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ onModalHide={() => {
+ InteractionManager.runAfterInteractions(() => {
+ if (!textInputRef.current) {
+ return;
+ }
+ textInputRef.current.focus();
+ });
+ }}
/>
-
-
-
+
+
+ toggleUser(item.accountID)}
+ onSelectAll={() => toggleAllUsers(data)}
+ onDismissError={dismissError}
+ showLoadingPlaceholder={isLoading}
+ showScrollIndicator
+ shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
+ textInputRef={textInputRef}
+ customListHeader={getCustomListHeader()}
+ listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ />
+
+ >
+ )}
+
);
}
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 4889c1dbe350..4b9b39458312 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -82,6 +82,12 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
* */
icon?: IconAsset;
+ /** Content to be added to the header */
+ headerContent?: ReactNode;
+
+ /** TestID of the component */
+ testID?: string;
+
/** Whether the page is loading, example any other API call in progres */
isLoading?: boolean;
};
@@ -112,6 +118,8 @@ function WorkspacePageWithSections({
shouldShowLoading = true,
shouldShowOfflineIndicatorInWideScreen = false,
shouldShowNonAdmin = false,
+ headerContent,
+ testID,
shouldShowNotFoundPage = false,
isLoading: isPageLoading = false,
}: WorkspacePageWithSectionsProps) {
@@ -160,7 +168,7 @@ function WorkspacePageWithSections({
includeSafeAreaPaddingBottom={false}
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
- testID={WorkspacePageWithSections.displayName}
+ testID={testID ?? WorkspacePageWithSections.displayName}
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
>
Navigation.goBack(backButtonRoute)}
icon={icon ?? undefined}
style={styles.headerBarDesktopHeight}
- />
+ >
+ {headerContent}
+
{(isLoading || firstRender.current) && shouldShowLoading && isFocused ? (
) : (
diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx
index 392fb90bbd22..c062b6a13f62 100644
--- a/src/pages/workspace/taxes/ValuePage.tsx
+++ b/src/pages/workspace/taxes/ValuePage.tsx
@@ -82,7 +82,7 @@ function ValuePage({
disablePressOnEnter={false}
shouldHideFixErrorsAlert
submitFlexEnabled={false}
- submitButtonStyles={[styles.mh5]}
+ submitButtonStyles={[styles.mh5, styles.mt0]}
>
void;
+ /** Whether the toggle should be disabled */
+ disabled?: boolean;
};
const ICON_SIZE = 48;
-function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError}: ToggleSettingOptionRowProps) {
+function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError, disabled = false}: ToggleSettingOptionRowProps) {
const styles = useThemeStyles();
return (
@@ -77,6 +79,7 @@ function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems,
accessibilityLabel={subtitle}
onToggle={onToggle}
isOn={isActive}
+ disabled={disabled}
/>
{isActive && subMenuItems}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index f165974119ff..f472834c4b33 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2861,7 +2861,7 @@ const styles = (theme: ThemeColors) =>
},
switchInactive: {
- backgroundColor: theme.border,
+ backgroundColor: theme.icon,
},
switchThumb: {
@@ -2870,6 +2870,8 @@ const styles = (theme: ThemeColors) =>
borderRadius: 11,
position: 'absolute',
left: 4,
+ justifyContent: 'center',
+ alignItems: 'center',
backgroundColor: theme.appBG,
},
@@ -2889,6 +2891,11 @@ const styles = (theme: ThemeColors) =>
alignItems: 'center',
},
+ toggleSwitchLockIcon: {
+ width: variables.iconSizeExtraSmall,
+ height: variables.iconSizeExtraSmall,
+ },
+
checkedContainer: {
backgroundColor: theme.checkBox,
},
diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts
index 98ce460a7669..5b9470c6ca6f 100644
--- a/src/types/onyx/Account.ts
+++ b/src/types/onyx/Account.ts
@@ -1,5 +1,6 @@
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
+import type DismissedReferralBanners from './DismissedReferralBanners';
import type * as OnyxCommon from './OnyxCommon';
type TwoFactorAuthStep = ValueOf | '';
@@ -60,6 +61,7 @@ type Account = {
success?: string;
codesAreCopied?: boolean;
twoFactorAuthStep?: TwoFactorAuthStep;
+ dismissedReferralBanners?: DismissedReferralBanners;
};
export default Account;
diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts
index 6bbcb174a617..7e1827f73954 100644
--- a/src/types/onyx/IOU.ts
+++ b/src/types/onyx/IOU.ts
@@ -21,6 +21,7 @@ type Participant = {
phoneNumber?: string;
text?: string;
isSelected?: boolean;
+ isSelfDM?: boolean;
};
type Split = {
diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts
index 6133f35afa47..dade2052e052 100644
--- a/src/types/onyx/ReportAction.ts
+++ b/src/types/onyx/ReportAction.ts
@@ -226,6 +226,9 @@ type ReportActionBase = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Flag for checking if data is from optimistic data */
isOptimisticAction?: boolean;
+
+ /** The admins's ID */
+ adminAccountID?: number;
}>;
type ReportAction = ReportActionBase & OriginalMessage;
diff --git a/src/types/utils/AssertTypesNotEqual.ts b/src/types/utils/AssertTypesNotEqual.ts
new file mode 100644
index 000000000000..237f54ec2921
--- /dev/null
+++ b/src/types/utils/AssertTypesNotEqual.ts
@@ -0,0 +1,11 @@
+import type {IsEqual} from 'type-fest';
+
+type MatchError = 'Error: Types do match';
+
+/**
+ * The 'AssertTypesNotEqual' type here enforces that `T1` and `T2` do not match.
+ * If `T1` or `T2` are the same this type will cause a compile-time error.
+ */
+type AssertTypesNotEqual extends false ? T1 : TMatchError, TMatchError = MatchError> = T1 & T2;
+
+export default AssertTypesNotEqual;
diff --git a/src/types/utils/isLoadingOnyxValue.ts b/src/types/utils/isLoadingOnyxValue.ts
new file mode 100644
index 000000000000..052c97ad40ef
--- /dev/null
+++ b/src/types/utils/isLoadingOnyxValue.ts
@@ -0,0 +1,7 @@
+import type {OnyxKey, UseOnyxResult} from 'react-native-onyx';
+
+function isLoadingOnyxValue(...results: Array[1]>): boolean {
+ return results.some((result) => result.status === 'loading');
+}
+
+export default isLoadingOnyxValue;
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 9f063c2be6c3..c8c74d4198ab 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -1,6 +1,6 @@
import isEqual from 'lodash/isEqual';
-import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import type {OptimisticChatReport} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import * as IOU from '@src/libs/actions/IOU';
diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts
index c673d39cd414..18a6337a9b93 100644
--- a/tests/actions/PolicyTest.ts
+++ b/tests/actions/PolicyTest.ts
@@ -1,5 +1,5 @@
-import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
import * as Policy from '@src/libs/actions/Policy';
diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx
index ea759a1201b2..33ee900f8b6c 100644
--- a/tests/perf-test/SearchPage.perf-test.tsx
+++ b/tests/perf-test/SearchPage.perf-test.tsx
@@ -9,6 +9,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {measurePerformance} from 'reassure';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
import type {WithNavigationFocusProps} from '@components/withNavigationFocus';
import type {RootStackParamList} from '@libs/Navigation/types';
import {createOptionList} from '@libs/OptionsListUtils';
@@ -75,6 +76,15 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType
return WithNavigationFocus;
});
+// mock of useDismissedReferralBanners
+jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ default: jest.fn(() => ({
+ isDismissed: false,
+ setAsDismissed: () => {},
+ })),
+}));
const getMockedReports = (length = 100) =>
createCollection(
@@ -124,7 +134,7 @@ type SearchPageProps = StackScreenProps
+
({
createNavigationContainerRef: jest.fn(),
}));
+jest.mock('../../src/hooks/useKeyboardState', () => ({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ __esModule: true,
+ default: jest.fn(() => ({
+ isKeyboardShown: false,
+ keyboardHeight: 0,
+ })),
+}));
+
function SelectionListWrapper({canSelectMultiple}: SelectionListWrapperProps) {
const [selectedIds, setSelectedIds] = useState([]);
diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts
index 6333ee6f1bc7..af5782b1ca32 100644
--- a/tests/unit/OptionsListUtilsTest.ts
+++ b/tests/unit/OptionsListUtilsTest.ts
@@ -262,9 +262,23 @@ describe('OptionsListUtils', () => {
},
};
+ const REPORTS_WITH_CHAT_ROOM = {
+ ...REPORTS,
+ 15: {
+ lastReadTime: '2021-01-14 11:25:39.301',
+ lastVisibleActionCreated: '2022-11-22 03:26:02.000',
+ isPinned: false,
+ reportID: '15',
+ participantAccountIDs: [3, 4],
+ visibleChatMemberAccountIDs: [3, 4],
+ reportName: 'Spider-Man, Black Panther',
+ type: CONST.REPORT.TYPE.CHAT,
+ chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL,
+ },
+ };
+
const PERSONAL_DETAILS_WITH_CONCIERGE: PersonalDetailsList = {
...PERSONAL_DETAILS,
-
'999': {
accountID: 999,
displayName: 'Concierge',
@@ -2581,4 +2595,98 @@ describe('OptionsListUtils', () => {
// `isDisabled` is always false
expect(formattedMembers.every((personalDetail) => !personalDetail.isDisabled)).toBe(true);
});
+
+ describe('filterOptions', () => {
+ it('should return all options when search is empty', () => {
+ const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]);
+ const filteredOptions = OptionsListUtils.filterOptions(options, '');
+
+ expect(options.recentReports.length + options.personalDetails.length).toBe(filteredOptions.recentReports.length);
+ });
+
+ it('should return filtered options in correct order', () => {
+ const searchText = 'man';
+ const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]);
+
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+ expect(filteredOptions.recentReports.length).toBe(5);
+ expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman');
+ expect(filteredOptions.recentReports[1].text).toBe('Spider-Man');
+ expect(filteredOptions.recentReports[2].text).toBe('Black Widow');
+ expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic');
+ expect(filteredOptions.recentReports[4].text).toBe("SHIELD's workspace (archived)");
+ });
+
+ it('should filter users by email', () => {
+ const searchText = 'mistersinister@marauders.com';
+ const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]);
+
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filteredOptions.recentReports.length).toBe(1);
+ expect(filteredOptions.recentReports[0].text).toBe('Mr Sinister');
+ });
+
+ it('should find archived chats', () => {
+ const searchText = 'Archived';
+ const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]);
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filteredOptions.recentReports.length).toBe(1);
+ expect(filteredOptions.recentReports[0].isArchivedRoom).toBe(true);
+ });
+
+ it('should filter options by email if dot is skipped in the email', () => {
+ const searchText = 'barryallen';
+ const OPTIONS_WITH_PERIODS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_PERIODS, REPORTS);
+ const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_PERIODS, '', [CONST.BETAS.ALL]);
+
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filteredOptions.recentReports.length).toBe(1);
+ expect(filteredOptions.recentReports[0].login).toBe('barry.allen@expensify.com');
+ });
+
+ it('should include workspaces in the search results', () => {
+ const searchText = 'avengers';
+ const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACES, '', [CONST.BETAS.ALL]);
+
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filteredOptions.recentReports.length).toBe(1);
+ expect(filteredOptions.recentReports[0].subtitle).toBe('Avengers Room');
+ });
+
+ it('should put exact match by login on the top of the list', () => {
+ const searchText = 'reedrichards@expensify.com';
+ const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]);
+
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filteredOptions.recentReports.length).toBe(2);
+ expect(filteredOptions.recentReports[0].login).toBe(searchText);
+ });
+
+ it('should prioritize options with matching display name over chatrooms', () => {
+ const searchText = 'spider';
+ const OPTIONS_WITH_CHATROOMS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_CHAT_ROOM);
+ const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_CHATROOMS, '', [CONST.BETAS.ALL]);
+
+ const filterOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filterOptions.recentReports.length).toBe(2);
+ expect(filterOptions.recentReports[1].isChatRoom).toBe(true);
+ });
+
+ it('should put the item with latest lastVisibleActionCreated on top when search value match multiple items', () => {
+ const searchText = 'fantastic';
+
+ const options = OptionsListUtils.getSearchOptions(OPTIONS, '');
+ const filteredOptions = OptionsListUtils.filterOptions(options, searchText);
+
+ expect(filteredOptions.recentReports.length).toBe(2);
+ expect(filteredOptions.recentReports[0].text).toBe('Mister Fantastic');
+ expect(filteredOptions.recentReports[1].text).toBe('Mister Fantastic');
+ });
+ });
});
diff --git a/tests/unit/StringUtilsTest.ts b/tests/unit/StringUtilsTest.ts
new file mode 100644
index 000000000000..04ce748c984b
--- /dev/null
+++ b/tests/unit/StringUtilsTest.ts
@@ -0,0 +1,20 @@
+import StringUtils from '@libs/StringUtils';
+
+describe('StringUtils', () => {
+ describe('getAcronym', () => {
+ it('should return the acronym of a string with multiple words', () => {
+ const acronym = StringUtils.getAcronym('Hello World');
+ expect(acronym).toBe('HW');
+ });
+
+ it('should return an acronym of a string with a single word', () => {
+ const acronym = StringUtils.getAcronym('Hello');
+ expect(acronym).toBe('H');
+ });
+
+ it('should return an acronym of a string when word in a string has a hyphen', () => {
+ const acronym = StringUtils.getAcronym('Hello Every-One');
+ expect(acronym).toBe('HEO');
+ });
+ });
+});
diff --git a/web/index.html b/web/index.html
index fb97293ebda5..115803573bbd 100644
--- a/web/index.html
+++ b/web/index.html
@@ -101,7 +101,7 @@
left: 0;
right: 0;
top: 0;
- background-color: #061B09;
+ background-color: #03D47C;
width: 100%;
height: 100%;
display: flex;