diff --git a/.github/ISSUE_TEMPLATE/Accessibility.md b/.github/ISSUE_TEMPLATE/Accessibility.md index 36a64aa25b43..1323e2c17e78 100644 --- a/.github/ISSUE_TEMPLATE/Accessibility.md +++ b/.github/ISSUE_TEMPLATE/Accessibility.md @@ -34,7 +34,7 @@ What can we do to fix the issue? -Which of our officially supported platforms is this issue occurring on? +Which of our officially supported platforms is this issue occurring on? Please only tick the box if you have provided a screen-recording in the thread for each platform: - [ ] Android / native - [ ] Android / Chrome - [ ] iOS / native diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index 11f4897ab322..ca7345ef9462 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -2,6 +2,10 @@ name: Deploy ExpensifyHelp on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/android/app/build.gradle b/android/app/build.gradle index 629f10f56267..0f6ee4dff858 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001037300 - versionName "1.3.73-0" + versionCode 1001037401 + versionName "1.3.74-1" } flavorDimensions "default" diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index b97d04b95e10..621673e5a487 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -47,7 +47,7 @@ Note: if you are hired for an Upwork job and have any job-specific questions, pl If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue. ## Payment for Contributions -We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing or reporting a bug, please create an Upwork account, apply for an available job in [GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22), and finally apply for the job in Upwork once your proposal gets selected in GitHub. PLease make sure your Upwork profile is **fully verified** before applying, otherwise you run the risk of not being paid. If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted. +We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing or reporting a bug, please create an Upwork account, apply for an available job in [GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22), and finally apply for the job in Upwork once your proposal gets selected in GitHub. Please make sure your Upwork profile is **fully verified** before applying, otherwise you run the risk of not being paid. If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted. Payment for your contributions and bug reports will be made no less than 7 days after the pull request is deployed to production to allow for [regression](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions) testing. If you have not received payment after 8 days of the PR being deployed to production, and there are no [regressions](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions), please add a comment to the issue mentioning the BugZero team member (Look for the melvin-bot "Triggered auto assignment to... (`Bug`)" to see who this is). diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index 20582b6b8c7e..89a93e989672 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -7,136 +7,136 @@ platforms: - href: expensify-classic title: Expensify Classic hub-title: Expensify Classic - Help & Resources - url: expensify.com - description: Your account settings will look something like this - image: /assets/images/paper-airplane.svg + url: www.expensify.com + description: "Your account settings look like this:" + image: /assets/images/settings-old-dot.svg # Hubs are comprised of sections and articles. Sections contain multiple related articles, but there can be standalone articles as well hubs: + - href: getting-started + title: Getting Started + icon: /assets/images/accounting.svg + description: From setting up your account to ensuring you get the most out of Expensify’s suite of features, click here to get started on streamlining your expense management journey. + - href: account-settings title: Account Settings icon: /assets/images/gears.svg - description: With only a couple of clicks, split bills with your friends or coworkers. + description: Discover how to personalize your profile, add secondary logins, and grant delegated access to employees with our comprehensive guide on Account Settings. - href: bank-accounts-and-credit-cards title: Bank Accounts & Credit Cards icon: /assets/images/bank-card.svg - description: Request money for work expenses, bills, or a night out with friends. + description: Find out how to connect Expensify to your financial institutions, track credit card transactions, and best practices for reconciling company cards. - href: billing-and-subscriptions title: Billing & Subscriptions icon: /assets/images/money-wings.svg - description: Best practices for how to best deploy Expensify for your business + description: Here is where you can review Expensify's billing and subscription options, plan types, and payment methods. - href: expense-and-report-features title: Expense & Report Features icon: /assets/images/money-receipt.svg - description: Everything else you're looking for is right here. + description: From enabling automatic expense auditing to tracking attendees, here is where you can review tips and tutorials to streamline expense management. - href: expensify-card title: Expensify Card icon: /assets/images/hand-card.svg - description: Request money for work expenses, bills, or a night out with friends. + description: Explore how the Expensify Card combines convenience and security to enhance everyday business transactions. Discover how to apply for, oversee, and maximize your card perks here. - href: exports title: Exports icon: /assets/images/monitor.svg - description: Best practices for how to best deploy Expensify for your business + description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. - href: get-paid-back title: Get Paid Back - description: Everything else you're looking for is right here. icon: /assets/images/money-into-wallet.svg - - - href: getting-started - title: Getting Started - description: Everything else you're looking for is right here. - icon: /assets/images/accounting.svg + description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. - href: integrations title: Integrations - description: Everything else you're looking for is right here. icon: /assets/images/workflow.svg + description: Enhance Expensify’s capabilities by integrating it with your accounting or HR software. Here is where you can learn more about creating a synchronized financial management ecosystem. - href: manage-employees-and-report-approvals title: Manage Employees & Report Approvals icon: /assets/images/envelope-receipt.svg - description: Everything else you're looking for is right here. + description: Master the art of overseeing employees and reports by utilizing Expensify’s automation features and approval workflows. - href: policy-and-domain-settings title: Policy & Domain Setting icon: /assets/images/shield.svg - description: Everything else you're looking for is right here. + description: Discover how to set up and manage policies, define user permissions, and implement compliance rules to maintain a secure and compliant financial management landscape. - href: send-payments title: Send Payments icon: /assets/images/money-wings.svg - description: Everything else you're looking for is right here. + description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. - href: new-expensify title: New Expensify hub-title: New Expensify - Help & Resources url: new.expensify.com - description: Your account settings will look something like this - image: /assets/images/paper-airplane.svg + description: "Your account settings look like this:" + image: /assets/images/settings-new-dot.svg hubs: + - href: getting-started + title: Getting Started + icon: /assets/images/accounting.svg + description: From setting up your account to ensuring you get the most out of Expensify’s suite of features, click here to get started on streamlining your expense management journey. + - href: account-settings title: Account Settings icon: /assets/images/gears.svg - description: With only a couple of clicks, split bills with your friends or coworkers. + description: Discover how to personalize your profile, add secondary logins, and grant delegated access to employees with our comprehensive guide on Account Settings. - href: bank-accounts-and-credit-cards title: Bank Accounts & Credit Cards icon: /assets/images/bank-card.svg - description: description + description: Find out how to connect Expensify to your financial institutions, track credit card transactions, and best practices for reconciling company cards. - href: billing-and-plan-types title: Billing & Plan Types icon: /assets/images/money-wings.svg - description: description + description: Here is where you can review Expensify's billing and subscription options, plan types, and payment methods. - href: expense-and-report-features title: Expense & Report Features icon: /assets/images/money-receipt.svg - description: description + description: From enabling automatic expense auditing to tracking attendees, here is where you can review tips and tutorials to streamline expense management. - href: expensify-card title: Expensify Card icon: /assets/images/hand-card.svg - description: description + description: Explore how the Expensify Card combines convenience and security to enhance everyday business transactions. Discover how to apply for, oversee, and maximize your card perks here. - href: exports title: Exports icon: /assets/images/monitor.svg - description: description + description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. - href: get-paid-back title: Get Paid Back icon: /assets/images/money-into-wallet.svg - description: description - - - href: getting-started - title: Getting Started - icon: /assets/images/accounting.svg - description: description + description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. - href: integrations title: Integrations icon: /assets/images/workflow.svg - description: description + description: Enhance Expensify’s capabilities by integrating it with your accounting or HR software. Here is where you can learn more about creating a synchronized financial management ecosystem. - href: manage-employees-and-report-approvals title: Manage Employees & Report Approvals icon: /assets/images/envelope-receipt.svg - description: description + description: Master the art of overseeing employees and reports by utilizing Expensify’s automation features and approval workflows. - href: send-payments title: Send Payments icon: /assets/images/money-wings.svg - description: description. + description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. - href: workspace-and-domain-settings title: Workspace & Domain Settings icon: /assets/images/shield.svg - description: description. + description: Discover how to set up and manage your workspace, define user permissions, and implement domain-level rules. diff --git a/docs/_includes/article-card.html b/docs/_includes/article-card.html index b6d8998c13ef..b66affebb4ae 100644 --- a/docs/_includes/article-card.html +++ b/docs/_includes/article-card.html @@ -1,6 +1,6 @@ - +
-

{{ include.title }}

+

{{ include.title }}

diff --git a/docs/_includes/hub-card.html b/docs/_includes/hub-card.html index b5188bda7670..859ee0004394 100644 --- a/docs/_includes/hub-card.html +++ b/docs/_includes/hub-card.html @@ -1,8 +1,8 @@ {% assign hub = include.hub %} {% assign platform = include.platform %} - +
-
+
{{ hub.href }}
diff --git a/docs/_includes/platform-card.html b/docs/_includes/platform-card.html index d56a234a5c14..7123f18a679a 100644 --- a/docs/_includes/platform-card.html +++ b/docs/_includes/platform-card.html @@ -1,13 +1,22 @@ {% assign platform = site.data.routes.platforms | where: "href", include.href | first %} - +
-
- {{ platform.href }} -
-

{{ platform.title }}

-

{{ platform.description }}

+
+
+

{{ platform.title }}

+

{{ platform.url }}

+
+
+ +
+
+ +

{{ platform.description }}

+
+
+ {{ platform.href }}
diff --git a/docs/_includes/platform.html b/docs/_includes/platform.html index f3867ee4f5b7..0323828f2d8c 100644 --- a/docs/_includes/platform.html +++ b/docs/_includes/platform.html @@ -5,7 +5,7 @@

{{ platform.hub-title }}

{{ site.data.routes.home.description }}

