diff --git a/.storybook/preview.js b/.storybook/preview.js index b198c0d2d626..a989960794f2 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -6,7 +6,7 @@ import './fonts.css'; import ComposeProviders from '../src/components/ComposeProviders'; import HTMLEngineProvider from '../src/components/HTMLEngineProvider'; import OnyxProvider from '../src/components/OnyxProvider'; -import {LocaleContextProvider} from '../src/components/withLocalize'; +import {LocaleContextProvider} from '../src/components/LocaleContextProvider'; import {KeyboardStateProvider} from '../src/components/withKeyboardState'; import {EnvironmentProvider} from '../src/components/withEnvironment'; import {WindowDimensionsProvider} from '../src/components/withWindowDimensions'; diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index a9e2b0383691..d6da0232f2fc 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -75,6 +75,10 @@ { "/": "/teachersunite/*", "comment": "Teachers Unite!" + }, + { + "/": "/search/*", + "comment": "Search" } ] } diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c5311ffea20..d85dda721838 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 1001037403 - versionName "1.3.74-3" + versionCode 1001037508 + versionName "1.3.75-8" } flavorDimensions "default" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8d69c62bfd1f..dc135fa9834e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -67,6 +67,7 @@ + @@ -83,6 +84,7 @@ + diff --git a/docs/Gemfile b/docs/Gemfile index 7cad729ee45b..701ae50ca381 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -32,3 +32,6 @@ gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] gem "webrick", "~> 1.7" gem 'jekyll-seo-tag' + +gem 'jekyll-redirect-from' + diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 1a5b26e2dc23..0963d3c73e6c 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -263,6 +263,7 @@ DEPENDENCIES github-pages http_parser.rb (~> 0.6.0) jekyll-feed (~> 0.12) + jekyll-redirect-from jekyll-seo-tag tzinfo (~> 1.2) tzinfo-data @@ -270,4 +271,4 @@ DEPENDENCIES webrick (~> 1.7) BUNDLED WITH - 2.4.3 + 2.4.19 diff --git a/docs/_config.yml b/docs/_config.yml index 114e562cae04..4a0ce8c053c5 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -17,3 +17,7 @@ exclude: [README.md, TEMPLATE.md, vendor] plugins: - jekyll-seo-tag + - jekyll-redirect-from + +whitelist: + - jekyll-redirect-from diff --git a/docs/articles/expensify-classic/account-settings/Account-Access.md b/docs/articles/expensify-classic/account-settings/Account-Access.md deleted file mode 100644 index b3126201715f..000000000000 --- a/docs/articles/expensify-classic/account-settings/Account-Access.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Account Access -description: Account Access ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/account-settings/Account-Details.md b/docs/articles/expensify-classic/account-settings/Account-Details.md new file mode 100644 index 000000000000..46a6c6ba0c25 --- /dev/null +++ b/docs/articles/expensify-classic/account-settings/Account-Details.md @@ -0,0 +1,5 @@ +--- +title: Account Details +description: Account Details +--- +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/exports/Insights.md b/docs/articles/expensify-classic/exports/Insights.md index 682c2a251228..6c71630015c5 100644 --- a/docs/articles/expensify-classic/exports/Insights.md +++ b/docs/articles/expensify-classic/exports/Insights.md @@ -1,6 +1,7 @@ --- title: Custom Reporting and Insights description: How to get the most out of the Custom Reporing and Insights +redirect_from: articles/other/Insights/ --- {% raw %} 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 e565e59dc754..7fa714189542 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 @@ -48,11 +48,11 @@ You can also create a number of future 'placeholder' expenses for your recurring # How to Edit Bulk Expenses Editing expenses in bulk will allow you to apply the same coding across multiple expenses and is a web-only feature. To bulk edit expenses: Go to the Expenses page. -To narrow down your selection, use the filters (e.g. "Merchant" and "Open") to find the specific expenses you want to edit. +To narrow down your selection, use the filters (e.g. "Merchant" and "Draft") to find the specific expenses you want to edit. Select all the expenses you want to edit. Click on the **Edit Multiple** button at the top of the page. # How to Edit Expenses on a Report -If you’d like to edit expenses within an Open report: +If you’d like to edit expenses within a Draft report: 1. Click on the Report containing all the expenses. 2. Click on **Details**. @@ -61,8 +61,8 @@ If you’d like to edit expenses within an Open report: If you've already submitted your report, you'll need to Retract it or have it Unapproved first before you can edit the expenses. - # FAQ + ## Does Expensify account for duplicates? Yes, Expensify will account for duplicates. Expensify works behind the scenes to identify duplicate expenses before they are submitted, warning employees when they exist. If a duplicate expense is submitted, the same warning will be shown to the approver responsible for reviewing the report. @@ -71,6 +71,7 @@ If two expenses are SmartScanned on the same day for the same amount, they will The expenses were split from a single expense, The expenses were imported from a credit card, or Matching email receipts sent to receipts@expensify.com were received with different timestamps. + ## How do I resolve a duplicate expense? If Concierge has let you know it's flagged a receipt as a duplicate, scanning the receipt again will trigger the same duplicate flagging.Users have the ability to resolve duplicates by either deleting the duplicated transactions, merging them, or ignoring them (if they are legitimately separate expenses of the same date and amount). @@ -88,12 +89,13 @@ Click the **Undelete** button and you're all set. You’ll find the expense on y ## What are the different Expense statuses? There are a number of different expense statuses in Expensify: -1. **Unreported**: Unreported expenses are not yet part of a report (and therefore unsubmitted) and are not viewable by anyone but the expense creator/owner. -2. **Open**: Open expenses are on a report that's still in progress, and are unsubmitted. Your Policy Admin will be able to view them, making it a collaborative step toward reimbursement. +1. **Personal**: Personal expenses are not yet part of a report (and therefore unsubmitted) and are not viewable by anyone but the expense creator/owner. +2. **Draft**: Draft expenses are seen as still in progress, and are unsubmitted. Your Policy Admin will be able to view them, making this a collaborative step toward reimbursement. 3. **Processing**: Processing expenses are submitted, but waiting for approval. 4. **Approved**: If it's a non-reimbursable expense, the workflow is complete at this point. If it's a reimbursable expense, you're one step closer to getting paid. 5. **Reimbursed**: Reimbursed expenses are fully settled. You can check the Report Comments to see when you'll get paid. 6. **Closed**: Sometimes an expense accidentally ends up on your Individual Policy, falling into the Closed status. You’ll need to reopen the report and change the Policy by clicking on the **Details** tab in order to resubmit your report. + ## What are Violations? Violations represent errors or discrepancies that Expensify has picked up and need to be corrected before a report can be successfully submitted. The one exception is when an expense comment is added, it will override the violation - as the user is providing a valid reason for submission. @@ -101,8 +103,9 @@ Violations represent errors or discrepancies that Expensify has picked up and ne To enable or configure violations according to your policy, go to **Settings > Policies > _Policy Name_ > Expenses > Expense Violations**. Keep in mind that Expensify includes certain system mandatory violations that can't be disabled, even if your policy has violations turned off. You can spot violations by the exclamation marks (!) attached to expenses. Hovering over the symbol will provide a brief description and you can find more detailed information below the list of expenses. The two types of violations are: -**Red**: These indicate violations directly tied to your report's Policy settings. They are clear rule violations that must be addressed before submission. -**Yellow**: Concierge will highlight items that require attention but may not necessarily need corrective action. For example, if a receipt was SmartScanned and then the amount was modified, we’ll bring it to your attention so that it can be manually reviewed. +1. **Red**: These indicate violations directly tied to your report's Policy settings. They are clear rule violations that must be addressed before submission. +2. **Yellow**: Concierge will highlight items that require attention but may not necessarily need corrective action. For example, if a receipt was SmartScanned and then the amount was modified, we’ll bring it to your attention so that it can be manually reviewed. + ## How to Track Attendees Attendee tracking makes it easy to track shared expenses and maintain transparency in your group spending. @@ -116,9 +119,10 @@ External attendees are considered users outside your group policy or domain. To 1. Click or tap the **Attendee** field within your expense. 2. Type in the individual's name or email address. 3. Tap **Add** to include the attendee. -You can continue adding more attendees or save the Expense. +4. You can continue adding more attendees or save the Expense. + To remove an attendee from an expense: -Open the expense. -Click or tap the **Attendees** field to display the list of attendees. -From the list, de-select the attendees you'd like to remove from the expense. +1. Open the expense. +2. Click or tap the **Attendees** field to display the list of attendees. +3. From the list, de-select the attendees you'd like to remove from the expense. diff --git a/docs/articles/expensify-classic/getting-started/Referral-Program.md b/docs/articles/expensify-classic/getting-started/Referral-Program.md index 683e93d0277a..b4a2b4a7de74 100644 --- a/docs/articles/expensify-classic/getting-started/Referral-Program.md +++ b/docs/articles/expensify-classic/getting-started/Referral-Program.md @@ -1,6 +1,7 @@ --- title: Expensify Referral Program description: Send your joining link, submit a receipt or invoice, and we'll pay you if your referral adopts Expensify. +redirect_from: articles/other/Referral-Program/ --- diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md index b18531d43200..a8e1b0690b72 100644 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md +++ b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md @@ -1,6 +1,7 @@ --- title: Expensify Card revenue share for ExpensifyApproved! partners description: Earn money when your clients adopt the Expensify Card +redirect_from: articles/other/Card-Revenue-Share-for-ExpensifyApproved!-Partners/ --- diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md index c7a5dc5a04ab..104cd49daf96 100644 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md +++ b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md @@ -1,6 +1,7 @@ --- title: Your Expensify Partner Manager description: Everything you need to know about your Expensify Partner Manager +redirect_from: articles/other/Your-Expensify-Partner-Manager/ --- diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md index 2b95a1d13fde..a7553e6ae179 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md @@ -1,6 +1,7 @@ --- title: Expensify Playbook for Small to Medium-Sized Businesses description: Best practices for how to deploy Expensify for your business +redirect_from: articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses/ --- ## Overview This guide provides practical tips and recommendations for small businesses with 100 to 250 employees to effectively use Expensify to improve spend visibility, facilitate employee reimbursements, and reduce the risk of fraudulent expenses. diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md index 86c6a583c758..bef59546a13d 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md @@ -1,6 +1,7 @@ --- title: Expensify Playbook for US-Based Bootstrapped Startups description: Best practices for how to deploy Expensify for your business +redirect_from: articles/playbooks/Expensify-Playbook-for-US-Based-Bootstrapped-Startups/ --- This playbook details best practices on how Bootstrapped Startups with less than 5 employees can use Expensify to prioritize product development while capturing business-related receipts for future reimbursement. diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md index 501d2f1538ef..bdce2cd7bf81 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md @@ -1,6 +1,7 @@ --- title: Expensify Playbook for US-Based VC-Backed Startups description: Best practices for how to deploy Expensify for your business +redirect_from: articles/playbooks/Expensify-Playbook-for-US-based-VC-Backed-Startups/ --- This playbook details best practices on how Seed to Series A startups with under 100 employees can use Expensify to prioritize top-line revenue growth while managing spend responsibly. diff --git a/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md b/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md index 3ef47337a74c..a6fa0220c0dc 100644 --- a/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md +++ b/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md @@ -1,6 +1,7 @@ --- title: Your Expensify Account Manager description: Everything you need to know about Having an Expensify account manager +redirect_from: articles/other/Your-Expensify-Account-Manager/ --- diff --git a/docs/articles/expensify-classic/getting-started/tips-and-tricks/Enable-Location-Access-On-Web.md b/docs/articles/expensify-classic/getting-started/tips-and-tricks/Enable-Location-Access-On-Web.md index 649212b00f7b..507d24503af8 100644 --- a/docs/articles/expensify-classic/getting-started/tips-and-tricks/Enable-Location-Access-On-Web.md +++ b/docs/articles/expensify-classic/getting-started/tips-and-tricks/Enable-Location-Access-On-Web.md @@ -1,6 +1,7 @@ --- title: Enable Location Access on Web description: How to enable location access for Expensify websites on your browser +redirect_from: articles/other/Enable-Location-Access-on-Web/ --- 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 a65dc378a793..65238457f1a9 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 @@ -9,34 +9,34 @@ This guide is for those who are part of a **Group Workspace**. Each member has a role that defines what they can see and do in the workspace. Most members will have the role of "Employee." -# How to Manage User Roles +# How to manage user roles -To find and edit the roles of group workspace members, go to **Settings > Workspaces > Group > [Your Specific Workspace Name] > Members > Workspace Members** +To find and edit the roles of group workspace members, go to **Settings > Workspaces > Group > _[Workspace Name]_ > Members > Workspace Members** Here you'll see the list of members in your group workspace. To change their roles, click **Settings** next to the member’s name and choose the role that the member needs. Next, let’s go over the various user roles that are available on a group workspace. -## The Employee Role +### The Employee Role - **What can they do:** Employees can only see their own expense reports or reports that have been submitted to or shared with them. They can't change settings or invite new users. - **Who is it for:** Regular employees who only need to manage their own expenses, or managers who are reviewing expense reports for a few users but don’t need global visibility. - **Approvers:** Members who approve expenses can either be Employees, Admins, or Workspace Auditors, depending on how much control they need. - **Billable:** Employees are billable actors if they take actions on a report on your Group Workspace (including **SmartScanning** a receipt). -## Workspace Admin Role +### Workspace Admin Role - **What can they do:** Admins have full control. They can change settings, invite members, and view all reports. They can also process reimbursements if they have access to the company’s account. - **Billing Owners:** Billing owners are Admins by default. **Workspace Admins** are assigned by the owner or another admin. - **Billable:** Yes, if they perform actions like changing settings or inviting users. Just viewing reports is not billable. -## Workspace Auditor Role +### Workspace Auditor Role - **What can they do:** Workspace Auditors can see all reports, make comments, and export them. They can also mark reports as reimbursed if they're the final approver. - **Who is it for:** Accountants, bookkeepers, and internal or external audit agents who need to view but not edit workspace settings. - **Billable:** Yes, if they perform any actions like commenting or exporting a report. Viewing alone doesn't incur a charge. -## Technical Contact +### Technical Contact - **What can they do:** In case of connection issues, alerts go to the billing owner by default. You can set a technical contact if you want alerts to go to an IT administrator instead. - **How to set one:** Go to **Settings > Workspaces > Group > [Workspace Name] > Connections > Technical Contact**. 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 e107734216f5..7c21b12a83e1 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 @@ -1,8 +1,54 @@ --- -title: Coming Soon -description: Coming Soon +title: Vacation Delegate +description: In Expensify, a vacation delegate is someone you choose to act on your behalf when you're on vacation or taking personal time off. --- -## Resource Coming Soon! + +# Overview + +A delegate is someone who can handle approving expense reports for you, which is especially useful when you're out of the office! + +In Expensify, a **Vacation Delegate** is someone you choose to act on your behalf when you're on vacation or taking personal time off. They will approve expense reports just like you would, and everything moves forward as usual afterward. + +The system keeps a detailed audit trail, showing exactly when your delegate stepped in to approve a report for you. And if your delegate also goes on vacation, they can have their own delegate, so reports keep getting approved. + +By using this feature, you ensure that all reports get the approvals they need, even when you're not around. + +# How to use Vacation Delegate + +If you're planning to take some time off, you can use the **Vacation Delegate** feature to assign someone to approve expense reports for you. The reports will continue on their usual path as if you had approved them yourself. + +## Set a Vacation Delegate for yourself + +1. Go to the Expensify website (note: you can't do this from the mobile app). +2. Navigate to **Settings > Your Account > Account Details** and scroll down to find **Vacation Delegate**. +3. Enter the email address of the person you're designating as your delegate and click **Set Delegate**. + +Voila! You've set a vacation delegate. Any reports that usually come to you will now go to your delegate instead. When you return, you can easily remove the delegate by clicking a link at the top of the Expensify homepage. + +## Setting a Vacation Delegate as a Domain Admin + +1. Head to **Settings > Domains > [Your Domain Name] > Domain Members > Edit Settings** +2. Enter the delegate's email address and click **Save.** + +Your delegate's actions will be noted in the history and comments of each report they approve, so you can keep track of what happened while you were away. + +# Deep Dive + +## An audit trail of delegate actions + +The system records every action your vacation delegate takes on your behalf in the **Report History and Comments**. So, you can see when they approved an expense report for you. + +# FAQs + +## Why can't my Vacation Delegate reimburse reports that they approve? + +If your **Vacation Delegate** also needs to reimburse reports on your behalf whilst you're away, they'll also need access to the reimbursement account. + +If they do not have access to the reimbursement account used on your workspace, they won’t have the option to reimburse reports, even as your **Vacation Delegate**. + +## What if my Vacation Delegate is also on vacation? + +Don't worry, your delegate can also pick their own **Vacation Delegate**. This way, expense reports continue to get approved even if multiple people are away. + -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 deleted file mode 100644 index cea96cfe2057..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Admins -description: Admins ---- -## 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 deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: 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 deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: 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 deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: 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 deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: 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 deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md new file mode 100644 index 000000000000..e5c9096fa610 --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md @@ -0,0 +1,5 @@ +--- +title: Currency +description: Currency +--- +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md new file mode 100644 index 000000000000..47b96f495a1c --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md @@ -0,0 +1,5 @@ +--- +title: Report Fields & Titles +description: Report Fields & Titles +--- +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md new file mode 100644 index 000000000000..c05df92bbbff --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md @@ -0,0 +1,38 @@ +--- +title: Scheduled Submit +description: How to use the Scheduled Submit feature +--- +# Overview + +Scheduled Submit reduces the delay between the time an employee creates an expense to when it is submitted to the admin. This gives admins significantly faster visibility into employee spend. Without Scheduled Submit enabled, expenses can be left Unreported giving workspace admins no visibility into employee spend. + +The biggest delay in expense management is the time it takes for an employee to actually submit the expense after it is incurred. Scheduled Submit allows you to automatically collect employee expenses on a schedule of your choosing without delaying the process while you wait for employees to submit them. + +It works like this: Employee expenses are automatically gathered onto a report. If there is not an existing report, a new one will be created. This report is submitted automatically at the cadence you choose (daily, weekly, monthly, twice month, by trip). + +# How to enable Scheduled Submit + +**For workspace admins**: To enable Scheduled Submit on your group workspace, follow **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu. +For individuals or employees: To enable Scheduled Submit on your individual workspace, follow **Settings > Workspaces > Individual > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu. + +## Scheduled Submit frequency options + +**Daily**: Each night, expenses without violations will be submitted. Expenses with violations will remain on an open report until the violations are corrected, after which they will be submitted in the evening (PDT). + +**Weekly**: Expenses that are free of violations will be submitted on a weekly basis. However, expenses with violations will be held in a new open report and combined with any new expenses. They will then be submitted at the end of the following weekly cycle, specifically on Sunday evening (PDT). + +**Twice a month**: Expenses that are violation-free will be submitted on both the 15th and the last day of each month, in the evening (PDT). Expenses with violations will not be submitted, but moved on to a new open report so the employee can resolve the violations and then will be submitted at the conclusion of the next cycle. + +**Monthly**: Expenses that are free from violations will be submitted on a monthly basis. Expenses with violations will be held back and moved to a new Open report so the violations can be resolved, and they will be submitted on the evening (PDT) of the specified date. + +**By trip**: Expenses are grouped by trip. This is calculated by grouping all expenses together that occur in a similar time frame. If two full days pass without any new expenses being created, the trip report will be submitted on the evening of the second day. Any expenses generated after this submission will initiate a new trip report. Please note that the "2-day" period refers to a date-based interval, not a 48-hour time frame. + +**Manually**: An open report will be created, and expenses will be added to it automatically. However, it's important to note that the report will not be submitted automatically; manual submission of reports will be required.This is a great option for automatically gathering all an employee’s expenses on a report for the employee’s convenience, but they will still need to review and submit the report. + +# Deep Dive + +## Schedule Submit Override +If Scheduled Submit is disabled at the group workspace level or configured the frequency as "Manually," the individual workspace settings of a user will take precedence and be applied. This means an employee can still set up Scheduled Submit for themselves even if the admin has not enabled it. We highly recommend Scheduled Submit as it helps put your expenses on auto-pilot! + +## Personal Card Transactions +Personal card transactions are handled differently compared to other expenses. If a user has added a card through Settings > Account > Credit Card Import, they need to make sure it is set as non-reimbursable and transactions must be automatically merged with a SmartScanned receipt. If transactions are set to come in as reimbursable or they aren’t merged with a SmartScanned receipt, Scheduled Submit settings will not apply. diff --git a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md index 0a8d6b3493e0..e157ede1969d 100644 --- a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md +++ b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md @@ -1,6 +1,7 @@ --- title: The Free Plan description: Everything you need to know about Expensify's Free Plan! +redirect_from: articles/split-bills/workspaces/The-Free-Plan/ --- 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 dc6de6656cc9..a2b765915af0 100644 --- a/docs/articles/new-expensify/get-paid-back/Request-Money.md +++ b/docs/articles/new-expensify/get-paid-back/Request-Money.md @@ -1,5 +1,6 @@ --- title: Request Money description: Request Money +redirect_from: articles/request-money/Request-and-Split-Bills/ --- ## Resource Coming Soon! diff --git a/docs/articles/new-expensify/getting-started/Expensify-Lounge.md b/docs/articles/new-expensify/getting-started/Expensify-Lounge.md index 01a2d7a9e250..bdccbe927769 100644 --- a/docs/articles/new-expensify/getting-started/Expensify-Lounge.md +++ b/docs/articles/new-expensify/getting-started/Expensify-Lounge.md @@ -1,6 +1,7 @@ --- title: Welcome to the Expensify Lounge! description: How to get the most out of the Expensify Lounge. +redirect_from: articles/other/Expensify-Lounge/ --- diff --git a/docs/articles/new-expensify/getting-started/chat/Everything-About-Chat.md b/docs/articles/new-expensify/getting-started/chat/Everything-About-Chat.md index 9f73d1c759c2..77bbe54e8e2c 100644 --- a/docs/articles/new-expensify/getting-started/chat/Everything-About-Chat.md +++ b/docs/articles/new-expensify/getting-started/chat/Everything-About-Chat.md @@ -1,6 +1,7 @@ --- title: Everything About Chat description: Everything you need to know about Expensify's Chat Features! +redirect_from: articles/other/Everything-About-Chat/ --- diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md index 31de150d5b5e..996d7896502f 100644 --- a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md +++ b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md @@ -1,6 +1,7 @@ --- title: Expensify Chat for Admins description: Best Practices for Admins settings up Expensify Chat +redirect_from: articles/other/Expensify-Chat-For-Admins/ --- ## Overview diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Attendees.md b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Attendees.md index 3d30237dca5a..20e15aaa6c72 100644 --- a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Attendees.md +++ b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Attendees.md @@ -1,6 +1,7 @@ --- title: Expensify Chat for Conference Attendees description: Best Practices for Conference Attendees +redirect_from: articles/other/Expensify-Chat-For-Conference-Attendees/ --- ## Overview diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Speakers.md b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Speakers.md index 5bd52425d92b..3e19cf6fe26a 100644 --- a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Speakers.md +++ b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Speakers.md @@ -1,6 +1,7 @@ --- title: Expensify Chat for Conference Speakers description: Best Practices for Conference Speakers +redirect_from: articles/other/Expensify-Chat-For-Conference-Speakers/ --- ## Overview diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-Playbook-For-Conferences.md b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-Playbook-For-Conferences.md index 8f806bb03146..a81aef2044a2 100644 --- a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-Playbook-For-Conferences.md +++ b/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-Playbook-For-Conferences.md @@ -1,6 +1,7 @@ --- title: Expensify Chat Playbook for Conferences description: Best practices for how to deploy Expensify Chat for your conference +redirect_from: articles/playbooks/Expensify-Chat-Playbook-for-Conferences/ --- ## Overview To help make setting up Expensify Chat for your event and your attendees super simple, we’ve created a guide for all of the technical setup details. diff --git a/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html b/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html new file mode 100644 index 000000000000..86641ee60b7d --- /dev/null +++ b/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html @@ -0,0 +1,5 @@ +--- +layout: default +--- + +{% include section.html %} diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 73e22053eda1..fb13a410dd8e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.74 + 1.3.75 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.74.3 + 1.3.75.8 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 5e7f02699579..2168da376988 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.74 + 1.3.75 CFBundleSignature ???? CFBundleVersion - 1.3.74.3 + 1.3.75.8 diff --git a/package-lock.json b/package-lock.json index 42755b09f8b6..a6ed0e6464d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.74-3", + "version": "1.3.75-8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.74-3", + "version": "1.3.75-8", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -86,11 +86,11 @@ "react-native-image-pan-zoom": "^2.1.12", "react-native-image-picker": "^5.1.0", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", - "react-native-key-command": "^1.0.1", + "react-native-key-command": "^1.0.5", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.94", + "react-native-onyx": "1.0.97", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -41151,19 +41151,25 @@ "license": "MIT" }, "node_modules/react-native-key-command": { - "version": "1.0.1", - "license": "MIT", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-native-key-command/-/react-native-key-command-1.0.5.tgz", + "integrity": "sha512-SJWf1e8f3yGFrFDNCmJ+aiGmnwokGgtMicfvuyukhQtXkncCQb9pBI4uhBen0Bd30uMmUDgGAA9O56OyIdf5jw==", "dependencies": { - "events": "^3.3.0", + "eventemitter3": "^5.0.1", "underscore": "^1.13.4" }, "peerDependencies": { "react": "^18.1.0", "react-dom": "18.1.0", "react-native": "^0.70.4", - "react-native-web": "^0.18.1" + "react-native-web": "^0.19.7" } }, + "node_modules/react-native-key-command/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/react-native-linear-gradient": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz", @@ -41204,9 +41210,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.94", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.94.tgz", - "integrity": "sha512-Xoh9LTdoCNLQjyeLB6HkBwyf5ipkSjnETLVijSIWKnecbZS8/fQehUuGz+yEk9I0xVEn43IhmnkQ+yqQvV9vEg==", + "version": "1.0.97", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.97.tgz", + "integrity": "sha512-6w4pp9Ktm4lQ6jIS+ZASQ5tYwRU1lt751yxfddvmN646XZefj4iDvC7uQaUnAgg1xL52dEV5RZWaI3sQ3e9AGQ==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -41491,21 +41497,23 @@ } }, "node_modules/react-native-web": { - "version": "0.18.12", - "license": "MIT", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.9.tgz", + "integrity": "sha512-m69arZbS6FV+BNSKE6R/NQwUX+CzxCkYM7AJlSLlS8dz3BDzlaxG8Bzqtzv/r3r1YFowhnZLBXVKIwovKDw49g==", "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", - "create-react-class": "^15.7.0", + "@react-native/normalize-color": "^2.1.0", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", - "normalize-css-color": "^1.0.2", + "memoize-one": "^6.0.0", + "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", - "styleq": "^0.1.2" + "styleq": "^0.1.3" }, "peerDependencies": { - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" } }, "node_modules/react-native-web-linear-gradient": { @@ -41526,6 +41534,12 @@ "react-native-web": "*" } }, + "node_modules/react-native-web/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "peer": true + }, "node_modules/react-native-webview": { "version": "11.23.0", "license": "MIT", @@ -45183,8 +45197,9 @@ } }, "node_modules/styleq": { - "version": "0.1.2", - "license": "MIT" + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==" }, "node_modules/sudo-prompt": { "version": "9.2.1", @@ -77245,10 +77260,19 @@ "from": "react-native-image-size@git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b" }, "react-native-key-command": { - "version": "1.0.1", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-native-key-command/-/react-native-key-command-1.0.5.tgz", + "integrity": "sha512-SJWf1e8f3yGFrFDNCmJ+aiGmnwokGgtMicfvuyukhQtXkncCQb9pBI4uhBen0Bd30uMmUDgGAA9O56OyIdf5jw==", "requires": { - "events": "^3.3.0", + "eventemitter3": "^5.0.1", "underscore": "^1.13.4" + }, + "dependencies": { + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + } } }, "react-native-linear-gradient": { @@ -77269,9 +77293,9 @@ } }, "react-native-onyx": { - "version": "1.0.94", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.94.tgz", - "integrity": "sha512-Xoh9LTdoCNLQjyeLB6HkBwyf5ipkSjnETLVijSIWKnecbZS8/fQehUuGz+yEk9I0xVEn43IhmnkQ+yqQvV9vEg==", + "version": "1.0.97", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.97.tgz", + "integrity": "sha512-6w4pp9Ktm4lQ6jIS+ZASQ5tYwRU1lt751yxfddvmN646XZefj4iDvC7uQaUnAgg1xL52dEV5RZWaI3sQ3e9AGQ==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -77439,16 +77463,27 @@ "requires": {} }, "react-native-web": { - "version": "0.18.12", + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.9.tgz", + "integrity": "sha512-m69arZbS6FV+BNSKE6R/NQwUX+CzxCkYM7AJlSLlS8dz3BDzlaxG8Bzqtzv/r3r1YFowhnZLBXVKIwovKDw49g==", "peer": true, "requires": { "@babel/runtime": "^7.18.6", - "create-react-class": "^15.7.0", + "@react-native/normalize-color": "^2.1.0", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", - "normalize-css-color": "^1.0.2", + "memoize-one": "^6.0.0", + "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", - "styleq": "^0.1.2" + "styleq": "^0.1.3" + }, + "dependencies": { + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "peer": true + } } }, "react-native-web-linear-gradient": { @@ -79905,7 +79940,9 @@ } }, "styleq": { - "version": "0.1.2" + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==" }, "sudo-prompt": { "version": "9.2.1", diff --git a/package.json b/package.json index 3b88d603ba52..bc52786de8ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.74-3", + "version": "1.3.75-8", "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.", @@ -129,11 +129,11 @@ "react-native-image-pan-zoom": "^2.1.12", "react-native-image-picker": "^5.1.0", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", - "react-native-key-command": "^1.0.1", + "react-native-key-command": "^1.0.5", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.94", + "react-native-onyx": "1.0.97", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", diff --git a/src/App.js b/src/App.js index 284c6115d7b8..1d2e07345c24 100644 --- a/src/App.js +++ b/src/App.js @@ -9,7 +9,7 @@ import {PickerStateProvider} from 'react-native-picker-select'; import CustomStatusBar from './components/CustomStatusBar'; import ErrorBoundary from './components/ErrorBoundary'; import Expensify from './Expensify'; -import {LocaleContextProvider} from './components/withLocalize'; +import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import PopoverContextProvider from './components/PopoverProvider'; diff --git a/src/CONST.ts b/src/CONST.ts index dbe47c6ed1a7..0a262d868de9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -992,6 +992,11 @@ const CONST = { STATEMENT: 'STATEMENT_NAVIGATE', CONCIERGE: 'CONCIERGE_NAVIGATE', }, + MTL_WALLET_PROGRAM_ID: '760', + PROGRAM_ISSUERS: { + EXPENSIFY_PAYMENTS: 'Expensify Payments LLC', + BANCORP_BANK: 'The Bancorp Bank', + }, }, PLAID: { @@ -1263,6 +1268,8 @@ const CONST = { DATE_TIME_FORMAT: /^\d{2}-\d{2} \d{2}:\d{2} [AP]M$/, ATTACHMENT_ROUTE: /\/r\/(\d*)\/attachment/, ILLEGAL_FILENAME_CHARACTERS: /\/|<|>|\*|"|:|\?|\\|\|/g, + + ENCODE_PERCENT_CHARACTER: /%(25)+/g, }, PRONOUNS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d2b3031220f1..a1afc4fef2c1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,5 +1,4 @@ import {ValueOf} from 'type-fest'; -import {OnyxUpdate} from 'react-native-onyx'; import DeepValueOf from './types/utils/DeepValueOf'; import * as OnyxTypes from './types/onyx'; import CONST from './CONST'; @@ -30,9 +29,6 @@ const ONYXKEYS = { /** Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe */ PERSISTED_REQUESTS: 'networkRequestQueue', - /** Onyx updates from a response, or success or failure data from a request. */ - QUEUED_ONYX_UPDATES: 'queuedOnyxUpdates', - /** Stores current date */ CURRENT_DATE: 'currentDate', @@ -307,7 +303,6 @@ type OnyxValues = { [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; [ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[]; - [ONYXKEYS.QUEUED_ONYX_UPDATES]: OnyxUpdate[]; [ONYXKEYS.CURRENT_DATE]: string; [ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.IOU]: OnyxTypes.IOU; diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 44075a4ec1eb..e7b18bbd8d69 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -358,7 +358,7 @@ function Composer({ const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); setTextInputWidth(computedStyle.width); - const computedNumberOfLines = ComposerUtils.getNumberOfLines(maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); + const computedNumberOfLines = ComposerUtils.getNumberOfLines(lineHeight, paddingTopAndBottom, textInput.current.scrollHeight, maxLines); const generalNumberOfLines = computedNumberOfLines === 0 ? numberOfLinesProp : computedNumberOfLines; onNumberOfLinesChange(generalNumberOfLines); diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 90f5c22e5b3c..5261d1258ad0 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -108,6 +108,7 @@ function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, c (values) => { const validateErrors = validate(values); setErrors(validateErrors); + return validateErrors; }, [validate], ); diff --git a/src/components/LocaleContextProvider.js b/src/components/LocaleContextProvider.js new file mode 100644 index 000000000000..b8838f253e74 --- /dev/null +++ b/src/components/LocaleContextProvider.js @@ -0,0 +1,135 @@ +import React, {createContext, useMemo} from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; + +import ONYXKEYS from '../ONYXKEYS'; +import * as Localize from '../libs/Localize'; +import DateUtils from '../libs/DateUtils'; +import * as NumberFormatUtils from '../libs/NumberFormatUtils'; +import * as LocaleDigitUtils from '../libs/LocaleDigitUtils'; +import CONST from '../CONST'; +import compose from '../libs/compose'; +import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; +import * as LocalePhoneNumber from '../libs/LocalePhoneNumber'; + +const LocaleContext = createContext(null); + +const localeProviderPropTypes = { + /** The user's preferred locale e.g. 'en', 'es-ES' */ + preferredLocale: PropTypes.string, + + /** Actual content wrapped by this component */ + children: PropTypes.node.isRequired, + + /** The current user's personalDetails */ + currentUserPersonalDetails: PropTypes.shape({ + /** Timezone of the current user */ + timezone: PropTypes.shape({ + /** Value of the selected timezone */ + selected: PropTypes.string, + }), + }), +}; + +const localeProviderDefaultProps = { + preferredLocale: CONST.LOCALES.DEFAULT, + currentUserPersonalDetails: {}, +}; + +function LocaleContextProvider({children, currentUserPersonalDetails, preferredLocale}) { + const selectedTimezone = useMemo(() => lodashGet(currentUserPersonalDetails, 'timezone.selected'), [currentUserPersonalDetails]); + + /** + * @param {String} phrase + * @param {Object} [variables] + * @returns {String} + */ + const translate = useMemo(() => (phrase, variables) => Localize.translate(preferredLocale, phrase, variables), [preferredLocale]); + + /** + * @param {Number} number + * @param {Intl.NumberFormatOptions} options + * @returns {String} + */ + const numberFormat = useMemo(() => (number, options) => NumberFormatUtils.format(preferredLocale, number, options), [preferredLocale]); + + /** + * @param {String} datetime + * @returns {String} + */ + const datetimeToRelative = useMemo(() => (datetime) => DateUtils.datetimeToRelative(preferredLocale, datetime), [preferredLocale]); + + /** + * @param {String} datetime - ISO-formatted datetime string + * @param {Boolean} [includeTimezone] + * @param {Boolean} isLowercase + * @returns {String} + */ + const datetimeToCalendarTime = useMemo( + () => + (datetime, includeTimezone, isLowercase = false) => + DateUtils.datetimeToCalendarTime(preferredLocale, datetime, includeTimezone, selectedTimezone, isLowercase), + [preferredLocale, selectedTimezone], + ); + + /** + * Updates date-fns internal locale to the user preferredLocale + */ + const updateLocale = useMemo(() => () => DateUtils.setLocale(preferredLocale), [preferredLocale]); + + /** + * @param {String} phoneNumber + * @returns {String} + */ + const formatPhoneNumber = LocalePhoneNumber.formatPhoneNumber; + + /** + * @param {String} digit + * @returns {String} + */ + const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(preferredLocale, digit), [preferredLocale]); + + /** + * @param {String} localeDigit + * @returns {String} + */ + const fromLocaleDigit = useMemo(() => (localeDigit) => LocaleDigitUtils.fromLocaleDigit(preferredLocale, localeDigit), [preferredLocale]); + + /** + * The context this component exposes to child components + * @returns {object} translation util functions and locale + */ + const contextValue = useMemo( + () => ({ + translate, + numberFormat, + datetimeToRelative, + datetimeToCalendarTime, + updateLocale, + formatPhoneNumber, + toLocaleDigit, + fromLocaleDigit, + preferredLocale, + }), + [translate, numberFormat, datetimeToRelative, datetimeToCalendarTime, updateLocale, formatPhoneNumber, toLocaleDigit, fromLocaleDigit, preferredLocale], + ); + + return {children}; +} + +LocaleContextProvider.propTypes = localeProviderPropTypes; +LocaleContextProvider.defaultProps = localeProviderDefaultProps; + +const Provider = compose( + withCurrentUserPersonalDetails, + withOnyx({ + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + }), +)(LocaleContextProvider); + +Provider.displayName = 'withOnyx(LocaleContextProvider)'; + +export {Provider as LocaleContextProvider, LocaleContext}; diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index b469f39c7037..bd6548607cb9 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -24,8 +24,8 @@ import useNativeDriver from '../../libs/useNativeDriver'; import * as Browser from '../../libs/Browser'; function BaseTextInput(props) { - const inputValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || inputValue.length > 0 || Boolean(props.prefixCharacter); + const initialValue = props.value || props.defaultValue || ''; + const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); @@ -145,30 +145,16 @@ function BaseTextInput(props) { [props.autoGrowHeight, props.multiline], ); - useEffect(() => { - // Handle side effects when the value gets changed programatically from the outside - - // In some cases, When the value prop is empty, it is not properly updated on the TextInput due to its uncontrolled nature, thus manually clearing the TextInput. - if (inputValue === '') { - input.current.clear(); - } - - if (inputValue) { - activateLabel(); - } - }, [activateLabel, inputValue]); - - // We capture whether the input has a value or not in a ref. - // It gets updated when the text gets changed. - const hasValueRef = useRef(inputValue.length > 0); + // The ref is needed when the component is uncontrolled and we don't have a value prop + const hasValueRef = useRef(initialValue.length > 0); + const inputValue = props.value || ''; + const hasValue = inputValue.length > 0 || hasValueRef.current; - // Activate or deactivate the label when the focus changes: + // Activate or deactivate the label when either focus changes, or for controlled + // components when the value prop changes: useEffect(() => { - // We can't use inputValue here directly, as it might contain - // the defaultValue, which doesn't get updated when the text changes. - // We can't use props.value either, as it might be undefined. if ( - hasValueRef.current || + hasValue || isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. @@ -178,7 +164,16 @@ function BaseTextInput(props) { } else { deactivateLabel(); } - }, [activateLabel, deactivateLabel, inputValue, isFocused]); + }, [activateLabel, deactivateLabel, hasValue, isFocused]); + + // When the value prop gets cleared externally, we need to keep the ref in sync: + useEffect(() => { + // Return early when component uncontrolled, or we still have a value + if (props.value === undefined || !_.isEmpty(props.value)) { + return; + } + hasValueRef.current = false; + }, [props.value]); /** * Set Value & activateLabel @@ -192,9 +187,13 @@ function BaseTextInput(props) { } Str.result(props.onChangeText, value); + if (value && value.length > 0) { hasValueRef.current = true; - activateLabel(); + // When the componment is uncontrolled, we need to manually activate the label: + if (props.value === undefined) { + activateLabel(); + } } else { hasValueRef.current = false; } diff --git a/src/components/withLocalize.js b/src/components/withLocalize.js index 5ce1b0bc6d74..65e98f78f312 100755 --- a/src/components/withLocalize.js +++ b/src/components/withLocalize.js @@ -1,19 +1,7 @@ -import React, {createContext, forwardRef} from 'react'; +import React, {forwardRef} from 'react'; import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; +import {LocaleContext} from './LocaleContextProvider'; import getComponentDisplayName from '../libs/getComponentDisplayName'; -import ONYXKEYS from '../ONYXKEYS'; -import * as Localize from '../libs/Localize'; -import DateUtils from '../libs/DateUtils'; -import * as NumberFormatUtils from '../libs/NumberFormatUtils'; -import * as LocaleDigitUtils from '../libs/LocaleDigitUtils'; -import CONST from '../CONST'; -import compose from '../libs/compose'; -import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; -import * as LocalePhoneNumber from '../libs/LocalePhoneNumber'; - -const LocaleContext = createContext(null); const withLocalizePropTypes = { /** Returns translated string for given locale and phrase */ @@ -42,140 +30,6 @@ const withLocalizePropTypes = { toLocaleDigit: PropTypes.func.isRequired, }; -const localeProviderPropTypes = { - /** The user's preferred locale e.g. 'en', 'es-ES' */ - preferredLocale: PropTypes.string, - - /** Actual content wrapped by this component */ - children: PropTypes.node.isRequired, - - /** The current user's personalDetails */ - currentUserPersonalDetails: PropTypes.shape({ - /** Timezone of the current user */ - timezone: PropTypes.shape({ - /** Value of the selected timezone */ - selected: PropTypes.string, - }), - }), -}; - -const localeProviderDefaultProps = { - preferredLocale: CONST.LOCALES.DEFAULT, - currentUserPersonalDetails: {}, -}; - -class LocaleContextProvider extends React.Component { - shouldComponentUpdate(nextProps) { - return ( - nextProps.preferredLocale !== this.props.preferredLocale || - lodashGet(nextProps, 'currentUserPersonalDetails.timezone.selected') !== lodashGet(this.props, 'currentUserPersonalDetails.timezone.selected') - ); - } - - /** - * The context this component exposes to child components - * @returns {object} translation util functions and locale - */ - getContextValue() { - return { - translate: this.translate.bind(this), - numberFormat: this.numberFormat.bind(this), - datetimeToRelative: this.datetimeToRelative.bind(this), - datetimeToCalendarTime: this.datetimeToCalendarTime.bind(this), - updateLocale: this.updateLocale.bind(this), - formatPhoneNumber: this.formatPhoneNumber.bind(this), - fromLocaleDigit: this.fromLocaleDigit.bind(this), - toLocaleDigit: this.toLocaleDigit.bind(this), - preferredLocale: this.props.preferredLocale, - }; - } - - /** - * @param {String} phrase - * @param {Object} [variables] - * @returns {String} - */ - translate(phrase, variables) { - return Localize.translate(this.props.preferredLocale, phrase, variables); - } - - /** - * @param {Number} number - * @param {Intl.NumberFormatOptions} options - * @returns {String} - */ - numberFormat(number, options) { - return NumberFormatUtils.format(this.props.preferredLocale, number, options); - } - - /** - * @param {String} datetime - * @returns {String} - */ - datetimeToRelative(datetime) { - return DateUtils.datetimeToRelative(this.props.preferredLocale, datetime); - } - - /** - * @param {String} datetime - ISO-formatted datetime string - * @param {Boolean} [includeTimezone] - * @param {Boolean} isLowercase - * @returns {String} - */ - datetimeToCalendarTime(datetime, includeTimezone, isLowercase = false) { - return DateUtils.datetimeToCalendarTime(this.props.preferredLocale, datetime, includeTimezone, lodashGet(this.props, 'currentUserPersonalDetails.timezone.selected'), isLowercase); - } - - /** - * Updates date-fns internal locale to the user preferredLocale - */ - updateLocale() { - DateUtils.setLocale(this.props.preferredLocale); - } - - /** - * @param {String} phoneNumber - * @returns {String} - */ - formatPhoneNumber(phoneNumber) { - return LocalePhoneNumber.formatPhoneNumber(phoneNumber); - } - - /** - * @param {String} digit - * @returns {String} - */ - toLocaleDigit(digit) { - return LocaleDigitUtils.toLocaleDigit(this.props.preferredLocale, digit); - } - - /** - * @param {String} localeDigit - * @returns {String} - */ - fromLocaleDigit(localeDigit) { - return LocaleDigitUtils.fromLocaleDigit(this.props.preferredLocale, localeDigit); - } - - render() { - return {this.props.children}; - } -} - -LocaleContextProvider.propTypes = localeProviderPropTypes; -LocaleContextProvider.defaultProps = localeProviderDefaultProps; - -const Provider = compose( - withCurrentUserPersonalDetails, - withOnyx({ - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - }), -)(LocaleContextProvider); - -Provider.displayName = 'withOnyx(LocaleContextProvider)'; - export default function withLocalize(WrappedComponent) { const WithLocalize = forwardRef((props, ref) => ( @@ -196,4 +50,4 @@ export default function withLocalize(WrappedComponent) { return WithLocalize; } -export {withLocalizePropTypes, Provider as LocaleContextProvider, LocaleContext}; +export {withLocalizePropTypes}; diff --git a/src/hooks/useLocalize.js b/src/hooks/useLocalize.js index 9ad5048729bd..7f7a610fca8b 100644 --- a/src/hooks/useLocalize.js +++ b/src/hooks/useLocalize.js @@ -1,5 +1,5 @@ import {useContext} from 'react'; -import {LocaleContext} from '../components/withLocalize'; +import {LocaleContext} from '../components/LocaleContextProvider'; export default function useLocalize() { return useContext(LocaleContext); diff --git a/src/languages/en.ts b/src/languages/en.ts index 9c49e2907702..f16097aa03d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -73,6 +73,7 @@ import type { RequestedAmountMessageParams, TagSelectionParams, TranslationBase, + WalletProgramParams, } from './types'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; @@ -909,7 +910,7 @@ export default { phrase2: 'Terms of Service', phrase3: 'and', phrase4: 'Privacy', - phrase5: 'Money transmission is provided by Expensify Payments LLC (NMLS ID:2017010) pursuant to its', + phrase5: `Money transmission is provided by ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) pursuant to its`, phrase6: 'licenses', }, validateCodeForm: { @@ -1170,7 +1171,7 @@ export default { electronicFundsWithdrawal: 'Electronic funds withdrawal', standard: 'Standard', shortTermsForm: { - expensifyPaymentsAccount: 'The Expensify Wallet is issued by The Bancorp Bank.', + expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `The Expensify Wallet is issued by ${walletProgram}.`, perPurchase: 'Per purchase', atmWithdrawal: 'ATM withdrawal', cashReload: 'Cash reload', @@ -1212,10 +1213,10 @@ export default { 'several minutes. The fee is 1.5% of the transfer amount (with a minimum fee of $0.25).', fdicInsuranceBancorp: 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + - 'transferred to The Bancorp Bank, an FDIC-insured institution. Once there, your funds are insured up ' + - 'to $250,000 by the FDIC in the event The Bancorp Bank fails. See', + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + + `to $250,000 by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, fdicInsuranceBancorp2: 'for details.', - contactExpensifyPayments: 'Contact Expensify Payments by calling +1 833-400-0904, by email at', + contactExpensifyPayments: `Contact ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} by calling +1 833-400-0904, by email at`, contactExpensifyPayments2: 'or sign in at', generalInformation: 'For general information about prepaid accounts, visit', generalInformation2: 'If you have a complaint about a prepaid account, call the Consumer Financial Protection Bureau at 1-855-411-2372 or visit', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2be3f9e96265..3860c34f6ef1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -73,6 +73,7 @@ import type { RequestedAmountMessageParams, TagSelectionParams, EnglishTranslation, + WalletProgramParams, } from './types'; /* eslint-disable max-len */ @@ -905,7 +906,7 @@ export default { phrase2: 'Términos de Servicio', phrase3: 'y', phrase4: 'Privacidad', - phrase5: 'El envío de dinero es brindado por Expensify Payments LLC (NMLS ID:2017010) de conformidad con sus', + phrase5: `El envío de dinero es brindado por ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} (NMLS ID:2017010) de conformidad con sus`, phrase6: 'licencias', }, validateCodeForm: { @@ -1187,7 +1188,7 @@ export default { electronicFundsWithdrawal: 'Retiro electrónico de fondos', standard: 'Estándar', shortTermsForm: { - expensifyPaymentsAccount: 'La billetera Expensify es emitida por The Bancorp Bank.', + expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `La billetera Expensify es emitida por ${walletProgram}.`, perPurchase: 'Por compra', atmWithdrawal: 'Retiro de cajero automático', cashReload: 'Recarga de efectivo', @@ -1230,10 +1231,10 @@ export default { 'transferencia (con una tarifa mínima de $ 0.25). ', fdicInsuranceBancorp: 'Sus fondos son elegibles para el seguro de la FDIC. Sus fondos se mantendrán en o ' + - 'transferido a The Bancorp Bank, una institución asegurada por la FDIC. Una vez allí, sus fondos ' + - 'están asegurados a $ 250,000 por la FDIC en caso de que The Bancorp Bank quiebre. Ver', + `transferido a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, sus fondos ` + + `están asegurados a $ 250,000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, fdicInsuranceBancorp2: 'para detalles.', - contactExpensifyPayments: 'Comuníquese con Expensify Payments llamando al + 1833-400-0904, por correoelectrónico a', + contactExpensifyPayments: `Comuníquese con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, por correoelectrónico a`, contactExpensifyPayments2: 'o inicie sesión en', generalInformation: 'Para obtener información general sobre cuentas prepagas, visite', generalInformation2: 'Si tiene una queja sobre una cuenta prepaga, llame al Consumer Financial Oficina de Protección al 1-855-411-2372 o visite', diff --git a/src/languages/types.ts b/src/languages/types.ts index 70bf2e4cae3d..52f2df8b3765 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -194,6 +194,8 @@ type FormattedMaxLengthParams = {formattedMaxLength: string}; type TagSelectionParams = {tagName: string}; +type WalletProgramParams = {walletProgram: string}; + /* Translation Object types */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type TranslationBaseValue = string | string[] | ((...args: any[]) => string); @@ -307,4 +309,5 @@ export type { RemovedTheRequestParams, FormattedMaxLengthParams, TagSelectionParams, + WalletProgramParams, }; diff --git a/src/libs/ComposerUtils/debouncedSaveReportComment.js b/src/libs/ComposerUtils/debouncedSaveReportComment.js deleted file mode 100644 index c39da78c2c3e..000000000000 --- a/src/libs/ComposerUtils/debouncedSaveReportComment.js +++ /dev/null @@ -1,13 +0,0 @@ -import _ from 'underscore'; -import * as Report from '../actions/Report'; - -/** - * Save draft report comment. Debounced to happen at most once per second. - * @param {String} reportID - * @param {String} comment - */ -const debouncedSaveReportComment = _.debounce((reportID, comment) => { - Report.saveReportComment(reportID, comment || ''); -}, 1000); - -export default debouncedSaveReportComment; diff --git a/src/libs/ComposerUtils/debouncedSaveReportComment.ts b/src/libs/ComposerUtils/debouncedSaveReportComment.ts new file mode 100644 index 000000000000..e449245edc52 --- /dev/null +++ b/src/libs/ComposerUtils/debouncedSaveReportComment.ts @@ -0,0 +1,11 @@ +import debounce from 'lodash/debounce'; +import * as Report from '../actions/Report'; + +/** + * Save draft report comment. Debounced to happen at most once per second. + */ +const debouncedSaveReportComment = debounce((reportID: string, comment = '') => { + Report.saveReportComment(reportID, comment); +}, 1000); + +export default debouncedSaveReportComment; diff --git a/src/libs/ComposerUtils/getDraftComment.js b/src/libs/ComposerUtils/getDraftComment.ts similarity index 75% rename from src/libs/ComposerUtils/getDraftComment.js rename to src/libs/ComposerUtils/getDraftComment.ts index 854df1ac65ee..ac3d2f3d09be 100644 --- a/src/libs/ComposerUtils/getDraftComment.js +++ b/src/libs/ComposerUtils/getDraftComment.ts @@ -1,7 +1,7 @@ -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; -const draftCommentMap = {}; +const draftCommentMap: Record> = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, callback: (value, key) => { @@ -18,9 +18,7 @@ Onyx.connect({ * Returns a draft comment from the onyx collection. * Note: You should use the HOCs/hooks to get onyx data, instead of using this directly. * A valid use case to use this is if the value is only needed once for an initial value. - * @param {String} reportID - * @returns {String|undefined} */ -export default function getDraftComment(reportID) { +export default function getDraftComment(reportID: string): OnyxEntry { return draftCommentMap[reportID]; } diff --git a/src/libs/ComposerUtils/getNumberOfLines/index.native.js b/src/libs/ComposerUtils/getNumberOfLines/index.native.js deleted file mode 100644 index ff4a1c6d74b1..000000000000 --- a/src/libs/ComposerUtils/getNumberOfLines/index.native.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Get the current number of lines in the composer - * - * @param {Number} lineHeight - * @param {Number} paddingTopAndBottom - * @param {Number} scrollHeight - * - * @returns {Number} - */ -function getNumberOfLines(lineHeight, paddingTopAndBottom, scrollHeight) { - return Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight); -} - -export default getNumberOfLines; diff --git a/src/libs/ComposerUtils/getNumberOfLines/index.native.ts b/src/libs/ComposerUtils/getNumberOfLines/index.native.ts new file mode 100644 index 000000000000..9a7340b9a035 --- /dev/null +++ b/src/libs/ComposerUtils/getNumberOfLines/index.native.ts @@ -0,0 +1,8 @@ +import GetNumberOfLines from './types'; + +/** + * Get the current number of lines in the composer + */ +const getNumberOfLines: GetNumberOfLines = (lineHeight, paddingTopAndBottom, scrollHeight) => Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight); + +export default getNumberOfLines; diff --git a/src/libs/ComposerUtils/getNumberOfLines/index.js b/src/libs/ComposerUtils/getNumberOfLines/index.ts similarity index 55% rename from src/libs/ComposerUtils/getNumberOfLines/index.js rename to src/libs/ComposerUtils/getNumberOfLines/index.ts index a469da7516bb..cf85b45443d5 100644 --- a/src/libs/ComposerUtils/getNumberOfLines/index.js +++ b/src/libs/ComposerUtils/getNumberOfLines/index.ts @@ -1,17 +1,12 @@ +import GetNumberOfLines from './types'; + /** * Get the current number of lines in the composer - * - * @param {Number} maxLines - * @param {Number} lineHeight - * @param {Number} paddingTopAndBottom - * @param {Number} scrollHeight - * - * @returns {Number} */ -function getNumberOfLines(maxLines, lineHeight, paddingTopAndBottom, scrollHeight) { +const getNumberOfLines: GetNumberOfLines = (lineHeight, paddingTopAndBottom, scrollHeight, maxLines = 0) => { let newNumberOfLines = Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight); newNumberOfLines = maxLines <= 0 ? newNumberOfLines : Math.min(newNumberOfLines, maxLines); return newNumberOfLines; -} +}; export default getNumberOfLines; diff --git a/src/libs/ComposerUtils/getNumberOfLines/types.ts b/src/libs/ComposerUtils/getNumberOfLines/types.ts new file mode 100644 index 000000000000..67bb790f726b --- /dev/null +++ b/src/libs/ComposerUtils/getNumberOfLines/types.ts @@ -0,0 +1,3 @@ +type GetNumberOfLines = (lineHeight: number, paddingTopAndBottom: number, scrollHeight: number, maxLines?: number) => number; + +export default GetNumberOfLines; diff --git a/src/libs/ComposerUtils/index.js b/src/libs/ComposerUtils/index.ts similarity index 69% rename from src/libs/ComposerUtils/index.js rename to src/libs/ComposerUtils/index.ts index dfe6cf446809..5e2a42fc65dd 100644 --- a/src/libs/ComposerUtils/index.js +++ b/src/libs/ComposerUtils/index.ts @@ -2,24 +2,22 @@ import getNumberOfLines from './getNumberOfLines'; import updateNumberOfLines from './updateNumberOfLines'; import * as DeviceCapabilities from '../DeviceCapabilities'; +type Selection = { + start: number; + end: number; +}; + /** * Replace substring between selection with a text. - * @param {String} text - * @param {Object} selection - * @param {String} textToInsert - * @returns {String} */ -function insertText(text, selection, textToInsert) { +function insertText(text: string, selection: Selection, textToInsert: string): string { return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length); } /** * Check whether we can skip trigger hotkeys on some specific devices. - * @param {Boolean} isSmallScreenWidth - * @param {Boolean} isKeyboardShown - * @returns {Boolean} */ -function canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown) { +function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boolean): boolean { // Do not trigger actions for mobileWeb or native clients that have the keyboard open // because for those devices, we want the return key to insert newlines rather than submit the form return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown; @@ -30,11 +28,9 @@ function canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown) { * The common suffix is the number of characters shared by both strings * at the end (suffix) until a mismatch is encountered. * - * @param {string} str1 - * @param {string} str2 - * @returns {number} The length of the common suffix between the strings. + * @returns The length of the common suffix between the strings. */ -function getCommonSuffixLength(str1, str2) { +function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; diff --git a/src/libs/ComposerUtils/types.ts b/src/libs/ComposerUtils/types.ts new file mode 100644 index 000000000000..a417d951ff51 --- /dev/null +++ b/src/libs/ComposerUtils/types.ts @@ -0,0 +1,6 @@ +type ComposerProps = { + isFullComposerAvailable: boolean; + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; +}; + +export default ComposerProps; diff --git a/src/libs/ComposerUtils/updateIsFullComposerAvailable.js b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts similarity index 66% rename from src/libs/ComposerUtils/updateIsFullComposerAvailable.js rename to src/libs/ComposerUtils/updateIsFullComposerAvailable.ts index 00b12d1742e3..5d73619482db 100644 --- a/src/libs/ComposerUtils/updateIsFullComposerAvailable.js +++ b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts @@ -1,11 +1,11 @@ import CONST from '../../CONST'; +import ComposerProps from './types'; /** * Update isFullComposerAvailable if needed - * @param {Object} props - * @param {Number} numberOfLines The number of lines in the text input + * @param numberOfLines The number of lines in the text input */ -function updateIsFullComposerAvailable(props, numberOfLines) { +function updateIsFullComposerAvailable(props: ComposerProps, numberOfLines: number) { const isFullComposerAvailable = numberOfLines >= CONST.COMPOSER.FULL_COMPOSER_MIN_LINES; if (isFullComposerAvailable !== props.isFullComposerAvailable) { props.setIsFullComposerAvailable(isFullComposerAvailable); diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.js b/src/libs/ComposerUtils/updateNumberOfLines/index.js deleted file mode 100644 index ff8b4c56321a..000000000000 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.js b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts similarity index 77% rename from src/libs/ComposerUtils/updateNumberOfLines/index.native.js rename to src/libs/ComposerUtils/updateNumberOfLines/index.native.ts index 5a13ae670d81..b22135b4f767 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.js +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts @@ -1,23 +1,21 @@ -import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; import updateIsFullComposerAvailable from '../updateIsFullComposerAvailable'; import getNumberOfLines from '../getNumberOfLines'; +import UpdateNumberOfLines from './types'; /** * Check the current scrollHeight of the textarea (minus any padding) and * divide by line height to get the total number of rows for the textarea. - * @param {Object} props - * @param {Event} e */ -function updateNumberOfLines(props, e) { +const updateNumberOfLines: UpdateNumberOfLines = (props, event) => { const lineHeight = styles.textInputCompose.lineHeight; const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2; - const inputHeight = lodashGet(e, 'nativeEvent.contentSize.height', null); + const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; if (!inputHeight) { return; } const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); updateIsFullComposerAvailable(props, numberOfLines); -} +}; export default updateNumberOfLines; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.ts new file mode 100644 index 000000000000..91a9c9c0f102 --- /dev/null +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.ts @@ -0,0 +1,5 @@ +import UpdateNumberOfLines from './types'; + +const updateNumberOfLines: UpdateNumberOfLines = () => {}; + +export default updateNumberOfLines; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/types.ts b/src/libs/ComposerUtils/updateNumberOfLines/types.ts new file mode 100644 index 000000000000..c0650be25433 --- /dev/null +++ b/src/libs/ComposerUtils/updateNumberOfLines/types.ts @@ -0,0 +1,6 @@ +import {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native'; +import ComposerProps from '../types'; + +type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent) => void; + +export default UpdateNumberOfLines; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 80b15690ac46..b94c240b6e92 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -314,8 +314,8 @@ function isReservedRoomName(roomName: string): boolean { /** * Checks if the room name already exists. */ -function isExistingRoomName(roomName: string, reports: Report[], policyID: string): boolean { - return reports.some((report) => report && report.policyID === policyID && report.reportName === roomName); +function isExistingRoomName(roomName: string, reports: Record, policyID: string): boolean { + return Object.values(reports).some((report) => report && report.policyID === policyID && report.reportName === roomName); } /** diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts index ac94e6f335e3..1707bebd6cb2 100644 --- a/src/libs/actions/QueuedOnyxUpdates.ts +++ b/src/libs/actions/QueuedOnyxUpdates.ts @@ -1,27 +1,21 @@ import Onyx, {OnyxUpdate} from 'react-native-onyx'; -import ONYXKEYS from '../../ONYXKEYS'; // In this file we manage a queue of Onyx updates while the SequentialQueue is processing. There are functions to get the updates and clear the queue after saving the updates in Onyx. let queuedOnyxUpdates: OnyxUpdate[] = []; -Onyx.connect({ - key: ONYXKEYS.QUEUED_ONYX_UPDATES, - callback: (val) => (queuedOnyxUpdates = val ?? []), -}); /** * @param updates Onyx updates to queue for later */ function queueOnyxUpdates(updates: OnyxUpdate[]): Promise { - return Onyx.set(ONYXKEYS.QUEUED_ONYX_UPDATES, [...queuedOnyxUpdates, ...updates]); -} - -function clear() { - Onyx.set(ONYXKEYS.QUEUED_ONYX_UPDATES, null); + queuedOnyxUpdates = queuedOnyxUpdates.concat(updates); + return Promise.resolve(); } function flushQueue(): Promise { - return Onyx.update(queuedOnyxUpdates).then(clear); + return Onyx.update(queuedOnyxUpdates).then(() => { + queuedOnyxUpdates = []; + }); } export {queueOnyxUpdates, flushQueue}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 66008ae5ae2a..e21d9fdd75c6 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1683,7 +1683,7 @@ function showReportActionNotification(reportID, reportAction) { const notificationParams = { report, reportAction, - onClick: () => Navigation.navigate(ROUTES.getReportRoute(reportID)), + onClick: () => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)), }; if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { LocalNotification.showModifiedExpenseNotification(notificationParams); diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 91267b9b1053..971289e5b9e4 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -825,6 +825,15 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, + value: { + [parentReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, ]; const failureData = [ @@ -843,6 +852,15 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum [optimisticReportActionID]: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, + value: { + [parentReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, ]; API.write('CancelTask', {cancelledTaskReportActionID: optimisticReportActionID, taskReportID}, {optimisticData, successData, failureData}); diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js index cee2b8877ef6..e508d096128d 100644 --- a/src/libs/fileDownload/FileUtils.js +++ b/src/libs/fileDownload/FileUtils.js @@ -157,7 +157,7 @@ const readFileAsync = (path, fileName) => return res.blob(); }) .then((blob) => { - const file = new File([blob], cleanFileName(fileName)); + const file = new File([blob], cleanFileName(fileName), {type: blob.type}); file.source = path; // For some reason, the File object on iOS does not have a uri property // so images aren't uploaded correctly to the backend diff --git a/src/pages/EditRequestReceiptPage.js b/src/pages/EditRequestReceiptPage.js index f45085a052e9..c5dd69624159 100644 --- a/src/pages/EditRequestReceiptPage.js +++ b/src/pages/EditRequestReceiptPage.js @@ -37,11 +37,11 @@ function EditRequestReceiptPage({route, transactionID}) { shouldEnableMaxHeight testID={EditRequestReceiptPage.displayName} > - + { + if (isOffline) { return; } Wallet.openEnablePaymentsPage(); - } + }, [isOffline]); - render() { - if (_.isEmpty(this.props.userWallet)) { - return ; - } - - return ( - - {() => { - if (this.props.userWallet.errorCode === CONST.WALLET.ERROR.KYC) { - return ( - <> - Navigation.goBack(ROUTES.SETTINGS_WALLET)} - /> - - - ); - } - - if (this.props.userWallet.shouldShowWalletActivationSuccess) { - return ; - } - - const currentStep = this.props.userWallet.currentStep || CONST.WALLET.STEP.ADDITIONAL_DETAILS; + if (_.isEmpty(userWallet)) { + return ; + } + return ( + + {() => { + if (userWallet.errorCode === CONST.WALLET.ERROR.KYC) { return ( <> - {(currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS || currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS_KBA) && } - {currentStep === CONST.WALLET.STEP.ONFIDO && } - {currentStep === CONST.WALLET.STEP.TERMS && } - {currentStep === CONST.WALLET.STEP.ACTIVATE && } + Navigation.goBack(ROUTES.SETTINGS_WALLET)} + /> + ); - }} - - ); - } + } + + if (userWallet.shouldShowWalletActivationSuccess) { + return ; + } + + const currentStep = userWallet.currentStep || CONST.WALLET.STEP.ADDITIONAL_DETAILS; + + switch (currentStep) { + case CONST.WALLET.STEP.ADDITIONAL_DETAILS: + case CONST.WALLET.STEP.ADDITIONAL_DETAILS_KBA: + return ; + case CONST.WALLET.STEP.ONFIDO: + return ; + case CONST.WALLET.STEP.TERMS: + return ; + case CONST.WALLET.STEP.ACTIVATE: + return ; + default: + return null; + } + }} + + ); } +EnablePaymentsPage.displayName = 'EnablePaymentsPage'; EnablePaymentsPage.propTypes = propTypes; EnablePaymentsPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - userWallet: { - key: ONYXKEYS.USER_WALLET, +export default withOnyx({ + userWallet: { + key: ONYXKEYS.USER_WALLET, - // We want to refresh the wallet each time the user attempts to activate the wallet so we won't use the - // stored values here. - initWithStoredValues: false, - }, - }), - withNetwork(), -)(EnablePaymentsPage); + // We want to refresh the wallet each time the user attempts to activate the wallet so we won't use the + // stored values here. + initWithStoredValues: false, + }, +})(EnablePaymentsPage); diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index a6f685fcb562..1b693add95b7 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -5,11 +5,26 @@ import Text from '../../../components/Text'; import * as Localize from '../../../libs/Localize'; import CONST from '../../../CONST'; import TextLink from '../../../components/TextLink'; +import userWalletPropTypes from '../userWalletPropTypes'; -function ShortTermsForm() { +const propTypes = { + /** The user's wallet */ + userWallet: userWalletPropTypes, +}; + +const defaultProps = { + userWallet: {}, +}; + +function ShortTermsForm(props) { return ( <> - {Localize.translateLocal('termsStep.shortTermsForm.expensifyPaymentsAccount')} + + {Localize.translateLocal('termsStep.shortTermsForm.expensifyPaymentsAccount', { + walletProgram: + props.userWallet.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, + })} + @@ -132,6 +147,8 @@ function ShortTermsForm() { ); } +ShortTermsForm.propTypes = propTypes; +ShortTermsForm.defaultProps = defaultProps; ShortTermsForm.displayName = 'ShortTermsForm'; export default ShortTermsForm; diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.js index e96d93dc4de9..39f4826ec0b2 100644 --- a/src/pages/EnablePayments/TermsStep.js +++ b/src/pages/EnablePayments/TermsStep.js @@ -15,8 +15,12 @@ import LongTermsForm from './TermsPage/LongTermsForm'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import walletTermsPropTypes from './walletTermsPropTypes'; import * as ErrorUtils from '../../libs/ErrorUtils'; +import userWalletPropTypes from './userWalletPropTypes'; const propTypes = { + /** The user's wallet */ + userWallet: userWalletPropTypes, + /** Comes from Onyx. Information about the terms for the wallet */ walletTerms: walletTermsPropTypes, @@ -24,6 +28,7 @@ const propTypes = { }; const defaultProps = { + userWallet: {}, walletTerms: {}, }; @@ -59,7 +64,7 @@ function TermsStep(props) { style={styles.flex1} contentContainerStyle={styles.ph5} > - + { - reportIDRef.current = '0'; - reportActionRef.current = {}; callbackWhenDeleteModalHide.current(); }} prompt={translate('reportActionContextMenu.deleteConfirmation', {action: reportAction})} diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index fe1dcf248f90..d109c972ec69 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -606,12 +606,7 @@ function ReportActionItem(props) { draftMessage={props.draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} /> - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} pendingAction={props.draftMessage ? null : props.action.pendingAction} diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index 1f61b44841bc..24501e307759 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -1,5 +1,4 @@ import React, {memo} from 'react'; -import {ActivityIndicator, View} from 'react-native'; import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; import reportActionFragmentPropTypes from './reportActionFragmentPropTypes'; @@ -27,9 +26,6 @@ const propTypes = { /** The message fragment needing to be displayed */ fragment: reportActionFragmentPropTypes.isRequired, - /** Is this fragment an attachment? */ - isAttachment: PropTypes.bool, - /** If this fragment is attachment than has info? */ attachmentInfo: PropTypes.shape({ /** The file name of attachment */ @@ -48,9 +44,6 @@ const propTypes = { /** Message(text) of an IOU report action */ iouMessage: PropTypes.string, - /** Does this fragment belong to a reportAction that has not yet loaded? */ - loading: PropTypes.bool, - /** The reportAction's source */ source: PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']), @@ -76,7 +69,6 @@ const propTypes = { }; const defaultProps = { - isAttachment: false, attachmentInfo: { name: '', size: 0, @@ -84,7 +76,6 @@ const defaultProps = { source: '', }, iouMessage: '', - loading: false, isSingleLine: false, source: '', style: [], @@ -96,20 +87,6 @@ const defaultProps = { function ReportActionItemFragment(props) { switch (props.fragment.type) { case 'COMMENT': { - // If this is an attachment placeholder, return the placeholder component - if (props.isAttachment && props.loading) { - return Str.isImage(props.attachmentInfo.name) ? ( - `} /> - ) : ( - - - - ); - } const {html, text} = props.fragment; const isPendingDelete = props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && props.network.isOffline; diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index bc92889158d0..a3d8494c38de 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -54,14 +54,12 @@ function ReportActionItemMessage(props) { )) diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index ca0467143e98..162f28021b94 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -241,8 +241,6 @@ function ReportActionItemSingle(props) { key={`person-${props.action.reportActionID}-${index}`} accountID={actorAccountID} fragment={fragment} - isAttachment={props.action.isAttachment} - isLoading={props.action.isLoading} delegateAccountID={props.action.delegateAccountID} isSingleLine actorIcon={icon} diff --git a/src/pages/home/report/reportActionPropTypes.js b/src/pages/home/report/reportActionPropTypes.js index e0c3aebe718c..4d4809cd781f 100644 --- a/src/pages/home/report/reportActionPropTypes.js +++ b/src/pages/home/report/reportActionPropTypes.js @@ -23,9 +23,6 @@ export default { IOUTransactionID: PropTypes.string, }), - /** Whether we have received a response back from the server */ - isLoading: PropTypes.bool, - /** Error message that's come back from the server. */ error: PropTypes.string, diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index 4de4e9bb9148..128782093718 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -99,7 +99,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator}) const appState = useRef(AppState.currentState); const iouType = lodashGet(route, 'params.iouType', ''); - const reportID = lodashGet(route, 'params.reportID', ''); const pageIndex = lodashGet(route, 'params.pageIndex', 1); const {translate} = useLocalize(); @@ -223,13 +222,13 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator}) return; } - IOU.navigateToNextPage(iou, iouType, reportID, report, route.path); + IOU.navigateToNextPage(iou, iouType, report, route.path); }) .catch((error) => { showCameraAlert(); Log.warn('Error taking photo', error); }); - }, [flash, iouType, iou, report, reportID, translate, transactionID, route.path]); + }, [flash, iouType, iou, report, translate, transactionID, route.path]); CameraPermission.getCameraPermissionStatus().then((permissionStatus) => { setPermissions(permissionStatus); diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index c4fc29957179..cc918e3ee3df 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -109,7 +109,7 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu if (!currency || !_.isNumber(amount)) { return; } - const amountAsStringForState = CurrencyUtils.convertToFrontendAmount(amount).toString(); + const amountAsStringForState = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : ''; setCurrentAmount(amountAsStringForState); setSelection({ start: amountAsStringForState.length, diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 170ee042bffa..05b206ce4147 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -143,7 +143,10 @@ function MoneyRequestParticipantsSelector({ if (newChatOptions.userToInvite && !OptionsListUtils.isCurrentUser(newChatOptions.userToInvite)) { newSections.push({ undefined, - data: [newChatOptions.userToInvite], + data: _.map([newChatOptions.userToInvite], (participant) => { + const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + }), shouldShow: true, indexOffset, }); @@ -201,30 +204,8 @@ function MoneyRequestParticipantsSelector({ } onAddParticipants(newSelectedOptions); - - const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - isOptionInList ? searchTerm : '', - newSelectedOptions, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, - - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - !isDistanceRequest, - ); - - setNewChatOptions({ - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }); }, - [participants, onAddParticipants, reports, personalDetails, betas, searchTerm, iouType, isDistanceRequest], + [participants, onAddParticipants], ); const headerMessage = OptionsListUtils.getHeaderMessage( diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index b8c817350a38..7e8baba5a9ce 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -25,6 +25,7 @@ import ValidateCodeForm from './ValidateCodeForm'; import ROUTES from '../../../../ROUTES'; import FullscreenLoadingIndicator from '../../../../components/FullscreenLoadingIndicator'; import FullPageNotFoundView from '../../../../components/BlockingViews/FullPageNotFoundView'; +import CONST from '../../../../CONST'; const propTypes = { /* Onyx Props */ @@ -131,7 +132,22 @@ class ContactMethodDetailsPage extends Component { * @returns {string} */ getContactMethod() { - return decodeURIComponent(lodashGet(this.props.route, 'params.contactMethod')); + const contactMethod = lodashGet(this.props.route, 'params.contactMethod'); + + // We find the number of times the url is encoded based on the last % sign and remove them. + const lastPercentIndex = contactMethod.lastIndexOf('%'); + const encodePercents = contactMethod.substring(lastPercentIndex).match(new RegExp('25', 'g')); + let numberEncodePercents = encodePercents ? encodePercents.length : 0; + const beforeAtSign = contactMethod.substring(0, lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, (match) => { + if (numberEncodePercents > 0) { + numberEncodePercents--; + return '%'; + } + return match; + }); + const afterAtSign = contactMethod.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%'); + + return decodeURIComponent(beforeAtSign + afterAtSign); } /** @@ -230,6 +246,7 @@ class ContactMethodDetailsPage extends Component { const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID; const hasMagicCodeBeenSent = lodashGet(this.props.loginList, [contactMethod, 'validateCodeSent'], false); const isFailedAddContactMethod = Boolean(lodashGet(loginData, 'errorFields.addedLogin')); + const isFailedRemovedContactMethod = Boolean(lodashGet(loginData, 'errorFields.deletedLogin')); return ( {isFailedAddContactMethod && ( @@ -289,9 +306,9 @@ class ContactMethodDetailsPage extends Component { {isDefaultContactMethod ? ( User.clearContactMethodErrors(contactMethod, 'defaultLogin')} + onClose={() => User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')} > {this.props.translate('contacts.yourDefaultContactMethod')} diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 190f18f8d969..0832b6a3978c 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -577,7 +577,7 @@ function getEmojiPickerStyle(isSmallScreenWidth: boolean): ViewStyle | CSSProper /** * Generate the styles for the ReportActionItem wrapper view. */ -function getReportActionItemStyle(isHovered = false, isLoading = false): ViewStyle | CSSProperties { +function getReportActionItemStyle(isHovered = false): ViewStyle | CSSProperties { // TODO: Remove this "eslint-disable-next" once the theme switching migration is done and styles are fully typed (GH Issue: https://github.com/Expensify/App/issues/27337) // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { @@ -587,7 +587,7 @@ function getReportActionItemStyle(isHovered = false, isLoading = false): ViewSty ? themeColors.hoverComponentBG : // Warning: Setting this to a non-transparent color will cause unread indicator to break on Android themeColors.transparent, - opacity: isLoading ? 0.5 : 1, + opacity: 1, ...styles.cursorInitial, }; } @@ -678,10 +678,10 @@ function extractValuesFromRGB(color: string): number[] | null { * @returns The theme color as an RGB value. */ function getThemeBackgroundColor(bgColor: string = themeColors.appBG): string { - const backdropOpacity = variables.modalFullscreenBackdropOpacity; + const backdropOpacity = variables.overlayOpacity; const [backgroundRed, backgroundGreen, backgroundBlue] = extractValuesFromRGB(bgColor) ?? hexadecimalToRGBArray(bgColor) ?? []; - const [backdropRed, backdropGreen, backdropBlue] = hexadecimalToRGBArray(themeColors.modalBackdrop) ?? []; + const [backdropRed, backdropGreen, backdropBlue] = hexadecimalToRGBArray(themeColors.overlay) ?? []; const normalizedBackdropRGB = convertRGBToUnitValues(backdropRed, backdropGreen, backdropBlue); const normalizedBackgroundRGB = convertRGBToUnitValues(backgroundRed, backgroundGreen, backgroundBlue); const [red, green, blue] = convertRGBAToRGB(normalizedBackdropRGB, normalizedBackgroundRGB, backdropOpacity); diff --git a/src/styles/styles.js b/src/styles/styles.js index 56868f930735..cef87c531972 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3674,6 +3674,7 @@ const styles = (theme) => ({ borderRadius: variables.componentBorderRadiusLarge, }, userReportStatusEmoji: { + flexShrink: 0, fontSize: variables.fontSizeNormal, marginRight: 4, }, diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js index 75db4be30e2b..ef64c4caec35 100644 --- a/src/styles/themes/default.js +++ b/src/styles/themes/default.js @@ -53,7 +53,6 @@ const darkTheme = { textMutedReversed: colors.darkIcons, textError: colors.red, offline: colors.darkIcons, - modalBackdrop: colors.darkHighlightBackground, modalBackground: colors.darkAppBackground, cardBG: colors.darkHighlightBackground, cardBorder: colors.darkHighlightBackground, diff --git a/src/styles/themes/light.js b/src/styles/themes/light.js index 8bc149c5af08..c459f9f10da6 100644 --- a/src/styles/themes/light.js +++ b/src/styles/themes/light.js @@ -51,7 +51,6 @@ const lightTheme = { textMutedReversed: colors.lightIcons, textError: colors.red, offline: colors.lightIcons, - modalBackdrop: colors.lightHighlightBackground, modalBackground: colors.lightAppBackground, cardBG: colors.lightHighlightBackground, cardBorder: colors.lightHighlightBackground, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 9ee9b64e6467..a7191ce5b002 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -78,7 +78,6 @@ export default { extraSmallMobileResponsiveWidthBreakpoint: 320, extraSmallMobileResponsiveHeightBreakpoint: 667, mobileResponsiveWidthBreakpoint: 800, - modalFullscreenBackdropOpacity: 0.5, tabletResponsiveWidthBreakpoint: 1024, safeInsertPercentage: 0.7, sideBarWidth: 375, diff --git a/src/types/onyx/UserWallet.ts b/src/types/onyx/UserWallet.ts index 8624f16000c9..c6ab5dbf1f67 100644 --- a/src/types/onyx/UserWallet.ts +++ b/src/types/onyx/UserWallet.ts @@ -34,6 +34,9 @@ type UserWallet = { /** The type of the linked account (debitCard or bankAccount) */ walletLinkedAccountType: WalletLinkedAccountType; + /** The wallet's programID, used to show the correct terms. */ + walletProgramID?: string; + /** The user's bank account ID */ bankAccountID?: number; diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 43090cf024e2..7cb69b23a578 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {render} from '@testing-library/react-native'; import ComposeProviders from '../../src/components/ComposeProviders'; import OnyxProvider from '../../src/components/OnyxProvider'; -import {LocaleContextProvider} from '../../src/components/withLocalize'; +import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; import SidebarLinksData from '../../src/pages/home/sidebar/SidebarLinksData'; import {EnvironmentProvider} from '../../src/components/withEnvironment'; import CONST from '../../src/CONST';