diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js index c279c3e67a51..a1c2c452273e 100644 --- a/.eslintrc.changed.js +++ b/.eslintrc.changed.js @@ -6,5 +6,24 @@ module.exports = { }, rules: { 'deprecation/deprecation': 'error', + 'rulesdir/no-default-id-values': 'error', }, + overrides: [ + { + files: [ + 'src/libs/ReportUtils.ts', + 'src/libs/actions/IOU.ts', + 'src/libs/actions/Report.ts', + 'src/libs/actions/Task.ts', + 'src/libs/OptionsListUtils.ts', + 'src/libs/ReportActionsUtils.ts', + 'src/libs/TransactionUtils/index.ts', + 'src/pages/home/ReportScreen.tsx', + 'src/pages/workspace/WorkspaceInitialPage.tsx', + ], + rules: { + 'rulesdir/no-default-id-values': 'off', + }, + }, + ], }; diff --git a/.gitmodules b/.gitmodules index 7b3a3d9f9432..59abf2448f1d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "Mobile-Expensify"] path = Mobile-Expensify - url = https://github.com/Expensify/Mobile-Expensify.git + url = git@github.com:Expensify/Mobile-Expensify.git diff --git a/.prettierignore b/.prettierignore index b428978a1563..8584ae14b917 100644 --- a/.prettierignore +++ b/.prettierignore @@ -22,3 +22,6 @@ src/libs/E2E/reactNativeLaunchingTest.ts # Automatically generated files src/libs/SearchParser/searchParser.js src/libs/SearchParser/autocompleteParser.js + +# Disable prettier in the submodule +Mobile-Expensify diff --git a/Mobile-Expensify b/Mobile-Expensify index d81135521e25..f370c5f31cdf 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit d81135521e25add10bfebebf83fa5ec4a67cca13 +Subproject commit f370c5f31cdfd750b0d42d75a471a9b8d30935ad diff --git a/README.md b/README.md index 8f7161b7fe96..455f2f61197d 100644 --- a/README.md +++ b/README.md @@ -456,12 +456,12 @@ You can only build HybridApp if you have been granted access to [`Mobile-Expensi ## Getting started with HybridApp 1. If you haven't, please follow [these instructions](https://github.com/Expensify/App?tab=readme-ov-file#getting-started) to setup the NewDot local environment. -2. Run `git submodule update --init --progress` to download the `Mobile-Expensify` sourcecode. -- If you have access to `Mobile-Expensify` and the command fails with a https-related error add this to your `~/.gitconfig` file: +2. Run `git submodule update --init --progress --depth 100` to download the `Mobile-Expensify` sourcecode. +- If you have access to `Mobile-Expensify` and the command fails, add this to your `~/.gitconfig` file: ``` - [url "ssh://git@github.com/"] - insteadOf = https://github.com/ + [url "https://github.com/"] + insteadOf = ssh://git@github.com/ ``` At this point, the default behavior of some `npm` scripts will change to target HybridApp: @@ -472,7 +472,7 @@ At this point, the default behavior of some `npm` scripts will change to target - `npm run pod-install` - install pods for HybridApp - `npm run clean` - clean native code of HybridApp -If for some reason, you need to target the standalone NewDot application, you can append `*-standalone` to each of these scripts (eg. `npm run ios-standalone` will build NewDot instead of HybridApp). +If for some reason, you need to target the standalone NewDot application, you can append `*-standalone` to each of these scripts (eg. `npm run ios-standalone` will build NewDot instead of HybridApp). The same concept applies to the installation of standalone NewDot node modules. To skip the installation of HybridApp-specific patches and node modules, use `npm run i-standalone` or `npm run install-standalone`. ## Working with HybridApp Day-to-day work with HybridApp shouldn't differ much from the work on the standalone NewDot repo. @@ -500,7 +500,7 @@ It's important to emphasise that a git submodule is just a **regular git reposit > #### For external contributors > > If you'd like to modify the `Mobile-Expensify` source code, it is best that you create your own fork. Then, you can swap origin of the remote repository by executing this command: -> +> > `cd Mobile-Expensify && git remote set-url origin ` > > This way, you'll attach the submodule to your fork repository. diff --git a/android/app/build.gradle b/android/app/build.gradle index 71b9f0481067..4c917b995331 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007606 - versionName "9.0.76-6" + versionCode 1009007706 + versionName "9.0.77-6" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/companyCards/card-bofa.svg b/assets/images/companyCards/card-bofa.svg index 3cc7cf1de2cc..c58229f1b242 100644 --- a/assets/images/companyCards/card-bofa.svg +++ b/assets/images/companyCards/card-bofa.svg @@ -1 +1,27 @@ - \ No newline at end of file + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/card-capitalone.svg b/assets/images/companyCards/card-capitalone.svg index a7c54c7bf529..9f1402298683 100644 --- a/assets/images/companyCards/card-capitalone.svg +++ b/assets/images/companyCards/card-capitalone.svg @@ -1 +1,23 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-bofa-large.svg b/assets/images/companyCards/large/card-bofa-large.svg index a842bc93d80b..c83e06ffb65d 100644 --- a/assets/images/companyCards/large/card-bofa-large.svg +++ b/assets/images/companyCards/large/card-bofa-large.svg @@ -1,6 +1,6 @@ - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-capital_one-large.svg b/assets/images/companyCards/large/card-capital_one-large.svg index b71e209a4c11..20f3bd442d9e 100644 --- a/assets/images/companyCards/large/card-capital_one-large.svg +++ b/assets/images/companyCards/large/card-capital_one-large.svg @@ -1,15 +1,15 @@ - + - - + + \ No newline at end of file diff --git a/assets/images/train.svg b/assets/images/train.svg new file mode 100644 index 000000000000..40d8c9d1af8a --- /dev/null +++ b/assets/images/train.svg @@ -0,0 +1,3 @@ + + + diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index c60670c72324..ac086d3a9bed 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -175,7 +175,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. { - test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'), + test: new RegExp('node_modules/pdfjs-dist/build/pdf.worker.min.mjs'), type: 'asset/source', }, diff --git a/docs/articles/expensify-classic/connections/Expensify-API.md b/docs/articles/expensify-classic/connections/Expensify-API.md new file mode 100644 index 000000000000..e2fbdbfd7703 --- /dev/null +++ b/docs/articles/expensify-classic/connections/Expensify-API.md @@ -0,0 +1,231 @@ +--- +title: Expensify API +description: User-sourced tips and tricks for using Expensify’s API. +--- +# Overview +An API (Application Programming Interface) allows two programs to communicate with each other. Expensify's API connects with various software platforms like NetSuite or Xero, and it can also link to other systems that don’t have a pre-made connection, such as [Workday](https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Workday). + +{% include info.html %} +To begin, review our [Integration Server Manual](https://integrations.expensify.com/Integration-Server/doc/#introduction) thoroughly, as it will be your primary resource. The Expensify API is a self-serve tool, and your internal team is responsible for setting it up and ensuring it meets your needs. We can assist with basic troubleshooting, but the level of support may vary based on the support agent or account manager. It’s important for your team to be familiar with the setup process. +{% include end-info.html %} + +We've compiled answers to some frequently asked questions to help you get started. + +## Should I give your support team my API credentials when I need help? + +If you’re seeking help with Expensify's API, do not share your partnerUserSecret. If you do, immediately rotate your credentials on [this page](https://www.expensify.com/tools/integrations/). + +## Is there a rate limit? + +To keep our platform stable and handle high traffic, Expensify limits how many API requests you can send: +- Up to 5 requests every 10 seconds +- Up to 20 requests every 60 seconds + +Sending more requests than allowed may result in an error with status code `429`. + +## What is a Policy ID? + +This is also known as a Workspace ID. To find your Policy/Workspace ID, +Hover over Settings and click Workspaces. +Click the name of the Workspace. +Copy the ID number from the URL. For example, if the URL is https://www.expensify.com/policy?param={"policyID":"0810E551A5F2A9C2”}, then your workspace ID is 0810E551A5F2A9C2. + +## Can I use the parent type `file` to export workspace/policy data? + +No. The parent type `file` can only be used to export expense and report data — not policy information. To export policy data (e.g., categories, tags), you must use the `get` type with `inputSettings.type` set to `policy`. + +## Can I use the API to create Domain Groups? + +No, you cannot create domain groups. You can only assign users to them. + +## I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this? + +Try prepending a non-numeric character like a quote to force Excel to interpret the value as a string and not a number (i.e., `'${expense.transactionID}`). + +## How can we export the person who will approve a report while the reports are still processing? + +Use the field ${report.managerEmail}. + +## Why won’t my boolean field return any data? + +Boolean fields won't output values without a string. For example, instead of using `${expense.billable}`, use `${expense.billable?string("Yes", "No")}`. This will display "Yes" if the expense is billable and "No" if it is not. + +## Can I export the reports for just one user? + +Not in a quick convenient way, as you would need to include the user in your template. The simplest approach is to export data for all users and then apply a filter in your preferred spreadsheet program. + +## Can I create expenses on behalf of users? + +Yes. However, to access the Expense Creator API on behalf of employees, Expensify needs to verify the following setup: + +Ensure you are properly configured (e.g., Domain Control, Domain Admin, Policy Admin). +Verify you have internal authorization to add data to other accounts within your domain. + +If you need this access, contact concierge@expensify.com and reference this help page. + +# Using Postman + +Many customers use Postman to help them build out their APIs. Below are some guides contributed by our customers. Please note, in all cases, you will need to first generate your authentication credentials, the steps for which can be found [here](https://integrations.expensify.com/Integration-Server/doc/#introduction) and have them ready: + +## Download expenses from a report as a CSV file + +**Step 1: Get the ID of a report you want to export in Expensify** + +Find the ID by opening the expense report and clicking Details at the top right corner of the page. At the top of the menu, the ID is provided as the “Long ID.” + +**Step 2: Export (generate) a "Report" as a CSV file** +{% include info.html %} +For this you'll use the Documentation under [Report Exporter](https://integrations.expensify.com/Integration-Server/doc/#export). +{% include end-info.html %} + +In Postman, set the following: + +- HTTP Action: POST +- URL: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +- Your only Parameters ("Params") will be "requestJobDescription", described below +- Body: "x-www-form-encoded", with a key "template", described below + +The requestJobDescription key will have a value like below: + +``` +{ + "type": "file", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "onReceive": { + "immediateResponse": [ + "returnRandomFileName" + ] + }, + "inputSettings": { + "type": "combinedReportData", + "filters": { + "reportIDList": "50352738" + } + }, + "outputSettings": { + "fileExtension": "csv" + } +} +``` +Take the above and replace it with your own partnerUserID, partnerUserSecret, and reportIDList. To download multiple reports, you can use a comma-separated list as the reportIDList, such as "12345,45678,11111". + +The template key will have the value like below: + +``` +<#if addHeader> + Merchant,Amount,Transaction Date<#lt> + +<#list reports as report> + <#list report.transactionList as expense> + <#if expense.modifiedMerchant?has_content> + <#assign merchant = expense.modifiedMerchant> + <#else> + <#assign merchant = expense.merchant> + + <#if expense.convertedAmount?has_content> + <#assign amount = expense.convertedAmount/100> + <#elseif expense.modifiedAmount?has_content> + <#assign amount = expense.modifiedAmount/100> + <#else> + <#assign amount = expense.amount/100> + + <#if expense.modifiedCreated?has_content> + <#assign created = expense.modifiedCreated> + <#else> + <#assign created = expense.created> + + ${merchant},<#t> + ${amount},<#t> + ${created}<#lt> + + +``` + +The template variable determines what information is saved in your CSV file. If you want more columns than merchant, amount, and transaction date, follow the syntax as defined in the export template format documentation. + +**Step 3: Save your generated file name** + +Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 2, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one. + +**Step 4: Download your exported report** + +Set up another API call in almost the same way you did before. You don't need the template key in the Body anymore, so delete that and set the Body type to "none". Then modify your requestJobDescription to read like below, but with your own credentials and file name: + +``` +{ + "type": "download", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "fileName": "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv", + "fileSystem": "integrationServer" +} +``` + +Click Go and you should see the CSV in the response body. + +*Thank you to our customer Frederico Pettinella who originally wrote and shared this guide.* + +## Use Advanced Employee Updater API with Postman + +1. Create a new request. +2. Select POST as the method. +3. Copy-paste this to the URL section: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +4. Do not add anything to "Params", "Authorization", or "Header". Go straight to "Body". +5. Select "x-www-form-urlencoded" and add 2 keys "requestJobDescription" and "data". +6. For "requestJobDescription" copy and paste the following text, and replace the values for "partnerUserID", "partner_UserSecret", and "recipients". Remember that "dry-run"=true means that it's just for testing. Set it to false whenever you are ready to modify that in production. + +``` +{ + "type": "update", + "dry-run" : true, + "credentials": { + "partnerUserID": "aa_api_domain_com", + "partnerUserSecret": "xxx" + }, + "dataSource" : "request", + "inputSettings": { + "type": "employees", + "entity": "generic" + }, + "onFinish":[ + {"actionName": "email", "recipients":"admin1@domain.com"} + ] + }' +For "data" copy-paste the following text and replace values as needed +{ + "Employees":[ + { + "employeeEmail": "user@domain.com", + "managerEmail": "usermanager@domain.com", + "policyID": "1D1BC525C4892584", +"isTerminated": "false", + } +]} +``` + +7. Click SEND. + +This is how it should look on Postman: + +![Image of API credentials request]({{site.url}}/assets/images/ExpensifyHelp-Postman-userID-userSecret-request.png){:width="100%"} + +![Image of API data request]({{site.url}}/assets/images/ExpensifyHelp-Postman-Request-data.png){:width="100%"} + +This is how the value looks inside those keys: + +![Image of API dry run]({{site.url}}/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png){:width="100%"} + +Remember that there are 4 [required fields](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/#api-principles) needed to make this API call to work: + +- employeeEmail +- managerEmail +- employeeID +- policyID + +*Thank you to our customer Raul Hernandez who originally wrote and shared this guide.* + diff --git a/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md index 0c9e6c87f9ab..1eb3f634a61c 100644 --- a/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md +++ b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md @@ -86,7 +86,7 @@ These settings are particularly relevant to billable expenses and can be configu ### Tax -As of September 2023, our Sage Intacct integration supports native VAT and GST tax. To enable this feature, open the Sage Intacct configuration settings in your workspace, go to the Coding tab, and enable Tax. For existing Sage Intacct connectings, simply resync your workspace and the tax toggle will appear. For new Sage Intacct connections, the tax toggle will be available when you complete the integration steps. +As of September 2023, our Sage Intacct integration supports native VAT and GST tax. To enable this feature, open the Sage Intacct configuration settings in your workspace, go to the Coding tab, and enable Tax. For existing Sage Intacct connections, simply resync your workspace and the tax toggle will appear. For new Sage Intacct connections, the tax toggle will be available when you complete the integration steps. Enabling this option will import your native tax rates from Sage Intacct into Expensify. From there, you can select default rates for each category. ### User-Defined Dimensions diff --git a/docs/articles/expensify-classic/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/Apply-Tax.md deleted file mode 100644 index 9360962cb2ba..000000000000 --- a/docs/articles/expensify-classic/expenses/Apply-Tax.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Apply Tax -description: This is article shows you how to apply taxes to your expenses! ---- - - - -# About - -There are two types of tax in Expensify: Simple Tax (i.e. one tax rate) and Complex Tax (i.e. more than one tax rate). This article shows you how to apply both to your expenses! - - -# How-to Apply Tax - -When Tax Tracking is enabled on a Workspace, the default tax rate is selected under **Settings > Workspace > _Workspace Name_ > Tax**, with the default tax rate applied to all expenses automatically. - -There may be multiple tax rates set up within your Workspace, so if the tax on your receipt is different to the default tax that has been applied, you can select the appropriate rate from the tax drop-down on the web expense editor or the mobile app. - -If the tax amount on your receipt is different to the calculated amount or the tax rate doesn’t show up, you can always manually type in the correct tax amount. - - -{% include faq-begin.md %} - -## How do I set up multiple taxes (GST/PST/QST) on indirect connections? -Expenses sometimes have more than one tax applied to them - for example in Canada, expenses can have both a Federal GST and a provincial PST or QST. - -To handle these, you can create a single tax that combines both taxes into a single effective tax rate. For example, if you have a GST of 5% and PST of 7%, adding the two tax rates together gives you an effective tax rate of 12%. - -From the Reports page, you can select Reports and then click **Export To > Tax Report** to generate a CSV containing all the expense information, including the split-out taxes. - -## Why is the tax amount different than I expect? - -In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. - -To determine the inclusive tax from a total price that already includes tax, you can use the following formula: - -### **Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** - -For example, if an item costs $100 and the tax rate is 20%: -Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** -This means the tax amount $16.67 is included in the total. - -If you are simply trying to calculate the price before tax, you can use the formula: - -### **Price before tax = (Total price) ÷ (1 + Tax rate)** - -# Deep Dive - -If you have a receipt that has more than one tax rate (i.e. Complex Tax) on it, then there are two options for handling this in Expensify! - -Many tax authorities do not require the reporting of tax amounts by rate and the easiest approach is to apply the highest rate on the receipt and then modify the tax amount to reflect the amount shown on the receipt if this is less. Please check with your local tax advisor if this approach will be allowed. - -Alternatively, you can apply each specific tax rate by splitting the expense into the components that each rate will be applied to. To do this, click on **Split Expense** and apply the correct tax rate to each part. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md deleted file mode 100644 index c47e5ed51f32..000000000000 --- a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Tax -description: How to track expense taxes ---- -# Overview -Expensify’s tax tracking feature allows you to: -- Add tax names, rates, and codes whether you’re connected to an accounting system or not. -- Enable/disable taxes you’d like to make available to users. -- Set a default tax for Workspace currency expenses and, optionally, another default tax (including exempt) for foreign currency expenses which - will automatically apply to all new expenses. - -# How to Enable Tax Tracking -Tax tracking can be enabled in the Tax section of the Workspace settings of any Workspace, whether group or individual. -## If Connected to an Accounting Integration -If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Workspaces > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. -## Not Connected to an Accounting Integration -If your Workspace is not connected to an accounting system, go to Settings > Workspaces > Group > [Workspace Name] > Tax to enable tax. - -# Tracking Tax by Expense Category -To set a different tax rate for a specific expense type in the Workspace currency, go to Settings > Workspaces > Group > [Workspace Name] > Categories page. Click "Edit Rules" next to the desired category and set the "Category default tax". This will be applied to new expenses, overriding the default Workspace currency tax rate. diff --git a/docs/articles/expensify-classic/workspaces/Track-Taxes.md b/docs/articles/expensify-classic/workspaces/Track-Taxes.md new file mode 100644 index 000000000000..c75058dc8447 --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Track-Taxes.md @@ -0,0 +1,76 @@ +--- +title: Track Taxes +description: How to track taxes and apply them to expenses +--- +Expensify's tax tracking allows you to create tax rates and codes for domestic and foreign currencies, and even for different expense categories. Once you've enabled tax tracking, your default tax rate is automatically applied to all expenses. + +# Tax Tracking - Connected to an accounting integration + +If your Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, you can run through the following steps to set up tax tracking: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Connections** tab on the left. +4. Click **Configure**. +5. Click **Sync Connection**. + +Your tax rates will be imported from the accounting system and indicated by its logo. + +# Tax Tracking - Not connected to an accounting integration + +If your Workspace is not connected to an accounting system, you can run through the following steps to set up tax tracking: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Tax** tab on the left. +4. Enable the toggle to allow taxes to be added to expenses. +5. You can modify the existing tax rate, or you can click New Option to add a new tax rate. For each tax rate, you can enable/disable them individually, add a specific name for the rate, add a percent value, and (if desired) add a unique tax code. +6. Once you have your tax codes added, go to the top of the screen to enter the name that taxes will appear as on expenses. You'll also select which of your tax rates you will use as your defaults for expenses submitted under your workspace currency and foreign currency. + +## Track tax by expense category + +You can also set tax rates for specific expense categories: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Categories** tab on the left. +4. Click **Edit** next to the desired category. +5. Click the Default Tax dropdown and select the desired tax rate. + +This rate will be applied to all new expenses under this category, overriding the workspace's default currency tax rate. + +{% include faq-begin.md %} + +## How do I set up multiple taxes (GST/PST/QST) for indirect connections? + +Expenses sometimes have more than one tax applied to them (for example in Canada, expenses can have both a Federal GST and a provincial PST or QST). + +To handle multiple tax rates, you can create a new tax rate that combines both into a single rate. For example, if you have a GST of 5% and PST of 7%, you can add them together and create a new tax rate of 12%. + +From the Reports page, you can generate a CSV containing all the expense information, including the split-out taxes, by going to the Reports tab, clicking **Export To**, and selecting **Tax Report**. + +## How do I handle the taxes for a receipt that includes more than one tax rate? + +If your receipt includes more than one tax rate, there are two ways you can handle the tax rate: + +- Many tax authorities do not require the reporting of tax amounts by rate; therefore, you can apply the highest rate on the receipt and then modify the tax amount on the receipt if necessary. Please check with your tax advisor to determine if this approach is appropriate for you. +- Alternatively, you can apply each specific tax rate by splitting the expense by the applicable expenses for each rate. To do this, open the expense and click **Split Expense**. Then apply the correct tax rate to each. + +## What if my workspace has multiple tax rates? + +You'll have the option to change the tax rate from within the expense as needed. + +## What should I do if the tax amount for my expense does not show up, or is it showing as a different amount than what I expected? + +In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. If the tax amount doesn't show up on your receipt or is different than the calculated amount, you can manually type in the correct tax amount. + +To determine the inclusive tax from a total price that already includes tax, you can use the following formula: + +**Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** + +For example, if an item costs $100 and the tax rate is 20%: +Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** +This means the tax amount of $16.67 is included in the total. + +If you are simply trying to calculate the price before tax, you can use the formula: + +**Price before tax = (Total price) ÷ (1 + Tax rate)** + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index ea058df9c1b1..1b1702c6fcc7 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -51,6 +51,11 @@ When an expense is submitted to a workspace, your approver will receive an email {% include end-selector.html %} +![Click Global Create]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-1.png){:width="100%"} +![Click Submit expense]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-2.png){:width="100%"} +![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-3.png){:width="100%"} +![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-4.png){:width="100%"} + {% include info.html %} You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. {% include end-info.html %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md index 6f7292245f00..8593ab65205b 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md @@ -15,7 +15,7 @@ The first step to exporting and downloading expenses is finding the data you nee 3. Select your Filters on the top right to filter by credit card used, coding, date range, keyword, expense value and a number of other useful criteria 4. Hit View Results to see all expenses that match your filters - ## Download Expenses +## Download Expenses 1. Select the checkbox to the left of the expenses or select all with the very top checkbox. 2. Click **# selected** at the top-right and select **Download**. diff --git a/docs/assets/images/search-hold-01.png b/docs/assets/images/search-hold-01.png new file mode 100644 index 000000000000..04745c570367 Binary files /dev/null and b/docs/assets/images/search-hold-01.png differ diff --git a/docs/assets/images/search-hold-02.png b/docs/assets/images/search-hold-02.png new file mode 100644 index 000000000000..3c7c39defd66 Binary files /dev/null and b/docs/assets/images/search-hold-02.png differ diff --git a/docs/assets/images/search-hold-03.png b/docs/assets/images/search-hold-03.png new file mode 100644 index 000000000000..81fbddcf5d75 Binary files /dev/null and b/docs/assets/images/search-hold-03.png differ diff --git a/docs/assets/images/search-hold-04.png b/docs/assets/images/search-hold-04.png new file mode 100644 index 000000000000..e5c1b71c0e37 Binary files /dev/null and b/docs/assets/images/search-hold-04.png differ diff --git a/docs/assets/images/search-hold-05.png b/docs/assets/images/search-hold-05.png new file mode 100644 index 000000000000..2d111abecb65 Binary files /dev/null and b/docs/assets/images/search-hold-05.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 751e072fb13f..04eba2e6152c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -385,7 +385,7 @@ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vac https://community.expensify.com/discussion/5678/deep-dive-secondary-login-merge-accounts-what-does-this-mean,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://community.expensify.com/discussion/5103/how-to-create-and-use-custom-units/,https://help.expensify.com/ https://community.expensify.com/discussion/6530/how-to-set-your-time-zone-for-report-history-comments,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-time-zone -https://community.expensify.com/discussion/5651/deep-dive--practices-when-youre-running-into-trouble-receiving-emails-from-expensify,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications +https://community.expensify.com/discussion/5651/deep-dive-best-practices-when-youre-running-into-trouble-receiving-emails-from-expensify,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses/,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security https://community.expensify.com/discussion/4641/how-to-add-a-u-s-deposit-account,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account @@ -608,3 +608,5 @@ https://help.expensify.com/articles/expensify-classic/travel/Edit-or-cancel-trav https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills https://help.expensify.com/articles/expensify-classic/settings/Set-Notifications,https://help.expensify.com/articles/expensify-classic/settings/Email-Notifications https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-download-expenses +https://help.expensify.com/articles/expensify-classic/expenses/Apply-Tax,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes +https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 27de0846d9d3..1e81fdedcaee 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.76 + 9.0.77 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.76.6 + 9.0.77.6 FullStory OrgId @@ -67,6 +67,8 @@ NSCameraUsageDescription Your camera is used to create chat attachments, documents, and facial capture. + NSContactsUsageDescription + Import contacts from your phone so your favorite people are always a tap away. NSLocationAlwaysAndWhenInUseUsageDescription Your location is used to determine your default currency and timezone. NSLocationWhenInUseUsageDescription diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d2bfbdefba61..2291b6e19e37 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.76 + 9.0.77 CFBundleSignature ???? CFBundleVersion - 9.0.76.6 + 9.0.77.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 0b61137e2127..f94a9a34f558 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.76 + 9.0.77 CFBundleVersion - 9.0.76.6 + 9.0.77.6 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 31ff58598c82..e9532fc1ae30 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1722,7 +1722,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.14.4): + - react-native-keyboard-controller (1.15.0): - DoubleConversion - glog - hermes-engine @@ -2410,7 +2410,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.207): + - RNLiveMarkdown (0.1.209): - DoubleConversion - glog - hermes-engine @@ -2430,10 +2430,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.207) + - RNLiveMarkdown/newarch (= 0.1.209) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.207): + - RNLiveMarkdown/newarch (0.1.209): - DoubleConversion - glog - hermes-engine @@ -3242,7 +3242,7 @@ SPEC CHECKSUMS: react-native-geolocation: b9bd12beaf0ebca61a01514517ca8455bd26fa06 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: aae312752fcdfaa2240be9a015fc41ce54087546 - react-native-keyboard-controller: 97bb7b48fa427c7455afdc8870c2978efd9bfa3a + react-native-keyboard-controller: 3428e4761623fd6a242d9bf3573112f8ebe92238 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 react-native-pager-view: abc5ef92699233eb726442c7f452cac82f73d0cb @@ -3292,7 +3292,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 8f9d9b32a25969ddb5f59eb92136b73823bbd141 + RNLiveMarkdown: f19d3c962fba4fb87bb9bc27ce9119216d86d92e RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/metro.config.js b/metro.config.js index c6e4ba6bb4ec..98bea7be80ed 100644 --- a/metro.config.js +++ b/metro.config.js @@ -4,6 +4,7 @@ const {getDefaultConfig: getReactNativeDefaultConfig} = require('@react-native/m const {mergeConfig} = require('@react-native/metro-config'); const defaultAssetExts = require('metro-config/src/defaults/defaults').assetExts; const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts; +const {wrapWithReanimatedMetroConfig} = require('react-native-reanimated/metro-config'); require('dotenv').config(); const defaultConfig = getReactNativeDefaultConfig(__dirname); @@ -26,4 +27,4 @@ const config = { }, }; -module.exports = mergeConfig(defaultConfig, expoConfig, config); +module.exports = wrapWithReanimatedMetroConfig(mergeConfig(defaultConfig, expoConfig, config)); diff --git a/package-lock.json b/package-lock.json index 947e3a4cc1bc..51773c06935e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.76-6", + "version": "9.0.77-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.76-6", + "version": "9.0.77-6", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.207", + "@expensify/react-native-live-markdown": "0.1.209", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -73,7 +73,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.20", + "react-fast-pdf": "1.0.21", "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", @@ -91,7 +91,7 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.4", + "react-native-keyboard-controller": "1.15.0", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", @@ -219,7 +219,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.74", + "eslint-config-expensify": "2.0.75", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -3498,9 +3498,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.207", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.207.tgz", - "integrity": "sha512-8snKeruLuHJCecnwQ+ru6pJhrDeI2Y3EywmXf/keT4aMk2xcW1fyCAr925zikTWANMDghcKkeuR/JqLe2b3rkA==", + "version": "0.1.209", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.209.tgz", + "integrity": "sha512-u+RRY+Jog/llEu9T1v0okSLgRhG5jGlX9H1Je0A8HWv0439XFLnAWSvN2eQ2T7bvT8Yjdj5CcC0hkgJiB9oCQw==", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -13485,7 +13485,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.9", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18467,7 +18469,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -18971,17 +18975,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/defaults": { "version": "1.0.4", "license": "MIT", @@ -20311,9 +20304,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.74", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.74.tgz", - "integrity": "sha512-NTA8fPbfkyCBZG+2/xJqB+HYD2D0XP8Sx1IDLWiwe/XJyNEESeqwQVbpA7FUP9sq4Ik2m2LPMf/G/aQHfw88rQ==", + "version": "2.0.75", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.75.tgz", + "integrity": "sha512-eSzQpxmVMGGXZSoB7aPZoWh75NC3oStyQnd+1JBFUQMDrdCyWjkMl8UJjzBqp/dOHazmVgLQUS1vDfk5cGXe6Q==", "dev": true, "license": "ISC", "dependencies": { @@ -23755,7 +23748,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -23779,6 +23774,8 @@ }, "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, "license": "MIT", "engines": { @@ -24186,9 +24183,10 @@ } }, "node_modules/internal-ip/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -25114,23 +25112,6 @@ "reflect.getprototypeof": "^1.0.3" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { "version": "10.8.7", "dev": true, @@ -29439,7 +29420,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -31771,9 +31754,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.20.tgz", - "integrity": "sha512-E2PJOO5oEqi6eNPllNOlQ8y0DiLZ3AW8t+MCN7AgJPp5pY04SeDveXHWvPN0nPU4X5sRBZ7CejeYce2QMMQDyg==", + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.21.tgz", + "integrity": "sha512-8Uuz/jPHjHqElH+aUj3ldS/Hg/NoZ5ZS/VupGzDkVJST0UiGzxkvDxxFIQuYuiaI4NGwGmqtQGGYsjJKpyWnig==", "license": "MIT", "dependencies": { "react-pdf": "^9.1.1", @@ -32122,9 +32105,9 @@ "license": "MIT" }, "node_modules/react-native-keyboard-controller": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.4.tgz", - "integrity": "sha512-hVt9KhK2dxBNtk4xHTnKLeO9Jv7v5h2TZlIeCQkbBLMd5NIJa4ll0GxIpbuutjP1ctPdhXUVpCfQzgXXJOYlzw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.15.0.tgz", + "integrity": "sha512-Laqszs0Uciu9MFkHurLwaHs9kftzUueew75HVOndbdcGR3MbKs2MqKdQEg1AgXSHcGoGg5nKafMOLVIoYjK6kA==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.1.6" @@ -36880,7 +36863,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.0.4", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -36897,23 +36882,20 @@ "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", - "rimraf": "^5.0.5", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.1.0", - "ws": "^8.16.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -36937,16 +36919,10 @@ } } }, - "node_modules/webpack-dev-server/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, @@ -36961,27 +36937,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "10.3.12", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", "dev": true, @@ -37004,28 +36959,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/minimatch": { - "version": "9.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/webpack-dev-server/node_modules/minipass": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.1.0", "dev": true, @@ -37043,25 +36976,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "5.0.5", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.2.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -37071,7 +36989,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -37079,7 +36997,9 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "7.2.1", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 02ef81489e01..c3f6d8e730d8 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,30 @@ { "name": "new.expensify", - "version": "9.0.76-6", + "version": "9.0.77-6", "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.", "license": "MIT", "private": true, "scripts": { + "i-standalone": "STANDALONE_NEW_DOT=true npm i", + "install-standalone": "STANDALONE_NEW_DOT=true npm install", "configure-mapbox": "./scripts/setup-mapbox-sdk-walkthrough.sh", "setupNewDotWebForEmulators": "./scripts/setup-newdot-web-emulators.sh", "startAndroidEmulator": "./scripts/start-android.sh", "postinstall": "./scripts/postInstall.sh", "clean": "./scripts/clean.sh", - "clean-standalone": "./scripts/clean.sh --new-dot", + "clean-standalone": "STANDALONE_NEW_DOT=true ./scripts/clean.sh", "android": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android", - "android-standalone": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android --new-dot", + "android-standalone": "./scripts/set-pusher-suffix.sh && STANDALONE_NEW_DOT=true ./scripts/run-build.sh --android", "ios": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios", - "ios-standalone": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios --new-dot", + "ios-standalone": "./scripts/set-pusher-suffix.sh && STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ios", "pod-install": "./scripts/pod-install.sh", - "pod-install-standalone": "./scripts/pod-install.sh --new-dot", + "pod-install-standalone": "STANDALONE_NEW_DOT=true ./scripts/pod-install.sh", "ipad": "concurrently \"./scripts/run-build.sh --ipad\"", - "ipad-standalone": "concurrently \"./scripts/run-build.sh --ipad --new-dot\"", + "ipad-standalone": "concurrently \"STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ipad\"", "ipad-sm": "concurrently \"./scripts/run-build.sh --ipad-sm\"", - "ipad-sm-standalone": "concurrently \"./scripts/run-build.sh --ipad-sm --new-dot\"", + "ipad-sm-standalone": "concurrently \"STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ipad-sm\"", "start": "npx react-native start", "web": "./scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "ts-node web/proxy.ts", @@ -74,7 +76,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.207", + "@expensify/react-native-live-markdown": "0.1.209", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -136,7 +138,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.20", + "react-fast-pdf": "1.0.21", "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", @@ -154,7 +156,7 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.4", + "react-native-keyboard-controller": "1.15.0", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", @@ -282,7 +284,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.74", + "eslint-config-expensify": "2.0.75", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", diff --git a/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch b/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch new file mode 100644 index 000000000000..974a0d090fb9 --- /dev/null +++ b/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch @@ -0,0 +1,67 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +index 572fb3d..0efa1ed 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp ++++ b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +@@ -468,7 +468,7 @@ void Binding::schedulerDidFinishTransaction( + mountingTransaction->getSurfaceId(); + }); + +- if (pendingTransaction != pendingTransactions_.end()) { ++ if (pendingTransaction != pendingTransactions_.end() && pendingTransaction->canMergeWith(*mountingTransaction)) { + pendingTransaction->mergeWith(std::move(*mountingTransaction)); + } else { + pendingTransactions_.push_back(std::move(*mountingTransaction)); +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +index d7dd1bc..d95d779 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +@@ -5,6 +5,8 @@ + * LICENSE file in the root directory of this source tree. + */ + ++#include ++ + #include "MountingTransaction.h" + + namespace facebook::react { +@@ -54,4 +56,21 @@ void MountingTransaction::mergeWith(MountingTransaction&& transaction) { + telemetry_ = std::move(transaction.telemetry_); + } + ++bool MountingTransaction::canMergeWith(MountingTransaction& transaction) { ++ std::set deletedTags; ++ for (const auto& mutation : mutations_) { ++ if (mutation.type == ShadowViewMutation::Type::Delete) { ++ deletedTags.insert(mutation.oldChildShadowView.tag); ++ } ++ } ++ ++ for (const auto& mutation : transaction.getMutations()) { ++ if (mutation.type == ShadowViewMutation::Type::Create && deletedTags.contains(mutation.newChildShadowView.tag)) { ++ return false; ++ } ++ } ++ ++ return true; ++} ++ + } // namespace facebook::react +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +index 277e9f4..38629db 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +@@ -85,6 +85,14 @@ class MountingTransaction final { + */ + void mergeWith(MountingTransaction&& transaction); + ++ /* ++ * Checks whether the two transactions can be safely merged. Due to ++ * reordering of mutations during mount, the sequence of ++ * REMOVE -> DELETE | CREATE -> INSERT (2 transactions) may get changed to ++ * INSERT -> REMOVE -> DELETE and the state will diverge from there. ++ */ ++ bool canMergeWith(MountingTransaction& transaction); ++ + private: + SurfaceId surfaceId_; + Number number_; diff --git a/patches/react-native-draggable-flatlist+4.0.1.patch b/patches/react-native-draggable-flatlist+4.0.1.patch index 348f1aa5de8a..a3d29b66de7a 100644 --- a/patches/react-native-draggable-flatlist+4.0.1.patch +++ b/patches/react-native-draggable-flatlist+4.0.1.patch @@ -12,7 +12,7 @@ index d7d98c2..2f59c7a 100644 runOnJS(onDragEnd)({ from: activeIndexAnim.value, diff --git a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx -index ea21575..66c5eed 100644 +index ea21575..dc6b095 100644 --- a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx +++ b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx @@ -1,14 +1,14 @@ @@ -32,14 +32,13 @@ index ea21575..66c5eed 100644 cellDataRef: React.MutableRefObject>; keyToIndexRef: React.MutableRefObject>; containerRef: React.RefObject; -@@ -54,8 +54,8 @@ function useSetupRefs({ +@@ -54,8 +54,7 @@ function useSetupRefs({ ...DEFAULT_PROPS.animationConfig, ...animationConfig, } as WithSpringConfig; - const animationConfigRef = useRef(animConfig); - animationConfigRef.current = animConfig; + const animationConfigRef = useSharedValue(animConfig); -+ animationConfigRef.value = animConfig; const cellDataRef = useRef(new Map()); const keyToIndexRef = useRef(new Map()); @@ -57,7 +56,7 @@ index ce4ab68..efea240 100644 return translate; diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts -index 7c20587..857c7d0 100644 +index 7c20587..33042e9 100644 --- a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts +++ b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts @@ -1,8 +1,9 @@ @@ -72,18 +71,17 @@ index 7c20587..857c7d0 100644 } from "react-native-reanimated"; import { DEFAULT_ANIMATION_CONFIG } from "../constants"; import { useAnimatedValues } from "../context/animatedValueContext"; -@@ -15,8 +16,8 @@ type Params = { +@@ -15,8 +16,7 @@ type Params = { export function useOnCellActiveAnimation( { animationConfig }: Params = { animationConfig: {} } ) { - const animationConfigRef = useRef(animationConfig); - animationConfigRef.current = animationConfig; + const animationConfigRef = useSharedValue(animationConfig); -+ animationConfigRef.value = animationConfig; const isActive = useIsActive(); -@@ -26,7 +27,7 @@ export function useOnCellActiveAnimation( +@@ -26,7 +26,7 @@ export function useOnCellActiveAnimation( const toVal = isActive && isTouchActiveNative.value ? 1 : 0; return withSpring(toVal, { ...DEFAULT_ANIMATION_CONFIG, diff --git a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch index 62cbf68f458d..52f8d76c4fe1 100644 --- a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch +++ b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm -index abb2cf6..fb81d52 100644 +index abb2cf6..c21b3e9 100644 --- a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm +++ b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm @@ -5,13 +5,14 @@ @@ -32,7 +32,7 @@ index abb2cf6..fb81d52 100644 } @@ -129,6 +130,8 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled } - + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0 + options:UIViewAnimationOptionCurveDefaultTransition @@ -66,25 +66,7 @@ index abb2cf6..fb81d52 100644 animations:animationBlock completion:completionBlock]; } else { -@@ -251,6 +260,8 @@ - (void)animateFadeWithTransitionContext:(id; replaceAnimation?: WithDefault; swipeDirection?: WithDefault; - hideKeyboardOnSwipe?: boolean; \ No newline at end of file + hideKeyboardOnSwipe?: boolean; diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh index 29e121acc968..fa87b4540b38 100755 --- a/scripts/applyPatches.sh +++ b/scripts/applyPatches.sh @@ -11,13 +11,18 @@ source "$SCRIPTS_DIR/shellUtils.sh" function patchPackage { # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" OS="$(uname)" if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then npx patch-package --error-on-fail --color=always - if [[ "$IS_HYBRID_APP_REPO" == "true" ]]; then + EXIT_CODE=$? + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "\n${GREEN}Applying HybridApp patches!${NC}" npx patch-package --patch-dir 'Mobile-Expensify/patches' --error-on-fail --color=always + EXIT_CODE+=$? fi + exit $EXIT_CODE else error "Unsupported OS: $OS" exit 1 diff --git a/scripts/clean.sh b/scripts/clean.sh index 1ecd73731b61..fbbfa070d442 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -7,7 +7,10 @@ NC='\033[0m' # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) -if [[ "$IS_HYBRID_APP_REPO" == "true" && "$1" != "--new-dot" ]]; then +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then echo -e "${BLUE}Cleaning HybridApp project...${NC}" # Navigate to Mobile-Expensify repository, and clean cd Mobile-Expensify diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh index 8e38f1706d6f..77237bb207b4 100755 --- a/scripts/pod-install.sh +++ b/scripts/pod-install.sh @@ -45,11 +45,9 @@ fi # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) -NEW_DOT_FLAG="false" -if [ "$1" == "--new-dot" ]; then - NEW_DOT_FLAG="true" -fi +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then echo -e "${BLUE}Executing npm run pod-install for HybridApp...${NC}" diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index db24f04f8a6c..c2adcadc4f43 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -10,7 +10,11 @@ cd "$ROOT_DIR" || exit 1 # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) -if [[ "$IS_HYBRID_APP_REPO" == "true" ]]; then +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "\n${GREEN}Installing node modules in Mobile-Expensify submodule!${NC}" cd Mobile-Expensify || exit 1 npm i diff --git a/scripts/run-build.sh b/scripts/run-build.sh index 7689aabbbf59..fd38f3c98861 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -3,8 +3,6 @@ set -e export PROJECT_ROOT_PATH -BUILD="$1" -NEW_DOT_FLAG="false" IOS_MODE="DebugDevelopment" ANDROID_MODE="developmentDebug" SCHEME="New Expensify Dev" @@ -20,26 +18,19 @@ function print_error_and_exit { exit 1 } -# Assign the arguments to variables -if [ "$#" -eq 1 ]; then - BUILD="$1" -elif [ "$#" -eq 2 ]; then - if [ "$1" == "--new-dot" ]; then - BUILD="$2" - NEW_DOT_FLAG="true" - elif [ "$2" == "--new-dot" ]; then - BUILD="$1" - NEW_DOT_FLAG="true" - else - print_error_and_exit - fi -else +# Assign the arguments to variables if arguments are correct +if [ "$#" -ne 1 ] || [[ "$1" != "--ios" && "$1" != "--ipad" && "$1" != "--ipad-sm" && "$1" != "--android" ]]; then print_error_and_exit fi +BUILD="$1" + # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then # Set HybridApp-specific arguments IOS_MODE="Debug" diff --git a/src/App.tsx b/src/App.tsx index cc824b78fa4c..52904e0a06c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,6 @@ import KeyboardProvider from './components/KeyboardProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; -import {ProductTrainingContextProvider} from './components/ProductTrainingContext'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext'; @@ -96,7 +95,6 @@ function App({url}: AppProps) { VideoPopoverMenuContextProvider, KeyboardProvider, SearchRouterContextProvider, - ProductTrainingContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 204ccaccf394..e317c19d96d2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -514,6 +514,7 @@ const CONST = { MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', ORDINAL_DAY_OF_MONTH: 'do', + MONTH_DAY_YEAR_ORDINAL_FORMAT: 'MMMM do, yyyy', }, SMS: { DOMAIN: '@expensify.sms', @@ -900,13 +901,8 @@ const CONST = { DEEP_DIVE_EXPENSIFY_CARD: 'https://community.expensify.com/discussion/4848/deep-dive-expensify-card-and-quickbooks-online-auto-reconciliation-how-it-works', DEEP_DIVE_ERECEIPTS: 'https://community.expensify.com/discussion/5542/deep-dive-what-are-ereceipts/', DEEP_DIVE_PER_DIEM: 'https://community.expensify.com/discussion/4772/how-to-add-a-single-rate-per-diem', + SET_NOTIFICATION_LINK: 'https://community.expensify.com/discussion/5651/deep-dive--practices-when-youre-running-into-trouble-receiving-emails-from-expensify', GITHUB_URL: 'https://github.com/Expensify/App', - TERMS_URL: `${EXPENSIFY_URL}/terms`, - PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, - LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, - ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, - WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, - BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', @@ -951,7 +947,14 @@ const CONST = { EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, - + OLD_DOT_PUBLIC_URLS: { + TERMS_URL: `${EXPENSIFY_URL}/terms`, + PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, + LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, + ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, + WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, + BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, + }, OLDDOT_URLS: { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', @@ -4451,7 +4454,7 @@ const CONST = { BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', - TRIP_ID_PATH: (tripID: string) => `trips/${tripID}`, + TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { @@ -5952,6 +5955,7 @@ const CONST = { CAR: 'car', HOTEL: 'hotel', FLIGHT: 'flight', + TRAIN: 'train', }, DOT_SEPARATOR: '•', @@ -5980,6 +5984,7 @@ const CONST = { DOWNLOADS_PATH: '/Downloads', DOWNLOADS_TIMEOUT: 5000, NEW_EXPENSIFY_PATH: '/New Expensify', + RECEIPTS_UPLOAD_PATH: '/Receipts-Upload', ENVIRONMENT_SUFFIX: { DEV: ' Dev', @@ -6435,17 +6440,6 @@ const CONST = { }, MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal', - - PRODUCT_TRAINING_TOOLTIP_NAMES: { - CONCEIRGE_LHN_GBR: 'conciergeLHNGBR', - RENAME_SAVED_SEARCH: 'renameSavedSearch', - QUICK_ACTION_BUTTON: 'quickActionButton', - WORKSAPCE_CHAT_CREATE: 'workspaceChatCreate', - SEARCH_FILTER_BUTTON_TOOLTIP: 'filterButtonTooltip', - BOTTOM_NAV_INBOX_TOOLTIP: 'bottomNavInboxTooltip', - LHN_WORKSPACE_CHAT_TOOLTIP: 'workspaceChatLHNTooltip', - GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bd77ed7e8af4..a43f1622ec9a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -117,6 +117,9 @@ const ONYXKEYS = { /** NVP keys */ + /** Boolean flag only true when first set */ + NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', + /** This NVP contains list of at most 5 recent attendees */ NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees', @@ -219,9 +222,18 @@ const ONYXKEYS = { /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** The NVP containing all information related to educational tooltip in workspace chat */ + NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** The NVP containing the target url to navigate to when deleting a transaction */ NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL: 'nvp_deleteTransactionNavigateBackURL', + /** Whether to show save search rename tooltip */ + SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP: 'shouldShowSavedSearchRenameTooltip', + + /** Whether to hide gbr tooltip */ + NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -721,6 +733,8 @@ const ONYXKEYS = { RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft', DEBUG_DETAILS_FORM: 'debugDetailsForm', DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft', + WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm', + WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', }, } as const; @@ -814,6 +828,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm; [ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm; [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm; + [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm; }; type OnyxFormDraftValuesMapping = { @@ -873,6 +888,7 @@ type OnyxCollectionValuesMapping = { type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; + [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; // NVP_ONBOARDING is an array for old users. [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; @@ -1015,7 +1031,9 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; + [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL]: string | undefined; + [ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP]: boolean; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; @@ -1023,6 +1041,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; + [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; [ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b9544d81bece..58d28a46a7b8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -716,6 +716,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile/address', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), }, + WORKSPACE_PROFILE_PLAN: { + route: 'settings/workspaces/:policyID/profile/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -979,9 +983,9 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const, }, WORKSPACE_UPGRADE: { - route: 'settings/workspaces/:policyID/upgrade/:featureName', - getRoute: (policyID: string, featureName: string, backTo?: string) => - getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), + route: 'settings/workspaces/:policyID/upgrade/:featureName?', + getRoute: (policyID: string, featureName?: string, backTo?: string) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), }, WORKSPACE_DOWNGRADE: { route: 'settings/workspaces/:policyID/downgrade/', @@ -1162,16 +1166,16 @@ const ROUTES = { }, WORKSPACE_REPORT_FIELDS_LIST_VALUES: { route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_ADD_VALUE: { route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: { route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?', getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => - `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, + `settings/workspaces/${policyID}/reportFields/${valueIndex}/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_EDIT_VALUE: { route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', @@ -1321,6 +1325,26 @@ const ROUTES = { route: 'settings/workspaces/:policyID/per-diem/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const, }, + WORKSPACE_PER_DIEM_DETAILS: { + route: 'settings/workspaces/:policyID/per-diem/details/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/details/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_DESTINATION: { + route: 'settings/workspaces/:policyID/per-diem/edit/destination/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/destination/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_SUBRATE: { + route: 'settings/workspaces/:policyID/per-diem/edit/subrate/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/subrate/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_AMOUNT: { + route: 'settings/workspaces/:policyID/per-diem/edit/amount/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/amount/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_CURRENCY: { + route: 'settings/workspaces/:policyID/per-diem/edit/currency/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/currency/${rateID}/${subRateID}` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, @@ -1365,6 +1389,15 @@ const ROUTES = { TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', + TRAVEL_TRIP_SUMMARY: { + route: 'r/:reportID/trip/:transactionID', + getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), + }, + TRAVEL_TRIP_DETAILS: { + route: 'r/:reportID/trip/:transactionID/:reservationIndex', + getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo), + }, ONBOARDING_ROOT: { route: 'onboarding', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6c4547e94c37..6274be1044b4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,6 +27,8 @@ const SCREENS = { TRAVEL: { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', + TRIP_SUMMARY: 'Travel_TripSummary', + TRIP_DETAILS: 'Travel_TripDetails', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', @@ -503,6 +505,7 @@ const SCREENS = { TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', + PLAN: 'Workspace_Profile_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', @@ -555,6 +558,11 @@ const SCREENS = { PER_DIEM_IMPORT: 'Per_Diem_Import', PER_DIEM_IMPORTED: 'Per_Diem_Imported', PER_DIEM_SETTINGS: 'Per_Diem_Settings', + PER_DIEM_DETAILS: 'Per_Diem_Details', + PER_DIEM_EDIT_DESTINATION: 'Per_Diem_Edit_Destination', + PER_DIEM_EDIT_SUBRATE: 'Per_Diem_Edit_Subrate', + PER_DIEM_EDIT_AMOUNT: 'Per_Diem_Edit_Amount', + PER_DIEM_EDIT_CURRENCY: 'Per_Diem_Edit_Currency', }, EDIT_REQUEST: { diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 9843996602f1..bcb3e27783e8 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -48,8 +48,8 @@ function IAcceptTheLabel() { return ( {`${translate('common.iAcceptThe')}`} - {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} - {` ${translate('common.privacyPolicy')} `} + {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} + {` ${translate('common.privacyPolicy')} `} ); } diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx index 0ac410013214..de65f40b3b4f 100644 --- a/src/components/AmountWithoutCurrencyForm.tsx +++ b/src/components/AmountWithoutCurrencyForm.tsx @@ -12,10 +12,13 @@ type AmountFormProps = { /** Callback to update the amount in the FormProvider */ onInputChange?: (value: string) => void; + + /** Should we allow negative number as valid input */ + shouldAllowNegative?: boolean; } & Partial; function AmountWithoutCurrencyForm( - {value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, ref: ForwardedRef, ) { const {toLocaleDigit} = useLocalize(); @@ -32,13 +35,13 @@ function AmountWithoutCurrencyForm( // More info: https://github.com/Expensify/App/issues/16974 const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); - const withLeadingZero = addLeadingZero(replacedCommasAmount); - if (!validateAmount(withLeadingZero, 2)) { + const withLeadingZero = addLeadingZero(replacedCommasAmount, shouldAllowNegative); + if (!validateAmount(withLeadingZero, 2, CONST.IOU.AMOUNT_MAX_LENGTH, shouldAllowNegative)) { return; } onInputChange?.(withLeadingZero); }, - [onInputChange], + [onInputChange, shouldAllowNegative], ); const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit); @@ -54,7 +57,7 @@ function AmountWithoutCurrencyForm( accessibilityLabel={accessibilityLabel} role={role} ref={ref} - keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} + keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined} // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. // See https://github.com/Expensify/App/issues/51868 for more information autoCapitalize="words" diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index c443b1ab8093..c0010af468af 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -7,7 +7,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; -import type {ReportAction, ReportActions} from '@src/types/onyx'; +import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; /** @@ -20,12 +20,11 @@ function extractAttachments( accountID, parentReportAction, reportActions, - reportID, - }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry; reportID: string}, + report, + }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry; report: OnyxEntry}, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; - const report = ReportUtils.getReport(reportID); const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index 9aa619eb1cda..68668ccc6ab0 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -34,9 +34,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, report}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, report}); } let newIndex = newAttachments.findIndex(compareImage); @@ -68,7 +68,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi } } // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [reportActions, compareImage]); + }, [reportActions, compareImage, report]); /** Updates the page state when the user navigates between attachments */ const updatePage = useCallback( diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f169416f1812..50caaac3dd81 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -89,9 +89,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, report}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, report}); } if (isEqual(attachments, newAttachments)) { @@ -130,19 +130,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi onNavigate(attachment); } } - }, [ - report.privateNotes, - reportActions, - parentReportActions, - compareImage, - report.parentReportActionID, - attachments, - setDownloadButtonVisibility, - onNavigate, - accountID, - type, - report.reportID, - ]); + }, [reportActions, parentReportActions, compareImage, attachments, setDownloadButtonVisibility, onNavigate, accountID, type, report]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 19bb98bff58e..f8e9e836c736 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -14,7 +14,7 @@ import RadioListItem from './SelectionList/RadioListItem'; import type {ListItem} from './SelectionList/types'; type CategoryPickerProps = { - policyID: string; + policyID: string | undefined; selectedCategory?: string; onSubmit: (item: ListItem) => void; }; diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index cea339de07e2..0cddb32f5aeb 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -16,6 +16,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; +import getPlatform from '@libs/getPlatform'; import CONST from '@src/CONST'; const excludeNoStyles: Array = []; @@ -140,7 +141,11 @@ function Composer( textAlignVertical="center" style={[composerStyle, maxHeightStyle]} markdownStyle={markdownStyle} - autoFocus={autoFocus} + // /* + // There are cases in hybird app on android that screen goes up when there is autofocus on keyboard. (e.g. https://github.com/Expensify/App/issues/53185) + // Workaround for this issue is to maunally focus keyboard after it's acutally rendered which is done by useAutoFocusInput hook. + // */ + autoFocus={getPlatform() !== 'android' ? autoFocus : false} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 98ac9e00a98a..5af76a2406b5 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -1,4 +1,5 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; +import {useIsFocused} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; @@ -252,7 +253,8 @@ function Composer( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isComposerFullSize]); - useHtmlPaste(textInput, handlePaste, true); + const isActive = useIsFocused(); + useHtmlPaste(textInput, handlePaste, isActive); useEffect(() => { setIsRendered(true); diff --git a/src/components/EmptySelectionListContent.tsx b/src/components/EmptySelectionListContent.tsx index a5ac2c84eb2b..67a9a2fc83f3 100644 --- a/src/components/EmptySelectionListContent.tsx +++ b/src/components/EmptySelectionListContent.tsx @@ -7,6 +7,7 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import BlockingView from './BlockingViews/BlockingView'; import * as Illustrations from './Icon/Illustrations'; +import ScrollView from './ScrollView'; import Text from './Text'; import TextLink from './TextLink'; @@ -39,17 +40,19 @@ function EmptySelectionListContent({contentType}: EmptySelectionListContentProps ); return ( - - - + + + + + ); } diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index e0f0ff4e6dcd..3c831301db8b 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -5,16 +5,10 @@ import type {GestureResponderEvent, Role, Text, View} from 'react-native'; import {Platform} from 'react-native'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; -import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; -import CONST from '@src/CONST'; import {PressableWithoutFeedback} from './Pressable'; -import {useProductTrainingContext} from './ProductTrainingContext'; -import EducationalTooltip from './Tooltip/EducationalTooltip'; const AnimatedPath = Animated.createAnimatedComponent(Path); AnimatedPath.displayName = 'AnimatedPath'; @@ -62,14 +56,6 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; const fabPressable = useRef(null); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const platform = getPlatform(); - const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB; - const isFocused = useBottomTabIsFocused(); - const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP, - isFocused, - ); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -111,45 +97,32 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo }; return ( - { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } }} - shouldUseOverlay - shiftHorizontal={isNarrowScreenOnWeb ? 0 : variables.fabTooltipShiftHorizontal} - renderTooltipContent={renderProductTrainingTooltip} - wrapperStyle={styles.productTrainingTooltipWrapper} - onHideTooltip={hideProductTrainingTooltip} + style={[styles.h100, styles.bottomTabBarItem]} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} > - { - fabPressable.current = el ?? null; - if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el ?? null; - } - }} - style={[styles.h100, styles.bottomTabBarItem]} - accessibilityLabel={accessibilityLabel} - onPress={toggleFabAction} - onLongPress={() => {}} - role={role} - shouldUseHapticsOnLongPress={false} - > - - - - - - - + + + + + + ); } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 2c07c48d52b7..b4d097e90994 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -10,7 +10,6 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import SearchButton from '@components/Search/SearchRouter/SearchButton'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; -import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -26,6 +25,9 @@ import type HeaderWithBackButtonProps from './types'; function HeaderWithBackButton({ icon, iconFill, + iconWidth, + iconHeight, + iconStyles, guidesCallTaskID = '', onBackButtonPress = () => Navigation.goBack(), onCloseButtonPress = () => Navigation.dismissModal(), @@ -46,6 +48,7 @@ function HeaderWithBackButton({ shouldSetModalVisibility = true, shouldShowThreeDotsButton = false, shouldDisableThreeDotsButton = false, + shouldUseHeadlineHeader = false, stepCounter, subtitle = '', title = '', @@ -72,10 +75,6 @@ function HeaderWithBackButton({ const StyleUtils = useStyleUtils(); const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState(); const {translate} = useLocalize(); - const {isKeyboardShown} = useKeyboardState(); - - // If the icon is present, the header bar should be taller and use different font. - const isCentralPaneSettings = !!icon; const middleContent = useMemo(() => { if (progressBarPercentage) { @@ -108,14 +107,14 @@ function HeaderWithBackButton({
); }, [ StyleUtils, subTitleLink, - isCentralPaneSettings, + shouldUseHeadlineHeader, policy, progressBarPercentage, report, @@ -140,7 +139,7 @@ function HeaderWithBackButton({ dataSet={{dragArea: false}} style={[ styles.headerBar, - isCentralPaneSettings && styles.headerBarDesktopHeight, + shouldUseHeadlineHeader && styles.headerBarDesktopHeight, shouldShowBorderBottom && styles.borderBottom, // progressBarPercentage can be 0 which would // be falsey, hence using !== undefined explicitly @@ -155,7 +154,7 @@ function HeaderWithBackButton({ { - if (isKeyboardShown) { + if (Keyboard.isVisible()) { Keyboard.dismiss(); } const topmostReportId = Navigation.getTopmostReportId(); @@ -180,9 +179,10 @@ function HeaderWithBackButton({ {!!icon && ( )} {!!policyAvatar && ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 6eef2b072eee..d2d4ba9e4e0f 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -38,6 +38,15 @@ type HeaderWithBackButtonProps = Partial & { * */ icon?: IconAsset; + /** Icon Width */ + iconWidth?: number; + + /** Icon Height */ + iconHeight?: number; + + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; + /** Method to trigger when pressing download button of the header */ onDownloadButtonPress?: () => void; @@ -119,6 +128,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should navigate to report page when the route have a topMostReport */ shouldNavigateToTopMostReport?: boolean; + /** Whether the header should use the headline header style */ + shouldUseHeadlineHeader?: boolean; + /** The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue'. */ iconFill?: string; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 51db1bc12c8e..4093b44743fe 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -187,6 +187,7 @@ import Task from '@assets/images/task.svg'; import Thread from '@assets/images/thread.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; +import Train from '@assets/images/train.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -413,5 +414,6 @@ export { Star, QBDSquare, GalleryNotFound, + Train, boltSlash, }; diff --git a/src/components/KeyboardAvoidingView/index.android.tsx b/src/components/KeyboardAvoidingView/index.android.tsx index 4d758511d7ad..ec2dc3bd18d7 100644 --- a/src/components/KeyboardAvoidingView/index.android.tsx +++ b/src/components/KeyboardAvoidingView/index.android.tsx @@ -1,133 +1,15 @@ -import React, {forwardRef, useCallback, useMemo, useState} from 'react'; -import type {LayoutRectangle, View, ViewProps} from 'react-native'; -import {useKeyboardContext, useKeyboardHandler} from 'react-native-keyboard-controller'; -import Reanimated, {interpolate, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue} from 'react-native-reanimated'; -import {useSafeAreaFrame} from 'react-native-safe-area-context'; -import type {KeyboardAvoidingViewProps} from './types'; - -const useKeyboardAnimation = () => { - const {reanimated} = useKeyboardContext(); - - // calculate it only once on mount, to avoid `SharedValue` reads during a render - const [initialHeight] = useState(() => -reanimated.height.get()); - const [initialProgress] = useState(() => reanimated.progress.get()); - - const heightWhenOpened = useSharedValue(initialHeight); - const height = useSharedValue(initialHeight); - const progress = useSharedValue(initialProgress); - const isClosed = useSharedValue(initialProgress === 0); - - useKeyboardHandler( - { - onStart: (e) => { - 'worklet'; - - progress.set(e.progress); - height.set(e.height); - - if (e.height > 0) { - isClosed.set(false); - heightWhenOpened.set(e.height); - } - }, - onEnd: (e) => { - 'worklet'; - - isClosed.set(e.height === 0); - height.set(e.height); - progress.set(e.progress); - }, - }, - [], - ); - - return {height, progress, heightWhenOpened, isClosed}; -}; - -const defaultLayout: LayoutRectangle = { - x: 0, - y: 0, - width: 0, - height: 0, -}; - -/** - * View that moves out of the way when the keyboard appears by automatically - * adjusting its height, position, or bottom padding. - * - * This `KeyboardAvoidingView` acts as a backward compatible layer for the previous Android behavior (prior to edge-to-edge mode). - * We can use `KeyboardAvoidingView` directly from the `react-native-keyboard-controller` package, but in this case animations are stuttering and it's better to handle as a separate task. +/* + * The KeyboardAvoidingView is only used on ios */ -const KeyboardAvoidingView = forwardRef>( - ({behavior, children, contentContainerStyle, enabled = true, keyboardVerticalOffset = 0, style, onLayout: onLayoutProps, ...props}, ref) => { - const initialFrame = useSharedValue(null); - const frame = useDerivedValue(() => initialFrame.get() ?? defaultLayout); - - const keyboard = useKeyboardAnimation(); - const {height: screenHeight} = useSafeAreaFrame(); - - const relativeKeyboardHeight = useCallback(() => { - 'worklet'; - - const keyboardY = screenHeight - keyboard.heightWhenOpened.get() - keyboardVerticalOffset; - - return Math.max(frame.get().y + frame.get().height - keyboardY, 0); - }, [screenHeight, keyboard.heightWhenOpened, keyboardVerticalOffset, frame]); - - const onLayoutWorklet = useCallback( - (layout: LayoutRectangle) => { - 'worklet'; - - if (keyboard.isClosed.get() || initialFrame.get() === null) { - initialFrame.set(layout); - } - }, - [initialFrame, keyboard.isClosed], - ); - const onLayout = useCallback>( - (e) => { - runOnUI(onLayoutWorklet)(e.nativeEvent.layout); - onLayoutProps?.(e); - }, - [onLayoutProps, onLayoutWorklet], - ); - - const animatedStyle = useAnimatedStyle(() => { - const bottom = interpolate(keyboard.progress.get(), [0, 1], [0, relativeKeyboardHeight()]); - const bottomHeight = enabled ? bottom : 0; - - switch (behavior) { - case 'height': - if (!keyboard.isClosed.get()) { - return { - height: frame.get().height - bottomHeight, - flex: 0, - }; - } - - return {}; - - case 'padding': - return {paddingBottom: bottomHeight}; +import React from 'react'; +import type {KeyboardAvoidingViewProps} from 'react-native-keyboard-controller'; +import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native-keyboard-controller'; - default: - return {}; - } - }, [behavior, enabled, relativeKeyboardHeight]); - const combinedStyles = useMemo(() => [style, animatedStyle], [style, animatedStyle]); +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} - return ( - - {children} - - ); - }, -); +KeyboardAvoidingView.displayName = 'KeyboardAvoidingView'; export default KeyboardAvoidingView; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index efdd9659c845..c423d3101d92 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -11,7 +11,6 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {useSession} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import {useProductTrainingContext} from '@components/ProductTrainingContext'; import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; @@ -23,6 +22,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; +import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import Performance from '@libs/Performance'; @@ -32,6 +32,7 @@ import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportA import FreeTrial from '@pages/settings/Subscription/FreeTrial'; import variables from '@styles/variables'; import Timing from '@userActions/Timing'; +import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -47,21 +48,18 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const isActiveWorkspaceChat = ReportUtils.isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat && activePolicyID === report?.policyID; + const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); + const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + selector: hasCompletedGuidedSetupFlowSelector, + }); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const session = useSession(); - const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); - const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); - - const {tooltipToRender, shouldShowTooltip} = useMemo(() => { - const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return {tooltipToRender: tooltip, shouldShowTooltip: shouldUseNarrowLayout ? isScreenFocused : true}; - }, [shouldShowGetStartedTooltip, isScreenFocused, shouldUseNarrowLayout]); + // Guides are assigned for the MANAGE_TEAM onboarding action, except for emails that have a '+'. + const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); + const shouldShowToooltipOnThisReport = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); + const [shouldHideGBRTooltip] = useOnyx(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, {initialValue: true}); - const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -74,6 +72,30 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); + const renderGBRTooltip = useCallback( + () => ( + + + {translate('sidebarScreen.tooltip')} + + ), + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], + ); + const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -158,18 +180,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti needsOffscreenAlphaCompositing > diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 19af05a1581b..00965d197937 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -177,14 +177,14 @@ function MoneyRequestConfirmationList({ shouldPlaySound = true, isConfirmed, }: MoneyRequestConfirmationListProps) { - const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID ?? '-1'}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID ?? '-1'}`); - const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '-1'}`); - const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`); - const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`, { + const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`); + const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, { selector: (selectedPolicy) => DistanceRequestUtils.getDefaultMileageRate(selectedPolicy), }); - const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID ?? '-1'}`); + const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`); const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES); const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); @@ -202,17 +202,22 @@ function MoneyRequestConfirmationList({ const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const isScanRequest = useMemo(() => TransactionUtils.isScanRequest(transaction), [transaction]); - const transactionID = transaction?.transactionID ?? '-1'; - const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '-1'; + const transactionID = transaction?.transactionID; + const customUnitRateID = TransactionUtils.getRateID(transaction); useEffect(() => { - if ((customUnitRateID && customUnitRateID !== '-1') || !isDistanceRequest) { + if (customUnitRateID !== '-1' || !isDistanceRequest || !transactionID || !policy?.id) { return; } - const defaultRate = defaultMileageRate?.customUnitRateID ?? ''; - const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate; + const defaultRate = defaultMileageRate?.customUnitRateID; + const lastSelectedRate = lastSelectedDistanceRates?.[policy.id] ?? defaultRate; const rateID = lastSelectedRate; + + if (!rateID) { + return; + } + IOU.setCustomUnitRateID(transactionID, rateID); }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, transactionID, isDistanceRequest]); @@ -242,6 +247,7 @@ function MoneyRequestConfirmationList({ if ( !shouldShowTax || !transaction || + !transactionID || (transaction.taxCode && previousTransactionModifiedCurrency === transaction.modifiedCurrency && previousTransactionCurrency === transaction.currency && @@ -296,7 +302,12 @@ function MoneyRequestConfirmationList({ return true; } - if (!participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1)) { + if ( + !participant.isInvoiceRoom && + !participant.isPolicyExpenseChat && + !participant.isSelfDM && + ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? CONST.DEFAULT_NUMBER_ID) + ) { return true; } @@ -325,7 +336,7 @@ function MoneyRequestConfirmationList({ if (isFirstUpdatedDistanceAmount.current) { return; } - if (!isDistanceRequest) { + if (!isDistanceRequest || !transactionID) { return; } const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); @@ -334,7 +345,7 @@ function MoneyRequestConfirmationList({ }, [distance, rate, unit, transactionID, currency, isDistanceRequest]); useEffect(() => { - if (!shouldCalculateDistanceAmount) { + if (!shouldCalculateDistanceAmount || !transactionID) { return; } @@ -342,7 +353,7 @@ function MoneyRequestConfirmationList({ IOU.setMoneyRequestAmount(transactionID, amount, currency ?? ''); // If it's a split request among individuals, set the split shares - const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? -1); + const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID); if (isTypeSplit && !isPolicyExpenseChat && amount && transaction?.currency) { IOU.setSplitShares(transaction, amount, currency, participantAccountIDs); } @@ -364,20 +375,25 @@ function MoneyRequestConfirmationList({ return; } - let taxableAmount: number; - let taxCode: string; + let taxableAmount: number | undefined; + let taxCode: string | undefined; if (isDistanceRequest) { - const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID); - taxCode = customUnitRate?.attributes?.taxRateExternalID ?? ''; - taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance); + if (customUnitRateID) { + const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID); + taxCode = customUnitRate?.attributes?.taxRateExternalID; + taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance); + } } else { taxableAmount = transaction.amount ?? 0; taxCode = transaction.taxCode ?? TransactionUtils.getDefaultTaxCode(policy, transaction) ?? ''; } - const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; - const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, transaction.currency); - const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); - IOU.setMoneyRequestTaxAmount(transaction.transactionID ?? '', taxAmountInSmallestCurrencyUnits); + + if (taxCode && taxableAmount) { + const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; + const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, transaction.currency); + const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString())); + IOU.setMoneyRequestTaxAmount(transaction.transactionID, taxAmountInSmallestCurrencyUnits); + } }, [ policy, shouldShowTax, @@ -522,7 +538,7 @@ function MoneyRequestConfirmationList({ rightElement: ( onSplitShareChange(participantOption.accountID ?? -1, Number(value))} + onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? CONST.DEFAULT_NUMBER_ID, Number(value))} maxLength={formattedTotalAmount.length} contentWidth={formattedTotalAmount.length * 8} /> @@ -637,7 +653,7 @@ function MoneyRequestConfirmationList({ }, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]); useEffect(() => { - if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { + if (!isDistanceRequest || isMovingTransactionFromTrackExpense || !transactionID) { return; } @@ -669,16 +685,20 @@ function MoneyRequestConfirmationList({ // Auto select the category if there is only one enabled category and it is required useEffect(() => { const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); - if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { + if (!transactionID || iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { return; } - IOU.setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? ''); + IOU.setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy?.id); // Keep 'transaction' out to ensure that we autoselect the option only once // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [shouldShowCategories, policyCategories, isCategoryRequired]); + }, [shouldShowCategories, policyCategories, isCategoryRequired, policy?.id]); // Auto select the tag if there is only one enabled tag and it is required useEffect(() => { + if (!transactionID) { + return; + } + let updatedTagsString = TransactionUtils.getTag(transaction); policyTagLists.forEach((tagList, index) => { const isTagListRequired = tagList.required ?? false; @@ -721,7 +741,7 @@ function MoneyRequestConfirmationList({ */ const confirm = useCallback( (paymentMethod: PaymentMethodType | undefined) => { - if (routeError) { + if (!!routeError || !transactionID) { return; } if (iouType === CONST.IOU.TYPE.INVOICE && !hasInvoicingDetails(policy)) { diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index e32c4eae410f..51cb2a6d6f39 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -163,7 +163,7 @@ type MoneyRequestConfirmationListFooterProps = { transaction: OnyxEntry; /** The transaction ID */ - transactionID: string; + transactionID: string | undefined; /** The unit */ unit: Unit | undefined; @@ -295,7 +295,7 @@ function MoneyRequestConfirmationListFooter({ description={translate('iou.amount')} interactive={!isReadOnly} onPress={() => { - if (isDistanceRequest) { + if (isDistanceRequest || !transactionID) { return; } @@ -326,6 +326,10 @@ function MoneyRequestConfirmationListFooter({ title={iouComment} description={translate('common.description')} onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); }} style={[styles.moneyRequestMenuItem]} @@ -349,7 +353,13 @@ function MoneyRequestConfirmationListFooter({ description={translate('common.distance')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} disabled={didConfirm} interactive={!isReadOnly} /> @@ -366,7 +376,13 @@ function MoneyRequestConfirmationListFooter({ description={translate('common.rate')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} disabled={didConfirm} interactive={!!rate && !isReadOnly && isPolicyExpenseChat} /> @@ -384,6 +400,10 @@ function MoneyRequestConfirmationListFooter({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); }} disabled={didConfirm} @@ -408,6 +428,10 @@ function MoneyRequestConfirmationListFooter({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); }} disabled={didConfirm} @@ -427,12 +451,16 @@ function MoneyRequestConfirmationListFooter({ title={iouCategory} description={translate('common.category')} numberOfLinesTitle={2} - onPress={() => + onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID), CONST.NAVIGATION.ACTION_TYPE.PUSH, - ) - } + ); + }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} disabled={didConfirm} @@ -454,9 +482,13 @@ function MoneyRequestConfirmationListFooter({ title={TransactionUtils.getTagForDisplay(transaction, index)} description={name} numberOfLinesTitle={2} - onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)) - } + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID)); + }} style={[styles.moneyRequestMenuItem]} disabled={didConfirm} interactive={!isReadOnly} @@ -476,7 +508,13 @@ function MoneyRequestConfirmationListFooter({ description={taxRates?.name} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); + }} disabled={didConfirm} interactive={canModifyTaxFields} /> @@ -493,7 +531,13 @@ function MoneyRequestConfirmationListFooter({ description={translate('iou.taxAmount')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); + }} disabled={didConfirm} interactive={canModifyTaxFields} /> @@ -512,7 +556,13 @@ function MoneyRequestConfirmationListFooter({ }`} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} interactive shouldRenderAsHTML /> @@ -557,7 +607,13 @@ function MoneyRequestConfirmationListFooter({ {isLocalFile && Str.isPDF(receiptFilename) ? ( Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID)); + }} accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} disabled={!shouldDisplayReceipt} @@ -570,7 +626,13 @@ function MoneyRequestConfirmationListFooter({ ) : ( Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))} + onPress={() => { + if (!transactionID) { + return; + } + + Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID)); + }} disabled={!shouldDisplayReceipt || isThumbnail} accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} @@ -625,7 +687,10 @@ function MoneyRequestConfirmationListFooter({ isLabelHoverable={false} interactive={!isReadOnly && canUpdateSenderWorkspace} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '-1', reportID, Navigation.getActiveRouteWithoutParams())); + if (!transaction?.transactionID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); }} style={styles.moneyRequestMenuItem} labelStyle={styles.mt2} @@ -644,11 +709,15 @@ function MoneyRequestConfirmationListFooter({ ? receiptThumbnailContent : shouldShowReceiptEmptyState && ( + onPress={() => { + if (!transactionID) { + return; + } + Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } + ); + }} /> ))} {primaryFields} diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index 495c14ff76e1..6b83afe603c1 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -1,6 +1,6 @@ import 'core-js/proposals/promise-with-resolvers'; // eslint-disable-next-line import/extensions -import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs'; +import pdfWorkerSource from 'pdfjs-dist/build/pdf.worker.min.mjs'; import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import {Document, pdfjs, Thumbnail} from 'react-pdf'; diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 6abf72e9e520..1896bc4f5f07 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -66,7 +66,9 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct {reportName} )} - {!!workspaceName && {` ${translate('threads.in')} ${workspaceName}`}} + {!!workspaceName && workspaceName !== reportName && ( + {` ${translate('threads.in')} ${workspaceName}`} + )} ); diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts deleted file mode 100644 index dc2a761a4903..000000000000 --- a/src/components/ProductTrainingContext/TOOLTIPS.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import {dismissProductTraining} from '@libs/actions/Welcome'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; - -const { - CONCEIRGE_LHN_GBR, - RENAME_SAVED_SEARCH, - WORKSAPCE_CHAT_CREATE, - QUICK_ACTION_BUTTON, - SEARCH_FILTER_BUTTON_TOOLTIP, - BOTTOM_NAV_INBOX_TOOLTIP, - LHN_WORKSPACE_CHAT_TOOLTIP, - GLOBAL_CREATE_TOOLTIP, -} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; - -type ProductTrainingTooltipName = ValueOf; - -type ShouldShowConditionProps = { - shouldUseNarrowLayout?: boolean; -}; - -type TooltipData = { - content: Array<{text: TranslationPaths; isBold: boolean}>; - onHideTooltip: () => void; - name: ProductTrainingTooltipName; - priority: number; - shouldShow: (props: ShouldShowConditionProps) => boolean; -}; - -const TOOLTIPS: Record = { - [CONCEIRGE_LHN_GBR]: { - content: [ - {text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false}, - {text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true}, - ], - onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR), - name: CONCEIRGE_LHN_GBR, - priority: 1300, - shouldShow: ({shouldUseNarrowLayout}) => !!shouldUseNarrowLayout, - }, - [RENAME_SAVED_SEARCH]: { - content: [ - {text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH), - name: RENAME_SAVED_SEARCH, - priority: 1250, - shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout, - }, - [GLOBAL_CREATE_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.globalCreateTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.globalCreateTooltip.part2', isBold: false}, - {text: 'productTrainingTooltip.globalCreateTooltip.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(GLOBAL_CREATE_TOOLTIP), - name: GLOBAL_CREATE_TOOLTIP, - priority: 1200, - shouldShow: () => true, - }, - [QUICK_ACTION_BUTTON]: { - content: [ - {text: 'productTrainingTooltip.quickActionButton.part1', isBold: true}, - {text: 'productTrainingTooltip.quickActionButton.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(QUICK_ACTION_BUTTON), - name: QUICK_ACTION_BUTTON, - priority: 1150, - shouldShow: () => true, - }, - [WORKSAPCE_CHAT_CREATE]: { - content: [ - {text: 'productTrainingTooltip.workspaceChatCreate.part1', isBold: true}, - {text: 'productTrainingTooltip.workspaceChatCreate.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(WORKSAPCE_CHAT_CREATE), - name: WORKSAPCE_CHAT_CREATE, - priority: 1100, - shouldShow: () => true, - }, - [SEARCH_FILTER_BUTTON_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.searchFilterButtonTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.searchFilterButtonTooltip.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(SEARCH_FILTER_BUTTON_TOOLTIP), - name: SEARCH_FILTER_BUTTON_TOOLTIP, - priority: 1000, - shouldShow: () => true, - }, - [BOTTOM_NAV_INBOX_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.bottomNavInboxTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.bottomNavInboxTooltip.part2', isBold: false}, - {text: 'productTrainingTooltip.bottomNavInboxTooltip.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(BOTTOM_NAV_INBOX_TOOLTIP), - name: BOTTOM_NAV_INBOX_TOOLTIP, - priority: 900, - shouldShow: () => true, - }, - [LHN_WORKSPACE_CHAT_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.workspaceChatTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.workspaceChatTooltip.part2', isBold: false}, - {text: 'productTrainingTooltip.workspaceChatTooltip.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(LHN_WORKSPACE_CHAT_TOOLTIP), - name: LHN_WORKSPACE_CHAT_TOOLTIP, - priority: 800, - shouldShow: () => true, - }, -}; - -export default TOOLTIPS; -export type {ProductTrainingTooltipName}; diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx deleted file mode 100644 index 7cfcf4d3bfa7..000000000000 --- a/src/components/ProductTrainingContext/index.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import type {ProductTrainingTooltipName} from './TOOLTIPS'; -import TOOLTIPS from './TOOLTIPS'; - -type ProductTrainingContextType = { - shouldRenderTooltip: (tooltipName: ProductTrainingTooltipName) => boolean; - registerTooltip: (tooltipName: ProductTrainingTooltipName) => void; - unregisterTooltip: (tooltipName: ProductTrainingTooltipName) => void; -}; - -const ProductTrainingContext = createContext({ - shouldRenderTooltip: () => false, - registerTooltip: () => {}, - unregisterTooltip: () => {}, -}); - -function ProductTrainingContextProvider({children}: ChildrenProps) { - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); - const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; - const [isOnboardingCompleted = true, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: hasCompletedGuidedSetupFlowSelector, - }); - const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - - const [activeTooltips, setActiveTooltips] = useState>(new Set()); - - const unregisterTooltip = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - setActiveTooltips((prev) => { - const next = new Set(prev); - next.delete(tooltipName); - return next; - }); - }, - [setActiveTooltips], - ); - - const determineVisibleTooltip = useCallback(() => { - if (activeTooltips.size === 0) { - return null; - } - - const sortedTooltips = Array.from(activeTooltips) - .map((name) => ({ - name, - priority: TOOLTIPS[name]?.priority ?? 0, - })) - .sort((a, b) => b.priority - a.priority); - - const highestPriorityTooltip = sortedTooltips.at(0); - - if (!highestPriorityTooltip) { - return null; - } - - return highestPriorityTooltip.name; - }, [activeTooltips]); - - const shouldTooltipBeVisible = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - if (isLoadingOnyxValue(isOnboardingCompletedMetadata)) { - return false; - } - - const isDismissed = !!dismissedProductTraining?.[tooltipName]; - - if (isDismissed) { - return false; - } - const tooltipConfig = TOOLTIPS[tooltipName]; - - // if hasBeenAddedToNudgeMigration is true, and welcome modal is not dismissed, don't show tooltip - if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.[CONST.MIGRATED_USER_WELCOME_MODAL]) { - return false; - } - if (isOnboardingCompleted === false) { - return false; - } - - return tooltipConfig.shouldShow({ - shouldUseNarrowLayout, - }); - }, - [dismissedProductTraining, hasBeenAddedToNudgeMigration, isOnboardingCompleted, isOnboardingCompletedMetadata, shouldUseNarrowLayout], - ); - - const registerTooltip = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - const shouldRegister = shouldTooltipBeVisible(tooltipName); - if (!shouldRegister) { - return; - } - setActiveTooltips((prev) => new Set([...prev, tooltipName])); - }, - [shouldTooltipBeVisible], - ); - - const shouldRenderTooltip = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - // First check base conditions - const shouldShow = shouldTooltipBeVisible(tooltipName); - if (!shouldShow) { - return false; - } - const visibleTooltip = determineVisibleTooltip(); - - // If this is the highest priority visible tooltip, show it - if (tooltipName === visibleTooltip) { - return true; - } - - return false; - }, - [shouldTooltipBeVisible, determineVisibleTooltip], - ); - - const contextValue = useMemo( - () => ({ - shouldRenderTooltip, - registerTooltip, - unregisterTooltip, - }), - [shouldRenderTooltip, registerTooltip, unregisterTooltip], - ); - - return {children}; -} - -const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shouldShow = true) => { - const context = useContext(ProductTrainingContext); - const styles = useThemeStyles(); - const theme = useTheme(); - const {translate} = useLocalize(); - - if (!context) { - throw new Error('useProductTourContext must be used within a ProductTourProvider'); - } - - const {shouldRenderTooltip, registerTooltip, unregisterTooltip} = context; - - useEffect(() => { - if (shouldShow) { - registerTooltip(tooltipName); - return () => { - unregisterTooltip(tooltipName); - }; - } - return () => {}; - }, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]); - - const renderProductTrainingTooltip = useCallback(() => { - const tooltip = TOOLTIPS[tooltipName]; - return ( - - - - {tooltip.content.map(({text, isBold}) => { - const translatedText = translate(text); - return ( - - {translatedText} - - ); - })} - - - ); - }, [ - styles.alignItemsCenter, - styles.flexRow, - styles.flexWrap, - styles.gap3, - styles.justifyContentCenter, - styles.mw100, - styles.p2, - styles.productTrainingTooltipText, - styles.textAlignCenter, - styles.textBold, - styles.textWrap, - theme.tooltipHighlightText, - tooltipName, - translate, - ]); - - const shouldShowProductTrainingTooltip = useMemo(() => { - return shouldRenderTooltip(tooltipName); - }, [shouldRenderTooltip, tooltipName]); - - const hideProductTrainingTooltip = useCallback(() => { - const tooltip = TOOLTIPS[tooltipName]; - tooltip.onHideTooltip(); - unregisterTooltip(tooltipName); - }, [tooltipName, unregisterTooltip]); - - return { - renderProductTrainingTooltip, - hideProductTrainingTooltip, - shouldShowProductTrainingTooltip, - }; -}; - -export {ProductTrainingContextProvider, useProductTrainingContext}; diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index 046026190a5b..08d83b6962af 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -21,13 +21,15 @@ type ReceiptEmptyStateProps = { }; // Returns an SVG icon indicating that the user should attach a receipt -function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = false, isThumbnail = false}: ReceiptEmptyStateProps) { +function ReceiptEmptyState({hasError = false, onPress, disabled = false, isThumbnail = false}: ReceiptEmptyStateProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const theme = useTheme(); + const Wrapper = onPress ? PressableWithoutFeedback : View; + return ( - {}, disabled = fal /> )} - + ); } diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 72e0a43ed5e5..ba0cda25d59e 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -353,7 +353,6 @@ function MoneyRequestPreviewContent({ images={receiptImages} isHovered={isHovered || isScanning} size={1} - onPress={shouldDisableOnPress ? undefined : onPreviewPressed} /> {isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e3d4a8d31cf6..18750bfc7a29 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -33,7 +33,6 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import * as IOU from '@userActions/IOU'; -import * as Link from '@userActions/Link'; import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -82,7 +81,6 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const parentReportID = report?.parentReportID ?? '-1'; const policyID = report?.policyID ?? '-1'; const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); @@ -91,7 +89,8 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals }); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const targetPolicyID = updatedTransaction?.reportID ? ReportUtils.getReport(updatedTransaction?.reportID)?.policyID : policyID; + const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${updatedTransaction?.reportID}`); + const targetPolicyID = updatedTransaction?.reportID ? transactionReport?.policyID : policyID; const [policyTagList] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`); const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { canEvict: false, @@ -187,7 +186,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); - const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID; const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !ReportUtils.isPaidGroupPolicy(report)); @@ -698,10 +697,12 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals { - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + const reservations = transaction?.receipt?.reservationList?.length ?? 0; + if (reservations > 1) { + Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())); + } + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', 0, Navigation.getReportRHPActiveRoute())); }} /> )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c7af8ce1f614..79497e5fab88 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -365,7 +365,8 @@ function ReportPreview({ const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation; - const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID) || ReportUtils.hasMissingInvoiceBankAccount(iouReportID); + const shouldPromptUserToAddBankAccount = + (ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID) || ReportUtils.hasMissingInvoiceBankAccount(iouReportID)) && !ReportUtils.isSettled(iouReportID); const shouldShowRBR = hasErrors && !iouSettled; /* @@ -503,7 +504,6 @@ function ReportPreview({ images={lastThreeReceipts} total={allTransactions.length} size={CONST.RECEIPT.MAX_REPORT_PREVIEW_RECEIPTS} - onPress={openReportFromPreview} /> diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index f6f436cbd51e..2ea295d16143 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -75,7 +75,8 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitleFromReport(taskReport, action?.childReportName ?? '')); - const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? -1; + const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; + const taskOwnerAccountID = taskReport?.ownerAccountID ?? action?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasAssignee = taskAssigneeAccountID > 0; const personalDetails = usePersonalDetails(); const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar; @@ -106,12 +107,12 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che { if (isTaskCompleted) { - Task.reopenTask(taskReport); + Task.reopenTask(taskReport, taskReportID); } else { - Task.completeTask(taskReport); + Task.completeTask(taskReport, taskReportID); } })} accessibilityLabel={translate('task.task')} diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 2b0dc9387927..7901426b33e0 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -10,8 +10,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -27,27 +26,28 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; -type TaskViewProps = WithCurrentUserPersonalDetailsProps & { +type TaskViewProps = { /** The report currently being looked at */ report: Report; }; -function TaskView({report, ...props}: TaskViewProps) { +function TaskView({report}: TaskViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const personalDetails = usePersonalDetails(); useEffect(() => { Task.setTaskReport(report); }, [report]); - const personalDetails = usePersonalDetails(); const taskTitle = convertToLTR(report.reportName ?? ''); const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips( OptionsListUtils.getPersonalDetailsForAccountIDs(report.managerID ? [report.managerID] : [], personalDetails), false, ); - const isCompleted = ReportUtils.isCompletedTaskReport(report); const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID); - const canActionTask = Task.canActionTask(report, props.currentUserPersonalDetails.accountID); + const isCompleted = ReportUtils.isCompletedTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const canActionTask = Task.canActionTask(report, currentUserPersonalDetails.accountID); const disableState = !canModifyTask; const isDisableInteractive = !canModifyTask || !isOpen; const {translate} = useLocalize(); @@ -77,10 +77,10 @@ function TaskView({report, ...props}: TaskViewProps) { styles.ph5, styles.pv2, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState, !isDisableInteractive), true), - isDisableInteractive && !disableState && styles.cursorDefault, + isDisableInteractive && styles.cursorDefault, ]} - disabled={disableState} accessibilityLabel={taskTitle || translate('task.task')} + disabled={isDisableInteractive} > {({pressed}) => ( @@ -104,7 +104,7 @@ function TaskView({report, ...props}: TaskViewProps) { containerBorderRadius={8} caretSize={16} accessibilityLabel={taskTitle || translate('task.task')} - disabled={!canModifyTask || !canActionTask} + disabled={!canActionTask} /> - {isOpen && ( + {!isDisableInteractive && ( Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} - shouldShowRightIcon={isOpen} + shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} shouldGreyOutWhenDisabled={false} numberOfLinesTitle={0} interactive={!isDisableInteractive} + shouldUseDefaultCursorWhenDisabled /> @@ -153,23 +154,25 @@ function TaskView({report, ...props}: TaskViewProps) { avatarSize={CONST.AVATAR_SIZE.SMALLER} titleStyle={styles.assigneeTextStyle} onPress={() => Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} - shouldShowRightIcon={isOpen} + shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2]} isSmallAvatarSubscriptMenu shouldGreyOutWhenDisabled={false} interactive={!isDisableInteractive} titleWithTooltips={assigneeTooltipDetails} + shouldUseDefaultCursorWhenDisabled /> ) : ( Navigation.navigate(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID, Navigation.getReportRHPActiveRoute()))} - shouldShowRightIcon={isOpen} + shouldShowRightIcon={!isDisableInteractive} disabled={disableState} wrapperStyle={[styles.pv2]} shouldGreyOutWhenDisabled={false} interactive={!isDisableInteractive} + shouldUseDefaultCursorWhenDisabled /> )} @@ -180,4 +183,4 @@ function TaskView({report, ...props}: TaskViewProps) { TaskView.displayName = 'TaskView'; -export default withCurrentUserPersonalDetails(TaskView); +export default TaskView; diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 32cbc5dd853e..a7fdef547bf9 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -11,16 +11,18 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import * as ReportUtils from '@src/libs/ReportUtils'; import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import ROUTES from '@src/ROUTES'; import type {Reservation, ReservationTimeDetails} from '@src/types/onyx/Transaction'; type TripDetailsViewProps = { /** The active tripRoomReportID, used for Onyx subscription */ - tripRoomReportID?: string; + tripRoomReportID: string; /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; @@ -28,9 +30,12 @@ type TripDetailsViewProps = { type ReservationViewProps = { reservation: Reservation; + transactionID: string; + tripRoomReportID: string; + reservationIndex: number; }; -function ReservationView({reservation}: ReservationViewProps) { +function ReservationView({reservation, transactionID, tripRoomReportID, reservationIndex}: ReservationViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -75,11 +80,14 @@ function ReservationView({reservation}: ReservationViewProps) { const vendor = reservation.vendor ? `${reservation.vendor} • ` : ''; return `${vendor}${reservation.start.location}`; } + if (reservation.type === CONST.RESERVATION_TYPE.TRAIN) { + return reservation.route?.name; + } return reservation.start.address ?? reservation.start.location; }, [reservation]); const titleComponent = () => { - if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) { + if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT || reservation.type === CONST.RESERVATION_TYPE.TRAIN) { return ( @@ -129,6 +137,7 @@ function ReservationView({reservation}: ReservationViewProps) { iconWidth={20} iconStyles={[StyleUtils.getTripReservationIconContainer(false), styles.mr3]} secondaryIconFill={theme.icon} + onPress={() => Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(tripRoomReportID, transactionID, reservationIndex, Navigation.getReportRHPActiveRoute()))} /> ); } @@ -138,7 +147,7 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai const {translate} = useLocalize(); const tripTransactions = ReportUtils.getTripTransactions(tripRoomReportID); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); return ( @@ -153,11 +162,18 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai <> - {reservations.map((reservation) => ( - - - - ))} + {reservationsData.map(({reservation, transactionID, reservationIndex}) => { + return ( + + + + ); + })} + {title} + + ); + + if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT || reservation.type === CONST.RESERVATION_TYPE.TRAIN) { + const startName = reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? reservation.start.shortName : reservation.start.longName; + const endName = reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? reservation.end.shortName : reservation.end.longName; + + titleComponent = ( - {reservation.start.shortName} + {startName} - {reservation.end.shortName} + {endName} - ) : ( - - {title} - ); + } return ( ; +const renderItem = ({item}: {item: TripReservationUtils.ReservationData}) => ; function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); @@ -112,31 +117,22 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`); - const tripTransactions = ReportUtils.getTripTransactions(chatReport?.iouReportID, 'reportID'); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const tripTransactions = ReportUtils.getTripTransactions(chatReport?.reportID); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : ''; const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport); + const currency = iouReport?.currency ?? chatReport?.currency; const displayAmount = useMemo(() => { if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, currency); } - // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") - let displayAmountValue = ''; - const actionMessage = getReportActionText(action) ?? ''; - const splits = actionMessage.split(' '); - - splits.forEach((split) => { - if (!/\d/.test(split)) { - return; - } - - displayAmountValue = split; - }); - - return displayAmountValue; - }, [action, iouReport?.currency, totalDisplaySpend]); + return CurrencyUtils.convertToDisplayString( + tripTransactions.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0), + currency, + ); + }, [currency, totalDisplaySpend, tripTransactions]); return ( diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 09315bfb8a8e..bb20b4abae11 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -7,7 +7,6 @@ import {PickerAvoidingView} from 'react-native-picker-select'; import type {EdgeInsets} from 'react-native-safe-area-context'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; -import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; @@ -158,18 +157,11 @@ function ScreenWrapper( const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {initialHeight} = useInitialDimensions(); const styles = useThemeStyles(); - const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; - const isKeyboardShown = keyboardState?.isKeyboardShown ?? false; - - const isKeyboardShownRef = useRef(false); - - // eslint-disable-next-line react-compiler/react-compiler - isKeyboardShownRef.current = keyboardState?.isKeyboardShown ?? false; const route = useRoute(); const shouldReturnToOldDot = useMemo(() => { @@ -191,7 +183,7 @@ function ScreenWrapper( PanResponder.create({ onMoveShouldSetPanResponderCapture: (_e, gestureState) => { const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy); - const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile(); + const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && Browser.isMobile(); return isHorizontalSwipe && shouldDismissKeyboard; }, @@ -221,7 +213,7 @@ function ScreenWrapper( // described here https://reactnavigation.org/docs/preventing-going-back/#limitations const beforeRemoveSubscription = shouldDismissKeyboardBeforeClose ? navigation.addListener('beforeRemove', () => { - if (!isKeyboardShownRef.current) { + if (!Keyboard.isVisible()) { return; } Keyboard.dismiss(); diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 21a5832052c0..a78845f126d2 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -1,4 +1,3 @@ -import {useIsFocused} from '@react-navigation/native'; import React, {useMemo, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -9,8 +8,6 @@ import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; -import {useProductTrainingContext} from '@components/ProductTrainingContext'; -import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -58,11 +55,6 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const isFocused = useIsFocused(); - const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP, - isFocused, - ); const {status, hash} = queryJSON; @@ -356,25 +348,12 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { shouldUseStyleUtilityForAnchorPosition /> ) : ( - -