-
+
{% for hub in platform.hubs %} {% include hub-card.html hub=hub platform=selectedPlatform %} {% endfor %} diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index bc9d19bfca11..cc021b94c695 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -445,18 +445,26 @@ button { } } -.cards-group { +.cards-group, .platform-cards-group { display: grid; grid-template-columns: auto; row-gap: 20px; column-gap: 20px; padding-bottom: 20px; +} +.cards-group { @include breakpoint($breakpoint-desktop) { grid-template-columns: 50% 50%; } } +.platform-cards-group { + @include breakpoint($breakpoint-desktop) { + grid-template-columns: 33.33% 33.33% 33.33%; + } +} + .card { display: flex; flex-wrap: nowrap; @@ -476,21 +484,55 @@ button { flex-basis:100%; } - .left-icon { + .body { display: flex; - align-items: center; - padding-right: 28px; + flex-wrap: nowrap; + flex-direction: column; + flex-grow: 2; + } - img { - width: 64px; - } + h3.title { + font-family: "ExpensifyNewKansas", "Helvetica Neue", "Helvetica", Arial, sans-serif; } + h3.title, + h4.title { + padding: 0; + margin: 0; + } + + p.description, + p.url { + margin: 0; + font-weight: normal; + } +} + +.article-card { + @extend .card; + .right-icon { display: flex; align-items: center; padding-left: 16px; } +} + +.platform-card { + @extend .card; + .row { + flex-direction: column; + } + + .platform-screenshot { + display: flex; + align-items: center; + + img { + border-radius: 12px; + width: 100%; + } + } .submit-button { display: flex; @@ -505,27 +547,62 @@ button { } .body { - display: flex; - flex-wrap: nowrap; - flex-direction: column; - flex-grow: 2; - } + .header { + display: flex; + align-items: center; + justify-content: space-between; - h3.title { - padding: 0; - margin: 0; + .select-button { + display: flex; + .success { + align-items: flex-end; + font-size: 0.8em; + } + } + } + } + h3.title, + h4.title { &.with-margin { margin: 0 0 4px 0; } } - + h3.title { + font-size: 1.4em; + font-weight: normal; + } p.description { + padding: 20px 0 20px 0; + } + + p.url { padding: 0; - margin: 0; + font-size: 0.8em; + color: $color-gray-label; + } +} + +.hub-card { + @extend .card; + padding: 24px; + + .row { + flex-direction: column; + } + + h3.title { + font-size: 1.2em; font-weight: normal; + &.with-margin { + margin: 20px 0 8px 0; + } + } + + p.description { + padding: 0; &.with-min-height { min-height: 68px; diff --git a/docs/articles/expensify-classic/account-settings/Account-Access.md b/docs/articles/expensify-classic/account-settings/Account-Access.md index f04b45c42639..b3126201715f 100644 --- a/docs/articles/expensify-classic/account-settings/Account-Access.md +++ b/docs/articles/expensify-classic/account-settings/Account-Access.md @@ -2,4 +2,4 @@ title: Account Access description: Account Access --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/account-settings/Close-Account.md b/docs/articles/expensify-classic/account-settings/Close-Account.md index cf5052fa56f1..5e18490fc357 100644 --- a/docs/articles/expensify-classic/account-settings/Close-Account.md +++ b/docs/articles/expensify-classic/account-settings/Close-Account.md @@ -2,4 +2,4 @@ title: Close Account description: Close Account --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md b/docs/articles/expensify-classic/account-settings/Merge-Accounts.md index 1c5f22478e17..073c74346d75 100644 --- a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md +++ b/docs/articles/expensify-classic/account-settings/Merge-Accounts.md @@ -2,4 +2,4 @@ title: Merge Accounts description: Merge Accounts --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/account-settings/Preferences.md b/docs/articles/expensify-classic/account-settings/Preferences.md index a3e53e1177a1..532da4d8a986 100644 --- a/docs/articles/expensify-classic/account-settings/Preferences.md +++ b/docs/articles/expensify-classic/account-settings/Preferences.md @@ -2,4 +2,4 @@ title: Preferences description: Preferences --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/account-settings/Profile-Settings.md b/docs/articles/expensify-classic/account-settings/Profile-Settings.md index bdc18036a46e..3b2a0b830926 100644 --- a/docs/articles/expensify-classic/account-settings/Profile-Settings.md +++ b/docs/articles/expensify-classic/account-settings/Profile-Settings.md @@ -2,4 +2,4 @@ title: Profile Settings description: Profile Settings --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-AUS.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-AUS.md index 44488defcd67..1c2edbbaefaa 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-AUS.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-AUS.md @@ -2,4 +2,4 @@ title: Business Bank Accounts - AUS description: Business Bank Accounts - AUS --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-USD.md index 218d6dcd1efa..375b00d62eac 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Business-Bank-Accounts-USD.md @@ -2,4 +2,4 @@ title: Business Bank Accounts - USD description: Business Bank Accounts - USD --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-AUS.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-AUS.md index dba02f6fc52c..9b42e3701310 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-AUS.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-AUS.md @@ -2,4 +2,4 @@ title: Deposit Accounts - AUS description: Deposit Accounts - AUS --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-USD.md index 8d3fe6e51484..19010be95980 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Deposit-Accounts-USD.md @@ -2,4 +2,4 @@ title: Deposit Accounts - USD description: Deposit Accounts - USD --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursement.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursement.md index 40bdfb7741ab..073d3a9bd700 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursement.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursement.md @@ -2,4 +2,4 @@ title: Global Reimbursement description: Global Reimbursement --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md index 016ca90ee7f7..f89729b69586 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md @@ -2,4 +2,4 @@ title: Personal Credit Cards description: Personal Credit Cards --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/ANZ.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/ANZ.md index 6bfc7b14c09a..7e6a76ecee24 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/ANZ.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/ANZ.md @@ -2,5 +2,5 @@ title: ANZ description: A guide to integrate with your ANZ card --- -## Resources Coming Soon! +## Resource Coming Soon! Coming Soon!! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Brex.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Brex.md index 7d5ad7bf0315..a060e37146a5 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Brex.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Brex.md @@ -2,4 +2,4 @@ title: Brex description: Brex --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md index db68d4431a3a..6debce6240ff 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md @@ -2,4 +2,4 @@ title: CSV Import description: CSV Import --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md index e49d0d61855c..25d11561755d 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md @@ -2,4 +2,4 @@ title: Commercial Card Feeds description: Commercial Card Feeds --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md index ecd4fc0a6538..112c3b9617c9 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md @@ -2,4 +2,4 @@ title: Connect Company Cards description: Connect Company Cards --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md index 6775b2684b61..f1d939ca9c89 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md @@ -2,4 +2,4 @@ title: Direct Bank Connections description: Direct Bank Connections --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Export-To-GL-Accounts.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Export-To-GL-Accounts.md index 58485888b921..85b534338b53 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Export-To-GL-Accounts.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Export-To-GL-Accounts.md @@ -2,4 +2,4 @@ title: Export to GL Accounts description: Export to GL Accounts --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md index be400ee2c13c..b51329f2a803 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md @@ -2,4 +2,4 @@ title: Reconciliation description: Reconciliation --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md index d9e0d1bb994b..e3d1307e6a05 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting.md @@ -2,4 +2,4 @@ title: Troubleshooting description: Troubleshooting --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md index c80a0d57400d..8e2aa7d4a377 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md @@ -2,4 +2,4 @@ title: Annual Subscription description: Annual Subscription --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md index 590fbc78007e..acb29d91e1d8 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md @@ -2,4 +2,4 @@ title: Billing-Owner description: Billing-Owner --- -## Resources Coming Soon! \ No newline at end of file +## Resource Coming Soon! \ No newline at end of file diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md index 2f593625a7d5..8ce4283dd17d 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md @@ -2,4 +2,4 @@ title: Change Plan or Subscription description: Change Plan or Subscription --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md index de6ec4a4a466..24edc553bd29 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md @@ -2,4 +2,4 @@ title: Consolidated Domain Billing description: Consolidated Domain Billing --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md index 8a7b7edd19d9..e08aaa3d6094 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md @@ -2,4 +2,4 @@ title: Free Trial description: Free Trial --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md index d6be489a1146..1ace758978aa 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md @@ -2,4 +2,4 @@ title: Individual Subscription description: Individual Subscription --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Overview.md b/docs/articles/expensify-classic/billing-and-subscriptions/Overview.md index 3352c72167cd..963186916f01 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Overview.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Overview.md @@ -2,4 +2,4 @@ title: Overview description: Overview --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md index be431a287557..77aca2a01678 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md @@ -2,4 +2,4 @@ title: Pay-per-use Subscription description: Pay-per-use Subscription --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md b/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md index 91c5d4e91eda..41a1fb96f56f 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md @@ -2,4 +2,4 @@ title: Payment Card description: Payment Card --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md b/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md index c8f781cbd59b..c4948b5b3083 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md @@ -2,4 +2,4 @@ title: Tax Exempt description: Tax Exempt --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md index bc7fbdfe84aa..a5b0b26b2610 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md @@ -2,4 +2,4 @@ title: Attendee Tracking description: Attendee Tracking --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/Currency.md b/docs/articles/expensify-classic/expense-and-report-features/Currency.md index 611365aa5013..e5c9096fa610 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Currency.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Currency.md @@ -2,4 +2,4 @@ title: Currency description: Currency --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md b/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md index 81c664497e14..304c93d1da6d 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md @@ -2,4 +2,4 @@ title: Expense Rules description: Expense Rules --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md b/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md index a75209e4dfb1..3f2e49952c4a 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md @@ -2,4 +2,4 @@ title: Expense Types description: Expense Types --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/Report-Comments.md b/docs/articles/expensify-classic/expense-and-report-features/Report-Comments.md index 3938c02bd333..b7ed120fb28b 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Report-Comments.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Report-Comments.md @@ -2,4 +2,4 @@ title: Report Comments description: Report Comments --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md b/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md index f202587568e5..f30dde9efc3d 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md +++ b/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md @@ -2,4 +2,4 @@ title: The Expenses Page description: The Expenses Page --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md b/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md index 37da613e750a..e72abfcad51a 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md +++ b/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md @@ -2,4 +2,4 @@ title: The Reports Page description: The Reports Page --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index e1d1a990b166..85202835a0e4 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -2,4 +2,4 @@ title: Auto-reconciliation description: Auto-reconciliation --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/CPA-Card.md b/docs/articles/expensify-classic/expensify-card/CPA-Card.md index 9f4c47a6a402..dfc1e71192db 100644 --- a/docs/articles/expensify-classic/expensify-card/CPA-Card.md +++ b/docs/articles/expensify-classic/expensify-card/CPA-Card.md @@ -2,4 +2,4 @@ title: CPA Card description: CPA Card --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Card-Settings.md b/docs/articles/expensify-classic/expensify-card/Card-Settings.md index ff9a959d38aa..ab212354974a 100644 --- a/docs/articles/expensify-classic/expensify-card/Card-Settings.md +++ b/docs/articles/expensify-classic/expensify-card/Card-Settings.md @@ -2,4 +2,4 @@ title: Card Settings description: Card Settings --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Connect-To-Indirect-Integration.md b/docs/articles/expensify-classic/expensify-card/Connect-To-Indirect-Integration.md index 0e05269f6501..9888edd139ac 100644 --- a/docs/articles/expensify-classic/expensify-card/Connect-To-Indirect-Integration.md +++ b/docs/articles/expensify-classic/expensify-card/Connect-To-Indirect-Integration.md @@ -2,4 +2,4 @@ title: Connect to Indirect Integration description: Connect to Indirect Integration --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/File-A-Dispute.md b/docs/articles/expensify-classic/expensify-card/File-A-Dispute.md index 296999410687..694bce3da059 100644 --- a/docs/articles/expensify-classic/expensify-card/File-A-Dispute.md +++ b/docs/articles/expensify-classic/expensify-card/File-A-Dispute.md @@ -2,4 +2,4 @@ title: File a Dispute description: File a Dispute --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Get-The-Card.md b/docs/articles/expensify-classic/expensify-card/Get-The-Card.md index 9c8e804f6363..e5233a3732a3 100644 --- a/docs/articles/expensify-classic/expensify-card/Get-The-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Get-The-Card.md @@ -2,4 +2,4 @@ title: Get the Card description: Get the Card --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Statements.md b/docs/articles/expensify-classic/expensify-card/Statements.md index 602fa610dd0b..b48d303a1a9b 100644 --- a/docs/articles/expensify-classic/expensify-card/Statements.md +++ b/docs/articles/expensify-classic/expensify-card/Statements.md @@ -2,4 +2,4 @@ title: Statements description: Statements --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/The-Reports-Page.md b/docs/articles/expensify-classic/expensify-card/The-Reports-Page.md index 37da613e750a..e72abfcad51a 100644 --- a/docs/articles/expensify-classic/expensify-card/The-Reports-Page.md +++ b/docs/articles/expensify-classic/expensify-card/The-Reports-Page.md @@ -2,4 +2,4 @@ title: The Reports Page description: The Reports Page --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/exports/Custom-Templates.md b/docs/articles/expensify-classic/exports/Custom-Templates.md index 5dcfe58b09f5..e01450a730cf 100644 --- a/docs/articles/expensify-classic/exports/Custom-Templates.md +++ b/docs/articles/expensify-classic/exports/Custom-Templates.md @@ -2,4 +2,4 @@ title: Custom Templates description: Custom Templates --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/exports/Default-Export-Templates.md b/docs/articles/expensify-classic/exports/Default-Export-Templates.md index 4dcb624698af..7650cff38946 100644 --- a/docs/articles/expensify-classic/exports/Default-Export-Templates.md +++ b/docs/articles/expensify-classic/exports/Default-Export-Templates.md @@ -2,4 +2,4 @@ title: Default Export Templates description: Default Export Templates --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/exports/The-Reports-Page.md b/docs/articles/expensify-classic/exports/The-Reports-Page.md index 37da613e750a..e72abfcad51a 100644 --- a/docs/articles/expensify-classic/exports/The-Reports-Page.md +++ b/docs/articles/expensify-classic/exports/The-Reports-Page.md @@ -2,4 +2,4 @@ title: The Reports Page description: The Reports Page --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/Mileage.md b/docs/articles/expensify-classic/get-paid-back/Mileage.md index 381bc28626f9..248e80e1c115 100644 --- a/docs/articles/expensify-classic/get-paid-back/Mileage.md +++ b/docs/articles/expensify-classic/get-paid-back/Mileage.md @@ -2,4 +2,4 @@ title: Mileage description: Mileage --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem.md index e5a57fc62bdf..780e5969c441 100644 --- a/docs/articles/expensify-classic/get-paid-back/Per-Diem.md +++ b/docs/articles/expensify-classic/get-paid-back/Per-Diem.md @@ -2,4 +2,4 @@ title: Per Diem description: Per Diem --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/Third-Party-Payments.md b/docs/articles/expensify-classic/get-paid-back/Third-Party-Payments.md index d472e54778e1..a8cddcdfdd42 100644 --- a/docs/articles/expensify-classic/get-paid-back/Third-Party-Payments.md +++ b/docs/articles/expensify-classic/get-paid-back/Third-Party-Payments.md @@ -2,4 +2,4 @@ title: Third Party Payments description: Third Party Payments --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/Trips.md b/docs/articles/expensify-classic/get-paid-back/Trips.md index 3499865c4ee9..7efba1875a90 100644 --- a/docs/articles/expensify-classic/get-paid-back/Trips.md +++ b/docs/articles/expensify-classic/get-paid-back/Trips.md @@ -2,4 +2,4 @@ title: Trips description: Trips --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md index 224b622cec3f..36e0a2194d24 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md @@ -2,4 +2,4 @@ title: Apply Tax description: Apply Tax --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md b/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md index 8f4d035e1fe7..8323be7b8e3f 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md @@ -2,4 +2,4 @@ title: Create Expenses description: Create Expenses --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md index c628244c9b2e..e7705a32f215 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md @@ -2,4 +2,4 @@ title: Merge Expenses description: Merge Expenses --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md index 2091b5f3e7f0..b71fd1a3c8bf 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md @@ -2,4 +2,4 @@ title: Upload Receipts description: Upload Receipts --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md index e6cc65290e73..fb4f756b2820 100644 --- a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md +++ b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md @@ -2,4 +2,4 @@ title: Create a Report description: Create a Report --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md b/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md index 91c4459d2ebd..c2cc25b32373 100644 --- a/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md +++ b/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md @@ -2,4 +2,4 @@ title: Reimbursements description: Reimbursements --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Best-Practices.md b/docs/articles/expensify-classic/getting-started/Best-Practices.md index 16b284ae60df..b02ea9d68fe6 100644 --- a/docs/articles/expensify-classic/getting-started/Best-Practices.md +++ b/docs/articles/expensify-classic/getting-started/Best-Practices.md @@ -2,4 +2,4 @@ title: Best Practices description: Best Practices --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Employees.md b/docs/articles/expensify-classic/getting-started/Employees.md index f139c40be926..6d3c2dc705e1 100644 --- a/docs/articles/expensify-classic/getting-started/Employees.md +++ b/docs/articles/expensify-classic/getting-started/Employees.md @@ -2,4 +2,4 @@ title: Employees description: Employees --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Individual-Users.md b/docs/articles/expensify-classic/getting-started/Individual-Users.md index 2e152ea515d7..de7a527df010 100644 --- a/docs/articles/expensify-classic/getting-started/Individual-Users.md +++ b/docs/articles/expensify-classic/getting-started/Individual-Users.md @@ -2,4 +2,4 @@ title: Individual Users description: Individual Users --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Invite-Employees.md b/docs/articles/expensify-classic/getting-started/Invite-Employees.md index 5cdb8eb086b0..73dc7b8274f0 100644 --- a/docs/articles/expensify-classic/getting-started/Invite-Employees.md +++ b/docs/articles/expensify-classic/getting-started/Invite-Employees.md @@ -2,4 +2,4 @@ title: Invite Employees description: Invite Employees --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Plan-Types.md b/docs/articles/expensify-classic/getting-started/Plan-Types.md index 7bb725a1aa35..f0323947ee12 100644 --- a/docs/articles/expensify-classic/getting-started/Plan-Types.md +++ b/docs/articles/expensify-classic/getting-started/Plan-Types.md @@ -2,4 +2,4 @@ title: Plan-Types description: Plan-Types --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Policy-Admins.md b/docs/articles/expensify-classic/getting-started/Policy-Admins.md index 91d56b0c4f71..484350f101a5 100644 --- a/docs/articles/expensify-classic/getting-started/Policy-Admins.md +++ b/docs/articles/expensify-classic/getting-started/Policy-Admins.md @@ -2,4 +2,4 @@ title: Policy Admins description: Policy Admins --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Security.md b/docs/articles/expensify-classic/getting-started/Security.md index 41451e2ba958..5a0036e3e161 100644 --- a/docs/articles/expensify-classic/getting-started/Security.md +++ b/docs/articles/expensify-classic/getting-started/Security.md @@ -2,4 +2,4 @@ title: Security description: Security --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Using-The-App.md b/docs/articles/expensify-classic/getting-started/Using-The-App.md index 37767ea9d78d..7fa57abbdf61 100644 --- a/docs/articles/expensify-classic/getting-started/Using-The-App.md +++ b/docs/articles/expensify-classic/getting-started/Using-The-App.md @@ -2,4 +2,4 @@ title: Using the App description: Using the App --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/tips-and-tricks.md b/docs/articles/expensify-classic/getting-started/tips-and-tricks.md index d85c7f3a0cb9..4d9150deb4c5 100644 --- a/docs/articles/expensify-classic/getting-started/tips-and-tricks.md +++ b/docs/articles/expensify-classic/getting-started/tips-and-tricks.md @@ -2,4 +2,4 @@ title: Tips and Tricks description: Tips and Tricks --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Bill-dot-com.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Bill-dot-com.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Bill-dot-com.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Bill-dot-com.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md b/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/ADP.md b/docs/articles/expensify-classic/integrations/hr-integrations/ADP.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/ADP.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/ADP.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/Greenhouse.md b/docs/articles/expensify-classic/integrations/hr-integrations/Greenhouse.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/Greenhouse.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/Greenhouse.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/Gusto.md b/docs/articles/expensify-classic/integrations/hr-integrations/Gusto.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/Gusto.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/Gusto.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/QuickBooks-Time.md b/docs/articles/expensify-classic/integrations/hr-integrations/QuickBooks-Time.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/QuickBooks-Time.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/QuickBooks-Time.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/Rippling.md b/docs/articles/expensify-classic/integrations/hr-integrations/Rippling.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/Rippling.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/Rippling.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/Workday.md b/docs/articles/expensify-classic/integrations/hr-integrations/Workday.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/Workday.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/Workday.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/hr-integrations/Zenefits.md b/docs/articles/expensify-classic/integrations/hr-integrations/Zenefits.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/hr-integrations/Zenefits.md +++ b/docs/articles/expensify-classic/integrations/hr-integrations/Zenefits.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md b/docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md +++ b/docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Bolt.md b/docs/articles/expensify-classic/integrations/travel-integrations/Bolt.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Bolt.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Bolt.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md b/docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Grab.md b/docs/articles/expensify-classic/integrations/travel-integrations/Grab.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Grab.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Grab.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md b/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md b/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md b/docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/TrainLine.md b/docs/articles/expensify-classic/integrations/travel-integrations/TrainLine.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/TrainLine.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/TrainLine.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md b/docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md b/docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/TripCatcher.md b/docs/articles/expensify-classic/integrations/travel-integrations/TripCatcher.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/TripCatcher.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/TripCatcher.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Adding-Users.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md index e10e0fafb77d..e107734216f5 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md @@ -2,7 +2,7 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! Kayak.md Lyft.md TrainLine.md TravelPerk.md Trip Actions.md TripCatcher.md Uber.md \ No newline at end of file diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md b/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md index 8c1267068d6b..cea96cfe2057 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md @@ -2,4 +2,4 @@ title: Admins description: Admins --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Categories.md b/docs/articles/expensify-classic/policy-and-domain-settings/Categories.md index 00ade2b9d04f..0db022f400d3 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Categories.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Categories.md @@ -2,4 +2,4 @@ title: Categories description: Categories --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md b/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md b/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/policy-and-domain-settings/Domains-Overview.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domains-Overview.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Domains-Overview.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/policy-and-domain-settings/Expenses.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Expenses.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Expenses.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Invoicing.md b/docs/articles/expensify-classic/policy-and-domain-settings/Invoicing.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Invoicing.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Invoicing.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md b/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Per-Diem.md b/docs/articles/expensify-classic/policy-and-domain-settings/Per-Diem.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Per-Diem.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Per-Diem.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Reimbursement.md b/docs/articles/expensify-classic/policy-and-domain-settings/Reimbursement.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Reimbursement.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Reimbursement.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md b/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/SAML.md b/docs/articles/expensify-classic/policy-and-domain-settings/SAML.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/SAML.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/SAML.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Tags.md b/docs/articles/expensify-classic/policy-and-domain-settings/Tags.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Tags.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Tags.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md b/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md b/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md index 4c91b7095a4a..3ee1c8656b4b 100644 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md +++ b/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md @@ -2,4 +2,4 @@ title: Coming Soon description: Coming Soon --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/send-payments/Pay-Bills.md b/docs/articles/expensify-classic/send-payments/Pay-Bills.md index e319196eb4bd..41c0146126ba 100644 --- a/docs/articles/expensify-classic/send-payments/Pay-Bills.md +++ b/docs/articles/expensify-classic/send-payments/Pay-Bills.md @@ -2,4 +2,4 @@ title: Pay Bills description: Pay Bills --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/send-payments/Pay-Invoices.md b/docs/articles/expensify-classic/send-payments/Pay-Invoices.md index 0ea4d28a731a..e5e6799c268c 100644 --- a/docs/articles/expensify-classic/send-payments/Pay-Invoices.md +++ b/docs/articles/expensify-classic/send-payments/Pay-Invoices.md @@ -2,4 +2,4 @@ title: Pay Invoices description: Pay Invoices --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md index 6c3309310ba8..834d0b159931 100644 --- a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md +++ b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md @@ -2,4 +2,4 @@ title: Reimbursing Reports description: Reimbursing Reports --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md b/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md index 4b1166cc9c00..f61f26d91059 100644 --- a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md +++ b/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md @@ -2,7 +2,7 @@ title: Third Party Payments description: Third Party Payments --- -## Resources Coming Soon! +## Resource Coming Soon! \ No newline at end of file diff --git a/docs/articles/new-expensify/get-paid-back/Request-Money.md b/docs/articles/new-expensify/get-paid-back/Request-Money.md index 55a3f3c8172e..dc6de6656cc9 100644 --- a/docs/articles/new-expensify/get-paid-back/Request-Money.md +++ b/docs/articles/new-expensify/get-paid-back/Request-Money.md @@ -2,4 +2,4 @@ title: Request Money description: Request Money --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md index ed4d127d5c26..aa5f40ee4e5d 100644 --- a/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md +++ b/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md @@ -2,4 +2,4 @@ title: QuickBooks Online description: QuickBooks Online --- -## Resources Coming Soon! +## Resource Coming Soon! diff --git a/docs/assets/images/settings-new-dot.svg b/docs/assets/images/settings-new-dot.svg new file mode 100644 index 000000000000..13338fc72362 --- /dev/null +++ b/docs/assets/images/settings-new-dot.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/images/settings-old-dot.svg b/docs/assets/images/settings-old-dot.svg new file mode 100644 index 000000000000..89302b65c70d --- /dev/null +++ b/docs/assets/images/settings-old-dot.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 0014eb50f5e8..bc37a65393fb 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.73 + 1.3.74 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.73.0 + 1.3.74.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 256676c4de81..83c733238921 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.73 + 1.3.74 CFBundleSignature ???? CFBundleVersion - 1.3.73.0 + 1.3.74.1 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 51b9f6af0e21..ba53d939e46c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -783,7 +783,7 @@ PODS: - React-Core - RNReactNativeHapticFeedback (1.14.0): - React-Core - - RNReanimated (3.4.0): + - RNReanimated (3.5.4): - DoubleConversion - FBLazyVector - glog @@ -1298,7 +1298,7 @@ SPEC CHECKSUMS: rnmapbox-maps: 6f638ec002aa6e906a6f766d69cd45f968d98e64 RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c - RNReanimated: 020859659f64be2d30849a1fe88c821a7c3e0cbf + RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87 RNScreens: d037903436160a4b039d32606668350d2a808806 RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d diff --git a/package-lock.json b/package-lock.json index 68ac5f47123d..92caec1991fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.73-0", + "version": "1.3.74-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.73-0", + "version": "1.3.74-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -50,7 +50,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1609f1848cc0c2528064519c3ea48b4953a708ee", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -90,7 +90,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.84", + "react-native-onyx": "1.0.87", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -99,7 +99,7 @@ "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "3.4.0", + "react-native-reanimated": "3.5.4", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.21.0", @@ -1777,10 +1777,11 @@ } }, "node_modules/@babel/plugin-transform-object-assign": { - "version": "7.18.6", - "license": "MIT", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.22.5.tgz", + "integrity": "sha512-iDhx9ARkXq4vhZ2CYOSnQXkmxkDgosLi3J8Z17mKz7LyzthtkdVchLD7WZ3aXeCuvJDOW3+1I5TpJmwIbF9MKQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -28106,8 +28107,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", - "integrity": "sha512-O0KbaMljSFyoZXcxtx5B2qBL+n0md3wYOrArMVT8N0W+4wKncn+p8/vrmVlUMHe/vDloUbVmUZNdHwOZNWdx3w==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1609f1848cc0c2528064519c3ea48b4953a708ee", + "integrity": "sha512-sUd/ky6xCB/mShVaD2nVkedGL2xy+h6Jf5MfX9GOiYX8wB2D8uZSpqswz515uwcp8RDWrA5wxM2cR6pBXNfgxw==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -41203,9 +41204,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.84", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.84.tgz", - "integrity": "sha512-qQ+o+qS5ucZLbKbG5kI0UsC42N4h1Pprg/1D7PqjDeVanS3iUv33rT4fbrHuar77g0DSTA1/M8bC2WmYrShS9A==", + "version": "1.0.87", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.87.tgz", + "integrity": "sha512-6mIhobSwpClDDGnJm9XEdjnpEdWfFesJ18J8Ifsb4tL6AVi+uxos5bnlZcOoMbtlUk3UozrgSyTjMfFrkD/aZA==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -41334,9 +41335,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.4.0.tgz", - "integrity": "sha512-B5cZJseoIkYlZTRBRN0xuU1NBxUza/6GSHhiEBQfbOufWVlUMMcWUecIRVglW49l8d2wXbfCdQlNyVoFqmHkaQ==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz", + "integrity": "sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -41356,7 +41357,8 @@ }, "node_modules/react-native-reanimated/node_modules/convert-source-map": { "version": "2.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/react-native-render-html": { "version": "6.3.1", @@ -49776,9 +49778,11 @@ } }, "@babel/plugin-transform-object-assign": { - "version": "7.18.6", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.22.5.tgz", + "integrity": "sha512-iDhx9ARkXq4vhZ2CYOSnQXkmxkDgosLi3J8Z17mKz7LyzthtkdVchLD7WZ3aXeCuvJDOW3+1I5TpJmwIbF9MKQ==", "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-object-rest-spread": { @@ -68254,9 +68258,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", - "integrity": "sha512-O0KbaMljSFyoZXcxtx5B2qBL+n0md3wYOrArMVT8N0W+4wKncn+p8/vrmVlUMHe/vDloUbVmUZNdHwOZNWdx3w==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#1609f1848cc0c2528064519c3ea48b4953a708ee", + "integrity": "sha512-sUd/ky6xCB/mShVaD2nVkedGL2xy+h6Jf5MfX9GOiYX8wB2D8uZSpqswz515uwcp8RDWrA5wxM2cR6pBXNfgxw==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#1609f1848cc0c2528064519c3ea48b4953a708ee", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", @@ -77265,9 +77269,9 @@ } }, "react-native-onyx": { - "version": "1.0.84", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.84.tgz", - "integrity": "sha512-qQ+o+qS5ucZLbKbG5kI0UsC42N4h1Pprg/1D7PqjDeVanS3iUv33rT4fbrHuar77g0DSTA1/M8bC2WmYrShS9A==", + "version": "1.0.87", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.87.tgz", + "integrity": "sha512-6mIhobSwpClDDGnJm9XEdjnpEdWfFesJ18J8Ifsb4tL6AVi+uxos5bnlZcOoMbtlUk3UozrgSyTjMfFrkD/aZA==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -77328,9 +77332,9 @@ "requires": {} }, "react-native-reanimated": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.4.0.tgz", - "integrity": "sha512-B5cZJseoIkYlZTRBRN0xuU1NBxUza/6GSHhiEBQfbOufWVlUMMcWUecIRVglW49l8d2wXbfCdQlNyVoFqmHkaQ==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz", + "integrity": "sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg==", "requires": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -77339,7 +77343,9 @@ }, "dependencies": { "convert-source-map": { - "version": "2.0.0" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" } } }, diff --git a/package.json b/package.json index 0f3420b6bb73..bdd102ab80fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.73-0", + "version": "1.3.74-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -62,9 +62,9 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", - "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@kie/act-js": "^2.0.1", "@kie/mock-github": "^1.0.0", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "7.4.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-camera-roll/camera-roll": "5.4.0", @@ -82,9 +82,9 @@ "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", + "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.2.6", "awesome-phonenumber": "^5.4.0", - "@types/node": "^18.14.0", "babel-plugin-transform-remove-console": "^6.9.4", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", @@ -93,7 +93,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1609f1848cc0c2528064519c3ea48b4953a708ee", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -133,7 +133,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.84", + "react-native-onyx": "1.0.87", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -142,7 +142,7 @@ "react-native-plaid-link-sdk": "^10.0.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", - "react-native-reanimated": "3.4.0", + "react-native-reanimated": "3.5.4", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.21.0", diff --git a/src/CONST.ts b/src/CONST.ts index eed1b98ae551..c6ae9cc5928d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -123,8 +123,6 @@ const CONST = { }, }, - RIGHT_MODAL_BACKGROUND_OVERLAY_OPACITY: 0.4, - NEW_EXPENSIFY_URL: ACTIVE_EXPENSIFY_URL, APP_DOWNLOAD_LINKS: { ANDROID: `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE_NAME}`, @@ -1145,6 +1143,7 @@ const CONST = { DISTANCE_UNIT_KILOMETERS: 'km', MILEAGE_IRS_RATE: 0.655, DEFAULT_RATE: 'Default Rate', + RATE_DECIMALS: 3, }, TERMS: { @@ -1360,6 +1359,7 @@ const CONST = { MERCHANT: 'merchant', CATEGORY: 'category', RECEIPT: 'receipt', + TAG: 'tag', }, FOOTER: { EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8f95dff079fc..6649a33fe15e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -329,7 +329,7 @@ type OnyxValues = { [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record; - [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoints[]; + [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; [ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData; [ONYXKEYS.IS_PLAID_DISABLED]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index feead4890114..1065b525f83a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,235 +1,155 @@ import {ValueOf} from 'type-fest'; -import * as Url from './libs/Url'; import CONST from './CONST'; /** * This is a file containing constants for all of the routes we want to be able to go to */ -type ParseReportRouteParams = { - reportID: string; - isSubReportPageRoute: boolean; -}; +// prettier-ignore +export default { + HOME: '', + /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ + CONCIERGE: 'concierge', + FLAG_COMMENT: { route: 'flag/:reportID/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`}, + SEARCH: 'search', + DETAILS: { route: 'details', getRoute: (login: string) => `details?login=${encodeURIComponent(login)}`}, + PROFILE: { route: 'a/:accountID', getRoute: (accountID: string | number, backTo = '') => { + const backToParam = backTo ? `?backTo=${encodeURIComponent(backTo)}` : ''; + return `a/${accountID}${backToParam}`; + }}, -const REPORT = 'r'; -const IOU_REQUEST = 'request/new'; -const IOU_SEND = 'send/new'; -const NEW_TASK = 'new/task'; -const SETTINGS_PERSONAL_DETAILS = 'settings/profile/personal-details'; -const SETTINGS_CONTACT_METHODS = 'settings/profile/contact-methods'; -const SETTINGS_STATUS = 'settings/profile/status'; -const SETTINGS_STATUS_SET = 'settings/profile/status/set'; + TRANSITION_BETWEEN_APPS: 'transition', + VALIDATE_LOGIN: 'v/:accountID/:validateCode', + GET_ASSISTANCE: { route: 'get-assistance/:taskID', getRoute: (taskID: string) => `get-assistance/${taskID}`}, + UNLINK_LOGIN: 'u/:accountID/:validateCode', + APPLE_SIGN_IN: 'sign-in-with-apple', + GOOGLE_SIGN_IN: 'sign-in-with-google', + DESKTOP_SIGN_IN_REDIRECT: 'desktop-signin-redirect', + // This is a special validation URL that will take the user to /workspace/new after validation. This is used + // when linking users from e.com in order to share a session in this app. + ENABLE_PAYMENTS: 'enable-payments', + WALLET_STATEMENT_WITH_DATE: 'statements/:yearMonth', + SIGN_IN_MODAL: 'sign-in-modal', -export default { - BANK_ACCOUNT: 'bank-account', - BANK_ACCOUNT_NEW: 'bank-account/new', - BANK_ACCOUNT_WITH_STEP_TO_OPEN: 'bank-account/:stepToOpen?', - BANK_ACCOUNT_PERSONAL: 'bank-account/personal', - getBankAccountRoute: (stepToOpen = '', policyID = '', backTo = ''): string => { + BANK_ACCOUNT: 'bank-account', + BANK_ACCOUNT_NEW: 'bank-account/new', + BANK_ACCOUNT_PERSONAL: 'bank-account/personal', + BANK_ACCOUNT_WITH_STEP_TO_OPEN: { route: 'bank-account/:stepToOpen?', getRoute: (stepToOpen = '', policyID = '', backTo = ''): string => { const backToParam = backTo ? `&backTo=${encodeURIComponent(backTo)}` : ''; return `bank-account/${stepToOpen}?policyID=${policyID}${backToParam}`; - }, - HOME: '', - SETTINGS: 'settings', - SETTINGS_PROFILE: 'settings/profile', - SETTINGS_SHARE_CODE: 'settings/shareCode', - SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', - SETTINGS_TIMEZONE: 'settings/profile/timezone', - SETTINGS_TIMEZONE_SELECT: 'settings/profile/timezone/select', - SETTINGS_PRONOUNS: 'settings/profile/pronouns', - SETTINGS_LOUNGE_ACCESS: 'settings/profile/lounge-access', - SETTINGS_PREFERENCES: 'settings/preferences', - SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', - SETTINGS_LANGUAGE: 'settings/preferences/language', - SETTINGS_THEME: 'settings/preferences/theme', - SETTINGS_WORKSPACES: 'settings/workspaces', - SETTINGS_SECURITY: 'settings/security', - SETTINGS_CLOSE: 'settings/security/closeAccount', - SETTINGS_ABOUT: 'settings/about', - SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', - SETTINGS_WALLET: 'settings/wallet', - SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', - SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', - SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', - getSettingsAddLoginRoute: (type: string) => `settings/addlogin/${type}`, - SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', - SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', - SETTINGS_PERSONAL_DETAILS, - SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: `${SETTINGS_PERSONAL_DETAILS}/legal-name`, - SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: `${SETTINGS_PERSONAL_DETAILS}/date-of-birth`, - SETTINGS_PERSONAL_DETAILS_ADDRESS: `${SETTINGS_PERSONAL_DETAILS}/address`, - SETTINGS_CONTACT_METHODS, - SETTINGS_CONTACT_METHOD_DETAILS: `${SETTINGS_CONTACT_METHODS}/:contactMethod/details`, - getEditContactMethodRoute: (contactMethod: string) => `${SETTINGS_CONTACT_METHODS}/${encodeURIComponent(contactMethod)}/details`, - SETTINGS_NEW_CONTACT_METHOD: `${SETTINGS_CONTACT_METHODS}/new`, - SETTINGS_2FA: 'settings/security/two-factor-auth', - SETTINGS_STATUS, - SETTINGS_STATUS_SET, - NEW: 'new', - NEW_CHAT: 'new/chat', - NEW_ROOM: 'new/room', - NEW_TASK, - REPORT, - REPORT_WITH_ID: 'r/:reportID?/:reportActionID?', - EDIT_REQUEST: 'r/:threadReportID/edit/:field', - getEditRequestRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}`, - EDIT_CURRENCY_REQUEST: 'r/:threadReportID/edit/currency', - getEditRequestCurrencyRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}`, - getReportRoute: (reportID: string) => `r/${reportID}`, - REPORT_WITH_ID_DETAILS_SHARE_CODE: 'r/:reportID/details/shareCode', - getReportShareCodeRoute: (reportID: string) => `r/${reportID}/details/shareCode`, - REPORT_ATTACHMENTS: 'r/:reportID/attachment', - getReportAttachmentRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}`, + }}, - /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ - CONCIERGE: 'concierge', + SETTINGS: 'settings', + SETTINGS_PROFILE: 'settings/profile', + SETTINGS_SHARE_CODE: 'settings/shareCode', + SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', + SETTINGS_TIMEZONE: 'settings/profile/timezone', + SETTINGS_TIMEZONE_SELECT: 'settings/profile/timezone/select', + SETTINGS_PRONOUNS: 'settings/profile/pronouns', + SETTINGS_LOUNGE_ACCESS: 'settings/profile/lounge-access', + SETTINGS_PREFERENCES: 'settings/preferences', + SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', + SETTINGS_LANGUAGE: 'settings/preferences/language', + SETTINGS_THEME: 'settings/preferences/theme', + SETTINGS_WORKSPACES: 'settings/workspaces', + SETTINGS_SECURITY: 'settings/security', + SETTINGS_CLOSE: 'settings/security/closeAccount', + SETTINGS_ABOUT: 'settings/about', + SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', + SETTINGS_WALLET: 'settings/wallet', + SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', + SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', + SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', + SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', + SETTINGS_PERSONAL_DETAILS: 'settings/profile/personal-details', + SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name', + SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: 'settings/profile/personal-details/date-of-birth', + SETTINGS_PERSONAL_DETAILS_ADDRESS: 'settings/profile/personal-details/address', + SETTINGS_CONTACT_METHODS: 'settings/profile/contact-methods', + SETTINGS_CONTACT_METHOD_DETAILS: { route: 'settings/profile/contact-methods/:contactMethod/details', getRoute: (contactMethod: string) => `settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`}, + SETTINGS_NEW_CONTACT_METHOD: 'settings/profile/contact-methods/new', + SETTINGS_2FA: 'settings/security/two-factor-auth', + SETTINGS_STATUS: 'settings/profile/status', + SETTINGS_STATUS_SET: 'settings/profile/status/set', - IOU_REQUEST, - IOU_SEND, + NEW: 'new', + NEW_CHAT: 'new/chat', + NEW_ROOM: 'new/room', - // To see the available iouType, please refer to CONST.IOU.MONEY_REQUEST_TYPE - MONEY_REQUEST: ':iouType/new/:reportID?', - MONEY_REQUEST_AMOUNT: ':iouType/new/amount/:reportID?', - MONEY_REQUEST_PARTICIPANTS: ':iouType/new/participants/:reportID?', - MONEY_REQUEST_CONFIRMATION: ':iouType/new/confirmation/:reportID?', - MONEY_REQUEST_DATE: ':iouType/new/date/:reportID?', - MONEY_REQUEST_CURRENCY: ':iouType/new/currency/:reportID?', - MONEY_REQUEST_DESCRIPTION: ':iouType/new/description/:reportID?', - MONEY_REQUEST_CATEGORY: ':iouType/new/category/:reportID?', - MONEY_REQUEST_TAG: ':iouType/new/tag/:reportID?', - MONEY_REQUEST_MERCHANT: ':iouType/new/merchant/:reportID?', - MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', - MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', - MONEY_REQUEST_DISTANCE_TAB: ':iouType/new/:reportID?/distance', - MONEY_REQUEST_WAYPOINT: ':iouType/new/waypoint/:waypointIndex', - MONEY_REQUEST_ADDRESS: ':iouType/new/address/:reportID?', - IOU_SEND_ADD_BANK_ACCOUNT: `${IOU_SEND}/add-bank-account`, - IOU_SEND_ADD_DEBIT_CARD: `${IOU_SEND}/add-debit-card`, - IOU_SEND_ENABLE_PAYMENTS: `${IOU_SEND}/enable-payments`, - getMoneyRequestRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}`, - getMoneyRequestAmountRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}`, - getMoneyRequestParticipantsRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}`, - getMoneyRequestConfirmationRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}`, - getMoneyRequestCreatedRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}`, - getMoneyRequestCurrencyRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}`, - getMoneyRequestDescriptionRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}`, - getMoneyRequestCategoryRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}`, - getMoneyRequestMerchantRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`, - getMoneyRequestDistanceTabRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`, - getMoneyRequestWaypointRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`, - getMoneyRequestAddressRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}`, - getMoneyRequestTagRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`, - SPLIT_BILL_DETAILS: `r/:reportID/split/:reportActionID`, - getSplitBillDetailsRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`, - getNewTaskRoute: (reportID: string) => `${NEW_TASK}/${reportID}`, - NEW_TASK_WITH_REPORT_ID: `${NEW_TASK}/:reportID?`, - TASK_TITLE: 'r/:reportID/title', - TASK_DESCRIPTION: 'r/:reportID/description', - TASK_ASSIGNEE: 'r/:reportID/assignee', - getTaskReportTitleRoute: (reportID: string) => `r/${reportID}/title`, - getTaskReportDescriptionRoute: (reportID: string) => `r/${reportID}/description`, - getTaskReportAssigneeRoute: (reportID: string) => `r/${reportID}/assignee`, - NEW_TASK_ASSIGNEE: `${NEW_TASK}/assignee`, - NEW_TASK_SHARE_DESTINATION: `${NEW_TASK}/share-destination`, - NEW_TASK_DETAILS: `${NEW_TASK}/details`, - NEW_TASK_TITLE: `${NEW_TASK}/title`, - NEW_TASK_DESCRIPTION: `${NEW_TASK}/description`, - FLAG_COMMENT: `flag/:reportID/:reportActionID`, - getFlagCommentRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`, - SEARCH: 'search', - TEACHERS_UNITE: 'teachersunite', - I_KNOW_A_TEACHER: 'teachersunite/i-know-a-teacher', - I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', - INTRO_SCHOOL_PRINCIPAL: 'teachersunite/intro-school-principal', - DETAILS: 'details', - getDetailsRoute: (login: string) => `details?login=${encodeURIComponent(login)}`, - PROFILE: 'a/:accountID', - getProfileRoute: (accountID: string | number, backTo = '') => { - const backToParam = backTo ? `?backTo=${encodeURIComponent(backTo)}` : ''; - return `a/${accountID}${backToParam}`; - }, - REPORT_PARTICIPANTS: 'r/:reportID/participants', - getReportParticipantsRoute: (reportID: string) => `r/${reportID}/participants`, - REPORT_WITH_ID_DETAILS: 'r/:reportID/details', - getReportDetailsRoute: (reportID: string) => `r/${reportID}/details`, - REPORT_SETTINGS: 'r/:reportID/settings', - getReportSettingsRoute: (reportID: string) => `r/${reportID}/settings`, - REPORT_SETTINGS_ROOM_NAME: 'r/:reportID/settings/room-name', - getReportSettingsRoomNameRoute: (reportID: string) => `r/${reportID}/settings/room-name`, - REPORT_SETTINGS_NOTIFICATION_PREFERENCES: 'r/:reportID/settings/notification-preferences', - getReportSettingsNotificationPreferencesRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences`, - REPORT_WELCOME_MESSAGE: 'r/:reportID/welcomeMessage', - getReportWelcomeMessageRoute: (reportID: string) => `r/${reportID}/welcomeMessage`, - REPORT_SETTINGS_WRITE_CAPABILITY: 'r/:reportID/settings/who-can-post', - getReportSettingsWriteCapabilityRoute: (reportID: string) => `r/${reportID}/settings/who-can-post`, - TRANSITION_BETWEEN_APPS: 'transition', - VALIDATE_LOGIN: 'v/:accountID/:validateCode', - GET_ASSISTANCE: 'get-assistance/:taskID', - getGetAssistanceRoute: (taskID: string) => `get-assistance/${taskID}`, - UNLINK_LOGIN: 'u/:accountID/:validateCode', + REPORT: 'r', + REPORT_WITH_ID: { route: 'r/:reportID?/:reportActionID?', getRoute: (reportID: string) => `r/${reportID}`}, + EDIT_REQUEST: { route: 'r/:threadReportID/edit/:field', getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}`}, + EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}`}, + REPORT_WITH_ID_DETAILS_SHARE_CODE: { route: 'r/:reportID/details/shareCode', getRoute: (reportID: string) => `r/${reportID}/details/shareCode`}, + REPORT_ATTACHMENTS: { route: 'r/:reportID/attachment', getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}`}, + REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', getRoute: (reportID: string) => `r/${reportID}/participants`}, + REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', getRoute: (reportID: string) => `r/${reportID}/details`}, + REPORT_SETTINGS: { route: 'r/:reportID/settings', getRoute: (reportID: string) => `r/${reportID}/settings`}, + REPORT_SETTINGS_ROOM_NAME: { route: 'r/:reportID/settings/room-name', getRoute: (reportID: string) => `r/${reportID}/settings/room-name`}, + REPORT_SETTINGS_NOTIFICATION_PREFERENCES: {route: 'r/:reportID/settings/notification-preferences', getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences`}, + REPORT_SETTINGS_WRITE_CAPABILITY: { route: 'r/:reportID/settings/who-can-post', getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post`}, + REPORT_WELCOME_MESSAGE: { route: 'r/:reportID/welcomeMessage', getRoute: (reportID: string) => `r/${reportID}/welcomeMessage`}, + SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`}, + TASK_TITLE: { route: 'r/:reportID/title', getRoute: (reportID: string) => `r/${reportID}/title`}, + TASK_DESCRIPTION: { route: 'r/:reportID/description', getRoute: (reportID: string) => `r/${reportID}/description`}, + TASK_ASSIGNEE: { route: 'r/:reportID/assignee', getRoute: (reportID: string) => `r/${reportID}/assignee`}, + PRIVATE_NOTES_VIEW: { route: 'r/:reportID/notes/:accountID', getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}`}, + PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', getRoute: (reportID: string) => `r/${reportID}/notes`}, + PRIVATE_NOTES_EDIT: { route: 'r/:reportID/notes/:accountID/edit', getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`}, - APPLE_SIGN_IN: 'sign-in-with-apple', - GOOGLE_SIGN_IN: 'sign-in-with-google', - DESKTOP_SIGN_IN_REDIRECT: 'desktop-signin-redirect', - - // Routes related to private notes added to the report - PRIVATE_NOTES_VIEW: 'r/:reportID/notes/:accountID', - getPrivateNotesViewRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}`, - PRIVATE_NOTES_LIST: 'r/:reportID/notes', - getPrivateNotesListRoute: (reportID: string) => `r/${reportID}/notes`, - PRIVATE_NOTES_EDIT: 'r/:reportID/notes/:accountID/edit', - getPrivateNotesEditRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`, + // To see the available iouType, please refer to CONST.IOU.MONEY_REQUEST_TYPE + MONEY_REQUEST: { route: ':iouType/new/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}`}, + MONEY_REQUEST_AMOUNT: { route: ':iouType/new/amount/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}`}, + MONEY_REQUEST_PARTICIPANTS: { route: ':iouType/new/participants/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}`}, + MONEY_REQUEST_CONFIRMATION: { route: ':iouType/new/confirmation/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}`}, + MONEY_REQUEST_DATE: { route: ':iouType/new/date/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}`}, + MONEY_REQUEST_CURRENCY: { route: ':iouType/new/currency/:reportID?', getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}`}, + MONEY_REQUEST_DESCRIPTION: { route: ':iouType/new/description/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}`}, + MONEY_REQUEST_CATEGORY: { route: ':iouType/new/category/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}`}, + MONEY_REQUEST_TAG: { route: ':iouType/new/tag/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`}, + MONEY_REQUEST_MERCHANT: { route: ':iouType/new/merchant/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`}, + MONEY_REQUEST_WAYPOINT: { route: ':iouType/new/waypoint/:waypointIndex', getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`}, + MONEY_REQUEST_RECEIPT: { route: ':iouType/new/receipt/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}`}, + MONEY_REQUEST_ADDRESS: { route: ':iouType/new/address/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}`}, + MONEY_REQUEST_DISTANCE_TAB: { route: ':iouType/new/:reportID?/distance', getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`}, + MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', + MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', - // This is a special validation URL that will take the user to /workspace/new after validation. This is used - // when linking users from e.com in order to share a session in this app. - ENABLE_PAYMENTS: 'enable-payments', - WALLET_STATEMENT_WITH_DATE: 'statements/:yearMonth', - getWalletStatementWithDateRoute: (yearMonth: string) => `statements/${yearMonth}`, - WORKSPACE_NEW: 'workspace/new', - WORKSPACE_INITIAL: 'workspace/:policyID', - WORKSPACE_INVITE: 'workspace/:policyID/invite', - WORKSPACE_INVITE_MESSAGE: 'workspace/:policyID/invite-message', - WORKSPACE_SETTINGS: 'workspace/:policyID/settings', - WORKSPACE_CARD: 'workspace/:policyID/card', - WORKSPACE_REIMBURSE: 'workspace/:policyID/reimburse', - WORKSPACE_RATE_AND_UNIT: 'workspace/:policyID/rateandunit', - WORKSPACE_BILLS: 'workspace/:policyID/bills', - WORKSPACE_INVOICES: 'workspace/:policyID/invoices', - WORKSPACE_TRAVEL: 'workspace/:policyID/travel', - WORKSPACE_MEMBERS: 'workspace/:policyID/members', - WORKSPACE_NEW_ROOM: 'workspace/new-room', - getWorkspaceInitialRoute: (policyID: string) => `workspace/${policyID}`, - getWorkspaceInviteRoute: (policyID: string) => `workspace/${policyID}/invite`, - getWorkspaceInviteMessageRoute: (policyID: string) => `workspace/${policyID}/invite-message`, - getWorkspaceSettingsRoute: (policyID: string) => `workspace/${policyID}/settings`, - getWorkspaceCardRoute: (policyID: string) => `workspace/${policyID}/card`, - getWorkspaceReimburseRoute: (policyID: string) => `workspace/${policyID}/reimburse`, - getWorkspaceRateAndUnitRoute: (policyID: string) => `workspace/${policyID}/rateandunit`, - getWorkspaceBillsRoute: (policyID: string) => `workspace/${policyID}/bills`, - getWorkspaceInvoicesRoute: (policyID: string) => `workspace/${policyID}/invoices`, - getWorkspaceTravelRoute: (policyID: string) => `workspace/${policyID}/travel`, - getWorkspaceMembersRoute: (policyID: string) => `workspace/${policyID}/members`, + IOU_REQUEST: 'request/new', + IOU_SEND: 'send/new', + IOU_SEND_ADD_BANK_ACCOUNT: 'send/new/add-bank-account', + IOU_SEND_ADD_DEBIT_CARD: 'send/new/add-debit-card', + IOU_SEND_ENABLE_PAYMENTS: 'send/new/enable-payments', - // These are some on-off routes that will be removed once they're no longer needed (see GH issues for details) - SAASTR: 'saastr', - SBE: 'sbe', + NEW_TASK: 'new/task', + NEW_TASK_WITH_REPORT_ID: 'new/task/:reportID?', + NEW_TASK_ASSIGNEE: 'new/task/assignee', + NEW_TASK_SHARE_DESTINATION: 'new/task/share-destination', + NEW_TASK_DETAILS: 'new/task/details', + NEW_TASK_TITLE: 'new/task/title', + NEW_TASK_DESCRIPTION: 'new/task/description', - parseReportRouteParams: (route: string): ParseReportRouteParams => { - let parsingRoute = route; - if (parsingRoute.at(0) === '/') { - // remove the first slash - parsingRoute = parsingRoute.slice(1); - } + TEACHERS_UNITE: 'teachersunite', + I_KNOW_A_TEACHER: 'teachersunite/i-know-a-teacher', + I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', + INTRO_SCHOOL_PRINCIPAL: 'teachersunite/intro-school-principal', - if (!parsingRoute.startsWith(Url.addTrailingForwardSlash(REPORT))) { - return {reportID: '', isSubReportPageRoute: false}; - } + WORKSPACE_NEW: 'workspace/new', + WORKSPACE_NEW_ROOM: 'workspace/new-room', + WORKSPACE_INITIAL: { route: 'workspace/:policyID', getRoute: (policyID: string) => `workspace/${policyID}`}, + WORKSPACE_INVITE: { route: 'workspace/:policyID/invite', getRoute: (policyID: string) => `workspace/${policyID}/invite`}, + WORKSPACE_INVITE_MESSAGE: { route: 'workspace/:policyID/invite-message', getRoute: (policyID: string) => `workspace/${policyID}/invite-message`}, + WORKSPACE_SETTINGS: { route: 'workspace/:policyID/settings', getRoute: (policyID: string) => `workspace/${policyID}/settings`}, + WORKSPACE_CARD: { route: 'workspace/:policyID/card', getRoute: (policyID: string) => `workspace/${policyID}/card`}, + WORKSPACE_REIMBURSE: { route: 'workspace/:policyID/reimburse', getRoute: (policyID: string) => `workspace/${policyID}/reimburse`}, + WORKSPACE_RATE_AND_UNIT: { route: 'workspace/:policyID/rateandunit', getRoute: (policyID: string) => `workspace/${policyID}/rateandunit`}, + WORKSPACE_BILLS: { route: 'workspace/:policyID/bills', getRoute: (policyID: string) => `workspace/${policyID}/bills`}, + WORKSPACE_INVOICES: { route: 'workspace/:policyID/invoices', getRoute: (policyID: string) => `workspace/${policyID}/invoices`}, + WORKSPACE_TRAVEL: { route: 'workspace/:policyID/travel', getRoute: (policyID: string) => `workspace/${policyID}/travel`}, + WORKSPACE_MEMBERS: { route: 'workspace/:policyID/members', getRoute: (policyID: string) => `workspace/${policyID}/members`}, - const pathSegments = parsingRoute.split('/'); - return { - reportID: pathSegments[1], - isSubReportPageRoute: pathSegments.length > 2, - }; - }, - SIGN_IN_MODAL: 'sign-in-modal', + // These are some on-off routes that will be removed once they're no longer needed (see GH issues for details) + SAASTR: 'saastr', + SBE: 'sbe', } as const; diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 946b5e2ddec9..3f89f4032061 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -363,7 +363,7 @@ function AttachmentModal(props) { icon: Expensicons.Camera, text: props.translate('common.replace'), onSelected: () => { - onModalHideCallbackRef.current = () => Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); + onModalHideCallbackRef.current = () => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); closeModal(); }, }, diff --git a/src/components/AvatarWithDisplayName.js b/src/components/AvatarWithDisplayName.js index e82dbe05a6d0..81eace444de4 100644 --- a/src/components/AvatarWithDisplayName.js +++ b/src/components/AvatarWithDisplayName.js @@ -57,12 +57,12 @@ const defaultProps = { const showActorDetails = (report) => { if (ReportUtils.isExpenseReport(report)) { - Navigation.navigate(ROUTES.getProfileRoute(report.ownerAccountID)); + Navigation.navigate(ROUTES.PROFILE.getRoute(report.ownerAccountID)); return; } if (ReportUtils.isIOUReport(report)) { - Navigation.navigate(ROUTES.getReportParticipantsRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report.reportID)); return; } @@ -71,13 +71,13 @@ const showActorDetails = (report) => { const actorAccountID = lodashGet(parentReportAction, 'actorAccountID', -1); // in an ideal situation account ID won't be 0 if (actorAccountID > 0) { - Navigation.navigate(ROUTES.getProfileRoute(actorAccountID)); + Navigation.navigate(ROUTES.PROFILE.getRoute(actorAccountID)); return; } } // report detail route is added as fallback but based on the current implementation this route won't be executed - Navigation.navigate(ROUTES.getReportDetailsRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); }; function AvatarWithDisplayName(props) { @@ -85,12 +85,13 @@ function AvatarWithDisplayName(props) { const subtitle = ReportUtils.getChatRoomSubtitle(props.report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(props.report) || ReportUtils.isMoneyRequest(props.report); - const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policy, true); + const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([props.report.ownerAccountID], props.personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(_.values(ownerPersonalDetails), false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(props.report); const isExpenseRequest = ReportUtils.isExpenseRequest(props.report); const defaultSubscriptSize = isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : props.size; + const avatarBorderColor = props.isAnonymous ? themeColors.highlightBG : themeColors.componentBG; return ( @@ -103,7 +104,7 @@ function AvatarWithDisplayName(props) { > {shouldShowSubscriptAvatar ? ( )} diff --git a/src/components/BaseMiniContextMenuItem.js b/src/components/BaseMiniContextMenuItem.js index acf5d165d7c7..0e9085b54c17 100644 --- a/src/components/BaseMiniContextMenuItem.js +++ b/src/components/BaseMiniContextMenuItem.js @@ -8,6 +8,8 @@ import getButtonState from '../libs/getButtonState'; import variables from '../styles/variables'; import Tooltip from './Tooltip'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; +import ReportActionComposeFocusManager from '../libs/ReportActionComposeFocusManager'; +import DomUtils from '../libs/DomUtils'; const propTypes = { /** @@ -53,7 +55,14 @@ function BaseMiniContextMenuItem(props) { e.preventDefault()} + onMouseDown={(e) => { + if (!ReportActionComposeFocusManager.isFocused() && !ReportActionComposeFocusManager.isEditFocused()) { + DomUtils.getActiveElement().blur(); + return; + } + + e.preventDefault(); + }} accessibilityLabel={props.tooltipText} style={({hovered, pressed}) => [ styles.reportActionContextMenuMiniButton, diff --git a/src/components/Button/index.js b/src/components/Button/index.js index c16860344837..4ca933a45d6f 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.js @@ -145,7 +145,7 @@ const defaultProps = { style: [], innerStyles: [], textStyles: [], - shouldUseDefaultHover: false, + shouldUseDefaultHover: true, success: false, danger: false, children: null, @@ -306,6 +306,7 @@ class Button extends Component { ]} nativeID={this.props.nativeID} accessibilityLabel={this.props.accessibilityLabel} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} hoverDimmingValue={1} > {this.renderContent()} diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index 90f72f183815..ef8b1d71ad1d 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -31,21 +31,36 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC ]; }, [selectedCategory]); + const sections = useMemo(() => { + const {categoryOptions} = OptionsListUtils.getFilteredOptions( + {}, + {}, + [], + searchValue, + selectedOptions, + [], + false, + false, + true, + policyCategories, + policyRecentlyUsedCategories, + false, + ); + + return categoryOptions; + }, [policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions]); + const initialFocusedIndex = useMemo(() => { - if (isCategoriesCountBelowThreshold && selectedOptions.length > 0) { - return _.chain(policyCategories) - .values() - .findIndex((category) => category.name === selectedOptions[0].name, true) - .value(); - } + let categoryInitialFocusedIndex = 0; - return 0; - }, [policyCategories, selectedOptions, isCategoriesCountBelowThreshold]); + if (!_.isEmpty(searchValue) || isCategoriesCountBelowThreshold) { + const index = _.findIndex(lodashGet(sections, '[0].data', []), (category) => category.searchText === selectedCategory); - const sections = useMemo( - () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions, - [policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions], - ); + categoryInitialFocusedIndex = index === -1 ? 0 : index; + } + + return categoryInitialFocusedIndex; + }, [selectedCategory, searchValue, isCategoriesCountBelowThreshold, sections]); const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, searchValue); const shouldShowTextInput = !isCategoriesCountBelowThreshold; diff --git a/src/components/ConfirmContent.js b/src/components/ConfirmContent.js index ab3e23d6b1c1..469efb4f25d9 100644 --- a/src/components/ConfirmContent.js +++ b/src/components/ConfirmContent.js @@ -133,7 +133,6 @@ function ConfirmContent(props) { style={[styles.mt3, styles.noSelect]} onPress={props.onCancel} text={props.cancelText || translate('common.no')} - shouldUseDefaultHover /> )} @@ -144,7 +143,6 @@ function ConfirmContent(props) { style={[styles.noSelect, styles.flex1]} onPress={props.onCancel} text={props.cancelText || translate('common.no')} - shouldUseDefaultHover medium /> )} diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js index 4ac49a5889a8..5e9b73f2eb3a 100644 --- a/src/components/DistanceRequest.js +++ b/src/components/DistanceRequest.js @@ -189,12 +189,12 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, useEffect(updateGradientVisibility, [scrollContainerHeight, scrollContentHeight]); const navigateBack = () => { - Navigation.goBack(isEditing ? ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID) : ROUTES.HOME); + Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); }; const navigateToNextPage = () => { if (isEditing) { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); return; } @@ -237,7 +237,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, secondaryIcon={waypointIcon} secondaryIconFill={theme.icon} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.getMoneyRequestWaypointRoute('request', index))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_WAYPOINT.getRoute('request', index))} key={key} /> ); @@ -263,7 +263,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, icon={Expensicons.Plus} onPress={() => { const newIndex = _.size(lodashGet(transaction, 'comment.waypoints', {})); - Navigation.navigate(ROUTES.getMoneyRequestWaypointRoute('request', newIndex)); + Navigation.navigate(ROUTES.MONEY_REQUEST_WAYPOINT.getRoute('request', newIndex)); }} text={translate('distance.addStop')} isDisabled={numberOfWaypoints === MAX_WAYPOINTS} @@ -297,8 +297,9 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, success style={[styles.w100, styles.mb4, styles.ph4, styles.flexShrink0]} onPress={navigateToNextPage} - isDisabled={_.size(validatedWaypoints) < 2 || hasRouteError} + isDisabled={_.size(validatedWaypoints) < 2 || hasRouteError || isLoadingRoute} text={translate('common.next')} + isLoading={isLoadingRoute || shouldFetchRoute} /> ); diff --git a/src/components/Form/FormContext.js b/src/components/Form/FormContext.js new file mode 100644 index 000000000000..40edaa7cca69 --- /dev/null +++ b/src/components/Form/FormContext.js @@ -0,0 +1,4 @@ +import {createContext} from 'react'; + +const FormContext = createContext({}); +export default FormContext; diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js new file mode 100644 index 000000000000..408f8c2c2b7f --- /dev/null +++ b/src/components/Form/FormProvider.js @@ -0,0 +1,260 @@ +import React, {createRef, useCallback, useMemo, useRef, useState} from 'react'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import Visibility from '../../libs/Visibility'; +import * as FormActions from '../../libs/actions/FormActions'; +import FormContext from './FormContext'; +import FormWrapper from './FormWrapper'; +import compose from '../../libs/compose'; +import {withNetwork} from '../OnyxProvider'; +import stylePropTypes from '../../styles/stylePropTypes'; +import networkPropTypes from '../networkPropTypes'; + +const propTypes = { + /** A unique Onyx key identifying the form */ + formID: PropTypes.string.isRequired, + + /** Text to be displayed in the submit button */ + submitButtonText: PropTypes.string.isRequired, + + /** Controls the submit button's visibility */ + isSubmitButtonVisible: PropTypes.bool, + + /** Callback to validate the form */ + validate: PropTypes.func, + + /** Callback to submit the form */ + onSubmit: PropTypes.func.isRequired, + + /** Children to render. */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /* Onyx Props */ + + /** Contains the form state that must be accessed outside of the component */ + formState: PropTypes.shape({ + /** Controls the loading state of the form */ + isLoading: PropTypes.bool, + + /** Server side errors keyed by microtime */ + errors: PropTypes.objectOf(PropTypes.string), + + /** Field-specific server side errors keyed by microtime */ + errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + }), + + /** Should the button be enabled when offline */ + enabledWhenOffline: PropTypes.bool, + + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous: PropTypes.bool, + + /** Whether ScrollWithContext should be used instead of regular ScrollView. + * Set to true when there's a nested Picker component in Form. + */ + scrollContextEnabled: PropTypes.bool, + + /** Container styles */ + style: stylePropTypes, + + /** Custom content to display in the footer after submit button */ + footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + + /** Information about the network */ + network: networkPropTypes.isRequired, + + shouldValidateOnBlur: PropTypes.bool, + + shouldValidateOnChange: PropTypes.bool, +}; + +const defaultProps = { + isSubmitButtonVisible: true, + formState: { + isLoading: false, + }, + enabledWhenOffline: false, + isSubmitActionDangerous: false, + scrollContextEnabled: false, + footerContent: null, + style: [], + validate: () => {}, + shouldValidateOnBlur: true, + shouldValidateOnChange: true, +}; + +function getInitialValueByType(valueType) { + switch (valueType) { + case 'string': + return ''; + case 'boolean': + return false; + case 'date': + return new Date(); + default: + return ''; + } +} + +function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, onSubmit, ...rest}) { + const inputRefs = useRef(null); + const touchedInputs = useRef({}); + const [inputValues, setInputValues] = useState({}); + const [errors, setErrors] = useState({}); + + const onValidate = useCallback( + (values) => { + const validateErrors = validate(values); + setErrors(validateErrors); + }, + [validate], + ); + + /** + * @param {String} inputID - The inputID of the input being touched + */ + const setTouchedInput = useCallback( + (inputID) => { + touchedInputs.current[inputID] = true; + }, + [touchedInputs], + ); + + const submit = useCallback(() => { + // Return early if the form is already submitting to avoid duplicate submission + if (formState.isLoading) { + return; + } + + // Touches all form inputs so we can validate the entire form + _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); + + // Validate form and return early if any errors are found + if (!_.isEmpty(onValidate(inputValues))) { + return; + } + + // Do not submit form if network is offline and the form is not enabled when offline + if (network.isOffline && !enabledWhenOffline) { + return; + } + + onSubmit(inputValues); + }, [enabledWhenOffline, formState.isLoading, inputValues, network.isOffline, onSubmit, onValidate]); + + const registerInput = useCallback( + (inputID, propsToParse = {}) => { + const newRef = propsToParse.ref || createRef(); + inputRefs[inputID] = newRef; + + if (!_.isUndefined(propsToParse.value)) { + inputValues[inputID] = propsToParse.value; + } else if (propsToParse.shouldUseDefaultValue) { + // We force the form to set the input value from the defaultValue props if there is a saved valid value + inputValues[inputID] = propsToParse.defaultValue; + } else if (_.isUndefined(inputValues[inputID])) { + // We want to initialize the input value if it's undefined + inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; + } + + const errorFields = lodashGet(formState, 'errorFields', {}); + const fieldErrorMessage = + _.chain(errorFields[inputID]) + .keys() + .sortBy() + .reverse() + .map((key) => errorFields[inputID][key]) + .first() + .value() || ''; + + return { + ...propsToParse, + ref: newRef, + inputID, + key: propsToParse.key || inputID, + errorText: errors[inputID] || fieldErrorMessage, + value: inputValues[inputID], + // As the text input is controlled, we never set the defaultValue prop + // as this is already happening by the value prop. + defaultValue: undefined, + onTouched: (event) => { + setTouchedInput(inputID); + if (_.isFunction(propsToParse.onTouched)) { + propsToParse.onTouched(event); + } + }, + onBlur: (event) => { + // Only run validation when user proactively blurs the input. + if (Visibility.isVisible() && Visibility.hasFocus()) { + // We delay the validation in order to prevent Checkbox loss of focus when + // the user is focusing a TextInput and proceeds to toggle a CheckBox in + // web and mobile web platforms. + setTimeout(() => { + setTouchedInput(inputID); + if (shouldValidateOnBlur) { + onValidate(inputValues); + } + }, 200); + } + + if (_.isFunction(propsToParse.onBlur)) { + propsToParse.onBlur(event); + } + }, + onInputChange: (value, key) => { + const inputKey = key || inputID; + setInputValues((prevState) => { + const newState = { + ...prevState, + [inputKey]: value, + }; + + if (shouldValidateOnChange) { + onValidate(newState); + } + return newState; + }); + + if (propsToParse.shouldSaveDraft) { + FormActions.setDraftValues(propsToParse.formID, {[inputKey]: value}); + } + + if (_.isFunction(propsToParse.onValueChange)) { + propsToParse.onValueChange(value, inputKey); + } + }, + }; + }, + [errors, formState, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + ); + const value = useMemo(() => ({registerInput}), [registerInput]); + + return ( + + {/* eslint-disable react/jsx-props-no-spreading */} + + {children} + + + ); +} + +FormProvider.displayName = 'Form'; +FormProvider.propTypes = propTypes; +FormProvider.defaultProps = defaultProps; + +export default compose( + withNetwork(), + withOnyx({ + formState: { + key: (props) => props.formID, + }, + }), +)(FormProvider); diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js new file mode 100644 index 000000000000..44bfee1a9e4a --- /dev/null +++ b/src/components/Form/FormWrapper.js @@ -0,0 +1,193 @@ +import React, {useCallback, useMemo, useRef} from 'react'; +import {Keyboard, ScrollView, StyleSheet} from 'react-native'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import * as ErrorUtils from '../../libs/ErrorUtils'; +import FormSubmit from '../FormSubmit'; +import FormAlertWithSubmitButton from '../FormAlertWithSubmitButton'; +import styles from '../../styles/styles'; +import SafeAreaConsumer from '../SafeAreaConsumer'; +import ScrollViewWithContext from '../ScrollViewWithContext'; + +import stylePropTypes from '../../styles/stylePropTypes'; + +const propTypes = { + /** A unique Onyx key identifying the form */ + formID: PropTypes.string.isRequired, + + /** Text to be displayed in the submit button */ + submitButtonText: PropTypes.string.isRequired, + + /** Controls the submit button's visibility */ + isSubmitButtonVisible: PropTypes.bool, + + /** Callback to submit the form */ + onSubmit: PropTypes.func.isRequired, + + /** Children to render. */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /* Onyx Props */ + + /** Contains the form state that must be accessed outside of the component */ + formState: PropTypes.shape({ + /** Controls the loading state of the form */ + isLoading: PropTypes.bool, + + /** Server side errors keyed by microtime */ + errors: PropTypes.objectOf(PropTypes.oneOf([PropTypes.string, PropTypes.arrayOf(PropTypes.string)])), + + /** Field-specific server side errors keyed by microtime */ + errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + }), + + /** Should the button be enabled when offline */ + enabledWhenOffline: PropTypes.bool, + + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous: PropTypes.bool, + + /** Whether ScrollWithContext should be used instead of regular ScrollView. + * Set to true when there's a nested Picker component in Form. + */ + scrollContextEnabled: PropTypes.bool, + + /** Container styles */ + style: stylePropTypes, + + /** Custom content to display in the footer after submit button */ + footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + + errors: PropTypes.objectOf(PropTypes.string).isRequired, + + inputRefs: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object])).isRequired, +}; + +const defaultProps = { + isSubmitButtonVisible: true, + formState: { + isLoading: false, + }, + enabledWhenOffline: false, + isSubmitActionDangerous: false, + scrollContextEnabled: false, + footerContent: null, + style: [], +}; + +function FormWrapper(props) { + const {onSubmit, children, formState, errors, inputRefs, submitButtonText, footerContent, isSubmitButtonVisible, style, enabledWhenOffline, isSubmitActionDangerous, formID} = props; + const formRef = useRef(null); + const formContentRef = useRef(null); + const errorMessage = useMemo(() => { + const latestErrorMessage = ErrorUtils.getLatestErrorMessage(formState); + return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; + }, [formState]); + + const scrollViewContent = useCallback( + (safeAreaPaddingBottomStyle) => ( + + {children} + {isSubmitButtonVisible && ( + 0 || Boolean(errorMessage) || !_.isEmpty(formState.errorFields)} + isLoading={formState.isLoading} + message={_.isEmpty(formState.errorFields) ? errorMessage : null} + onSubmit={onSubmit} + footerContent={footerContent} + onFixTheErrorsLinkPressed={() => { + const errorFields = !_.isEmpty(errors) ? errors : formState.errorFields; + const focusKey = _.find(_.keys(inputRefs), (key) => _.keys(errorFields).includes(key)); + const focusInput = inputRefs[focusKey].current; + + // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. + if (typeof focusInput.isFocused !== 'function') { + Keyboard.dismiss(); + } + + // We subtract 10 to scroll slightly above the input + if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') { + // We measure relative to the content root, not the scroll view, as that gives + // consistent results across mobile and web + focusInput.measureLayout(formContentRef.current, (x, y) => + formRef.current.scrollTo({ + y: y - 10, + animated: false, + }), + ); + } + + // Focus the input after scrolling, as on the Web it gives a slightly better visual result + if (focusInput.focus && typeof focusInput.focus === 'function') { + focusInput.focus(); + } + }} + containerStyles={[styles.mh0, styles.mt5, styles.flex1]} + enabledWhenOffline={enabledWhenOffline} + isSubmitActionDangerous={isSubmitActionDangerous} + disablePressOnEnter + /> + )} + + ), + [ + children, + enabledWhenOffline, + errorMessage, + errors, + footerContent, + formID, + formState.errorFields, + formState.isLoading, + inputRefs, + isSubmitActionDangerous, + isSubmitButtonVisible, + onSubmit, + style, + submitButtonText, + ], + ); + + return ( + + {({safeAreaPaddingBottomStyle}) => + props.scrollContextEnabled ? ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) : ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) + } + + ); +} + +FormWrapper.displayName = 'FormWrapper'; +FormWrapper.propTypes = propTypes; +FormWrapper.defaultProps = defaultProps; + +export default withOnyx({ + formState: { + key: (props) => props.formID, + }, +})(FormWrapper); diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js new file mode 100644 index 000000000000..43064b5a6690 --- /dev/null +++ b/src/components/Form/InputWrapper.js @@ -0,0 +1,34 @@ +import React, {forwardRef, useContext} from 'react'; +import PropTypes from 'prop-types'; +import FormContext from './FormContext'; + +const propTypes = { + InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, + inputID: PropTypes.string.isRequired, + valueType: PropTypes.string, + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), +}; + +const defaultProps = { + forwardedRef: undefined, + valueType: 'string', +}; + +function InputWrapper(props) { + const {InputComponent, inputID, forwardedRef, ...rest} = props; + const {registerInput} = useContext(FormContext); + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +InputWrapper.propTypes = propTypes; +InputWrapper.defaultProps = defaultProps; +InputWrapper.displayName = 'InputWrapper'; + +export default forwardRef((props, ref) => ( + +)); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index f8c5bbc066a6..92a313cf1e0a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -44,7 +44,7 @@ function AnchorRenderer(props) { // the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab. if (hasExpensifyOrigin && attrHref.indexOf('newdotreport?reportID=') > -1) { const reportID = attrHref.split('newdotreport?reportID=').pop(); - const reportRoute = ROUTES.getReportRoute(reportID); + const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID); Navigation.navigate(reportRoute); return; } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index 74cf83a4a6f0..f5fac7b49e0f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -62,7 +62,7 @@ function ImageRenderer(props) { { - const route = ROUTES.getReportAttachmentRoute(report.reportID, source); + const route = ROUTES.REPORT_ATTACHMENTS.getRoute(report.reportID, source); Navigation.navigate(route); }} onLongPress={(event) => diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 947a15ac6efb..812f4e951f74 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -1,6 +1,8 @@ import React from 'react'; import _ from 'underscore'; import {TNodeChildrenRenderer} from 'react-native-render-html'; +import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import Text from '../../Text'; @@ -10,53 +12,67 @@ import withCurrentUserPersonalDetails from '../../withCurrentUserPersonalDetails import personalDetailsPropType from '../../../pages/personalDetailsPropType'; import * as StyleUtils from '../../../styles/StyleUtils'; import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; +import compose from '../../../libs/compose'; import TextLink from '../../TextLink'; +import ONYXKEYS from '../../../ONYXKEYS'; +import useLocalize from '../../../hooks/useLocalize'; import CONST from '../../../CONST'; const propTypes = { ...htmlRendererPropTypes, - /** - * Current user personal details - */ + /** Current user personal details */ currentUserPersonalDetails: personalDetailsPropType.isRequired, -}; -/** - * Navigates to user details screen based on email - * @param {String} email - * @returns {void} - * */ -const showUserDetails = (email) => Navigation.navigate(ROUTES.getDetailsRoute(email)); + /** Personal details of all users */ + personalDetails: personalDetailsPropType.isRequired, +}; function MentionUserRenderer(props) { + const {translate} = useLocalize(); const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); + const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid'); + + let accountID; + let displayNameOrLogin; + let navigationRoute; - // We need to remove the LTR unicode and leading @ from data as it is not part of the login - const loginWithoutLeadingAt = props.tnode.data ? props.tnode.data.replace(CONST.UNICODE.LTR, '').slice(1) : ''; + if (!_.isEmpty(htmlAttribAccountID)) { + const user = lodashGet(props.personalDetails, htmlAttribAccountID); + accountID = parseInt(htmlAttribAccountID, 10); + displayNameOrLogin = lodashGet(user, 'login', '') || lodashGet(user, 'displayName', '') || translate('common.hidden'); + navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); + } else if (!_.isEmpty(props.tnode.data)) { + // We need to remove the LTR unicode and leading @ from data as it is not part of the login + displayNameOrLogin = props.tnode.data.replace(CONST.UNICODE.LTR, '').slice(1); - const accountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([loginWithoutLeadingAt])); + accountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])); + navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); + } else { + // If neither an account ID or email is provided, don't render anything + return null; + } - const isOurMention = loginWithoutLeadingAt === props.currentUserPersonalDetails.login; + const isOurMention = accountID === props.currentUserPersonalDetails.accountID; return ( showUserDetails(loginWithoutLeadingAt)} + onPress={() => Navigation.navigate(navigationRoute)} // Add testID so it is NOT selected as an anchor tag by SelectionScraper testID="span" > - + {!_.isEmpty(htmlAttribAccountID) ? `@${displayNameOrLogin}` : } @@ -66,4 +82,11 @@ function MentionUserRenderer(props) { MentionUserRenderer.propTypes = propTypes; MentionUserRenderer.displayName = 'MentionUserRenderer'; -export default withCurrentUserPersonalDetails(MentionUserRenderer); +export default compose( + withCurrentUserPersonalDetails, + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + }), +)(MentionUserRenderer); diff --git a/src/components/HeaderPageLayout.js b/src/components/HeaderPageLayout.js index 17c2255593e9..c53ec37a0b34 100644 --- a/src/components/HeaderPageLayout.js +++ b/src/components/HeaderPageLayout.js @@ -68,7 +68,7 @@ function HeaderPageLayout({backgroundColor, children, footer, headerContainerSty titleColor={titleColor} iconFill={iconFill} /> - + {/** Safari on ios/mac has a bug where overscrolling the page scrollview shows green background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */} {Browser.isSafari() && ( diff --git a/src/components/HeaderWithBackButton/index.js b/src/components/HeaderWithBackButton/index.js index 7bcd57385d5f..c720529371d7 100755 --- a/src/components/HeaderWithBackButton/index.js +++ b/src/components/HeaderWithBackButton/index.js @@ -121,7 +121,7 @@ function HeaderWithBackButton({ {shouldShowGetAssistanceButton && ( Navigation.navigate(ROUTES.getGetAssistanceRoute(guidesCallTaskID))} + onPress={() => Navigation.navigate(ROUTES.GET_ASSISTANCE.getRoute(guidesCallTaskID))} style={[styles.touchableButtonImage]} accessibilityRole="button" accessibilityLabel={translate('getAssistancePage.questionMarkButtonTooltip')} diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index d9f51e111a43..9370cc95fb82 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -71,6 +71,7 @@ const MapView = forwardRef(({accessToken, style, ma onMapIdle={setMapIdle} pitchEnabled={pitchEnabled} attributionPosition={{...styles.r2, ...styles.b2}} + scaleBarEnabled={false} logoPosition={{...styles.l2, ...styles.b2}} // eslint-disable-next-line {...responder.panHandlers} diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 13471407914f..0d554ff0eca4 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -38,6 +38,7 @@ import transactionPropTypes from './transactionPropTypes'; import DistanceRequestUtils from '../libs/DistanceRequestUtils'; import * as IOU from '../libs/actions/IOU'; import * as TransactionUtils from '../libs/TransactionUtils'; +import * as PolicyUtils from '../libs/PolicyUtils'; const propTypes = { /** Callback to inform parent modal of success */ @@ -142,13 +143,7 @@ const propTypes = { policyCategories: PropTypes.objectOf(categoryPropTypes), /** Collection of tags attached to a policy */ - policyTags: PropTypes.objectOf( - PropTypes.shape({ - name: PropTypes.string, - required: PropTypes.bool, - tags: PropTypes.objectOf(tagPropTypes), - }), - ), + policyTags: tagPropTypes, }; const defaultProps = { @@ -184,7 +179,7 @@ function MoneyRequestConfirmationList(props) { // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks. // Prop functions pass props itself as a "this" value to the function which means they change every time props change. const {onSendMoney, onConfirm, onSelectParticipant, transaction} = props; - const {translate} = useLocalize(); + const {translate, toLocaleDigit} = useLocalize(); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -202,12 +197,12 @@ function MoneyRequestConfirmationList(props) { const shouldShowCategories = isPolicyExpenseChat && Permissions.canUseCategories(props.betas) && OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories)); // Fetches the first tag list of the policy - const tagListKey = _.first(_.keys(props.policyTags)); - const tagList = lodashGet(props.policyTags, [tagListKey, 'tags'], []); - const tagListName = lodashGet(props.policyTags, [tagListKey, 'name'], ''); + const policyTag = PolicyUtils.getTag(props.policyTags); + const policyTagList = lodashGet(policyTag, 'tags', {}); + const policyTagListName = lodashGet(policyTag, 'name', translate('common.tag')); const canUseTags = Permissions.canUseTags(props.betas); // A flag for showing the tags field - const shouldShowTags = isPolicyExpenseChat && canUseTags && _.any(tagList, (tag) => tag.enabled); + const shouldShowTags = isPolicyExpenseChat && canUseTags && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); // A flag for showing the billable field const shouldShowBillable = canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true); @@ -338,9 +333,9 @@ function MoneyRequestConfirmationList(props) { if (!props.isDistanceRequest) { return; } - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate); + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant(distanceMerchant); - }, [hasRoute, distance, unit, rate, currency, translate, props.isDistanceRequest]); + }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest]); /** * @param {Object} option @@ -364,9 +359,9 @@ function MoneyRequestConfirmationList(props) { if (option.accountID) { const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, ''); - Navigation.navigate(ROUTES.getProfileRoute(option.accountID, activeRoute)); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { - Navigation.navigate(ROUTES.getReportDetailsRoute(option.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); } }; @@ -463,7 +458,7 @@ function MoneyRequestConfirmationList(props) { shouldShowRightIcon={!props.isReadOnly && !props.isDistanceRequest} title={formattedAmount} description={translate('iou.amount')} - onPress={() => !props.isDistanceRequest && Navigation.navigate(ROUTES.getMoneyRequestAmountRoute(props.iouType, props.reportID))} + onPress={() => !props.isDistanceRequest && Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mt2]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={didConfirm || props.isReadOnly} @@ -474,7 +469,7 @@ function MoneyRequestConfirmationList(props) { shouldParseTitle title={props.iouComment} description={translate('common.description')} - onPress={() => Navigation.navigate(ROUTES.getMoneyRequestDescriptionRoute(props.iouType, props.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DESCRIPTION.getRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mb2]} titleStyle={styles.flex1} disabled={didConfirm || props.isReadOnly} @@ -503,7 +498,7 @@ function MoneyRequestConfirmationList(props) { description={translate('common.date')} style={[styles.moneyRequestMenuItem, styles.mb2]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getMoneyRequestCreatedRoute(props.iouType, props.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DATE.getRoute(props.iouType, props.reportID))} disabled={didConfirm || props.isReadOnly || !isTypeRequest} /> {props.isDistanceRequest ? ( @@ -513,7 +508,7 @@ function MoneyRequestConfirmationList(props) { description={translate('common.distance')} style={[styles.moneyRequestMenuItem, styles.mb2]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getMoneyRequestAddressRoute(props.iouType, props.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_ADDRESS.getRoute(props.iouType, props.reportID))} disabled={didConfirm || props.isReadOnly || !isTypeRequest} /> ) : ( @@ -523,7 +518,7 @@ function MoneyRequestConfirmationList(props) { description={translate('common.merchant')} style={[styles.moneyRequestMenuItem, styles.mb2]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getMoneyRequestMerchantRoute(props.iouType, props.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_MERCHANT.getRoute(props.iouType, props.reportID))} disabled={didConfirm || props.isReadOnly || !isTypeRequest} /> )} @@ -532,7 +527,7 @@ function MoneyRequestConfirmationList(props) { shouldShowRightIcon={!props.isReadOnly} title={props.iouCategory} description={translate('common.category')} - onPress={() => Navigation.navigate(ROUTES.getMoneyRequestCategoryRoute(props.iouType, props.reportID))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mb2]} disabled={didConfirm || props.isReadOnly} /> @@ -541,8 +536,8 @@ function MoneyRequestConfirmationList(props) { Navigation.navigate(ROUTES.getMoneyRequestTagRoute(props.iouType, props.reportID))} + description={policyTagListName} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID))} style={[styles.moneyRequestMenuItem, styles.mb2]} disabled={didConfirm || props.isReadOnly} /> diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 7f1e58912128..9db9c87c4eb1 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -11,6 +11,7 @@ import participantPropTypes from './participantPropTypes'; import styles from '../styles/styles'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; +import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; import * as IOU from '../libs/actions/IOU'; import ConfirmModal from './ConfirmModal'; @@ -85,6 +86,15 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, shouldShowPinButton={false} shouldShowThreeDotsButton={isActionOwner && !isSettled} threeDotsMenuItems={[ + ...(TransactionUtils.hasReceipt(transaction) + ? [] + : [ + { + icon: Expensicons.Receipt, + text: translate('receipt.addReceipt'), + onSelected: () => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)), + }, + ]), { icon: Expensicons.Trashcan, text: translate('reportActionContextMenu.deleteAction', {action: parentReportAction}), diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index a07510f7603d..7f6eb0a490b7 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -220,18 +220,14 @@ class OptionRow extends Component { ) : ( ))} diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index bff9f8b6d7d0..aa02701b1c98 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -107,13 +107,13 @@ class BaseOptionsSelector extends Component { }); return; } - const newFocusedIndex = this.props.selectedOptions.length; + const newFocusedIndex = this.props.selectedOptions.length; // eslint-disable-next-line react/no-did-update-set-state this.setState( { allOptions: newOptions, - focusedIndex: newFocusedIndex, + focusedIndex: _.isNumber(this.props.initialFocusedIndex) ? this.props.initialFocusedIndex : newFocusedIndex, }, () => { // If we just toggled an option on a multi-selection page or cleared the search input, scroll to top diff --git a/src/components/ParentNavigationSubtitle.js b/src/components/ParentNavigationSubtitle.js index fd388b00a8a7..037489294073 100644 --- a/src/components/ParentNavigationSubtitle.js +++ b/src/components/ParentNavigationSubtitle.js @@ -38,7 +38,7 @@ function ParentNavigationSubtitle(props) { return ( { - Navigation.navigate(ROUTES.getReportRoute(props.parentReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.parentReportID)); }} accessibilityLabel={translate('threads.parentNavigationSummary', {rootReportName, workspaceName})} accessibilityRole={CONST.ACCESSIBILITY_ROLE.LINK} diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 3b310bb6b4fa..b4a5e010b7a8 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -98,7 +98,7 @@ function MoneyRequestAction({ const onMoneyRequestPreviewPressed = () => { if (isSplitBillAction) { const reportActionID = lodashGet(action, 'reportActionID', '0'); - Navigation.navigate(ROUTES.getSplitBillDetailsRoute(chatReportID, reportActionID)); + Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(chatReportID, reportActionID)); return; } @@ -108,11 +108,11 @@ function MoneyRequestAction({ const thread = ReportUtils.buildTransactionThread(action, requestReportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); Report.openReport(thread.reportID, userLogins, thread, action.reportActionID); - Navigation.navigate(ROUTES.getReportRoute(thread.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } Report.openReport(childReportID); - Navigation.navigate(ROUTES.getReportRoute(childReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; let shouldShowPendingConversionMessage = false; diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 178cab75a0c2..0220bbdf520a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -17,6 +17,7 @@ import * as ReportUtils from '../../libs/ReportUtils'; import * as OptionsListUtils from '../../libs/OptionsListUtils'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; import * as StyleUtils from '../../styles/StyleUtils'; +import * as PolicyUtils from '../../libs/PolicyUtils'; import CONST from '../../CONST'; import * as Expensicons from '../Icon/Expensicons'; import iouReportPropTypes from '../../pages/iouReportPropTypes'; @@ -32,6 +33,7 @@ import * as TransactionUtils from '../../libs/TransactionUtils'; import OfflineWithFeedback from '../OfflineWithFeedback'; import categoryPropTypes from '../categoryPropTypes'; import SpacerView from '../SpacerView'; +import tagPropTypes from '../tagPropTypes'; const propTypes = { /** The report currently being looked at */ @@ -53,6 +55,9 @@ const propTypes = { /** The transaction associated with the transactionThread */ transaction: transactionPropTypes, + /** Collection of tags attached to a policy */ + policyTags: tagPropTypes, + ...withCurrentUserPersonalDetailsPropTypes, }; @@ -65,9 +70,10 @@ const defaultProps = { currency: CONST.CURRENCY.USD, comment: {comment: ''}, }, + policyTags: {}, }; -function MoneyRequestView({betas, report, parentReport, policyCategories, shouldShowHorizontalRule, transaction}) { +function MoneyRequestView({report, betas, parentReport, policyCategories, shouldShowHorizontalRule, transaction, policyTags}) { const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); @@ -80,6 +86,7 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should comment: transactionDescription, merchant: transactionMerchant, category: transactionCategory, + tag: transactionTag, } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; @@ -89,8 +96,14 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should const canEdit = ReportUtils.canEditMoneyRequest(parentReportAction); // A flag for verifying that the current report is a sub-report of a workspace chat const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); - // A flag for showing categories + + // Fetches only the first tag, for now + const policyTag = PolicyUtils.getTag(policyTags); + const policyTagsList = lodashGet(policyTag, 'tags', {}); + + // Flags for showing categories and tags const shouldShowCategory = isPolicyExpenseChat && Permissions.canUseCategories(betas) && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); + const shouldShowTag = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList))); let description = `${translate('iou.amount')} • ${translate('iou.cash')}`; if (isSettled) { @@ -143,7 +156,7 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should titleStyle={styles.newKansasLarge} interactive={canEdit} shouldShowRightIcon={canEdit} - onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))} + onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))} brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} subtitle={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''} subtitleTextStyle={styles.textLabelError} @@ -157,7 +170,7 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))} + onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} numberOfLinesTitle={0} /> @@ -169,7 +182,7 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))} + onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))} brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} subtitle={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''} subtitleTextStyle={styles.textLabelError} @@ -182,7 +195,7 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))} + onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))} brickRoadIndicator={hasErrors && isEmptyMerchant ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} subtitle={hasErrors && isEmptyMerchant ? translate('common.error.enterMerchant') : ''} subtitleTextStyle={styles.textLabelError} @@ -196,7 +209,19 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))} + onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))} + /> + + )} + {shouldShowTag && ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAG))} /> )} @@ -237,5 +262,8 @@ export default compose( return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`, + }, }), )(MoneyRequestView); diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js index 070f534f4924..98bdede0fe26 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.js +++ b/src/components/ReportActionItem/ReportActionItemImage.js @@ -59,7 +59,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal}) { { - const route = ROUTES.getReportAttachmentRoute(report.reportID, imageSource); + const route = ROUTES.REPORT_ATTACHMENTS.getRoute(report.reportID, imageSource); Navigation.navigate(route); }} accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 1350c62bda88..a79cfd315b12 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -170,7 +170,7 @@ function ReportPreview(props) { { - Navigation.navigate(ROUTES.getReportRoute(props.iouReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.iouReportID)); }} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index d0181e3d736a..1da348bb067b 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -81,7 +81,7 @@ function TaskPreview(props) { return ( Navigation.navigate(ROUTES.getReportRoute(props.taskReportID))} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.taskReportID))} style={[styles.flexRow, styles.justifyContentBetween]} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('task.task')} diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js index ae77a18b980f..39807ab037d5 100644 --- a/src/components/ReportActionItem/TaskView.js +++ b/src/components/ReportActionItem/TaskView.js @@ -72,7 +72,7 @@ function TaskView(props) { e.currentTarget.blur(); } - Navigation.navigate(ROUTES.getTaskReportTitleRoute(props.report.reportID)); + Navigation.navigate(ROUTES.TASK_TITLE.getRoute(props.report.reportID)); })} style={({pressed}) => [ styles.ph5, @@ -132,7 +132,7 @@ function TaskView(props) { shouldParseTitle description={props.translate('task.description')} title={props.report.description || ''} - onPress={() => Navigation.navigate(ROUTES.getTaskReportDescriptionRoute(props.report.reportID))} + onPress={() => Navigation.navigate(ROUTES.TASK_DESCRIPTION.getRoute(props.report.reportID))} shouldShowRightIcon={isOpen} disabled={disableState} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} @@ -150,7 +150,7 @@ function TaskView(props) { iconType={CONST.ICON_TYPE_AVATAR} avatarSize={CONST.AVATAR_SIZE.SMALLER} titleStyle={styles.assigneeTextStyle} - onPress={() => Navigation.navigate(ROUTES.getTaskReportAssigneeRoute(props.report.reportID))} + onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(props.report.reportID))} shouldShowRightIcon={isOpen} disabled={disableState} wrapperStyle={[styles.pv2]} @@ -162,7 +162,7 @@ function TaskView(props) { ) : ( Navigation.navigate(ROUTES.getTaskReportAssigneeRoute(props.report.reportID))} + onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(props.report.reportID))} shouldShowRightIcon={isOpen} disabled={disableState} wrapperStyle={[styles.pv2]} diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js index 80c26b5d6b3f..071e53de1776 100644 --- a/src/components/ReportWelcomeText.js +++ b/src/components/ReportWelcomeText.js @@ -102,7 +102,7 @@ function ReportWelcomeText(props) { {roomWelcomeMessage.showReportName && ( Navigation.navigate(ROUTES.getReportDetailsRoute(props.report.reportID))} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} suppressHighlighting > {ReportUtils.getReportName(props.report)} @@ -122,7 +122,7 @@ function ReportWelcomeText(props) { ) : ( Navigation.navigate(ROUTES.getProfileRoute(accountID))} + onPress={() => Navigation.navigate(ROUTES.PROFILE.getRoute(accountID))} suppressHighlighting > {displayName} diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 0bb9cb0fdc6e..8d894e4c983a 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -136,6 +136,9 @@ function BaseSelectionList({ }; }, [canSelectMultiple, sections]); + // Disable `Enter` hotkey if the active element is a button or checkbox + const shouldDisableHotkeys = activeElement && [CONST.ACCESSIBILITY_ROLE.BUTTON, CONST.ACCESSIBILITY_ROLE.CHECKBOX].includes(activeElement.role); + // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); @@ -168,23 +171,35 @@ function BaseSelectionList({ listRef.current.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight}); }; - const selectRow = (item, index) => { + /** + * Logic to run when a row is selected, either with click/press or keyboard hotkeys. + * + * @param {Object} item - the list item + * @param {Boolean} shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) + */ + const selectRow = (item, shouldUnfocusRow = false) => { // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item if (canSelectMultiple) { - if (sections.length === 1) { - // If the list has only 1 section (e.g. Workspace Members list), we always focus the next available item - const nextAvailableIndex = _.findIndex(flattenedSections.allOptions, (option, i) => i > index && !option.isDisabled); - setFocusedIndex(nextAvailableIndex); - } else { - // If the list has multiple sections (e.g. Workspace Invite list), we focus the first one after all the selected (selected items are always at the top) + if (sections.length > 1) { + // If the list has only 1 section (e.g. Workspace Members list), we do nothing. + // If the list has multiple sections (e.g. Workspace Invite list), and `shouldUnfocusRow` is false, + // we focus the first one after all the selected (selected items are always at the top). const selectedOptionsCount = item.isSelected ? flattenedSections.selectedOptions.length - 1 : flattenedSections.selectedOptions.length + 1; - setFocusedIndex(selectedOptionsCount); + + if (!shouldUnfocusRow) { + setFocusedIndex(selectedOptionsCount); + } if (!item.isSelected) { // If we're selecting an item, scroll to it's position at the top, so we can see it scrollToIndex(Math.max(selectedOptionsCount - 1, 0), true); } } + + if (shouldUnfocusRow) { + // Unfocus all rows when selecting row with click/press + setFocusedIndex(-1); + } } onSelectRow(item); @@ -197,7 +212,7 @@ function BaseSelectionList({ return; } - selectRow(focusedOption, focusedIndex); + selectRow(focusedOption); }; /** @@ -254,7 +269,7 @@ function BaseSelectionList({ selectRow(item, index)} + onSelectRow={() => selectRow(item, true)} onDismissError={onDismissError} showTooltip={showTooltip} /> @@ -266,7 +281,7 @@ function BaseSelectionList({ item={item} isFocused={isItemFocused} isDisabled={isDisabled} - onSelectRow={() => selectRow(item, index)} + onSelectRow={() => selectRow(item, true)} /> ); }; @@ -290,7 +305,7 @@ function BaseSelectionList({ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: !activeElement && isFocused, + isActive: !shouldDisableHotkeys && isFocused, }); /** Calls confirm action when pressing CTRL (CMD) + Enter */ diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.js index df022992e24a..530af66d91d3 100644 --- a/src/components/SelectionList/RadioListItem.js +++ b/src/components/SelectionList/RadioListItem.js @@ -18,7 +18,6 @@ function RadioListItem({item, isFocused = false, isDisabled = false, onSelectRow accessibilityRole="button" hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} - focusStyle={styles.hoveredComponentBG} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > diff --git a/src/components/SelectionList/UserListItem.js b/src/components/SelectionList/UserListItem.js index 014e0cf879a5..98241c91deb1 100644 --- a/src/components/SelectionList/UserListItem.js +++ b/src/components/SelectionList/UserListItem.js @@ -62,7 +62,6 @@ function UserListItem({item, isFocused = false, showTooltip, onSelectRow, onDism accessibilityState={{checked: item.isSelected}} hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} - focusStyle={styles.hoveredComponentBG} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index c46ca1b57b22..8e7cf11f7e5a 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -7,6 +7,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; import useLocalize from '../../hooks/useLocalize'; import * as OptionsListUtils from '../../libs/OptionsListUtils'; +import * as PolicyUtils from '../../libs/PolicyUtils'; import OptionsSelector from '../OptionsSelector'; import {propTypes, defaultProps} from './tagPickerPropTypes'; @@ -15,7 +16,7 @@ function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubm const [searchValue, setSearchValue] = useState(''); const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []); - const policyTagList = lodashGet(policyTags, [tag, 'tags'], {}); + const policyTagList = PolicyUtils.getTagList(policyTags, tag); const policyTagsCount = _.size(_.filter(policyTagList, (policyTag) => policyTag.enabled)); const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js index a5d94605a76a..011885fe0f81 100644 --- a/src/components/TagPicker/tagPickerPropTypes.js +++ b/src/components/TagPicker/tagPickerPropTypes.js @@ -16,12 +16,7 @@ const propTypes = { /* Onyx Props */ /** Collection of tags attached to a policy */ - policyTags: PropTypes.objectOf( - PropTypes.shape({ - name: PropTypes.string, - tags: PropTypes.objectOf(tagPropTypes), - }), - ), + policyTags: tagPropTypes, /** List of recently used tags */ policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 244d6a8d8490..065c67349748 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -21,6 +21,7 @@ import isInputAutoFilled from '../../libs/isInputAutoFilled'; import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; import withLocalize from '../withLocalize'; import useNativeDriver from '../../libs/useNativeDriver'; +import * as Browser from '../../libs/Browser'; function BaseTextInput(props) { const inputValue = props.value || props.defaultValue || ''; @@ -382,11 +383,17 @@ function BaseTextInput(props) { This Text component is intentionally positioned out of the screen. */} {(props.autoGrow || props.autoGrowHeight) && ( - // Add +2 to width so that the first digit of amount do not cut off on mWeb - https://github.com/Expensify/App/issues/8158. + // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value + // https://github.com/Expensify/App/issues/8158 + // https://github.com/Expensify/App/issues/26628 { - setTextInputWidth(e.nativeEvent.layout.width + 2); + let additionalWidth = 0; + if (Browser.isMobileSafari() || Browser.isSafari()) { + additionalWidth = 2; + } + setTextInputWidth(e.nativeEvent.layout.width + additionalWidth); setTextInputHeight(e.nativeEvent.layout.height); }} > diff --git a/src/components/tagPropTypes.js b/src/components/tagPropTypes.js index 29d913dcd035..2108b65ebbd5 100644 --- a/src/components/tagPropTypes.js +++ b/src/components/tagPropTypes.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -export default PropTypes.shape({ +const tagListPropTypes = PropTypes.shape({ /** Name of a tag */ name: PropTypes.string.isRequired, @@ -10,3 +10,11 @@ export default PropTypes.shape({ /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ 'GL Code': PropTypes.string, }); + +export default PropTypes.objectOf( + PropTypes.shape({ + name: PropTypes.string, + required: PropTypes.bool, + tags: PropTypes.objectOf(tagListPropTypes), + }), +); diff --git a/src/hooks/useActiveElement/index.js b/src/hooks/useActiveElement/index.js index c973eb8eda18..0db0ed604067 100644 --- a/src/hooks/useActiveElement/index.js +++ b/src/hooks/useActiveElement/index.js @@ -1,5 +1,11 @@ import {useEffect, useState} from 'react'; +/** + * Listens for the focusin and focusout events and sets the DOM activeElement to the state. + * On native, we just return null. + * + * @return {Element} the active element in the DOM + */ export default function useActiveElement() { const [active, setActive] = useState(document.activeElement); diff --git a/src/hooks/useActiveElement/index.native.js b/src/hooks/useActiveElement/index.native.js index 2f658d48ca9a..afdfe8a047e4 100644 --- a/src/hooks/useActiveElement/index.native.js +++ b/src/hooks/useActiveElement/index.native.js @@ -1,3 +1,8 @@ +/** + * Native doesn't have the DOM, so we just return null. + * + * @return {null} + */ export default function useActiveElement() { return null; } diff --git a/src/languages/en.ts b/src/languages/en.ts index 6cd21b1e3da8..def4b351e112 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -489,6 +489,7 @@ export default { flash: 'flash', shutter: 'shutter', gallery: 'gallery', + addReceipt: 'Add receipt', }, iou: { amount: 'Amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index f2065ac74089..a78a30375fe9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -481,6 +481,7 @@ export default { flash: 'flash', shutter: 'obturador', gallery: 'galería', + addReceipt: 'Añadir recibo', }, iou: { amount: 'Importe', diff --git a/src/libs/DistanceRequestUtils.js b/src/libs/DistanceRequestUtils.js index 34fa14163835..7744ee3d8bb1 100644 --- a/src/libs/DistanceRequestUtils.js +++ b/src/libs/DistanceRequestUtils.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import CONST from '../CONST'; import * as CurrencyUtils from './CurrencyUtils'; +import * as PolicyUtils from './PolicyUtils'; /** * Retrieves the default mileage rate based on a given policy. @@ -79,16 +80,17 @@ const getRoundedDistanceInUnits = (distanceInMeters, unit) => { * @param {Number} rate Expensable amount allowed per unit * @param {String} currency The currency associated with the rate * @param {Function} translate Translate function + * @param {Function} toLocaleDigit Function to convert to localized digit * @returns {String} A string that describes the distance traveled and the rate used for expense calculation */ -const getDistanceMerchant = (hasRoute, distanceInMeters, unit, rate, currency, translate) => { +const getDistanceMerchant = (hasRoute, distanceInMeters, unit, rate, currency, translate, toLocaleDigit) => { const distanceInUnits = hasRoute ? getRoundedDistanceInUnits(distanceInMeters, unit) : translate('common.tbd'); const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === 1 ? singularDistanceUnit : distanceUnit; - const ratePerUnit = rate * 0.01; + const ratePerUnit = PolicyUtils.getUnitRateValue({rate}, toLocaleDigit); const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `; return `${distanceInUnits} ${unitString} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index f3939eabe6f7..71b150680716 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -13,19 +13,18 @@ const defaultSubRouteOptions = { /** * Create a modal stack navigator with an array of sub-screens. * - * @param {Object[]} screens array of screen config objects + * @param {Object} screens key/value pairs where the key is the name of the screen and the value is a functon that returns the lazy-loaded component * @returns {Function} */ function createModalStackNavigator(screens) { const ModalStackNavigator = createStackNavigator(); return () => ( - {_.map(screens, (screen) => ( + {_.map(screens, (getComponent, name) => ( ))} @@ -33,759 +32,171 @@ function createModalStackNavigator(screens) { } // We use getComponent/require syntax so that file used by screens are not loaded until we need them. -const MoneyRequestModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const MoneyRequestSelectorPage = require('../../../pages/iou/MoneyRequestSelectorPage').default; - return MoneyRequestSelectorPage; - }, - name: 'Money_Request', - }, - { - getComponent: () => { - const MoneyRequestEditAmountPage = require('../../../pages/iou/steps/NewRequestAmountPage').default; - return MoneyRequestEditAmountPage; - }, - name: 'Money_Request_Amount', - }, - { - getComponent: () => { - const MoneyRequestParticipantsPage = require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default; - return MoneyRequestParticipantsPage; - }, - name: 'Money_Request_Participants', - }, - { - getComponent: () => { - const MoneyRequestConfirmPage = require('../../../pages/iou/steps/MoneyRequestConfirmPage').default; - return MoneyRequestConfirmPage; - }, - name: 'Money_Request_Confirmation', - }, - { - getComponent: () => { - const IOUCurrencySelection = require('../../../pages/iou/IOUCurrencySelection').default; - return IOUCurrencySelection; - }, - name: 'Money_Request_Currency', - }, - { - getComponent: () => { - const MoneyRequestDatePage = require('../../../pages/iou/MoneyRequestDatePage').default; - return MoneyRequestDatePage; - }, - name: 'Money_Request_Date', - }, - { - getComponent: () => { - const MoneyRequestDescriptionPage = require('../../../pages/iou/MoneyRequestDescriptionPage').default; - return MoneyRequestDescriptionPage; - }, - name: 'Money_Request_Description', - }, - { - getComponent: () => { - const MoneyRequestCategoryPage = require('../../../pages/iou/MoneyRequestCategoryPage').default; - return MoneyRequestCategoryPage; - }, - name: 'Money_Request_Category', - }, - { - getComponent: () => { - const MoneyRequestTagPage = require('../../../pages/iou/MoneyRequestTagPage').default; - return MoneyRequestTagPage; - }, - name: 'Money_Request_Tag', - }, - { - getComponent: () => { - const MoneyRequestMerchantPage = require('../../../pages/iou/MoneyRequestMerchantPage').default; - return MoneyRequestMerchantPage; - }, - name: 'Money_Request_Merchant', - }, - { - getComponent: () => { - const AddPersonalBankAccountPage = require('../../../pages/AddPersonalBankAccountPage').default; - return AddPersonalBankAccountPage; - }, - name: 'IOU_Send_Add_Bank_Account', - }, - { - getComponent: () => { - const AddDebitCardPage = require('../../../pages/settings/Wallet/AddDebitCardPage').default; - return AddDebitCardPage; - }, - name: 'IOU_Send_Add_Debit_Card', - }, - { - getComponent: () => { - const EnablePaymentsPage = require('../../../pages/EnablePayments/EnablePaymentsPage').default; - return EnablePaymentsPage; - }, - name: 'IOU_Send_Enable_Payments', - }, - { - getComponent: () => { - const WaypointEditorPage = require('../../../pages/iou/WaypointEditorPage').default; - return WaypointEditorPage; - }, - name: 'Money_Request_Waypoint', - }, - { - getComponent: () => { - const DistanceRequestEditPage = require('../../../pages/iou/DistanceRequestPage').default; - return DistanceRequestEditPage; - }, - name: 'Money_Request_Address', - }, -]); - -const SplitDetailsModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const SplitBillDetailsPage = require('../../../pages/iou/SplitBillDetailsPage').default; - return SplitBillDetailsPage; - }, - name: 'SplitDetails_Root', - }, -]); - -const DetailsModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const DetailsPage = require('../../../pages/DetailsPage').default; - return DetailsPage; - }, - name: 'Details_Root', - }, -]); - -const ProfileModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const ProfilePage = require('../../../pages/ProfilePage').default; - return ProfilePage; - }, - name: 'Profile_Root', - }, -]); - -const ReportDetailsModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const ReportDetailsPage = require('../../../pages/ReportDetailsPage').default; - return ReportDetailsPage; - }, - name: 'Report_Details_Root', - }, - { - getComponent: () => { - const ShareCodePage = require('../../../pages/home/report/ReportDetailsShareCodePage').default; - return ShareCodePage; - }, - name: 'Report_Details_Share_Code', - }, -]); - -const ReportSettingsModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const ReportSettingsPage = require('../../../pages/settings/Report/ReportSettingsPage').default; - return ReportSettingsPage; - }, - name: 'Report_Settings_Root', - }, - { - getComponent: () => { - const RoomNamePage = require('../../../pages/settings/Report/RoomNamePage').default; - return RoomNamePage; - }, - name: 'Report_Settings_Room_Name', - }, - { - getComponent: () => { - const NotificationPreferencesPage = require('../../../pages/settings/Report/NotificationPreferencePage').default; - return NotificationPreferencesPage; - }, - name: 'Report_Settings_Notification_Preferences', - }, - { - getComponent: () => { - const WriteCapabilityPage = require('../../../pages/settings/Report/WriteCapabilityPage').default; - return WriteCapabilityPage; - }, - name: 'Report_Settings_Write_Capability', - }, -]); - -const TaskModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const TaskTitlePage = require('../../../pages/tasks/TaskTitlePage').default; - return TaskTitlePage; - }, - name: 'Task_Title', - }, - { - getComponent: () => { - const TaskDescriptionPage = require('../../../pages/tasks/TaskDescriptionPage').default; - return TaskDescriptionPage; - }, - name: 'Task_Description', - }, - { - getComponent: () => { - const TaskAssigneeSelectorPage = require('../../../pages/tasks/TaskAssigneeSelectorModal').default; - return TaskAssigneeSelectorPage; - }, - name: 'Task_Assignee', - }, -]); - -const ReportWelcomeMessageModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const ReportWelcomeMessagePage = require('../../../pages/ReportWelcomeMessagePage').default; - return ReportWelcomeMessagePage; - }, - name: 'Report_WelcomeMessage_Root', - }, -]); - -const ReportParticipantsModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const ReportParticipantsPage = require('../../../pages/ReportParticipantsPage').default; - return ReportParticipantsPage; - }, - name: 'ReportParticipants_Root', - }, -]); - -const SearchModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const SearchPage = require('../../../pages/SearchPage').default; - return SearchPage; - }, - name: 'Search_Root', - }, -]); - -const NewChatModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const NewChatSelectorPage = require('../../../pages/NewChatSelectorPage').default; - return NewChatSelectorPage; - }, - name: 'NewChat_Root', - }, -]); - -const NewTaskModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const NewTaskPage = require('../../../pages/tasks/NewTaskPage').default; - return NewTaskPage; - }, - name: 'NewTask_Root', - }, - { - getComponent: () => { - const NewTaskAssigneeSelectorPage = require('../../../pages/tasks/TaskAssigneeSelectorModal').default; - return NewTaskAssigneeSelectorPage; - }, - name: 'NewTask_TaskAssigneeSelector', - }, - { - getComponent: () => { - const NewTaskTaskShareDestinationPage = require('../../../pages/tasks/TaskShareDestinationSelectorModal').default; - return NewTaskTaskShareDestinationPage; - }, - name: 'NewTask_TaskShareDestinationSelector', - }, - { - getComponent: () => { - const NewTaskDetailsPage = require('../../../pages/tasks/NewTaskDetailsPage').default; - return NewTaskDetailsPage; - }, - name: 'NewTask_Details', - }, - { - getComponent: () => { - const NewTaskTitlePage = require('../../../pages/tasks/NewTaskTitlePage').default; - return NewTaskTitlePage; - }, - name: 'NewTask_Title', - }, - { - getComponent: () => { - const NewTaskDescriptionPage = require('../../../pages/tasks/NewTaskDescriptionPage').default; - return NewTaskDescriptionPage; - }, - name: 'NewTask_Description', - }, -]); - -const NewTeachersUniteNavigator = createModalStackNavigator([ - { - getComponent: () => { - const SaveTheWorldPage = require('../../../pages/TeachersUnite/SaveTheWorldPage').default; - return SaveTheWorldPage; - }, - name: SCREENS.SAVE_THE_WORLD.ROOT, - }, - { - getComponent: () => { - const KnowATeacherPage = require('../../../pages/TeachersUnite/KnowATeacherPage').default; - return KnowATeacherPage; - }, - name: 'I_Know_A_Teacher', - }, - { - getComponent: () => { - const IntroSchoolPrincipalPage = require('../../../pages/TeachersUnite/ImTeacherPage').default; - return IntroSchoolPrincipalPage; - }, - name: 'Intro_School_Principal', - }, - { - getComponent: () => { - const ImTeacherPage = require('../../../pages/TeachersUnite/ImTeacherPage').default; - return ImTeacherPage; - }, - name: 'I_Am_A_Teacher', - }, -]); - -const SettingsModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const SettingsInitialPage = require('../../../pages/settings/InitialSettingsPage').default; - return SettingsInitialPage; - }, - name: SCREENS.SETTINGS.ROOT, - }, - { - getComponent: () => { - const ShareCodePage = require('../../../pages/ShareCodePage').default; - return ShareCodePage; - }, - name: 'Settings_Share_Code', - }, - { - getComponent: () => { - const SettingsWorkspacesPage = require('../../../pages/workspace/WorkspacesListPage').default; - return SettingsWorkspacesPage; - }, - name: SCREENS.SETTINGS.WORKSPACES, - }, - { - getComponent: () => { - const SettingsProfilePage = require('../../../pages/settings/Profile/ProfilePage').default; - return SettingsProfilePage; - }, - name: 'Settings_Profile', - }, - { - getComponent: () => { - const SettingsPronounsPage = require('../../../pages/settings/Profile/PronounsPage').default; - return SettingsPronounsPage; - }, - name: 'Settings_Pronouns', - }, - { - getComponent: () => { - const SettingsDisplayNamePage = require('../../../pages/settings/Profile/DisplayNamePage').default; - return SettingsDisplayNamePage; - }, - name: 'Settings_Display_Name', - }, - { - getComponent: () => { - const SettingsTimezoneInitialPage = require('../../../pages/settings/Profile/TimezoneInitialPage').default; - return SettingsTimezoneInitialPage; - }, - name: 'Settings_Timezone', - }, - { - getComponent: () => { - const SettingsTimezoneSelectPage = require('../../../pages/settings/Profile/TimezoneSelectPage').default; - return SettingsTimezoneSelectPage; - }, - name: 'Settings_Timezone_Select', - }, - { - getComponent: () => { - const SettingsPersonalDetailsInitialPage = require('../../../pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage').default; - return SettingsPersonalDetailsInitialPage; - }, - name: 'Settings_PersonalDetails_Initial', - }, - { - getComponent: () => { - const SettingsLegalNamePage = require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default; - return SettingsLegalNamePage; - }, - name: 'Settings_PersonalDetails_LegalName', - }, - { - getComponent: () => { - const SettingsDateOfBirthPage = require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default; - return SettingsDateOfBirthPage; - }, - name: 'Settings_PersonalDetails_DateOfBirth', - }, - { - getComponent: () => { - const SettingsAddressPage = require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default; - return SettingsAddressPage; - }, - name: 'Settings_PersonalDetails_Address', - }, - { - getComponent: () => { - const SettingsContactMethodsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default; - return SettingsContactMethodsPage; - }, - name: 'Settings_ContactMethods', - }, - { - getComponent: () => { - const SettingsContactMethodDetailsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default; - return SettingsContactMethodDetailsPage; - }, - name: 'Settings_ContactMethodDetails', - }, - { - getComponent: () => { - const SettingsNewContactMethodPage = require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default; - return SettingsNewContactMethodPage; - }, - name: 'Settings_NewContactMethod', - }, - { - getComponent: () => { - const SettingsPreferencesPage = require('../../../pages/settings/Preferences/PreferencesPage').default; - return SettingsPreferencesPage; - }, - name: SCREENS.SETTINGS.PREFERENCES, - }, - { - getComponent: () => { - const SettingsPreferencesPriorityModePage = require('../../../pages/settings/Preferences/PriorityModePage').default; - return SettingsPreferencesPriorityModePage; - }, - name: 'Settings_Preferences_PriorityMode', - }, - { - getComponent: () => { - const SettingsPreferencesLanguagePage = require('../../../pages/settings/Preferences/LanguagePage').default; - return SettingsPreferencesLanguagePage; - }, - name: 'Settings_Preferences_Language', - }, +const MoneyRequestModalStackNavigator = createModalStackNavigator({ + Money_Request: () => require('../../../pages/iou/MoneyRequestSelectorPage').default, + Money_Request_Amount: () => require('../../../pages/iou/steps/NewRequestAmountPage').default, + Money_Request_Participants: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default, + Money_Request_Confirmation: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default, + Money_Request_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default, + Money_Request_Date: () => require('../../../pages/iou/MoneyRequestDatePage').default, + Money_Request_Description: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default, + Money_Request_Category: () => require('../../../pages/iou/MoneyRequestCategoryPage').default, + Money_Request_Tag: () => require('../../../pages/iou/MoneyRequestTagPage').default, + Money_Request_Merchant: () => require('../../../pages/iou/MoneyRequestMerchantPage').default, + IOU_Send_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default, + IOU_Send_Add_Debit_Card: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default, + IOU_Send_Enable_Payments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, + Money_Request_Waypoint: () => require('../../../pages/iou/WaypointEditorPage').default, + Money_Request_Receipt: () => require('../../../pages/EditRequestReceiptPage').default, + Money_Request_Address: () => require('../../../pages/iou/DistanceRequestPage').default, +}); + +const SplitDetailsModalStackNavigator = createModalStackNavigator({ + SplitDetails_Root: () => require('../../../pages/iou/SplitBillDetailsPage').default, +}); + +const DetailsModalStackNavigator = createModalStackNavigator({ + Details_Root: () => require('../../../pages/DetailsPage').default, +}); + +const ProfileModalStackNavigator = createModalStackNavigator({ + Profile_Root: () => require('../../../pages/ProfilePage').default, +}); + +const ReportDetailsModalStackNavigator = createModalStackNavigator({ + Report_Details_Root: () => require('../../../pages/ReportDetailsPage').default, + Report_Details_Share_Code: () => require('../../../pages/home/report/ReportDetailsShareCodePage').default, +}); + +const ReportSettingsModalStackNavigator = createModalStackNavigator({ + Report_Settings_Root: () => require('../../../pages/settings/Report/ReportSettingsPage').default, + Report_Settings_Room_Name: () => require('../../../pages/settings/Report/RoomNamePage').default, + Report_Settings_Notification_Preferences: () => require('../../../pages/settings/Report/NotificationPreferencePage').default, + Report_Settings_Write_Capability: () => require('../../../pages/settings/Report/WriteCapabilityPage').default, +}); + +const TaskModalStackNavigator = createModalStackNavigator({ + Task_Title: () => require('../../../pages/tasks/TaskTitlePage').default, + Task_Description: () => require('../../../pages/tasks/TaskDescriptionPage').default, + Task_Assignee: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default, +}); + +const ReportWelcomeMessageModalStackNavigator = createModalStackNavigator({ + Report_WelcomeMessage_Root: () => require('../../../pages/ReportWelcomeMessagePage').default, +}); + +const ReportParticipantsModalStackNavigator = createModalStackNavigator({ + ReportParticipants_Root: () => require('../../../pages/ReportParticipantsPage').default, +}); + +const SearchModalStackNavigator = createModalStackNavigator({ + Search_Root: () => require('../../../pages/SearchPage').default, +}); + +const NewChatModalStackNavigator = createModalStackNavigator({ + NewChat_Root: () => require('../../../pages/NewChatSelectorPage').default, +}); + +const NewTaskModalStackNavigator = createModalStackNavigator({ + NewTask_Root: () => require('../../../pages/tasks/NewTaskPage').default, + NewTask_TaskAssigneeSelector: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default, + NewTask_TaskShareDestinationSelector: () => require('../../../pages/tasks/TaskShareDestinationSelectorModal').default, + NewTask_Details: () => require('../../../pages/tasks/NewTaskDetailsPage').default, + NewTask_Title: () => require('../../../pages/tasks/NewTaskTitlePage').default, + NewTask_Description: () => require('../../../pages/tasks/NewTaskDescriptionPage').default, +}); + +const NewTeachersUniteNavigator = createModalStackNavigator({ + [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../pages/TeachersUnite/SaveTheWorldPage').default, + I_Know_A_Teacher: () => require('../../../pages/TeachersUnite/KnowATeacherPage').default, + Intro_School_Principal: () => require('../../../pages/TeachersUnite/ImTeacherPage').default, + I_Am_A_Teacher: () => require('../../../pages/TeachersUnite/ImTeacherPage').default, +}); + +const SettingsModalStackNavigator = createModalStackNavigator({ + [SCREENS.SETTINGS.ROOT]: () => require('../../../pages/settings/InitialSettingsPage').default, + Settings_Share_Code: () => require('../../../pages/ShareCodePage').default, + [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../pages/workspace/WorkspacesListPage').default, + Settings_Profile: () => require('../../../pages/settings/Profile/ProfilePage').default, + Settings_Pronouns: () => require('../../../pages/settings/Profile/PronounsPage').default, + Settings_Display_Name: () => require('../../../pages/settings/Profile/DisplayNamePage').default, + Settings_Timezone: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default, + Settings_Timezone_Select: () => require('../../../pages/settings/Profile/TimezoneSelectPage').default, + Settings_PersonalDetails_Initial: () => require('../../../pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage').default, + Settings_PersonalDetails_LegalName: () => require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default, + Settings_PersonalDetails_DateOfBirth: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default, + Settings_PersonalDetails_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default, + Settings_ContactMethods: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, + Settings_ContactMethodDetails: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default, + Settings_NewContactMethod: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default, + [SCREENS.SETTINGS.PREFERENCES]: () => require('../../../pages/settings/Preferences/PreferencesPage').default, + Settings_Preferences_PriorityMode: () => require('../../../pages/settings/Preferences/PriorityModePage').default, + Settings_Preferences_Language: () => require('../../../pages/settings/Preferences/LanguagePage').default, // Will be uncommented as part of https://github.com/Expensify/App/issues/21670 - // { - // getComponent: () => { - // const SettingsPreferencesThemePage = require('../../../pages/settings/Preferences/ThemePage').default; - // return SettingsPreferencesThemePage; - // }, - // name: 'Settings_Preferences_Theme', - // }, - { - getComponent: () => { - const SettingsCloseAccountPage = require('../../../pages/settings/Security/CloseAccountPage').default; - return SettingsCloseAccountPage; - }, - name: 'Settings_Close', - }, - { - getComponent: () => { - const SettingsSecurityPage = require('../../../pages/settings/Security/SecuritySettingsPage').default; - return SettingsSecurityPage; - }, - name: SCREENS.SETTINGS.SECURITY, - }, - { - getComponent: () => { - const SettingsAboutPage = require('../../../pages/settings/AboutPage/AboutPage').default; - return SettingsAboutPage; - }, - name: 'Settings_About', - }, - { - getComponent: () => { - const SettingsAppDownloadLinks = require('../../../pages/settings/AppDownloadLinks').default; - return SettingsAppDownloadLinks; - }, - name: 'Settings_App_Download_Links', - }, - { - getComponent: () => { - const SettingsLoungeAccessPage = require('../../../pages/settings/Profile/LoungeAccessPage').default; - return SettingsLoungeAccessPage; - }, - name: 'Settings_Lounge_Access', - }, - { - getComponent: () => { - const SettingsWalletPage = require('../../../pages/settings/Wallet/WalletPage').default; - return SettingsWalletPage; - }, - name: 'Settings_Wallet', - }, - { - getComponent: () => { - const TransferBalancePage = require('../../../pages/settings/Wallet/TransferBalancePage').default; - return TransferBalancePage; - }, - name: 'Settings_Wallet_Transfer_Balance', - }, - { - getComponent: () => { - const ChooseTransferAccountPage = require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default; - return ChooseTransferAccountPage; - }, - name: 'Settings_Wallet_Choose_Transfer_Account', - }, - { - getComponent: () => { - const EnablePaymentsPage = require('../../../pages/EnablePayments/EnablePaymentsPage').default; - return EnablePaymentsPage; - }, - name: 'Settings_Wallet_EnablePayments', - }, - { - getComponent: () => { - const AddDebitCardPage = require('../../../pages/settings/Wallet/AddDebitCardPage').default; - return AddDebitCardPage; - }, - name: 'Settings_Add_Debit_Card', - }, - { - getComponent: () => { - const AddPersonalBankAccountPage = require('../../../pages/AddPersonalBankAccountPage').default; - return AddPersonalBankAccountPage; - }, - name: 'Settings_Add_Bank_Account', - }, - { - getComponent: () => { - const SettingsStatus = require('../../../pages/settings/Profile/CustomStatus/StatusPage').default; - return SettingsStatus; - }, - name: SCREENS.SETTINGS.STATUS, - }, - { - getComponent: () => { - const SettingsStatusSet = require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default; - return SettingsStatusSet; - }, - name: 'Settings_Status_Set', - }, - { - getComponent: () => { - const WorkspaceInitialPage = require('../../../pages/workspace/WorkspaceInitialPage').default; - return WorkspaceInitialPage; - }, - name: 'Workspace_Initial', - }, - { - getComponent: () => { - const WorkspaceSettingsPage = require('../../../pages/workspace/WorkspaceSettingsPage').default; - return WorkspaceSettingsPage; - }, - name: 'Workspace_Settings', - }, - { - getComponent: () => { - const WorkspaceCardPage = require('../../../pages/workspace/card/WorkspaceCardPage').default; - return WorkspaceCardPage; - }, - name: 'Workspace_Card', - }, - { - getComponent: () => { - const WorkspaceReimbursePage = require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default; - return WorkspaceReimbursePage; - }, - name: 'Workspace_Reimburse', - }, - { - getComponent: () => { - const WorkspaceRateAndUnitPage = require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default; - return WorkspaceRateAndUnitPage; - }, - name: 'Workspace_RateAndUnit', - }, - { - getComponent: () => { - const WorkspaceBillsPage = require('../../../pages/workspace/bills/WorkspaceBillsPage').default; - return WorkspaceBillsPage; - }, - name: 'Workspace_Bills', - }, - { - getComponent: () => { - const WorkspaceInvoicesPage = require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default; - return WorkspaceInvoicesPage; - }, - name: 'Workspace_Invoices', - }, - { - getComponent: () => { - const WorkspaceTravelPage = require('../../../pages/workspace/travel/WorkspaceTravelPage').default; - return WorkspaceTravelPage; - }, - name: 'Workspace_Travel', - }, - { - getComponent: () => { - const WorkspaceMembersPage = require('../../../pages/workspace/WorkspaceMembersPage').default; - return WorkspaceMembersPage; - }, - name: 'Workspace_Members', - }, - { - getComponent: () => { - const WorkspaceInvitePage = require('../../../pages/workspace/WorkspaceInvitePage').default; - return WorkspaceInvitePage; - }, - name: 'Workspace_Invite', - }, - { - getComponent: () => { - const WorkspaceInviteMessagePage = require('../../../pages/workspace/WorkspaceInviteMessagePage').default; - return WorkspaceInviteMessagePage; - }, - name: 'Workspace_Invite_Message', - }, - { - getComponent: () => { - const ReimbursementAccountPage = require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default; - return ReimbursementAccountPage; - }, - name: 'ReimbursementAccount', - initialParams: {stepToOpen: ''}, - }, - { - getComponent: () => { - const GetAssistancePage = require('../../../pages/GetAssistancePage').default; - return GetAssistancePage; - }, - name: 'GetAssistance', - }, - { - getComponent: () => { - const SettingsTwoFactorAuth = require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default; - return SettingsTwoFactorAuth; - }, - name: 'Settings_TwoFactorAuth', - }, -]); - -const EnablePaymentsStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const EnablePaymentsPage = require('../../../pages/EnablePayments/EnablePaymentsPage').default; - return EnablePaymentsPage; - }, - name: 'EnablePayments_Root', - }, -]); - -const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const AddPersonalBankAccountPage = require('../../../pages/AddPersonalBankAccountPage').default; - return AddPersonalBankAccountPage; - }, - name: 'AddPersonalBankAccount_Root', - }, -]); - -const ReimbursementAccountModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const ReimbursementAccountPage = require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default; - return ReimbursementAccountPage; - }, - name: 'ReimbursementAccount_Root', - }, -]); - -const WalletStatementStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const WalletStatementPage = require('../../../pages/wallet/WalletStatementPage').default; - return WalletStatementPage; - }, - name: 'WalletStatement_Root', - }, -]); - -const FlagCommentStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const FlagCommentPage = require('../../../pages/FlagCommentPage').default; - return FlagCommentPage; - }, - name: 'FlagComment_Root', - }, -]); - -const EditRequestStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const EditRequestPage = require('../../../pages/EditRequestPage').default; - return EditRequestPage; - }, - name: 'EditRequest_Root', - }, - { - getComponent: () => { - const IOUCurrencySelection = require('../../../pages/iou/IOUCurrencySelection').default; - return IOUCurrencySelection; - }, - name: 'EditRequest_Currency', - }, -]); - -const PrivateNotesModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const PrivateNotesPage = require('../../../pages/PrivateNotes/PrivateNotesViewPage').default; - return PrivateNotesPage; - }, - name: 'PrivateNotes_View', - }, - { - getComponent: () => { - const PrivateNotesListPage = require('../../../pages/PrivateNotes/PrivateNotesListPage').default; - return PrivateNotesListPage; - }, - name: 'PrivateNotes_List', - }, - { - getComponent: () => { - const PrivateNotesEditPage = require('../../../pages/PrivateNotes/PrivateNotesEditPage').default; - return PrivateNotesEditPage; - }, - name: 'PrivateNotes_Edit', - }, -]); - -const SignInModalStackNavigator = createModalStackNavigator([ - { - getComponent: () => { - const SignInModal = require('../../../pages/signin/SignInModal').default; - return SignInModal; - }, - name: 'SignIn_Root', - }, -]); + // Settings_Preferences_Theme: () => require('../../../pages/settings/Preferences/ThemePage').default, + Settings_Close: () => require('../../../pages/settings/Security/CloseAccountPage').default, + [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default, + Settings_About: () => require('../../../pages/settings/AboutPage/AboutPage').default, + Settings_App_Download_Links: () => require('../../../pages/settings/AppDownloadLinks').default, + Settings_Lounge_Access: () => require('../../../pages/settings/Profile/LoungeAccessPage').default, + Settings_Wallet: () => require('../../../pages/settings/Wallet/WalletPage').default, + Settings_Wallet_Transfer_Balance: () => require('../../../pages/settings/Wallet/TransferBalancePage').default, + Settings_Wallet_Choose_Transfer_Account: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default, + Settings_Wallet_EnablePayments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, + Settings_Add_Debit_Card: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default, + Settings_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default, + [SCREENS.SETTINGS.STATUS]: () => require('../../../pages/settings/Profile/CustomStatus/StatusPage').default, + Settings_Status_Set: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default, + Workspace_Initial: () => require('../../../pages/workspace/WorkspaceInitialPage').default, + Workspace_Settings: () => require('../../../pages/workspace/WorkspaceSettingsPage').default, + Workspace_Card: () => require('../../../pages/workspace/card/WorkspaceCardPage').default, + Workspace_Reimburse: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default, + Workspace_RateAndUnit: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default, + Workspace_Bills: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default, + Workspace_Invoices: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default, + Workspace_Travel: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default, + Workspace_Members: () => require('../../../pages/workspace/WorkspaceMembersPage').default, + Workspace_Invite: () => require('../../../pages/workspace/WorkspaceInvitePage').default, + Workspace_Invite_Message: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default, + ReimbursementAccount: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, + GetAssistance: () => require('../../../pages/GetAssistancePage').default, + Settings_TwoFactorAuth: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default, +}); + +const EnablePaymentsStackNavigator = createModalStackNavigator({ + EnablePayments_Root: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, +}); + +const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator({ + AddPersonalBankAccount_Root: () => require('../../../pages/AddPersonalBankAccountPage').default, +}); + +const ReimbursementAccountModalStackNavigator = createModalStackNavigator({ + ReimbursementAccount_Root: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, +}); + +const WalletStatementStackNavigator = createModalStackNavigator({ + WalletStatement_Root: () => require('../../../pages/wallet/WalletStatementPage').default, +}); + +const FlagCommentStackNavigator = createModalStackNavigator({ + FlagComment_Root: () => require('../../../pages/FlagCommentPage').default, +}); + +const EditRequestStackNavigator = createModalStackNavigator({ + EditRequest_Root: () => require('../../../pages/EditRequestPage').default, + EditRequest_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default, +}); + +const PrivateNotesModalStackNavigator = createModalStackNavigator({ + PrivateNotes_View: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default, + PrivateNotes_List: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default, + PrivateNotes_Edit: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default, +}); + +const SignInModalStackNavigator = createModalStackNavigator({ + SignIn_Root: () => require('../../../pages/signin/SignInModal').default, +}); export { MoneyRequestModalStackNavigator, diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index 1264ec777b28..dc4f35a59cba 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -165,7 +165,7 @@ function dismissModal(targetReportID) { case SCREENS.REPORT_ATTACHMENTS: // if we are not in the target report, we need to navigate to it after dismissing the modal if (targetReportID && targetReportID !== getTopmostReportId(rootState)) { - const state = getStateFromPath(ROUTES.getReportRoute(targetReportID)); + const state = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID)); const action = getActionFromState(state, linkingConfig.config); action.type = 'REPLACE'; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index f4420330fbd9..8c278e4aad59 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -16,11 +16,7 @@ export default { AppleSignInDesktop: ROUTES.APPLE_SIGN_IN, GoogleSignInDesktop: ROUTES.GOOGLE_SIGN_IN, [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT, - [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS, - - // Demo routes - [CONST.DEMO_PAGES.SAASTR]: ROUTES.SAASTR, - [CONST.DEMO_PAGES.SBE]: ROUTES.SBE, + [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route, // Sidebar [SCREENS.HOME]: { @@ -29,7 +25,7 @@ export default { [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: { screens: { - [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID, + [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, }, }, [SCREENS.NOT_FOUND]: '*', @@ -126,7 +122,7 @@ export default { exact: true, }, Settings_ContactMethodDetails: { - path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS, + path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.route, }, Settings_Lounge_Access: { path: ROUTES.SETTINGS_LOUNGE_ACCESS, @@ -168,79 +164,79 @@ export default { exact: true, }, Workspace_Initial: { - path: ROUTES.WORKSPACE_INITIAL, + path: ROUTES.WORKSPACE_INITIAL.route, }, Workspace_Settings: { - path: ROUTES.WORKSPACE_SETTINGS, + path: ROUTES.WORKSPACE_SETTINGS.route, }, Workspace_Card: { - path: ROUTES.WORKSPACE_CARD, + path: ROUTES.WORKSPACE_CARD.route, }, Workspace_Reimburse: { - path: ROUTES.WORKSPACE_REIMBURSE, + path: ROUTES.WORKSPACE_REIMBURSE.route, }, Workspace_RateAndUnit: { - path: ROUTES.WORKSPACE_RATE_AND_UNIT, + path: ROUTES.WORKSPACE_RATE_AND_UNIT.route, }, Workspace_Bills: { - path: ROUTES.WORKSPACE_BILLS, + path: ROUTES.WORKSPACE_BILLS.route, }, Workspace_Invoices: { - path: ROUTES.WORKSPACE_INVOICES, + path: ROUTES.WORKSPACE_INVOICES.route, }, Workspace_Travel: { - path: ROUTES.WORKSPACE_TRAVEL, + path: ROUTES.WORKSPACE_TRAVEL.route, }, Workspace_Members: { - path: ROUTES.WORKSPACE_MEMBERS, + path: ROUTES.WORKSPACE_MEMBERS.route, }, Workspace_Invite: { - path: ROUTES.WORKSPACE_INVITE, + path: ROUTES.WORKSPACE_INVITE.route, }, Workspace_Invite_Message: { - path: ROUTES.WORKSPACE_INVITE_MESSAGE, + path: ROUTES.WORKSPACE_INVITE_MESSAGE.route, }, ReimbursementAccount: { - path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN, + path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, }, GetAssistance: { - path: ROUTES.GET_ASSISTANCE, + path: ROUTES.GET_ASSISTANCE.route, }, }, }, Private_Notes: { screens: { - PrivateNotes_View: ROUTES.PRIVATE_NOTES_VIEW, - PrivateNotes_List: ROUTES.PRIVATE_NOTES_LIST, - PrivateNotes_Edit: ROUTES.PRIVATE_NOTES_EDIT, + PrivateNotes_View: ROUTES.PRIVATE_NOTES_VIEW.route, + PrivateNotes_List: ROUTES.PRIVATE_NOTES_LIST.route, + PrivateNotes_Edit: ROUTES.PRIVATE_NOTES_EDIT.route, }, }, Report_Details: { screens: { - Report_Details_Root: ROUTES.REPORT_WITH_ID_DETAILS, - Report_Details_Share_Code: ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE, + Report_Details_Root: ROUTES.REPORT_WITH_ID_DETAILS.route, + Report_Details_Share_Code: ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.route, }, }, Report_Settings: { screens: { Report_Settings_Root: { - path: ROUTES.REPORT_SETTINGS, + path: ROUTES.REPORT_SETTINGS.route, }, Report_Settings_Room_Name: { - path: ROUTES.REPORT_SETTINGS_ROOM_NAME, + path: ROUTES.REPORT_SETTINGS_ROOM_NAME.route, }, Report_Settings_Notification_Preferences: { - path: ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES, + path: ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.route, }, Report_Settings_Write_Capability: { - path: ROUTES.REPORT_SETTINGS_WRITE_CAPABILITY, + path: ROUTES.REPORT_SETTINGS_WRITE_CAPABILITY.route, }, }, }, Report_WelcomeMessage: { screens: { - Report_WelcomeMessage_Root: ROUTES.REPORT_WELCOME_MESSAGE, + Report_WelcomeMessage_Root: ROUTES.REPORT_WELCOME_MESSAGE.route, }, }, NewChat: { @@ -286,23 +282,23 @@ export default { }, Details: { screens: { - Details_Root: ROUTES.DETAILS, + Details_Root: ROUTES.DETAILS.route, }, }, Profile: { screens: { - Profile_Root: ROUTES.PROFILE, + Profile_Root: ROUTES.PROFILE.route, }, }, Participants: { screens: { - ReportParticipants_Root: ROUTES.REPORT_PARTICIPANTS, + ReportParticipants_Root: ROUTES.REPORT_PARTICIPANTS.route, }, }, MoneyRequest: { screens: { Money_Request: { - path: ROUTES.MONEY_REQUEST, + path: ROUTES.MONEY_REQUEST.route, exact: true, screens: { manual: { @@ -314,22 +310,23 @@ export default { exact: true, }, distance: { - path: ROUTES.MONEY_REQUEST_DISTANCE_TAB, + path: ROUTES.MONEY_REQUEST_DISTANCE_TAB.route, exact: true, }, }, }, - Money_Request_Amount: ROUTES.MONEY_REQUEST_AMOUNT, - Money_Request_Participants: ROUTES.MONEY_REQUEST_PARTICIPANTS, - Money_Request_Confirmation: ROUTES.MONEY_REQUEST_CONFIRMATION, - Money_Request_Date: ROUTES.MONEY_REQUEST_DATE, - Money_Request_Currency: ROUTES.MONEY_REQUEST_CURRENCY, - Money_Request_Description: ROUTES.MONEY_REQUEST_DESCRIPTION, - Money_Request_Category: ROUTES.MONEY_REQUEST_CATEGORY, - Money_Request_Tag: ROUTES.MONEY_REQUEST_TAG, - Money_Request_Merchant: ROUTES.MONEY_REQUEST_MERCHANT, - Money_Request_Waypoint: ROUTES.MONEY_REQUEST_WAYPOINT, - Money_Request_Address: ROUTES.MONEY_REQUEST_ADDRESS, + Money_Request_Amount: ROUTES.MONEY_REQUEST_AMOUNT.route, + Money_Request_Participants: ROUTES.MONEY_REQUEST_PARTICIPANTS.route, + Money_Request_Confirmation: ROUTES.MONEY_REQUEST_CONFIRMATION.route, + Money_Request_Date: ROUTES.MONEY_REQUEST_DATE.route, + Money_Request_Currency: ROUTES.MONEY_REQUEST_CURRENCY.route, + Money_Request_Description: ROUTES.MONEY_REQUEST_DESCRIPTION.route, + Money_Request_Category: ROUTES.MONEY_REQUEST_CATEGORY.route, + Money_Request_Tag: ROUTES.MONEY_REQUEST_TAG.route, + Money_Request_Merchant: ROUTES.MONEY_REQUEST_MERCHANT.route, + Money_Request_Waypoint: ROUTES.MONEY_REQUEST_WAYPOINT.route, + Money_Request_Receipt: ROUTES.MONEY_REQUEST_RECEIPT.route, + Money_Request_Address: ROUTES.MONEY_REQUEST_ADDRESS.route, IOU_Send_Enable_Payments: ROUTES.IOU_SEND_ENABLE_PAYMENTS, IOU_Send_Add_Bank_Account: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT, IOU_Send_Add_Debit_Card: ROUTES.IOU_SEND_ADD_DEBIT_CARD, @@ -337,14 +334,14 @@ export default { }, SplitDetails: { screens: { - SplitDetails_Root: ROUTES.SPLIT_BILL_DETAILS, + SplitDetails_Root: ROUTES.SPLIT_BILL_DETAILS.route, }, }, Task_Details: { screens: { - Task_Title: ROUTES.TASK_TITLE, - Task_Description: ROUTES.TASK_DESCRIPTION, - Task_Assignee: ROUTES.TASK_ASSIGNEE, + Task_Title: ROUTES.TASK_TITLE.route, + Task_Description: ROUTES.TASK_DESCRIPTION.route, + Task_Assignee: ROUTES.TASK_ASSIGNEE.route, }, }, AddPersonalBankAccount: { @@ -364,13 +361,13 @@ export default { }, Flag_Comment: { screens: { - FlagComment_Root: ROUTES.FLAG_COMMENT, + FlagComment_Root: ROUTES.FLAG_COMMENT.route, }, }, EditRequest: { screens: { - EditRequest_Root: ROUTES.EDIT_REQUEST, - EditRequest_Currency: ROUTES.EDIT_CURRENCY_REQUEST, + EditRequest_Root: ROUTES.EDIT_REQUEST.route, + EditRequest_Currency: ROUTES.EDIT_CURRENCY_REQUEST.route, }, }, SignIn: { diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js index 8e16bb72f656..aadc7d6c3983 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js @@ -31,7 +31,7 @@ export default function subscribeToReportCommentPushNotifications() { } Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - Navigation.navigate(ROUTES.getReportRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); } catch (error) { Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: error.message}); } diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 3bdf77745432..7c36fa095029 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -539,7 +539,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.text = reportName; result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); - result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), false, personalDetail.login, personalDetail.accountID); + result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), personalDetail.login, personalDetail.accountID); result.subtitle = subtitle; return result; diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.js index d2de5b1c0d7e..347a825f59cc 100644 --- a/src/libs/PolicyUtils.js +++ b/src/libs/PolicyUtils.js @@ -58,6 +58,42 @@ function hasCustomUnitsError(policy) { return !_.isEmpty(_.pick(lodashGet(policy, 'customUnits', {}), 'errors')); } +/** + * @param {Number} value + * @param {Function} toLocaleDigit + * @returns {Number} + */ +function getNumericValue(value, toLocaleDigit) { + const numValue = parseFloat(value.toString().replace(toLocaleDigit('.'), '.')); + if (Number.isNaN(numValue)) { + return NaN; + } + return numValue.toFixed(CONST.CUSTOM_UNITS.RATE_DECIMALS); +} + +/** + * @param {Number} value + * @param {Function} toLocaleDigit + * @returns {String} + */ +function getRateDisplayValue(value, toLocaleDigit) { + const numValue = getNumericValue(value, toLocaleDigit); + if (Number.isNaN(numValue)) { + return ''; + } + return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.length); +} + +/** + * @param {Object} customUnitRate + * @param {Number} customUnitRate.rate + * @param {Function} toLocaleDigit + * @returns {String} + */ +function getUnitRateValue(customUnitRate, toLocaleDigit) { + return getRateDisplayValue(lodashGet(customUnitRate, 'rate', 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit); +} + /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. * @@ -167,6 +203,56 @@ function getIneligibleInvitees(policyMembers, personalDetails) { return memberEmailsToExclude; } +/** + * Gets the tag from policy tags, defaults to the first if no key is provided. + * + * @param {Object} policyTags + * @param {String} [tagKey] + * @returns {Object} + */ +function getTag(policyTags, tagKey) { + if (_.isEmpty(policyTags)) { + return {}; + } + + const policyTagKey = tagKey || _.first(_.keys(policyTags)); + + return lodashGet(policyTags, policyTagKey, {}); +} + +/** + * Gets the first tag name from policy tags. + * + * @param {Object} policyTags + * @returns {String} + */ +function getTagListName(policyTags) { + if (_.isEmpty(policyTags)) { + return ''; + } + + const policyTagKeys = _.keys(policyTags) || []; + + return lodashGet(policyTags, [_.first(policyTagKeys), 'name'], ''); +} + +/** + * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. + * + * @param {Object} policyTags + * @param {String} [tagKey] + * @returns {String} + */ +function getTagList(policyTags, tagKey) { + if (_.isEmpty(policyTags)) { + return {}; + } + + const policyTagKey = tagKey || _.first(_.keys(policyTags)); + + return lodashGet(policyTags, [policyTagKey, 'tags'], {}); +} + /** * @param {Object} policy * @returns {Boolean} @@ -181,6 +267,8 @@ export { hasPolicyError, hasPolicyErrorFields, hasCustomUnitsError, + getNumericValue, + getUnitRateValue, getPolicyBrickRoadIndicatorStatus, shouldShowPolicy, isExpensifyTeam, @@ -188,5 +276,8 @@ export { isPolicyAdmin, getMemberAccountIDsForWorkspace, getIneligibleInvitees, + getTag, + getTagListName, + getTagList, isPendingDeletePolicy, }; diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 83493ed71fc5..ca4f9d77898b 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -4,6 +4,7 @@ import {TextInput} from 'react-native'; type FocusCallback = () => void; const composerRef = React.createRef(); +const editComposerRef = React.createRef(); // There are two types of composer: general composer (edit composer) and main composer. // The general composer callback will take priority if it exists. let focusCallback: FocusCallback | null = null; @@ -57,10 +58,19 @@ function isFocused(): boolean { return !!composerRef.current?.isFocused(); } +/** + * Exposes the current focus state of the edit message composer. + */ +function isEditFocused(): boolean { + return !!editComposerRef.current?.isFocused(); +} + export default { composerRef, onComposerFocus, focus, clear, isFocused, + editComposerRef, + isEditFocused, }; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index cda1f39d3087..19b0513b7e41 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -249,6 +249,23 @@ function extractLinksFromMessageHtml(reportAction) { return _.map([...htmlContent.matchAll(regex)], (match) => match[1]); } +/** + * Returns the report action immediately before the specified index. + * @param {Array} reportActions - all actions + * @param {Number} actionIndex - index of the action + * @returns {Object|null} + */ +function findPreviousAction(reportActions, actionIndex) { + for (let i = actionIndex + 1; i < reportActions.length; i++) { + // Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list. + // If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete. + if (isNetworkOffline || reportActions[i].pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return reportActions[i]; + } + } + return null; +} + /** * Returns true when the report action immediately before the specified index is a comment made by the same actor who who is leaving a comment in the action at the specified index. * Also checks to ensure that the comment is not too old to be shown as a grouped comment. @@ -258,9 +275,7 @@ function extractLinksFromMessageHtml(reportAction) { * @returns {Boolean} */ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { - // Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list. - // If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete. - const previousAction = _.find(_.drop(reportActions, actionIndex + 1), (action) => isNetworkOffline || action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const previousAction = findPreviousAction(reportActions, actionIndex); const currentAction = reportActions[actionIndex]; // It's OK for there to be no previous action, and in that case, false will be returned diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index eee9d6549f6c..fbc6f6a73033 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -15,6 +15,7 @@ import ROUTES from '../ROUTES'; import * as NumberUtils from './NumberUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; +import * as Url from './Url'; import Permissions from './Permissions'; import DateUtils from './DateUtils'; import linkingConfig from './Navigation/linkingConfig'; @@ -422,7 +423,7 @@ function isPublicAnnounceRoom(report) { * @returns {String} */ function getBankAccountRoute(report) { - return isPolicyExpenseChat(report) ? ROUTES.getBankAccountRoute('', report.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; + return isPolicyExpenseChat(report) ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', report.policyID) : ROUTES.SETTINGS_ADD_BANK_ACCOUNT; } /** @@ -1007,13 +1008,12 @@ function getWorkspaceIcon(report, policy = undefined) { * @param {Object} report * @param {Object} personalDetails * @param {*} [defaultIcon] - * @param {Boolean} [isPayer] * @param {String} [defaultName] * @param {Number} [defaultAccountID] * @param {Object} [policy] * @returns {Array<*>} */ -function getIcons(report, personalDetails, defaultIcon = null, isPayer = false, defaultName = '', defaultAccountID = -1, policy = undefined) { +function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined) { if (_.isEmpty(report)) { const fallbackIcon = { source: defaultIcon || Expensicons.FallbackAvatar, @@ -1101,13 +1101,13 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false, type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [report.managerID, 'displayName'], ''), }; - const ownerIcon = { id: report.ownerAccountID, source: UserUtils.getAvatar(lodashGet(personalDetails, [report.ownerAccountID, 'avatar']), report.ownerAccountID), type: CONST.ICON_TYPE_AVATAR, name: lodashGet(personalDetails, [report.ownerAccountID, 'displayName'], ''), }; + const isPayer = currentUserAccountID === report.managerID; return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } @@ -1183,6 +1183,17 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR }); } +/** + * Get the report given a reportID + * + * @param {String} reportID + * @returns {Object} + */ +function getReport(reportID) { + // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check + return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; +} + /** * Determines if a report has an IOU that is waiting for an action from the current user (either Pay or Add a credit bank account) * @@ -1194,6 +1205,10 @@ function isWaitingForIOUActionFromCurrentUser(report) { return false; } + if (isArchivedRoom(getReport(report.parentReportID))) { + return false; + } + const policy = getPolicy(report.policyID); if (policy.type === CONST.POLICY.TYPE.CORPORATE) { // If the report is already settled, there's no action required from any user. @@ -1311,17 +1326,6 @@ function getMoneyRequestReportName(report, policy = undefined) { return payerPaidAmountMesssage; } -/** - * Get the report given a reportID - * - * @param {String} reportID - * @returns {Object} - */ -function getReport(reportID) { - // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check - return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; -} - /** * Gets transaction created, amount, currency and comment * @@ -1337,6 +1341,7 @@ function getTransactionDetails(transaction) { comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), category: TransactionUtils.getCategory(transaction), + tag: TransactionUtils.getTag(transaction), }; } @@ -1588,6 +1593,11 @@ function getModifiedExpenseMessage(reportAction) { if (hasModifiedCategory) { return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true); } + + const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag'); + if (hasModifiedTag) { + return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.tag, reportActionOriginalMessage.oldTag, Localize.translateLocal('common.tag'), true); + } } /** @@ -1633,6 +1643,11 @@ function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, i originalMessage.category = transactionChanges.category; } + if (_.has(transactionChanges, 'tag')) { + originalMessage.oldTag = TransactionUtils.getTag(oldTransaction); + originalMessage.tag = transactionChanges.tag; + } + return originalMessage; } @@ -1815,14 +1830,14 @@ function navigateToDetailsPage(report) { const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []); if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report)) { - Navigation.navigate(ROUTES.getReportDetailsRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); return; } if (participantAccountIDs.length === 1) { - Navigation.navigate(ROUTES.getProfileRoute(participantAccountIDs[0])); + Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0])); return; } - Navigation.navigate(ROUTES.getReportParticipantsRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report.reportID)); } /** @@ -2229,7 +2244,7 @@ function buildOptimisticIOUReportAction( created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, - whisperedToAccountIDs: !_.isEmpty(receipt) ? [currentUserAccountID] : [], + whisperedToAccountIDs: _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], receipt.state) ? [currentUserAccountID] : [], }; } /** @@ -2282,6 +2297,7 @@ function buildOptimisticApprovedReportAction(amount, currency, expenseReportID) */ function buildOptimisticReportPreview(chatReport, iouReport, comment = '', transaction = undefined) { const hasReceipt = TransactionUtils.hasReceipt(transaction); + const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const message = getReportPreviewMessage(iouReport); return { reportActionID: NumberUtils.rand64(), @@ -2306,7 +2322,7 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '', trans childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, childLastReceiptTransactionIDs: hasReceipt ? transaction.transactionID : '', - whisperedToAccountIDs: hasReceipt ? [currentUserAccountID] : [], + whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [], }; } @@ -2677,12 +2693,12 @@ function buildOptimisticWorkspaceChats(policyID, policyName) { * @param {String} parentReportID - Report ID of the chat where the Task is. * @param {String} title - Task title. * @param {String} description - Task description. - * @param {String | undefined} policyID - PolicyID of the parent report + * @param {String} policyID - PolicyID of the parent report * * @returns {Object} */ -function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parentReportID, title, description, policyID = undefined) { +function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parentReportID, title, description, policyID = CONST.POLICY.OWNER_EMAIL_FAKE) { return { reportID: generateReportID(), reportName: title, @@ -2692,9 +2708,9 @@ function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parent managerID: assigneeAccountID, type: CONST.REPORT.TYPE.TASK, parentReportID, + policyID, stateNum: CONST.REPORT.STATE_NUM.OPEN, statusNum: CONST.REPORT.STATUS.OPEN, - ...(_.isUndefined(policyID) ? {} : {policyID}), }; } @@ -2987,28 +3003,6 @@ function getChatByParticipants(newParticipantList) { }); } -/** - * Attempts to find a report in onyx with the provided email list of participants. Does not include threads - * This is temporary function while migrating from PersonalDetails to PersonalDetailsList - * - * @deprecated - use getChatByParticipants() - * - * @param {Array} participantsLoginList - * @returns {Array|undefined} - */ -function getChatByParticipantsByLoginList(participantsLoginList) { - participantsLoginList.sort(); - return _.find(allReports, (report) => { - // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it - if (!report || _.isEmpty(report.participantAccountIDs) || isThread(report)) { - return false; - } - - // Only return the room if it has all the participants and is not a policy room - return !isUserCreatedPolicyRoom(report) && _.isEqual(participantsLoginList, _.sortBy(report.participants)); - }); -} - /** * Attempts to find a report in onyx with the provided list of participants in given policy * @param {Array} newParticipantList @@ -3146,13 +3140,35 @@ function getRouteFromLink(url) { return route; } +/** + * @param {String} route + * @returns {Object} + */ +function parseReportRouteParams(route) { + let parsingRoute = route; + if (parsingRoute.at(0) === '/') { + // remove the first slash + parsingRoute = parsingRoute.slice(1); + } + + if (!parsingRoute.startsWith(Url.addTrailingForwardSlash('r'))) { + return {reportID: '', isSubReportPageRoute: false}; + } + + const pathSegments = parsingRoute.split('/'); + return { + reportID: pathSegments[1], + isSubReportPageRoute: pathSegments.length > 2, + }; +} + /** * @param {String|null} url * @returns {String} */ function getReportIDFromLink(url) { const route = getRouteFromLink(url); - const {reportID, isSubReportPageRoute} = ROUTES.parseReportRouteParams(route); + const {reportID, isSubReportPageRoute} = parseReportRouteParams(route); if (isSubReportPageRoute) { // We allow the Sub-Report deep link routes (settings, details, etc.) to be handled by their respective component pages return ''; @@ -3664,7 +3680,6 @@ export { getOptimisticDataForParentReportAction, shouldReportBeInOptionList, getChatByParticipants, - getChatByParticipantsByLoginList, getChatByParticipantsAndPolicy, getAllPolicyReports, getIOUReportActionMessage, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index f645697690e6..8520f34b8681 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -71,6 +71,16 @@ function isSidebarLoadedReady() { return sidebarIsReadyPromise; } +function compareStringDates(stringA, stringB) { + if (stringA < stringB) { + return -1; + } + if (stringA > stringB) { + return 1; + } + return 0; +} + function setIsSidebarLoadedReady() { resolveSidebarIsReadyPromise(); } @@ -184,12 +194,13 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p pinnedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); outstandingIOUReports.sort((a, b) => b.iouReportAmount - a.iouReportAmount || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); draftReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); + if (isInDefaultMode) { nonArchivedReports.sort( - (a, b) => new Date(b.lastVisibleActionCreated) - new Date(a.lastVisibleActionCreated) || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()), + (a, b) => compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()), ); // For archived reports ensure that most recent reports are at the top by reversing the order - archivedReports.sort((a, b) => new Date(a.lastVisibleActionCreated) - new Date(b.lastVisibleActionCreated)); + archivedReports.sort((a, b) => compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated)); } else { nonArchivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); archivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); @@ -385,7 +396,7 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), true, '', -1, policy); + result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), '', -1, policy); result.searchText = OptionsListUtils.getSearchText(report, reportName, participantPersonalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.displayNamesWithTooltips = displayNamesWithTooltips; result.isLastMessageDeletedParentAction = report.isLastMessageDeletedParentAction; diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 5dcfbc467c20..58fb23a8811a 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -151,6 +151,10 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep updatedTransaction.category = transactionChanges.category; } + if (_.has(transactionChanges, 'tag')) { + updatedTransaction.tag = transactionChanges.tag; + } + if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) { updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; } @@ -162,6 +166,7 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep ...(_.has(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(_.has(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(_.has(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(_.has(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }; return updatedTransaction; @@ -253,6 +258,16 @@ function getCategory(transaction) { return lodashGet(transaction, 'category', ''); } +/** + * Return the tag from the transaction. This "tag" field has no "modified" complement. + * + * @param {Object} transaction + * @return {String} + */ +function getTag(transaction) { + return lodashGet(transaction, 'tag', ''); +} + /** * Return the created field from the transaction, return the modifiedCreated if present. * @@ -399,6 +414,7 @@ export { getMerchant, getCreated, getCategory, + getTag, getLinkedTransaction, getAllReportTransactions, hasReceipt, diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 570b25040855..b8be35aa1919 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -325,10 +325,10 @@ function createWorkspaceAndNavigateToIt(policyOwnerEmail = '', makeMeAdmin = fal } if (shouldNavigateToAdminChat) { - Navigation.navigate(ROUTES.getReportRoute(adminsChatReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminsChatReportID)); } - Navigation.navigate(ROUTES.getWorkspaceInitialRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); }) .then(endSignOnTransition); } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 86cd791f7fce..413caee144eb 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -558,6 +558,7 @@ function getMoneyRequestInformation( ); return { + payerAccountID, payerEmail, iouReport, chatReport, @@ -665,27 +666,14 @@ function requestMoney( // If the report is 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 {payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( - currentChatReport, - participant, - comment, - amount, - currency, - created, - merchant, - payeeAccountID, - payeeEmail, - receipt, - undefined, - category, - tag, - billable, - ); + const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = + getMoneyRequestInformation(currentChatReport, participant, comment, amount, currency, created, merchant, payeeAccountID, payeeEmail, receipt, undefined, category, tag, billable); API.write( 'RequestMoney', { debtorEmail: payerEmail, + debtorAccountID: payerAccountID, amount, currency, comment, @@ -699,6 +687,7 @@ function requestMoney( createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction.reportActionID, receipt, + receiptState: lodashGet(receipt, 'state'), category, tag, billable, @@ -1147,6 +1136,17 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC updatedChatReport.lastMessageHtml = messageText; } + const optimisticPolicyRecentlyUsedTags = {}; + if (_.has(transactionChanges, 'tag')) { + const tagListName = transactionChanges.tagListName; + const recentlyUsedPolicyTags = allRecentlyUsedTags[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`]; + + if (recentlyUsedPolicyTags) { + const uniquePolicyRecentlyUsedTags = _.filter(recentlyUsedPolicyTags[tagListName], (recentlyUsedPolicyTag) => recentlyUsedPolicyTag !== transactionChanges.tag); + optimisticPolicyRecentlyUsedTags[tagListName] = [transactionChanges.tag, ...uniquePolicyRecentlyUsedTags]; + } + } + // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); const optimisticData = [ @@ -1182,6 +1182,14 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC }, ]; + if (!_.isEmpty(optimisticPolicyRecentlyUsedTags)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`, + value: optimisticPolicyRecentlyUsedTags, + }); + } + const successData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1201,6 +1209,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC currency: null, merchant: null, category: null, + tag: null, }, }, }, @@ -1247,7 +1256,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC ]; // STEP 6: Call the API endpoint - const {created, amount, currency, comment, merchant, category} = ReportUtils.getTransactionDetails(updatedTransaction); + const {created, amount, currency, comment, merchant, category, tag} = ReportUtils.getTransactionDetails(updatedTransaction); API.write( 'EditMoneyRequest', { @@ -1259,6 +1268,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC comment, merchant, category, + tag, }, {optimisticData, successData, failureData}, ); @@ -1473,14 +1483,14 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView if (isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { // Pop the deleted report screen before navigating. This prevents navigating to the Concierge chat due to the missing report. Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.getReportRoute(iouReport.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID)); return; } if (shouldDeleteIOUReport) { // Pop the deleted report screen before navigating. This prevents navigating to the Concierge chat due to the missing report. Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.getReportRoute(iouReport.chatReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReport.chatReportID)); } } @@ -1982,7 +1992,7 @@ function replaceReceipt(transactionID, receipt, filePath) { */ function startMoneyRequest(iouType, reportID = '') { resetMoneyRequestInfo(`${iouType}${reportID}`); - Navigation.navigate(ROUTES.getMoneyRequestRoute(iouType, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); } /** @@ -2086,8 +2096,9 @@ function createEmptyTransaction() { * @param {String} iouType * @param {String} reportID * @param {Object} report + * @param {String} path */ -function navigateToNextPage(iou, iouType, reportID, report) { +function navigateToNextPage(iou, iouType, reportID, report, path = '') { const moneyRequestID = `${iouType}${reportID}`; const shouldReset = iou.id !== moneyRequestID; @@ -2097,6 +2108,12 @@ function navigateToNextPage(iou, iouType, reportID, report) { resetMoneyRequestInfo(moneyRequestID); } + // If we're adding a receipt, that means the user came from the confirmation page and we need to navigate back to it. + if (path.slice(1) === ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType, reportID)) { + Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); + return; + } + // If a request is initiated on a report, skip the participants selection step and navigate to the confirmation page. if (report.reportID) { // If the report is iou or expense report, we should get the chat report to set participant for request money @@ -2112,10 +2129,10 @@ function navigateToNextPage(iou, iouType, reportID, report) { .value(); setMoneyRequestParticipants(participants); } - Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); return; } - Navigation.navigate(ROUTES.getMoneyRequestParticipantsRoute(iouType)); + Navigation.navigate(ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType)); } export { diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 93b46f2e53da..e42ef1ac4823 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -714,13 +714,15 @@ function updateWorkspaceCustomUnitAndRate(policyID, currentCustomUnit, newCustom }, ]; + const newCustomUnitParam = _.clone(newCustomUnit); + newCustomUnitParam.rates = _.omit(newCustomUnitParam.rates, ['pendingAction', 'errors']); API.write( 'UpdateWorkspaceCustomUnitAndRate', { policyID, lastModified, - customUnit: JSON.stringify(newCustomUnit), - customUnitRate: JSON.stringify(newCustomUnit.rates), + customUnit: JSON.stringify(newCustomUnitParam), + customUnitRate: JSON.stringify(newCustomUnitParam.rates), }, {optimisticData, successData, failureData}, ); diff --git a/src/libs/actions/ReimbursementAccount/navigation.js b/src/libs/actions/ReimbursementAccount/navigation.js index ae655c1ab8cf..1b75e6f16a58 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.js +++ b/src/libs/actions/ReimbursementAccount/navigation.js @@ -20,7 +20,7 @@ function goToWithdrawalAccountSetupStep(stepID, newAchData) { * @param {string} [backTo=''] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. */ function navigateToBankAccountRoute(policyId, backTo) { - Navigation.navigate(ROUTES.getBankAccountRoute('', policyId, backTo)); + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyId, backTo)); } export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9fa1e5fe0567..c7d8195b8803 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -25,7 +25,6 @@ import * as ErrorUtils from '../ErrorUtils'; import * as UserUtils from '../UserUtils'; import * as Welcome from './Welcome'; import * as PersonalDetailsUtils from '../PersonalDetailsUtils'; -import * as OptionsListUtils from '../OptionsListUtils'; import * as Environment from '../Environment/Environment'; import * as Session from './Session'; @@ -609,10 +608,11 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p */ function navigateToAndOpenReport(userLogins, shouldDismissModal = true) { let newChat = {}; - const formattedUserLogins = _.map(userLogins, (login) => OptionsListUtils.addSMSDomainIfPhoneNumber(login).toLowerCase()); - const chat = ReportUtils.getChatByParticipantsByLoginList(formattedUserLogins); + + const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); + const chat = ReportUtils.getChatByParticipants(participantAccountIDs); + if (!chat) { - const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); newChat = ReportUtils.buildOptimisticChatReport(participantAccountIDs); } const reportID = chat ? chat.reportID : newChat.reportID; @@ -622,7 +622,7 @@ function navigateToAndOpenReport(userLogins, shouldDismissModal = true) { if (shouldDismissModal) { Navigation.dismissModal(reportID); } else { - Navigation.navigate(ROUTES.getReportRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); } } @@ -655,7 +655,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs) { function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = {}, parentReportID = '0') { if (childReportID !== '0') { openReport(childReportID); - Navigation.navigate(ROUTES.getReportRoute(childReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); } else { const participantAccountIDs = _.uniq([currentUserAccountID, Number(parentReportAction.actorAccountID)]); const parentReport = allReports[parentReportID]; @@ -676,7 +676,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction = const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat.participantAccountIDs); openReport(newChat.reportID, participantLogins, newChat, parentReportAction.reportActionID); - Navigation.navigate(ROUTES.getReportRoute(newChat.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newChat.reportID)); } } @@ -946,7 +946,7 @@ function handleReportChanged(report) { // Only re-route them if they are still looking at the optimistically created report if (Navigation.getActiveRoute().includes(`/r/${report.reportID}`)) { // Pass 'FORCED_UP' type to replace new report on second login with proper one in the Navigation - Navigation.navigate(ROUTES.getReportRoute(report.preexistingReportID), CONST.NAVIGATION.TYPE.FORCED_UP); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID), CONST.NAVIGATION.TYPE.FORCED_UP); } return; } @@ -1273,7 +1273,7 @@ function saveReportActionDraftNumberOfLines(reportID, reportActionID, numberOfLi */ function updateNotificationPreferenceAndNavigate(reportID, previousValue, newValue) { if (previousValue === newValue) { - Navigation.goBack(ROUTES.getReportSettingsRoute(reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); return; } const optimisticData = [ @@ -1291,7 +1291,7 @@ function updateNotificationPreferenceAndNavigate(reportID, previousValue, newVal }, ]; API.write('UpdateReportNotificationPreference', {reportID, notificationPreference: newValue}, {optimisticData, failureData}); - Navigation.goBack(ROUTES.getReportSettingsRoute(reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); } /** @@ -1331,7 +1331,7 @@ function updateWelcomeMessage(reportID, previousValue, newValue) { */ function updateWriteCapabilityAndNavigate(report, newValue) { if (report.writeCapability === newValue) { - Navigation.goBack(ROUTES.getReportSettingsRoute(report.reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID)); return; } @@ -1351,7 +1351,7 @@ function updateWriteCapabilityAndNavigate(report, newValue) { ]; API.write('UpdateReportWriteCapability', {reportID: report.reportID, writeCapability: newValue}, {optimisticData, failureData}); // Return to the report settings page since this field utilizes push-to-page - Navigation.goBack(ROUTES.getReportSettingsRoute(report.reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID)); } /** @@ -1366,7 +1366,7 @@ function navigateToConciergeChat() { navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], false); }); } else { - Navigation.navigate(ROUTES.getReportRoute(conciergeChatReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID)); } } @@ -1516,7 +1516,7 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { // No change needed, navigate back if (previousName === policyRoomName) { - Navigation.goBack(ROUTES.getReportSettingsRoute(reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); return; } const optimisticData = [ @@ -1555,7 +1555,7 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport, policyRoomName) { }, ]; API.write('UpdatePolicyRoomName', {reportID, policyRoomName}, {optimisticData, successData, failureData}); - Navigation.goBack(ROUTES.getReportSettingsRoute(reportID)); + Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); } /** @@ -1651,7 +1651,7 @@ function showReportActionNotification(reportID, action) { reportAction: action, onClick: () => { // Navigate to this report onClick - Navigation.navigate(ROUTES.getReportRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }, }); notifyNewAction(reportID, action.actorAccountID, action.reportActionID); @@ -1890,7 +1890,7 @@ function leaveRoom(reportID) { Navigation.goBack(ROUTES.HOME); } if (report.parentReportID) { - Navigation.navigate(ROUTES.getReportRoute(report.parentReportID), CONST.NAVIGATION.TYPE.FORCED_UP); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.parentReportID), CONST.NAVIGATION.TYPE.FORCED_UP); return; } navigateToConciergeChat(); @@ -1911,7 +1911,7 @@ function setLastOpenedPublicRoom(reportID) { function openLastOpenedPublicRoom(lastOpenedPublicRoomID) { Navigation.isNavigationReady().then(() => { setLastOpenedPublicRoom(''); - Navigation.navigate(ROUTES.getReportRoute(lastOpenedPublicRoomID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastOpenedPublicRoomID)); }); } diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.ts similarity index 85% rename from src/libs/actions/ReportActions.js rename to src/libs/actions/ReportActions.ts index d270876840ac..3faa1dbe3574 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.ts @@ -3,14 +3,15 @@ import ONYXKEYS from '../../ONYXKEYS'; import CONST from '../../CONST'; import * as ReportActionUtils from '../ReportActionsUtils'; import * as ReportUtils from '../ReportUtils'; +import ReportAction from '../../types/onyx/ReportAction'; -/** - * @param {String} reportID - * @param {Object} reportAction - */ -function clearReportActionErrors(reportID, reportAction) { +function clearReportActionErrors(reportID: string, reportAction: ReportAction) { const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + if (!reportAction.reportActionID) { + return; + } + if (reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { // Delete the optimistic action Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, { diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 4e71a8793f1e..963bfebb7eb2 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -59,9 +59,9 @@ function clearOutTaskInfo() { * @param {String} assigneeEmail * @param {Number} assigneeAccountID * @param {Object} assigneeChatReport - The chat report between you and the assignee - * @param {String | undefined} policyID - the policyID of the parent report + * @param {String} policyID - the policyID of the parent report */ -function createTaskAndNavigate(parentReportID, title, description, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null, policyID = undefined) { +function createTaskAndNavigate(parentReportID, title, description, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null, policyID = CONST.POLICY.OWNER_EMAIL_FAKE) { const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, assigneeAccountID, parentReportID, title, description, policyID); const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : 0; @@ -642,7 +642,7 @@ function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, is let chatReport; if (!isCurrentUser) { - chatReport = ReportUtils.getChatByParticipantsByLoginList([assigneeEmail]) || ReportUtils.getChatByParticipants([assigneeAccountID]); + chatReport = ReportUtils.getChatByParticipants([assigneeAccountID]); if (!chatReport) { chatReport = ReportUtils.buildOptimisticChatReport([assigneeAccountID]); chatReport.isOptimisticReport = true; @@ -743,7 +743,7 @@ function getShareDestination(reportID, reports, personalDetails) { subtitle = ReportUtils.getChatRoomSubtitle(report); } return { - icons: ReportUtils.getIcons(report, personalDetails, Expensicons.FallbackAvatar, ReportUtils.isIOUReport(report)), + icons: ReportUtils.getIcons(report, personalDetails, Expensicons.FallbackAvatar), displayName: ReportUtils.getReportName(report), subtitle, }; diff --git a/src/libs/actions/Transaction.js b/src/libs/actions/Transaction.ts similarity index 77% rename from src/libs/actions/Transaction.js rename to src/libs/actions/Transaction.ts index 81764a9c62be..cc26dccc25b6 100644 --- a/src/libs/actions/Transaction.js +++ b/src/libs/actions/Transaction.ts @@ -1,19 +1,20 @@ -import _ from 'underscore'; import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import lodashHas from 'lodash/has'; +import lodashClone from 'lodash/clone'; import ONYXKEYS from '../../ONYXKEYS'; import * as CollectionUtils from '../CollectionUtils'; import * as API from '../API'; +import {RecentWaypoint, Transaction} from '../../types/onyx'; +import {WaypointCollection} from '../../types/onyx/Transaction'; import * as TransactionUtils from '../TransactionUtils'; -let recentWaypoints = []; +let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ key: ONYXKEYS.NVP_RECENT_WAYPOINTS, - callback: (val) => (recentWaypoints = val || []), + callback: (val) => (recentWaypoints = val ?? []), }); -const allTransactions = {}; +const allTransactions: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, callback: (transaction, key) => { @@ -25,10 +26,7 @@ Onyx.connect({ }, }); -/** - * @param {String} transactionID - */ -function createInitialWaypoints(transactionID) { +function createInitialWaypoints(transactionID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { waypoints: { @@ -41,14 +39,11 @@ function createInitialWaypoints(transactionID) { /** * Add a stop to the transaction - * - * @param {String} transactionID - * @param {Number} newLastIndex */ -function addStop(transactionID) { - const transaction = lodashGet(allTransactions, transactionID, {}); - const existingWaypoints = lodashGet(transaction, 'comment.waypoints', {}); - const newLastIndex = _.size(existingWaypoints); +function addStop(transactionID: string) { + const transaction = allTransactions?.[transactionID] ?? {}; + const existingWaypoints = transaction?.comment?.waypoints ?? {}; + const newLastIndex = Object.keys(existingWaypoints).length; Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { @@ -61,11 +56,8 @@ function addStop(transactionID) { /** * Saves the selected waypoint to the transaction - * @param {String} transactionID - * @param {String} index - * @param {Object} waypoint */ -function saveWaypoint(transactionID, index, waypoint) { +function saveWaypoint(transactionID: string, index: string, waypoint: RecentWaypoint | null) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { waypoints: { @@ -93,33 +85,31 @@ function saveWaypoint(transactionID, index, waypoint) { if (!lodashHas(waypoint, 'lat') || !lodashHas(waypoint, 'lng')) { return; } - - const recentWaypointAlreadyExists = _.find(recentWaypoints, (recentWaypoint) => recentWaypoint.address === waypoint.address); - if (!recentWaypointAlreadyExists) { - const clonedWaypoints = _.clone(recentWaypoints); + const recentWaypointAlreadyExists = recentWaypoints.find((recentWaypoint) => recentWaypoint?.address === waypoint?.address); + if (!recentWaypointAlreadyExists && waypoint !== null) { + const clonedWaypoints = lodashClone(recentWaypoints); clonedWaypoints.unshift(waypoint); Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, clonedWaypoints.slice(0, 5)); } } -function removeWaypoint(transactionID, currentIndex) { +function removeWaypoint(transactionID: string, currentIndex: string) { // Index comes from the route params and is a string const index = Number(currentIndex); - const transaction = lodashGet(allTransactions, transactionID, {}); - const existingWaypoints = lodashGet(transaction, 'comment.waypoints', {}); - const totalWaypoints = _.size(existingWaypoints); - + const transaction = allTransactions?.[transactionID] ?? {}; + const existingWaypoints = transaction?.comment?.waypoints ?? {}; + const totalWaypoints = Object.keys(existingWaypoints).length; // Prevents removing the starting or ending waypoint but clear the stored address only if there are only two waypoints if (totalWaypoints === 2 && (index === 0 || index === totalWaypoints - 1)) { - saveWaypoint(transactionID, index, null); + saveWaypoint(transactionID, index.toString(), null); return; } - const waypointValues = _.values(existingWaypoints); + const waypointValues = Object.values(existingWaypoints); const removed = waypointValues.splice(index, 1); - const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0]); + const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {}); - const reIndexedWaypoints = {}; + const reIndexedWaypoints: WaypointCollection = {}; waypointValues.forEach((waypoint, idx) => { reIndexedWaypoints[`waypoint${idx}`] = waypoint; }); @@ -127,7 +117,7 @@ function removeWaypoint(transactionID, currentIndex) { // Onyx.merge won't remove the null nested object values, this is a workaround // to remove nested keys while also preserving other object keys // Doing a deep clone of the transaction to avoid mutating the original object and running into a cache issue when using Onyx.set - let newTransaction = { + let newTransaction: Transaction = { ...transaction, comment: { ...transaction.comment, @@ -145,6 +135,7 @@ function removeWaypoint(transactionID, currentIndex) { // Clear the existing route so that we don't show an old route routes: { route0: { + distance: null, geometry: { coordinates: null, }, @@ -158,10 +149,8 @@ function removeWaypoint(transactionID, currentIndex) { /** * Gets the route for a set of waypoints * Used so we can generate a map view of the provided waypoints - * @param {String} transactionID - * @param {Object} waypoints */ -function getRoute(transactionID, waypoints) { +function getRoute(transactionID: string, waypoints: WaypointCollection) { API.read( 'GetRoute', { diff --git a/src/libs/actions/Welcome.js b/src/libs/actions/Welcome.js index 6e6fe2512dff..8e1832edb9a7 100644 --- a/src/libs/actions/Welcome.js +++ b/src/libs/actions/Welcome.js @@ -134,7 +134,7 @@ function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => {}}) { } if (shouldNavigateToWorkspaceChat && workspaceChatReport) { - Navigation.navigate(ROUTES.getReportRoute(workspaceChatReport.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReport.reportID)); // If showPopoverMenu exists and returns true then it opened the Popover Menu successfully, and we can update isFirstTimeNewExpensifyUser // so the Welcome logic doesn't run again diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 588e1a5642aa..4a115536e654 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -1,6 +1,5 @@ import _ from 'underscore'; import Log from './Log'; -import AddEncryptedAuthToken from './migrations/AddEncryptedAuthToken'; import RenamePriorityModeKey from './migrations/RenamePriorityModeKey'; import RenameExpensifyNewsStatus from './migrations/RenameExpensifyNewsStatus'; import AddLastVisibleActionCreated from './migrations/AddLastVisibleActionCreated'; @@ -13,7 +12,7 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [RenamePriorityModeKey, AddEncryptedAuthToken, RenameExpensifyNewsStatus, AddLastVisibleActionCreated, PersonalDetailsByAccountID, RenameReceiptFilename]; + const migrationPromises = [RenamePriorityModeKey, RenameExpensifyNewsStatus, AddLastVisibleActionCreated, PersonalDetailsByAccountID, RenameReceiptFilename]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/libs/migrations/AddEncryptedAuthToken.js b/src/libs/migrations/AddEncryptedAuthToken.js deleted file mode 100644 index 1c8d6a1e2a43..000000000000 --- a/src/libs/migrations/AddEncryptedAuthToken.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable rulesdir/no-api-in-views */ -import _ from 'underscore'; -import Onyx from 'react-native-onyx'; -import Log from '../Log'; -import ONYXKEYS from '../../ONYXKEYS'; -import * as Authentication from '../Authentication'; - -/** - * This migration adds an encryptedAuthToken to the SESSION key, if it is not present. - * - * @returns {Promise} - */ -export default function () { - return new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.SESSION, - callback: (session) => { - Onyx.disconnect(connectionID); - - if (session && !_.isEmpty(session.encryptedAuthToken)) { - Log.info('[Migrate Onyx] Skipped migration AddEncryptedAuthToken'); - return resolve(); - } - - if (!session || !session.authToken) { - Log.info('[Migrate Onyx] Skipped migration AddEncryptedAuthToken'); - return resolve(); - } - - // If there is an auth token but no encrypted auth token, reauthenticate. - if (session.authToken && _.isUndefined(session.encryptedAuthToken)) { - return Authentication.reauthenticate('Onyx_Migration_AddEncryptedAuthToken').then(() => { - Log.info('[Migrate Onyx] Ran migration AddEncryptedAuthToken'); - return resolve(); - }); - } - - return resolve(); - }, - }); - }); -} diff --git a/src/pages/EditRequestAmountPage.js b/src/pages/EditRequestAmountPage.js index b24275cbdfc0..9f72c9afbc23 100644 --- a/src/pages/EditRequestAmountPage.js +++ b/src/pages/EditRequestAmountPage.js @@ -43,7 +43,7 @@ function EditRequestAmountPage({defaultAmount, defaultCurrency, onSubmit, report const navigateToCurrencySelectionPage = () => { // Remove query from the route and encode it. const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, '')); - Navigation.navigate(ROUTES.getEditRequestCurrencyRoute(reportID, defaultCurrency, activeRoute)); + Navigation.navigate(ROUTES.EDIT_CURRENCY_REQUEST.getRoute(reportID, defaultCurrency, activeRoute)); }; useFocusEffect( diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 3e7297082088..5e6e0dd3f17b 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -2,25 +2,28 @@ import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; -import compose from '../libs/compose'; import CONST from '../CONST'; -import Navigation from '../libs/Navigation/Navigation'; import ONYXKEYS from '../ONYXKEYS'; +import compose from '../libs/compose'; +import Navigation from '../libs/Navigation/Navigation'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; import * as ReportUtils from '../libs/ReportUtils'; +import * as PolicyUtils from '../libs/PolicyUtils'; import * as TransactionUtils from '../libs/TransactionUtils'; import * as Policy from '../libs/actions/Policy'; +import * as IOU from '../libs/actions/IOU'; +import * as CurrencyUtils from '../libs/CurrencyUtils'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../components/withCurrentUserPersonalDetails'; +import tagPropTypes from '../components/tagPropTypes'; +import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView'; import EditRequestDescriptionPage from './EditRequestDescriptionPage'; import EditRequestMerchantPage from './EditRequestMerchantPage'; import EditRequestCreatedPage from './EditRequestCreatedPage'; import EditRequestAmountPage from './EditRequestAmountPage'; import EditRequestReceiptPage from './EditRequestReceiptPage'; -import reportPropTypes from './reportPropTypes'; -import * as IOU from '../libs/actions/IOU'; -import * as CurrencyUtils from '../libs/CurrencyUtils'; -import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView'; import EditRequestCategoryPage from './EditRequestCategoryPage'; +import EditRequestTagPage from './EditRequestTagPage'; +import reportPropTypes from './reportPropTypes'; const propTypes = { /** Route from navigation */ @@ -35,6 +38,7 @@ const propTypes = { }), }).isRequired, + /** Onyx props */ /** The report object for the thread report */ report: reportPropTypes, @@ -56,6 +60,9 @@ const propTypes = { email: PropTypes.string, }), + /** Collection of tags attached to a policy */ + policyTags: tagPropTypes, + ...withCurrentUserPersonalDetailsPropTypes, }; @@ -66,9 +73,10 @@ const defaultProps = { session: { email: null, }, + policyTags: {}, }; -function EditRequestPage({report, route, parentReport, policy, session}) { +function EditRequestPage({report, route, parentReport, policy, session, policyTags}) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const transaction = TransactionUtils.getLinkedTransaction(parentReportAction); const { @@ -77,6 +85,7 @@ function EditRequestPage({report, route, parentReport, policy, session}) { comment: transactionDescription, merchant: transactionMerchant, category: transactionCategory, + tag: transactionTag, } = ReportUtils.getTransactionDetails(transaction); const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; @@ -92,6 +101,9 @@ function EditRequestPage({report, route, parentReport, policy, session}) { const isRequestor = ReportUtils.isMoneyRequestReport(parentReport) && lodashGet(session, 'accountID', null) === parentReportAction.actorAccountID; const canEdit = !isSettled && !isDeleted && (isAdmin || isRequestor); + // For now, it always defaults to the first tag of the policy + const tagListName = PolicyUtils.getTagListName(policyTags); + // Dismiss the modal when the request is paid or deleted useEffect(() => { if (canEdit) { @@ -105,7 +117,7 @@ function EditRequestPage({report, route, parentReport, policy, session}) { // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { IOU.editMoneyRequest(transaction.transactionID, report.reportID, transactionChanges); - Navigation.dismissModal(); + Navigation.dismissModal(report.reportID); } if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) { @@ -196,6 +208,25 @@ function EditRequestPage({report, route, parentReport, policy, session}) { ); } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAG) { + return ( + { + let updatedTag = transactionChanges.tag; + + // In case the same tag has been selected, reset the tag. + if (transactionTag === updatedTag) { + updatedTag = ''; + } + editMoneyRequest({tag: updatedTag, tagListName}); + }} + /> + ); + } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { return ( `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + }, }), )(EditRequestPage); diff --git a/src/pages/EditRequestReceiptPage.js b/src/pages/EditRequestReceiptPage.js index f3c12fe2f9ee..f45085a052e9 100644 --- a/src/pages/EditRequestReceiptPage.js +++ b/src/pages/EditRequestReceiptPage.js @@ -21,7 +21,11 @@ const propTypes = { }).isRequired, /** The id of the transaction we're editing */ - transactionID: PropTypes.string.isRequired, + transactionID: PropTypes.string, +}; + +const defaultProps = { + transactionID: '', }; function EditRequestReceiptPage({route, transactionID}) { @@ -49,6 +53,7 @@ function EditRequestReceiptPage({route, transactionID}) { } EditRequestReceiptPage.propTypes = propTypes; +EditRequestReceiptPage.defaultProps = defaultProps; EditRequestReceiptPage.displayName = 'EditRequestReceiptPage'; export default EditRequestReceiptPage; diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js new file mode 100644 index 000000000000..72ed072eec16 --- /dev/null +++ b/src/pages/EditRequestTagPage.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Navigation from '../libs/Navigation/Navigation'; +import useLocalize from '../hooks/useLocalize'; +import ScreenWrapper from '../components/ScreenWrapper'; +import HeaderWithBackButton from '../components/HeaderWithBackButton'; +import TagPicker from '../components/TagPicker'; + +const propTypes = { + /** Transaction default tag value */ + defaultTag: PropTypes.string.isRequired, + + /** The policyID we are getting tags for */ + policyID: PropTypes.string.isRequired, + + /** The tag name to which the default tag belongs to */ + tagName: PropTypes.string.isRequired, + + /** Callback to fire when the Save button is pressed */ + onSubmit: PropTypes.func.isRequired, +}; + +function EditRequestTagPage({defaultTag, policyID, tagName, onSubmit}) { + const {translate} = useLocalize(); + + const selectTag = (tag) => { + onSubmit({tag: tag.searchText}); + }; + + return ( + + + + + + ); +} + +EditRequestTagPage.propTypes = propTypes; +EditRequestTagPage.displayName = 'EditRequestTagPage'; + +export default EditRequestTagPage; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index 9d837c1c6be6..de3fafa7018d 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -86,7 +86,7 @@ function PrivateNotesEditPage({route, personalDetailsList, session, report}) { ); const savePrivateNote = () => { - const editedNote = parser.replace(privateNote); + const editedNote = parser.replace(privateNote.trim()); Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote); Keyboard.dismiss(); @@ -138,6 +138,7 @@ function PrivateNotesEditPage({route, personalDetailsList, session, report}) { label={translate('privateNotes.composerLabel')} accessibilityLabel={translate('privateNotes.title')} autoCompleteType="off" + maxLength={CONST.MAX_COMMENT_LENGTH} autoCorrect={false} autoGrowHeight textAlignVertical="top" diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index fdfaa4c60e33..ab096285788d 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -110,7 +110,7 @@ function PrivateNotesListPage({report, personalDetailsList, network, session}) { title: Number(lodashGet(session, 'accountID', null)) === Number(accountID) ? translate('privateNotes.myNote') : lodashGet(personalDetailsList, [accountID, 'login'], ''), icon: UserUtils.getAvatar(lodashGet(personalDetailsList, [accountID, 'avatar'], UserUtils.getDefaultAvatar(accountID)), accountID), iconType: CONST.ICON_TYPE_AVATAR, - action: () => Navigation.navigate(ROUTES.getPrivateNotesViewRoute(report.reportID, accountID)), + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, accountID)), brickRoadIndicator: privateNoteBrickRoadIndicator(accountID), })) .value(); @@ -139,6 +139,7 @@ function PrivateNotesListPage({report, personalDetailsList, network, session}) { PrivateNotesListPage.propTypes = propTypes; PrivateNotesListPage.defaultProps = defaultProps; +PrivateNotesListPage.displayName = 'PrivateNotesListPage'; export default compose( withLocalize, diff --git a/src/pages/PrivateNotes/PrivateNotesViewPage.js b/src/pages/PrivateNotes/PrivateNotesViewPage.js index 4c6d960d5d9a..d09f1bbdee47 100644 --- a/src/pages/PrivateNotes/PrivateNotesViewPage.js +++ b/src/pages/PrivateNotes/PrivateNotesViewPage.js @@ -75,7 +75,7 @@ function PrivateNotesViewPage({route, personalDetailsList, session, report}) { isCurrentUserNote && Navigation.navigate(ROUTES.getPrivateNotesEditRoute(report.reportID, route.params.accountID))} + onPress={() => isCurrentUserNote && Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, route.params.accountID))} shouldShowRightIcon={isCurrentUserNote} numberOfLinesTitle={0} shouldRenderAsHTML diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index ddec4e5a86ae..60037e4755c0 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -243,7 +243,7 @@ function ProfilePage(props) { title={`${props.translate('privateNotes.title')}`} titleStyle={styles.flex1} icon={Expensicons.Pencil} - onPress={() => Navigation.navigate(ROUTES.getPrivateNotesListRoute(chatReportWithCurrentUser.reportID))} + onPress={() => Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(chatReportWithCurrentUser.reportID))} wrapperStyle={styles.breakAll} shouldShowRightIcon brickRoadIndicator={Report.hasErrorInPrivateNotes(chatReportWithCurrentUser) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index e6d508da4392..de5ed03ed823 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -49,6 +49,9 @@ const propTypes = { /* The workspace name */ policyName: PropTypes.string, + + /* The workspace ID */ + policyID: PropTypes.string, }; const defaultProps = { @@ -57,6 +60,7 @@ const defaultProps = { user: {}, isPlaidDisabled: false, policyName: '', + policyID: '', }; function BankAccountStep(props) { @@ -66,7 +70,7 @@ function BankAccountStep(props) { subStep = CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID; } const plaidDesktopMessage = getPlaidDesktopMessage(); - const bankAccountRoute = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.BANK_ACCOUNT}`; + const bankAccountRoute = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.getBankAccountRoute('new', props.policyID, ROUTES.getWorkspaceInitialRoute(props.policyID))}`; if (subStep === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) { return ( diff --git a/src/pages/ReimbursementAccount/Enable2FAPrompt.js b/src/pages/ReimbursementAccount/Enable2FAPrompt.js index 829ac9b63848..33424669903f 100644 --- a/src/pages/ReimbursementAccount/Enable2FAPrompt.js +++ b/src/pages/ReimbursementAccount/Enable2FAPrompt.js @@ -13,7 +13,9 @@ const propTypes = { ...withLocalizePropTypes, }; function Enable2FAPrompt(props) { - const secureYourAccountUrl = encodeURI(`settings?param={"section":"account","action":"enableTwoFactorAuth","exitTo":"${ROUTES.getBankAccountRoute()}","isFromNewDot":"true"}`); + const secureYourAccountUrl = encodeURI( + `settings?param={"section":"account","action":"enableTwoFactorAuth","exitTo":"${ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute()}","isFromNewDot":"true"}`, + ); return (
); } diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 4cc3c2b1a5ba..98dae2e94780 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -79,7 +79,7 @@ function ReportDetailsPage(props) { translationKey: 'common.shareCode', icon: Expensicons.QrCode, isAnonymousAction: true, - action: () => Navigation.navigate(ROUTES.getReportShareCodeRoute(props.report.reportID)), + action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(props.report.reportID)), }, ]; @@ -95,7 +95,7 @@ function ReportDetailsPage(props) { subtitle: participants.length, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.getReportParticipantsRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID)); }, }); } @@ -107,7 +107,7 @@ function ReportDetailsPage(props) { icon: Expensicons.Gear, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.getReportSettingsRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(props.report.reportID)); }, }); } @@ -119,7 +119,7 @@ function ReportDetailsPage(props) { translationKey: 'privateNotes.title', icon: Expensicons.Pencil, isAnonymousAction: false, - action: () => Navigation.navigate(ROUTES.getPrivateNotesListRoute(props.report.reportID)), + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID)), brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }); } @@ -177,7 +177,7 @@ function ReportDetailsPage(props) { accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={chatRoomSubtitle} onPress={() => { - Navigation.navigate(ROUTES.getWorkspaceInitialRoute(props.report.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(props.report.policyID)); }} > {chatRoomSubtitleText} diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index c10401fb90db..7f453d16817b 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -130,7 +130,7 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate(ROUTES.getProfileRoute(option.accountID)); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID)); }} hideSectionHeaders showTitleTooltip diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index f19c79db5459..75bf5fd24f1d 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -47,8 +47,8 @@ class ShareCodePage extends React.Component { const subtitle = isReport ? ReportUtils.getParentNavigationSubtitle(this.props.report).workspaceName || ReportUtils.getChatRoomSubtitle(this.props.report) : formattedEmail; const urlWithTrailingSlash = Url.addTrailingForwardSlash(this.props.environmentURL); const url = isReport - ? `${urlWithTrailingSlash}${ROUTES.getReportRoute(this.props.report.reportID)}` - : `${urlWithTrailingSlash}${ROUTES.getProfileRoute(this.props.session.accountID)}`; + ? `${urlWithTrailingSlash}${ROUTES.REPORT_WITH_ID.getRoute(this.props.report.reportID)}` + : `${urlWithTrailingSlash}${ROUTES.PROFILE.getRoute(this.props.session.accountID)}`; const platform = getPlatform(); const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; @@ -57,7 +57,7 @@ class ShareCodePage extends React.Component { Navigation.goBack(isReport ? ROUTES.getReportDetailsRoute(this.props.report.reportID) : ROUTES.SETTINGS)} + onBackButtonPress={() => Navigation.goBack(isReport ? ROUTES.REPORT_WITH_ID_DETAILS.getRoute(this.props.report.reportID) : ROUTES.SETTINGS)} /> diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index bb0c14ce7db9..100615b836c4 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -251,7 +251,7 @@ function ReportScreen({ }, []); const chatWithAccountManager = useCallback(() => { - Navigation.navigate(ROUTES.getReportRoute(accountManagerReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID)); }, [accountManagerReportID]); /** diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 173bda0e5221..2a65bc2e67ab 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -298,11 +298,11 @@ export default [ const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); - Navigation.navigate(ROUTES.getReportRoute(thread.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } Report.openReport(childReportID); - Navigation.navigate(ROUTES.getReportRoute(childReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } const editAction = () => Report.saveReportActionDraft(reportID, reportAction.reportActionID, _.isEmpty(draftMessage) ? getActionText(reportAction) : ''); @@ -382,10 +382,10 @@ export default [ reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { - hideContextMenu(false, () => Navigation.navigate(ROUTES.getFlagCommentRoute(reportID, reportAction.reportActionID))); + hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID))); } - Navigation.navigate(ROUTES.getFlagCommentRoute(reportID, reportAction.reportActionID)); + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID)); }, getDescription: () => {}, }, diff --git a/src/pages/home/report/ReactionList/BaseReactionList.js b/src/pages/home/report/ReactionList/BaseReactionList.js index 0b77fca7c645..b082ca510edd 100755 --- a/src/pages/home/report/ReactionList/BaseReactionList.js +++ b/src/pages/home/report/ReactionList/BaseReactionList.js @@ -79,7 +79,7 @@ function BaseReactionList(props) { hoverStyle={styles.hoveredComponentBG} onSelectRow={() => { props.onClose(); - Navigation.navigate(ROUTES.getProfileRoute(item.accountID)); + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); }} option={{ text: Str.removeSMSDomain(item.displayName), diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index c5179290bf2c..6ef0a9867ece 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -35,6 +35,7 @@ import useLocalize from '../../../../hooks/useLocalize'; import getModalState from '../../../../libs/getModalState'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; +import getDraftComment from '../../../../libs/ComposerUtils/getDraftComment'; const propTypes = { /** A method to call when the form is submitted */ @@ -103,7 +104,6 @@ function ReportActionCompose({ reportID, reportActions, shouldShowComposeInput, - isCommentEmpty: isCommentEmptyProp, }) { const {translate} = useLocalize(); const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -123,7 +123,10 @@ function ReportActionCompose({ * Updates the should clear state of the composer */ const [textInputShouldClear, setTextInputShouldClear] = useState(false); - const [isCommentEmpty, setIsCommentEmpty] = useState(isCommentEmptyProp); + const [isCommentEmpty, setIsCommentEmpty] = useState(() => { + const draftComment = getDraftComment(reportID); + return !draftComment || !!draftComment.match(/^(\s)*$/); + }); /** * Updates the visibility state of the menu @@ -438,10 +441,6 @@ export default compose( withNetwork(), withCurrentUserPersonalDetails, withOnyx({ - isCommentEmpty: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, - selector: (comment) => _.isEmpty(comment), - }, blockedFromConcierge: { key: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, }, diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index 90b34d21092e..489784201ccf 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -374,6 +374,7 @@ function ReportActionItemMessageEdit(props) { { + ReportActionComposeFocusManager.editComposerRef.current = el; textInputRef.current = el; // eslint-disable-next-line no-param-reassign props.forwardedRef.current = el; diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index bfbce8aed336..97dd5e37482e 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -80,11 +80,11 @@ const defaultProps = { }; const showUserDetails = (accountID) => { - Navigation.navigate(ROUTES.getProfileRoute(accountID)); + Navigation.navigate(ROUTES.PROFILE.getRoute(accountID)); }; const showWorkspaceDetails = (reportID) => { - Navigation.navigate(ROUTES.getReportDetailsRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)); }; function ReportActionItemSingle(props) { @@ -149,7 +149,7 @@ function ReportActionItemSingle(props) { } else { // Show participants page IOU report preview if (displayAllActors) { - Navigation.navigate(ROUTES.getReportParticipantsRoute(iouReportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } showUserDetails(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID); diff --git a/src/pages/home/report/ReportAttachments.js b/src/pages/home/report/ReportAttachments.js index c8b5dd1ae685..4146be0d3971 100644 --- a/src/pages/home/report/ReportAttachments.js +++ b/src/pages/home/report/ReportAttachments.js @@ -32,7 +32,7 @@ function ReportAttachments(props) { source={source} onModalHide={() => Navigation.dismissModal(reportID)} onCarouselAttachmentChange={(attachment) => { - const route = ROUTES.getReportAttachmentRoute(reportID, attachment.source); + const route = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, attachment.source); Navigation.navigate(route); }} /> diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index c38ac9e01ccb..9ff9cc261af4 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -141,7 +141,7 @@ class SidebarLinks extends React.PureComponent { ) { return; } - Navigation.navigate(ROUTES.getReportRoute(option.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); this.props.onLinkClick(); } diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 966cd2f87af6..e8c5cd6b3a92 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -138,7 +138,7 @@ function IOUCurrencySelection(props) { <> Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID))} /> { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); }; const updateCategory = (category) => { @@ -56,7 +56,7 @@ function MoneyRequestCategoryPage({route, report, iou}) { IOU.setMoneyRequestCategory(category.searchText); } - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); }; return ( diff --git a/src/pages/iou/MoneyRequestDatePage.js b/src/pages/iou/MoneyRequestDatePage.js index 2d2f9c519c50..e7607277899e 100644 --- a/src/pages/iou/MoneyRequestDatePage.js +++ b/src/pages/iou/MoneyRequestDatePage.js @@ -62,12 +62,12 @@ function MoneyRequestDatePage({iou, route, selectedTab}) { } if (!isDistanceRequest && (_.isEmpty(iou.participantAccountIDs) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } }, [iou.id, iou.participantAccountIDs, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]); function navigateBack() { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); } /** @@ -111,6 +111,7 @@ function MoneyRequestDatePage({iou, route, selectedTab}) { MoneyRequestDatePage.propTypes = propTypes; MoneyRequestDatePage.defaultProps = defaultProps; +MoneyRequestDatePage.displayName = 'MoneyRequestDatePage'; export default withOnyx({ iou: { diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 61f688f5d4bd..8d274a77a19a 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -84,12 +84,12 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { } if (!isDistanceRequest && (_.isEmpty(iou.participantAccountIDs) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } }, [iou.id, iou.participantAccountIDs, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]); function navigateBack() { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); } /** @@ -150,6 +150,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { MoneyRequestDescriptionPage.propTypes = propTypes; MoneyRequestDescriptionPage.defaultProps = defaultProps; +MoneyRequestDescriptionPage.displayName = 'MoneyRequestDescriptionPage'; export default withOnyx({ iou: { diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js index dddf63a49673..9d4ce5cc367c 100644 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ b/src/pages/iou/MoneyRequestMerchantPage.js @@ -59,12 +59,12 @@ function MoneyRequestMerchantPage({iou, route}) { } if (_.isEmpty(iou.participantAccountIDs) || (iou.amount === 0 && !iou.receiptPath) || shouldReset) { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } }, [iou.id, iou.participantAccountIDs, iou.amount, iou.receiptPath, iouType, reportID]); function navigateBack() { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); } const validate = useCallback((value) => { @@ -126,6 +126,7 @@ function MoneyRequestMerchantPage({iou, route}) { MoneyRequestMerchantPage.propTypes = propTypes; MoneyRequestMerchantPage.defaultProps = defaultProps; +MoneyRequestMerchantPage.displayName = 'MoneyRequestMerchantPage'; export default withOnyx({ iou: { diff --git a/src/pages/iou/MoneyRequestTagPage.js b/src/pages/iou/MoneyRequestTagPage.js index fcd2b412fbe8..43c0cd9d1480 100644 --- a/src/pages/iou/MoneyRequestTagPage.js +++ b/src/pages/iou/MoneyRequestTagPage.js @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; import compose from '../../libs/compose'; import ROUTES from '../../ROUTES'; import * as IOU from '../../libs/actions/IOU'; +import * as PolicyUtils from '../../libs/PolicyUtils'; import Navigation from '../../libs/Navigation/Navigation'; import useLocalize from '../../hooks/useLocalize'; import ScreenWrapper from '../../components/ScreenWrapper'; @@ -36,12 +37,7 @@ const propTypes = { report: reportPropTypes, /** Collection of tags attached to a policy */ - policyTags: PropTypes.objectOf( - PropTypes.shape({ - name: PropTypes.string, - tags: PropTypes.objectOf(tagPropTypes), - }), - ), + policyTags: tagPropTypes, /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: iouPropTypes, @@ -60,11 +56,10 @@ function MoneyRequestTagPage({route, report, policyTags, iou}) { // Fetches the first tag list of the policy const tagListKey = _.first(_.keys(policyTags)); - const tagList = lodashGet(policyTags, tagListKey, {}); - const tagListName = lodashGet(tagList, 'name', translate('common.tag')); + const policyTagListName = PolicyUtils.getTagListName(policyTags) || translate('common.tag'); const navigateBack = () => { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, report.reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report.reportID)); }; const updateTag = (selectedTag) => { @@ -83,10 +78,10 @@ function MoneyRequestTagPage({route, report, policyTags, iou}) { testID={MoneyRequestTagPage.displayName} > - {translate('iou.tagSelection', {tagName: tagListName})} + {translate('iou.tagSelection', {tagName: policyTagListName})} { showCameraAlert(); Log.warn('Error taking photo', error); }); - }, [flash, iouType, iou, report, reportID, translate, transactionID]); + }, [flash, iouType, iou, report, reportID, translate, transactionID, route.path]); CameraPermission.getCameraPermissionStatus().then((permissionStatus) => { setPermissions(permissionStatus); @@ -295,7 +298,7 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator}) return; } - IOU.navigateToNextPage(iou, iouType, reportID, report); + IOU.navigateToNextPage(iou, iouType, reportID, report, route.path); }) .catch(() => { Log.info('User did not select an image from gallery'); diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index 11459cca3aa8..3397fa8701b6 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -147,13 +147,13 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI } // Other flows will be handled by selecting a waypoint with selectWaypoint as this is mainly for the offline flow - Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); }; const deleteStopAndHideModal = () => { Transaction.removeWaypoint(transactionID, waypointIndex); setIsDeleteStopModalOpen(false); - Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); }; const selectWaypoint = (values) => { @@ -163,7 +163,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI address: values.address, }; saveWaypoint(waypoint); - Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); }; const focusAddressInput = () => { @@ -187,7 +187,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI title={translate(wayPointDescriptionKey)} shouldShowBackButton onBackButtonPress={() => { - Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); }} shouldShowThreeDotsButton={shouldShowThreeDotsButton} threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 82b3c4a4909d..015707db71f2 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -27,6 +27,7 @@ import useNetwork from '../../../hooks/useNetwork'; import useWindowDimensions from '../../../hooks/useWindowDimensions'; import * as StyleUtils from '../../../styles/StyleUtils'; import {iouPropTypes, iouDefaultProps} from '../propTypes'; +import * as Expensicons from '../../../components/Icon/Expensicons'; const propTypes = { /** React Navigation route */ @@ -61,7 +62,7 @@ const defaultProps = { function MoneyRequestConfirmPage(props) { const {isOffline} = useNetwork(); - const {windowHeight} = useWindowDimensions(); + const {windowHeight, windowWidth} = useWindowDimensions(); const prevMoneyRequestId = useRef(props.iou.id); const iouType = useRef(lodashGet(props.route, 'params.iouType', '')); const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, props.selectedTab); @@ -74,6 +75,7 @@ function MoneyRequestConfirmPage(props) { }), [props.iou.participants, props.personalDetails], ); + const isManualRequestDM = props.selectedTab === CONST.TAB.MANUAL && iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST; useEffect(() => { const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); @@ -91,7 +93,7 @@ function MoneyRequestConfirmPage(props) { if (!isDistanceRequest && prevMoneyRequestId.current !== props.iou.id) { // The ID is cleared on completing a request. In that case, we will do nothing. if (props.iou.id) { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), true); } return; } @@ -104,7 +106,7 @@ function MoneyRequestConfirmPage(props) { } if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset) { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), true); } return () => { @@ -115,9 +117,9 @@ function MoneyRequestConfirmPage(props) { const navigateBack = () => { let fallback; if (reportID.current) { - fallback = ROUTES.getMoneyRequestRoute(iouType.current, reportID.current); + fallback = ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current); } else { - fallback = ROUTES.getMoneyRequestParticipantsRoute(iouType.current); + fallback = ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType.current); } Navigation.goBack(fallback); }; @@ -214,7 +216,9 @@ function MoneyRequestConfirmPage(props) { } if (props.iou.receiptPath && props.iou.receiptSource) { - FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptSource).then((receipt) => { + FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptSource).then((file) => { + const receipt = file; + receipt.state = file && isManualRequestDM ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; requestMoney(selectedParticipants, trimmedComment, receipt); }); return; @@ -238,6 +242,7 @@ function MoneyRequestConfirmPage(props) { isDistanceRequest, requestMoney, createDistanceRequest, + isManualRequestDM, ], ); @@ -286,6 +291,15 @@ function MoneyRequestConfirmPage(props) { Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType.current, reportID.current)), + }, + ]} /> {/* * The MoneyRequestConfirmationList component uses a SectionList which uses a VirtualizedList internally. diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index b737e40b8e4b..7f232cb9e1cf 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -17,6 +17,7 @@ import * as IOU from '../../../../libs/actions/IOU'; import * as MoneyRequestUtils from '../../../../libs/MoneyRequestUtils'; import {iouPropTypes, iouDefaultProps} from '../../propTypes'; import useLocalize from '../../../../hooks/useLocalize'; +import * as TransactionUtils from '../../../../libs/TransactionUtils'; const propTypes = { /** React Navigation route */ @@ -53,7 +54,8 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); const isSplitRequest = iou.id === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT; const [headerTitle, setHeaderTitle] = useState(); - + const transaction = TransactionUtils.getTransaction(iou.transactionID); + const isEmptyWaypoint = _.isEmpty(lodashGet(transaction, 'comment.waypoint.waypoint0', {})); useEffect(() => { if (isDistanceRequest) { setHeaderTitle(translate('common.distance')); @@ -67,28 +69,30 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { if (option.reportID) { isNewReportIDSelectedLocally.current = true; IOU.setMoneyRequestId(`${moneyRequestType}${option.reportID}`); - Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, option.reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, option.reportID)); return; } IOU.setMoneyRequestId(moneyRequestType); - Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, reportID.current)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID.current)); }; const navigateToSplitStep = (moneyRequestType) => { IOU.setMoneyRequestId(moneyRequestType); - Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, reportID.current)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID.current)); }; const navigateBack = (forceFallback = false) => { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType.current, reportID.current), forceFallback); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), forceFallback); }; useEffect(() => { + const isInvalidDistanceRequest = !isDistanceRequest || isEmptyWaypoint; + // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request if (prevMoneyRequestId.current !== iou.id) { // The ID is cleared on completing a request. In that case, we will do nothing - if (iou.id && !isDistanceRequest && !isSplitRequest && !isNewReportIDSelectedLocally.current) { + if (iou.id && isInvalidDistanceRequest && !isSplitRequest && !isNewReportIDSelectedLocally.current) { navigateBack(true); } return; @@ -100,14 +104,14 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { if (shouldReset) { IOU.resetMoneyRequestInfo(moneyRequestId); } - if (!isDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) { + if (isInvalidDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) { navigateBack(true); } return () => { prevMoneyRequestId.current = iou.id; }; - }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest]); + }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, isEmptyWaypoint]); return ( { - // Component may not be initialized due to navigation transitions - // Wait until interactions are complete before trying to focus - InteractionManager.runAfterInteractions(() => { - // Focus text input - if (!textInput.current) { - return; - } - - textInput.current.focus(); - }); - }; + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { - focusTextInput(); + focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION); + return () => { + if (!focusTimeoutRef.current) { + return; + } + clearTimeout(focusTimeoutRef.current); + }; }, []), ); @@ -106,7 +101,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { if (!iou.id) { return; } - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); return; } const moneyRequestID = `${iouType}${reportID}`; @@ -116,7 +111,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { } if (!isDistanceRequestTab && (_.isEmpty(iou.participantAccountIDs) || iou.amount === 0 || shouldReset)) { - Navigation.goBack(ROUTES.getMoneyRequestRoute(iouType, reportID), true); + Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); } } @@ -126,7 +121,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { }, [iou.participantAccountIDs, iou.amount, iou.id, isEditing, iouType, reportID, isDistanceRequestTab]); const navigateBack = () => { - Navigation.goBack(isEditing ? ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID) : ROUTES.HOME); + Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); }; const navigateToCurrencySelectionPage = () => { @@ -138,7 +133,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { // Remove query from the route and encode it. const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, '')); - Navigation.navigate(ROUTES.getMoneyRequestCurrencyRoute(iouType, reportID, currency, activeRoute)); + Navigation.navigate(ROUTES.MONEY_REQUEST_CURRENCY.getRoute(iouType, reportID, currency, activeRoute)); }; const navigateToNextPage = (currentAmount) => { @@ -147,7 +142,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { IOU.setMoneyRequestCurrency(currency); if (isEditing) { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); + Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); return; } @@ -175,7 +170,6 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) { {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index b5e05cf017df..c08ed600b570 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -100,7 +100,7 @@ function ContactMethodsPage(props) { Navigation.navigate(ROUTES.getEditContactMethodRoute(partnerUserID))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID))} brickRoadIndicator={indicator} shouldShowBasicTitle shouldShowRightIcon @@ -119,7 +119,7 @@ function ContactMethodsPage(props) { title={props.translate('contacts.contactMethods')} onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> - + {props.translate('contacts.helpTextBeforeEmail')} @@ -131,15 +131,15 @@ function ContactMethodsPage(props) { {loginMenuItems} + +