diff --git a/.github/actions/composite/setupGitForOSBotifyApp/action.yml b/.github/actions/composite/setupGitForOSBotifyApp/action.yml
index 94ea94d27505..7a90cc45257d 100644
--- a/.github/actions/composite/setupGitForOSBotifyApp/action.yml
+++ b/.github/actions/composite/setupGitForOSBotifyApp/action.yml
@@ -29,9 +29,9 @@ runs:
shell: bash
run: |
if [[ -f .github/workflows/OSBotify-private-key.asc.gpg ]]; then
- echo "::set-output name=key_exists::true"
+ echo "key_exists=true" >> "$GITHUB_OUTPUT"
fi
-
+
- name: Checkout
uses: actions/checkout@v4
if: steps.key_check.outputs.key_exists != 'true'
diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml
index a89b1d3827dc..b55354b95571 100644
--- a/.github/workflows/deployBlocker.yml
+++ b/.github/workflows/deployBlocker.yml
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4
- name: Give the issue/PR the Hourly, Engineering labels
- run: gh issue edit --add-label 'Engineering,Hourly' --remove-label 'Daily,Weekly,Monthly'
+ run: gh issue edit ${{ github.event.issue.number }} --add-label 'Engineering,Hourly' --remove-label 'Daily,Weekly,Monthly'
env:
GITHUB_TOKEN: ${{ github.token }}
diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml
index 84735e95e0e9..efdcf41b63d5 100644
--- a/docs/_data/_routes.yml
+++ b/docs/_data/_routes.yml
@@ -88,18 +88,18 @@ platforms:
image: /assets/images/settings-new-dot.svg
hubs:
- - href: getting-started
- title: Getting Started
- icon: /assets/images/accounting.svg
- description: From setting up your account to ensuring you get the most out of Expensify’s suite of features, click here to get started on streamlining your expense management journey.
-
+ - href: chat
+ title: Chat
+ icon: /assets/images/chat-bubble.svg
+ description: Enhance your financial experience using Expensify's chat feature, offering quick and secure communication for personalized support and payment transfers.
+
- href: account-settings
title: Account Settings
icon: /assets/images/gears.svg
description: Discover how to personalize your profile, add secondary logins, and grant delegated access to employees with our comprehensive guide on Account Settings.
- - href: bank-accounts-and-credit-cards
- title: Bank Accounts & Credit Cards
+ - href: bank-accounts
+ title: Bank Accounts
icon: /assets/images/bank-card.svg
description: Find out how to connect Expensify to your financial institutions, track credit card transactions, and best practices for reconciling company cards.
@@ -108,11 +108,6 @@ platforms:
icon: /assets/images/money-wings.svg
description: Here is where you can review Expensify's billing and subscription options, plan types, and payment methods.
- - href: expense-and-report-features
- title: Expense & Report Features
- icon: /assets/images/money-receipt.svg
- description: From enabling automatic expense auditing to tracking attendees, here is where you can review tips and tutorials to streamline expense management.
-
- href: expensify-card
title: Expensify Card
icon: /assets/images/hand-card.svg
@@ -128,21 +123,6 @@ platforms:
icon: /assets/images/money-into-wallet.svg
description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time.
- - href: insights-and-custom-reporting
- title: Insights & Custom Reporting
- icon: /assets/images/monitor.svg
- description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options.
-
- - href: integrations
- title: Integrations
- icon: /assets/images/workflow.svg
- description: Enhance Expensify’s capabilities by integrating it with your accounting or HR software. Here is where you can learn more about creating a synchronized financial management ecosystem.
-
- - href: manage-employees-and-report-approvals
- title: Manage Employees & Report Approvals
- icon: /assets/images/envelope-receipt.svg
- description: Master the art of overseeing employees and reports by utilizing Expensify’s automation features and approval workflows.
-
- href: send-payments
title: Send Payments
icon: /assets/images/money-wings.svg
diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss
index 46434787d6df..7a0804b0f962 100644
--- a/docs/_sass/_main.scss
+++ b/docs/_sass/_main.scss
@@ -657,6 +657,7 @@ button {
p.description {
padding: 0;
+ color: $color-text-supporting;
&.with-min-height {
min-height: 68px;
diff --git a/docs/articles/new-expensify/account-settings/Coming-Soon.md b/docs/articles/new-expensify/account-settings/Coming-Soon.md
deleted file mode 100644
index 6b85bb0364b5..000000000000
--- a/docs/articles/new-expensify/account-settings/Coming-Soon.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
diff --git a/docs/articles/new-expensify/getting-started/Security.md b/docs/articles/new-expensify/account-settings/Security.md
similarity index 100%
rename from docs/articles/new-expensify/getting-started/Security.md
rename to docs/articles/new-expensify/account-settings/Security.md
diff --git a/docs/articles/new-expensify/bank-accounts-and-credit-cards/Coming-Soon.md b/docs/articles/new-expensify/bank-accounts-and-credit-cards/Coming-Soon.md
deleted file mode 100644
index 6b85bb0364b5..000000000000
--- a/docs/articles/new-expensify/bank-accounts-and-credit-cards/Coming-Soon.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
diff --git a/docs/articles/new-expensify/bank-accounts-and-credit-cards/Connect-a-Bank-Account.md b/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md
similarity index 100%
rename from docs/articles/new-expensify/bank-accounts-and-credit-cards/Connect-a-Bank-Account.md
rename to docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md
diff --git a/docs/articles/new-expensify/getting-started/Referral-Program.md b/docs/articles/new-expensify/billing-and-plan-types/Referral-Program.md
similarity index 100%
rename from docs/articles/new-expensify/getting-started/Referral-Program.md
rename to docs/articles/new-expensify/billing-and-plan-types/Referral-Program.md
diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md
similarity index 97%
rename from docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md
rename to docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md
index 17c7a60b8e5a..5128484adc9d 100644
--- a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Admins.md
+++ b/docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md
@@ -1,7 +1,6 @@
---
title: Expensify Chat for Admins
description: Best Practices for Admins settings up Expensify Chat
-redirect_from: articles/other/Expensify-Chat-For-Admins/
---
# Overview
diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Attendees.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md
similarity index 100%
rename from docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Attendees.md
rename to docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md
diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Speakers.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md
similarity index 100%
rename from docs/articles/new-expensify/getting-started/chat/Expensify-Chat-For-Conference-Speakers.md
rename to docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md
diff --git a/docs/articles/new-expensify/getting-started/chat/Expensify-Chat-Playbook-For-Conferences.md b/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md
similarity index 100%
rename from docs/articles/new-expensify/getting-started/chat/Expensify-Chat-Playbook-For-Conferences.md
rename to docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md
diff --git a/docs/articles/new-expensify/getting-started/chat/Get-to-know-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md
similarity index 99%
rename from docs/articles/new-expensify/getting-started/chat/Get-to-know-Expensify-Chat.md
rename to docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md
index da78061027fa..669d960275e6 100644
--- a/docs/articles/new-expensify/getting-started/chat/Get-to-know-Expensify-Chat.md
+++ b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md
@@ -1,5 +1,5 @@
---
-title: Get to know Expensify Chat
+title: Introducing Expensify Chat
description: Everything you need to know about Expensify Chat!
redirect_from: articles/other/Everything-About-Chat/
---
diff --git a/docs/articles/new-expensify/expense-and-report-features/Coming-Soon.md b/docs/articles/new-expensify/expense-and-report-features/Coming-Soon.md
deleted file mode 100644
index 6b85bb0364b5..000000000000
--- a/docs/articles/new-expensify/expense-and-report-features/Coming-Soon.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
diff --git a/docs/articles/new-expensify/insights-and-custom-reporting/Coming-Soon.md b/docs/articles/new-expensify/insights-and-custom-reporting/Coming-Soon.md
deleted file mode 100644
index 6b85bb0364b5..000000000000
--- a/docs/articles/new-expensify/insights-and-custom-reporting/Coming-Soon.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
diff --git a/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md
deleted file mode 100644
index 7a0717eeb5d1..000000000000
--- a/docs/articles/new-expensify/integrations/accounting-integrations/QuickBooks-Online.md
+++ /dev/null
@@ -1,320 +0,0 @@
----
-title: The QuickBooks Online Integration
-description: Expensify's integration with QuickBooks Online streamlines your expense management.
-
----
-# Overview
-
-The Expensify integration with QuickBooks Online brings in your expense accounts and other data and even exports reports directly to QuickBooks for easy reconciliation. Plus, with advanced features in QuickBooks Online, you can fine-tune coding settings in Expensify for automated data export to optimize your accounting workflow.
-
-## Before connecting
-
-It's crucial to understand the requirements based on your specific QuickBooks subscription:
-
-- While all the features are available in Expensify, their accessibility may vary depending on your QuickBooks Online subscription.
-- An error will occur if you try to export to QuickBooks with a feature enabled that isn't part of your subscription.
-- Please be aware that Expensify does not support the Self-Employed subscription in QuickBooks Online.
-
-# How to connect to QuickBooks Online
-
-## Step 1: Setup employees in QuickBooks Online
-
-Employees must be set up as either Vendors or Employees in QuickBooks Online. Make sure to include the submitter's email in their record.
-
-If you use vendor records, you can export as Vendor Bills, Checks, or Journal Entries. If you use employee records, you can export as Checks or Journal Entries (if exporting against a liability account).
-
-Additional Options for Streamlined Setup:
-
-- Automatic Vendor Creation: Enable “Automatically Create Entities” in your connection settings to automatically generate Vendor or Employee records upon export for submitters that don't already exist in QBO.
-- Employee Setup Considerations: If setting up submitters as Employees, ensure you activate QuickBooks Online Payroll. This will grant access to the Employee Profile tab to input employee email addresses.
-
-## Step 2: Connect Expensify and QuickBooks Online
-
-- Navigate to Settings > Workspaces > Group > [Workspace Name] > Connections > QuickBooks Online. Click Connect to QuickBooks.
-- Enter your QuickBooks Online Administrator’s login information and choose the QuickBooks Online Company File you want to connect to Expensify (you can connect one Company File per Workspace). Then Click Authorize.
-- Enter your QuickBooks Online Administrator’s login information and choose the QuickBooks Online Company File you want to connect to Expensify (you can connect one Company File per Workspace):
-
-Exporting Historical Reports to QuickBooks Online:
-
-After connecting QuickBooks Online to Expensify, you may receive a prompt to export all historical reports from Expensify. To export multiple reports at once, follow these steps:
-
-a. Go to the Reports page on the web.
-
-b. Tick the checkbox next to the reports you want to export.
-
-c. Click 'Export To' and select 'QuickBooks Online' from the drop-down list.
-
-If you don't want to export specific reports, click “Mark as manually entered” on the report.
-
-# How to configure export settings for QuickBooks Online
-
-Our QuickBooks Online integration offers a range of features. This section will focus on Export Settings and how to set them up.
-
-## Preferred Exporter
-
-Any Workspace admin can export to your accounting integration, but the Preferred Exporter can be chosen to automate specific steps. You can set this role from Settings > Workspaces > Group > [Workspace Name] > Connections > Configure > Export > Preferred Exporter.
-
-The Preferred Exporter:
-
-- Is the user whose Concierge performs all automated exports on behalf of.
-- Is the only user who will see reports awaiting export in their **Home.**
-- Must be a **Domain Admin** if you have set individual GL accounts for Company Card export.
-- Must be a **Domain Admin** if this is the Preferred Workspace for any Expensify Card domain using Automatic Reconciliation.
-
-## Date
-
-When exporting reports to QuickBooks Online, you can choose the report's **submitted date**, the report's **exported date**, or the **date of the last expense on the report.**
-
-Most export options (Check, Journal Entry, and Vendor Bill) will create a single itemized entry with one date.
-Please note that if you choose a Credit Card or Debit Card for non-reimbursable expenses, we'll use the transaction date on each expense during export.
-
-# Reimbursable expenses
-
-Reimbursable expenses export to QuickBooks Online as:
-
-- Vendor Bills
-- Checks
-- Journal Entries
-
-## Vendor bill (recommended)
-
-This is a single itemized vendor bill for each Expensify report. If the accounting period is closed, we will post the vendor bill on the first day of the next open period. If you export as Vendor Bills, you can also choose to Sync reimbursed reports (set on the Advanced tab). **An A/P account is required to export to a vendor bill. Here is a screenshot of how your expenses map in QuickBooks.**
-
-The submitter will be listed as the vendor in the vendor bill.
-
-## Check
-
-This is a single itemized check for each Expensify report. You can mark a check to be printed later in QuickBooks Online.
-
-## Journal entry
-
-This is a single itemized journal entry for each Expensify report.
-
-# Non-reimbursable expenses
-
-Non-reimbursable expenses export to QuickBooks Online as:
-
-- Credit Card expenses
-- Debit Card Expenses
-- Vendor Bills
-
-## Credit/debit card
-
-Using Credit/Debit Card Transactions:
-
-- Each expense will be exported as a bank transaction with its transaction date.
-- If you split an expense in Expensify, we'll consolidate it into a single credit card transaction in QuickBooks with multiple line items posted to the corresponding General Ledger accounts.
-
-Pro-Tip: To ensure the payee field in QuickBooks Online reflects the merchant name for Credit Card expenses, ensure there's a matching Vendor in QuickBooks Online. Expensify checks for an exact match during export. If none are found, the payee will be mapped to a vendor we create and labeled as Credit Card Misc. or Debit Card Misc.
-
-If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in QuickBooks.
-
-## Vendor Bill
-
-- A single detailed vendor bill is generated for each Expensify report. If the accounting period is closed, the vendor bill will be posted on the first day of the next open period. If you choose to export non-reimbursable expenses as Vendor Bills, you can assign a default vendor to the bill.
-- The export will use your default vendor if you have Default Vendor enabled. If the Default Vendor is disabled, the report's submitter will be set as the Vendor in QuickBooks.
-
-Billable Expenses:
-
-- In Expensify, you can designate expenses as billable. These will be exported to QuickBooks Online with the billable flag. - This feature applies only to expenses exported as Vendor Bills or Checks. To maximize this functionality, ensure that any billable expense is associated with a Customer/Job.
-
-## Export Invoices
-
-If you are creating Invoices in Expensify and exporting these to QuickBooks Online, this is the account the invoice will appear against.
-
-# Configure coding for QuickBooks Online
-
-The coding tab is where your information is configured for Expensify; this will allow employees to code expenses and reports accurately.
-
-- Categories
-- Classes and/or Customers/Projects
-- Locations
-- Items
-- Tax
-
-## Categories
-
-QuickBooks Online expense accounts will be automatically imported into Expensify as Categories.
-
-## Account Import
-
-Equity type accounts will also be imported as categories.
-
-Important notes:
-
-- Other Current Liabilities can only be exported as Journal Entries if the submitter is set up as an Employee in QuickBooks.
-- Exchange Gain or Loss detail type does not import.
-
-Recommended steps to take after importing the expense accounts from QuickBooks to Expensify:
-
-- Go to Settings > Workspaces > Groups > [Workspace Name] > Categories to see the accounts imported from QuickBooks Online.
-- Use the enable/disable button to choose which Categories to make available to your employees, and set Category specific rules via the blue settings cog.
-- If necessary, edit the names of imported Categories to make expense coding easier for your employees. (Please Note: If you make any changes to these accounts in QuickBooks Online, the category names on Expensify's side will revert to match the name of the account in QuickBooks Online the next time you sync).
-- If you use Items in QuickBooks Online, you can import them into Expensify as Categories.
-
-Please note that each expense has to have a category selected to export to QuickBooks Online. The chosen category has to be imported from QuickBooks Online and cannot be manually created within the Workspace settings.
-
-## Classes and Customers/Projects
-
-If you use Classes or Customers/Projects in QuickBooks Online, you can import those into Expensify as Tags or Report Fields:
-
-- Tags let you apply a Class and/or Customer/Project to each expense
-- Report Fields enables you to apply a Class and/or Customer/Project to all expenses on a report.
-
-Note: Although Projects can be imported into Expensify and coded to expenses, due to the limitations of the QuickBooks API, expenses cannot be created within the Projects module in QuickBooks.
-
-## Locations
-
-Locations can be imported into Expensify as a Report Field or, if you export reimbursable expenses as Journal Entries and non-reimbursable expenses as Credit/Debit Card, you can import Locations as Tags.
-
-## Items
-
-If you use Items in QuickBooks Online, you can import Items defined with Purchasing Information (with or without Sales Information) into Expensify as Categories.
-## Tax
-
-- Using our tax tracking feature, you can assign a tax rate and amount to each expense.
--To activate tax tracking, go to connection configuration and enable it. This will automatically import purchasing taxes from QuickBooks Online into Expensify.
-- After the connection is set, navigate to Settings > Worspaces > Groups > Workspace Name] > Tax. Here, you can view the taxes imported from QuickBooks Online.
-- Use the enable/disable button to choose which taxes are accessible to your employees.
-- Set a default tax for the Company Workspace, which will automatically apply to all new expenses.
-- Please note that, at present, tax cannot be exported to Journal Entries in QuickBooks Online.
-- Expensify performs a daily sync to ensure your information is up-to-date. This minimizes errors from outdated QuickBooks Online data and saves you time on syncing.
-
-# How to configure advanced settings for QuickBooks Online
-
-The advanced settings are where functionality for automating and customizing the QuickBooks Online integration can be enabled.
-Navigate to this section of your Workspace by following Settings > Workspaces > Group > [Workspace Name] > Connections > Configure button > Advanced tab.
-## Auto Sync
-With QuickBooks Online auto-sync, once a non-reimbursable report is final approved in Expensify, it's automatically queued for export to QuickBooks Online. For expenses eligible for reimbursement with a linked business bank account, they'll sync when marked as reimbursed.
-
-## Newly Imported Categories
-
-This setting determines the default status of newly imported categories from QuickBooks Online to Expensify, either enabled or disabled.
-
-## Invite Employees
-
-Enabling this automatically invites all Employees from QuickBooks Online to the connected Expensify Company Workspace. If not, you can manually invite or import them using a CSV file.
-
-## Automatically Create Entities
-
-When exporting reimbursable expenses as Vendor Bills or Journal Entries, Expensify will automatically create a vendor in QuickBooks if one doesn't exist. It will also generate a customer when exporting Invoices.
-
-## Sync Reimbursed Reports
-
-Enabling this marks the Vendor Bill as paid in QuickBooks Online when you reimburse a report via ACH direct deposit in Expensify. If reimbursing outside Expensify, marking the Vendor Bill as paid will automatically in QuickBooks Online update the report as reimbursed in Expensify. Note: After enabling this feature, select your QuickBooks Account in the drop-down, indicating the bank account for reimbursements.
-
-## Collection Account
-
-If you are exporting Invoices from Expensify to Quickbooks Online, this is the account the Invoice will appear against once marked as Paid.
-
-# Deep Dive
-
-## Preventing Duplicate Transactions in QuickBooks
-
-When importing a banking feed directly into QuickBooks Online while also importing transactions from Expensify, it's possible to encounter duplicate entries in QuickBooks. To prevent this, follow these steps:
-
-Step 1: Complete the Approval Process in Expensify
-
-- Before exporting any expenses to QuickBooks Online, ensure they are added to a report and the report receives approval. Depending on your Workspace setup, reports may require approval from one or more individuals. The approval process concludes when the last user who views the report selects "Final Approve."
-
-Step 2: Exporting Reports to QuickBooks Online
-
-- To ensure expenses exported from Expensify match seamlessly in the QuickBooks Banking platform, make sure these expenses are marked as non-reimbursable within Expensify and that “Credit Card” is selected as the non-reimbursable export option for your expenses.
-
-Step 3: Importing Your Credit Card Transactions into QuickBooks Online
-
-- After completing Steps 1 and 2, you can import your credit card transactions into QuickBooks Online. These imported banking transactions will align with the ones brought in from Expensify. QuickBooks Online will guide you through the process of matching these transactions, similar to the example below:
-
-## Tax in QuickBooks Online
-
-If your country applies taxes on sales (like GST, HST, or VAT), you can utilize Expensify's Tax Tracking along with your QuickBooks Online tax rates. Please note: Tax Tracking is not available for Workspaces linked to the US version of QuickBooks Online. If you need assistance applying taxes after reports are exported, contact QuickBooks.
-
-To get started:
-
-- Go to Settings > Workpaces > Group > [Workspace Name] > Connections, and click Configure.
-- Navigate to the Coding tab.
-- Turn on 'T.''.
-- Click Save. This imports the Tax Name and rate from QuickBooks Online.
-- Visit Settings > Workspaces > Group > [Workspace Name] > Tax to view the imported taxes.
-- Use the enable/disable button in the Tax tab to choose which taxes your employees can use.
-
-Remember, you can also set a default tax rate for the entire Workspace. This will be automatically applied to all new expenses. The user can still choose a different tax rate for each expense.
-
-Tax information can't be sent to Journal Entries in QuickBooks Online. Also, when dealing with multiple tax rates, where one receipt has different tax rates (like in the EU, UK, and Canada), users should split the expense into the respective parts and set the appropriate tax rate for each part.
-
-## Multi-currency
-
-When working with QuickBooks Online Multi-Currency, there are some things to remember when exporting Vendor Bills and Check! Make sure the vendor's currency and the Accounts Payable (A/P) bank account match.
-
-In QuickBooks Online, the currency conversion rates are not applied when exporting. All transactions will be exported with a 1:1 conversion rate, so for example, if a vendor's currency is CAD (Canadian Dollar) and the home currency is USD (US Dollar), the export will show these currencies without applying conversion rates.
-
-To correct this, you must manually update the conversion rate after the report has been exported to QuickBooks Online.
-
-Specifically for Vendor Bills:
-
-If multi-currency is enabled and the Vendor's currency is different from the Workspace currency, OR if QuickBooks Online home currency is foreign from the Workspace currency, then:
-
-- We create the Vendor Bill in the Vendor's currency (this is a QuickBooks Online requirement - we don't have a choice)
-- We set the exchange rate between the home currency and the Vendor's currency
-- We convert line item amounts to the vendor's currency
-
-Let's consider this example:
-
-- QuickBooks Online home currency is USD
-- Vendor's currency is VND
-- Workspace (report) currency is JPY
-
-Upon export, we:
-
-1. Specified the bill is in VND
-2. Set the exchange rate between VND and USD (home currency), computed at the time of export.
-3. Converted line items from JPY (currency in Expensify) to VND
-4. QuickBooks Online automatically computed the USD amount (home currency) based on the exchange rate we specified
-5. Journal Entries, Credit Card, and Debit Card:
-
-Multi-currency exports will fail as the account currency must match both the vendor and home currencies.
-
-## Report Fields
-
-Report fields are a handy way to collect specific information for a report tailored to your organization's needs. They can specify a project, business trip, client, location, and more!
-
-When integrating Expensify with Your Accounting Software, you can create your report fields in your accounting software so the next time you sync your Workspace, these fields will be imported into Expensify.
-
-To select how a specific field imports to Expensify, head to Settings > Workspaces > Group >
-[Workspace Name] > Connections > Accounting Integrations > QuickBooks Online > Configure > Coding.
-
-Here are the QuickBooks Online fields that can be mapped as a report field within Expensify:
-
-- Classes
-- Customers/Projects
-- Locations
-
-# FAQ
-
-## What happens if the report can't be exported to QuickBooks Online automatically?
-
-If a report encounters an issue during automatic export to QuickBooks Online, you'll receive an email with details about the problem, including any specific error messages. These messages will also be recorded in the report's history section.
-
-The report will be placed in your Home for your attention. You can address the issues there. If you need further assistance, refer to our QuickBooks Online Export Errors page or export the report manually.
-
-## How can I ensure that I final approve reports before they're exported to QuickBooks Online?
-
-To ensure reports are reviewed before export, set up your Workspaces with the appropriate workflow in Expensify. Additionally, consider changing your Workspace settings to enforce expense Workspace workflows strictly. This guarantees that your Workspace's workflow is consistently followed.
-
-## What happens to existing approved and reimbursed reports if I enable Auto Sync?
-
-- If Auto Sync was disabled when your Workspace was linked to QuickBooks Online, enabling it won't impact existing reports that haven't been exported.
-- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in QuickBooks Online during the next sync.
-- If a report has been exported and marked as paid in QuickBooks Online, it will be automatically marked as reimbursed in Expensify during the next sync.
-- Reports that have yet to be exported to QuickBooks Online won't be automatically exported.
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/articles/new-expensify/manage-employees-and-report-approvals/Coming-Soon.md b/docs/articles/new-expensify/manage-employees-and-report-approvals/Coming-Soon.md
deleted file mode 100644
index 6b85bb0364b5..000000000000
--- a/docs/articles/new-expensify/manage-employees-and-report-approvals/Coming-Soon.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
diff --git a/docs/assets/images/chat-bubble.svg b/docs/assets/images/chat-bubble.svg
new file mode 100644
index 000000000000..afa13dc39820
--- /dev/null
+++ b/docs/assets/images/chat-bubble.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/docs/new-expensify/hubs/bank-accounts-and-credit-cards/index.html b/docs/new-expensify/hubs/bank-accounts/index.html
similarity index 100%
rename from docs/new-expensify/hubs/bank-accounts-and-credit-cards/index.html
rename to docs/new-expensify/hubs/bank-accounts/index.html
diff --git a/docs/new-expensify/hubs/chat/index.html b/docs/new-expensify/hubs/chat/index.html
new file mode 100644
index 000000000000..9fa1f1547c0f
--- /dev/null
+++ b/docs/new-expensify/hubs/chat/index.html
@@ -0,0 +1,6 @@
+---
+layout: default
+title: Chat
+---
+
+{% include hub.html %}
diff --git a/docs/new-expensify/hubs/expense-and-report-features/index.html b/docs/new-expensify/hubs/expense-and-report-features/index.html
deleted file mode 100644
index 0057ae0fa46c..000000000000
--- a/docs/new-expensify/hubs/expense-and-report-features/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: default
-title: Expense and Report Features
----
-
-{% include hub.html %}
\ No newline at end of file
diff --git a/docs/new-expensify/hubs/getting-started/chat.html b/docs/new-expensify/hubs/getting-started/chat.html
deleted file mode 100644
index 86641ee60b7d..000000000000
--- a/docs/new-expensify/hubs/getting-started/chat.html
+++ /dev/null
@@ -1,5 +0,0 @@
----
-layout: default
----
-
-{% include section.html %}
diff --git a/docs/new-expensify/hubs/getting-started/index.html b/docs/new-expensify/hubs/getting-started/index.html
deleted file mode 100644
index 14ca13d0c2e8..000000000000
--- a/docs/new-expensify/hubs/getting-started/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: default
-title: Getting Started
----
-
-{% include hub.html %}
\ No newline at end of file
diff --git a/docs/new-expensify/hubs/insights-and-custom-reporting/index.html b/docs/new-expensify/hubs/insights-and-custom-reporting/index.html
deleted file mode 100644
index 16c96cb51d01..000000000000
--- a/docs/new-expensify/hubs/insights-and-custom-reporting/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: default
-title: Exports
----
-
-{% include hub.html %}
\ No newline at end of file
diff --git a/docs/new-expensify/hubs/integrations/accounting-integrations.html b/docs/new-expensify/hubs/integrations/accounting-integrations.html
deleted file mode 100644
index 86641ee60b7d..000000000000
--- a/docs/new-expensify/hubs/integrations/accounting-integrations.html
+++ /dev/null
@@ -1,5 +0,0 @@
----
-layout: default
----
-
-{% include section.html %}
diff --git a/docs/new-expensify/hubs/integrations/index.html b/docs/new-expensify/hubs/integrations/index.html
deleted file mode 100644
index d1f173534c8a..000000000000
--- a/docs/new-expensify/hubs/integrations/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: default
-title: Integrations
----
-
-{% include hub.html %}
\ No newline at end of file
diff --git a/docs/new-expensify/hubs/manage-employees-and-report-approvals/index.html b/docs/new-expensify/hubs/manage-employees-and-report-approvals/index.html
deleted file mode 100644
index 31e992f32d5d..000000000000
--- a/docs/new-expensify/hubs/manage-employees-and-report-approvals/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: default
-title: Manage Employees & Report Approvals
----
-
-{% include hub.html %}
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index 55beabb8e85e..b43d75e5cb80 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -218,9 +218,8 @@ const CONST = {
REGEX: {
US_ACCOUNT_NUMBER: /^[0-9]{4,17}$/,
- // If the account number length is from 4 to 13 digits, we show the last 4 digits and hide the rest with X
- // If the length is longer than 13 digits, we show the first 6 and last 4 digits, hiding the rest with X
- MASKED_US_ACCOUNT_NUMBER: /^[X]{0,9}[0-9]{4}$|^[0-9]{6}[X]{4,7}[0-9]{4}$/,
+ // The back-end is always returning account number with 4 last digits and mask the rest with X
+ MASKED_US_ACCOUNT_NUMBER: /^[X]{0,13}[0-9]{4}$/,
SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/,
},
VERIFICATION_MAX_ATTEMPTS: 7,
@@ -2880,6 +2879,8 @@ const CONST = {
*/
ADDITIONAL_ALLOWED_CHARACTERS: 20,
+ /** types that will show a virtual keyboard in a mobile browser */
+ INPUT_TYPES_WITH_KEYBOARD: ['text', 'search', 'tel', 'url', 'email', 'password'],
/**
* native IDs for close buttons in Overlay component
*/
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index f7de8cfab4b6..afc368858f55 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -2,15 +2,20 @@
* This is a file containing constants for all of the screen names. In most cases, we should use the routes for
* navigation. But there are situations where we may need to access screen names directly.
*/
-export default {
+
+const PROTECTED_SCREENS = {
HOME: 'Home',
+ CONCIERGE: 'Concierge',
+ REPORT_ATTACHMENTS: 'ReportAttachments',
+} as const;
+
+export default {
+ ...PROTECTED_SCREENS,
LOADING: 'Loading',
REPORT: 'Report',
- REPORT_ATTACHMENTS: 'ReportAttachments',
NOT_FOUND: 'not-found',
TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps',
VALIDATE_LOGIN: 'ValidateLogin',
- CONCIERGE: 'Concierge',
SETTINGS: {
ROOT: 'Settings_Root',
PREFERENCES: 'Settings_Preferences',
@@ -28,3 +33,5 @@ export default {
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
SAML_SIGN_IN: 'SAMLSignIn',
} as const;
+
+export {PROTECTED_SCREENS};
diff --git a/src/components/ExpensifyWordmark.js b/src/components/ExpensifyWordmark.tsx
similarity index 55%
rename from src/components/ExpensifyWordmark.js
rename to src/components/ExpensifyWordmark.tsx
index efb3b20dbe87..45c0c9bcef1e 100644
--- a/src/components/ExpensifyWordmark.js
+++ b/src/components/ExpensifyWordmark.tsx
@@ -1,7 +1,5 @@
-import PropTypes from 'prop-types';
import React from 'react';
-import {View} from 'react-native';
-import _ from 'underscore';
+import {StyleProp, View, ViewStyle} from 'react-native';
import AdHocLogo from '@assets/images/expensify-logo--adhoc.svg';
import DevLogo from '@assets/images/expensify-logo--dev.svg';
import StagingLogo from '@assets/images/expensify-logo--staging.svg';
@@ -12,40 +10,36 @@ import useTheme from '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
-import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
+import withWindowDimensions from './withWindowDimensions';
+import type {WindowDimensionsProps} from './withWindowDimensions/types';
-const propTypes = {
+type ExpensifyWordmarkProps = WindowDimensionsProps & {
/** Additional styles to add to the component */
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
- ...windowDimensionsPropTypes,
-};
-
-const defaultProps = {
- style: {},
+ style?: StyleProp;
};
const logoComponents = {
[CONST.ENVIRONMENT.DEV]: DevLogo,
[CONST.ENVIRONMENT.STAGING]: StagingLogo,
[CONST.ENVIRONMENT.PRODUCTION]: ProductionLogo,
+ [CONST.ENVIRONMENT.ADHOC]: AdHocLogo,
};
-function ExpensifyWordmark(props) {
+function ExpensifyWordmark({isSmallScreenWidth, style}: ExpensifyWordmarkProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {environment} = useEnvironment();
// PascalCase is required for React components, so capitalize the const here
+ const LogoComponent = environment ? logoComponents[environment] : AdHocLogo;
- const LogoComponent = logoComponents[environment] || AdHocLogo;
return (
<>
@@ -55,6 +49,5 @@ function ExpensifyWordmark(props) {
}
ExpensifyWordmark.displayName = 'ExpensifyWordmark';
-ExpensifyWordmark.defaultProps = defaultProps;
-ExpensifyWordmark.propTypes = propTypes;
+
export default withWindowDimensions(ExpensifyWordmark);
diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js
index 5e77947187e9..0d300c5e2179 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.js
+++ b/src/components/LHNOptionsList/LHNOptionsList.js
@@ -1,7 +1,8 @@
+import {FlashList} from '@shopify/flash-list';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
-import {FlatList, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import participantPropTypes from '@components/participantPropTypes';
@@ -11,6 +12,7 @@ import compose from '@libs/compose';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import reportPropTypes from '@pages/reportPropTypes';
+import stylePropTypes from '@styles/stylePropTypes';
import useThemeStyles from '@styles/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -19,12 +21,10 @@ import OptionRowLHNData from './OptionRowLHNData';
const propTypes = {
/** Wrapper style for the section list */
- // eslint-disable-next-line react/forbid-prop-types
- style: PropTypes.arrayOf(PropTypes.object),
+ style: stylePropTypes,
/** Extra styles for the section list container */
- // eslint-disable-next-line react/forbid-prop-types
- contentContainerStyles: PropTypes.arrayOf(PropTypes.object).isRequired,
+ contentContainerStyles: stylePropTypes.isRequired,
/** Sections for the section list */
data: PropTypes.arrayOf(PropTypes.string).isRequired,
@@ -80,7 +80,7 @@ const defaultProps = {
...withCurrentReportIDDefaultProps,
};
-const keyExtractor = (item) => item;
+const keyExtractor = (item) => `report_${item}`;
function LHNOptionsList({
style,
@@ -99,28 +99,6 @@ function LHNOptionsList({
currentReportID,
}) {
const styles = useThemeStyles();
- /**
- * This function is used to compute the layout of any given item in our list. Since we know that each item will have the exact same height, this is a performance optimization
- * so that the heights can be determined before the options are rendered. Otherwise, the heights are determined when each option is rendering and it causes a lot of overhead on large
- * lists.
- *
- * @param {Array} itemData - This is the same as the data we pass into the component
- * @param {Number} index the current item's index in the set of data
- *
- * @returns {Object}
- */
- const getItemLayout = useCallback(
- (itemData, index) => {
- const optionHeight = optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight;
- return {
- length: optionHeight,
- offset: index * optionHeight,
- index,
- };
- },
- [optionMode],
- );
-
/**
* Function which renders a row in the list
*
@@ -164,20 +142,17 @@ function LHNOptionsList({
return (
-
);
diff --git a/src/components/SwipeableView/index.native.tsx b/src/components/SwipeableView/index.native.tsx
index 91d851101d4e..07f1d785d0a6 100644
--- a/src/components/SwipeableView/index.native.tsx
+++ b/src/components/SwipeableView/index.native.tsx
@@ -3,30 +3,40 @@ import {PanResponder, View} from 'react-native';
import CONST from '@src/CONST';
import SwipeableViewProps from './types';
-function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
+function SwipeableView({children, onSwipeDown, onSwipeUp}: SwipeableViewProps) {
const minimumPixelDistance = CONST.COMPOSER_MAX_HEIGHT;
const oldYRef = useRef(0);
+ const directionRef = useRef<'UP' | 'DOWN' | null>(null);
+
const panResponder = useRef(
PanResponder.create({
- // The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards
- // eslint-disable-next-line @typescript-eslint/naming-convention
- onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
+ onMoveShouldSetPanResponderCapture: (event, gestureState) => {
if (gestureState.dy - oldYRef.current > 0 && gestureState.dy > minimumPixelDistance) {
+ directionRef.current = 'DOWN';
+ return true;
+ }
+
+ if (gestureState.dy - oldYRef.current < 0 && Math.abs(gestureState.dy) > minimumPixelDistance) {
+ directionRef.current = 'UP';
return true;
}
oldYRef.current = gestureState.dy;
return false;
},
- // Calls the callback when the swipe down is released; after the completion of the gesture
- onPanResponderRelease: onSwipeDown,
+ onPanResponderRelease: () => {
+ if (directionRef.current === 'DOWN' && onSwipeDown) {
+ onSwipeDown();
+ } else if (directionRef.current === 'UP' && onSwipeUp) {
+ onSwipeUp();
+ }
+ directionRef.current = null; // Reset the direction after the gesture completes
+ },
}),
).current;
- return (
- // eslint-disable-next-line react/jsx-props-no-spreading
- {children}
- );
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return {children};
}
SwipeableView.displayName = 'SwipeableView';
diff --git a/src/components/SwipeableView/index.tsx b/src/components/SwipeableView/index.tsx
index 335c3e7dcf03..478935173841 100644
--- a/src/components/SwipeableView/index.tsx
+++ b/src/components/SwipeableView/index.tsx
@@ -1,4 +1,77 @@
+import React, {useEffect, useRef} from 'react';
+import {View} from 'react-native';
+import DomUtils from '@libs/DomUtils';
import SwipeableViewProps from './types';
-// Swipeable View is available just on Android/iOS for now.
-export default ({children}: SwipeableViewProps) => children;
+// Min delta y in px to trigger swipe
+const MIN_DELTA_Y = 25;
+
+function SwipeableView({onSwipeUp, onSwipeDown, style, children}: SwipeableViewProps) {
+ const ref = useRef(null);
+ const scrollableChildRef = useRef(null);
+ const startY = useRef(0);
+ const isScrolling = useRef(false);
+
+ useEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+
+ const element = ref.current as unknown as HTMLElement;
+
+ const handleTouchStart = (event: TouchEvent) => {
+ startY.current = event.touches[0].clientY;
+ };
+
+ const handleTouchEnd = (event: TouchEvent) => {
+ const deltaY = event.changedTouches[0].clientY - startY.current;
+ const isSelecting = DomUtils.isActiveTextSelection();
+ let canSwipeDown = true;
+ let canSwipeUp = true;
+ if (scrollableChildRef.current) {
+ canSwipeUp = scrollableChildRef.current.scrollHeight - scrollableChildRef.current.scrollTop === scrollableChildRef.current.clientHeight;
+ canSwipeDown = scrollableChildRef.current.scrollTop === 0;
+ }
+
+ if (deltaY > MIN_DELTA_Y && onSwipeDown && !isSelecting && canSwipeDown && !isScrolling.current) {
+ onSwipeDown();
+ }
+
+ if (deltaY < -MIN_DELTA_Y && onSwipeUp && !isSelecting && canSwipeUp && !isScrolling.current) {
+ onSwipeUp();
+ }
+ isScrolling.current = false;
+ };
+
+ const handleScroll = (event: Event) => {
+ isScrolling.current = true;
+ if (!event.target || scrollableChildRef.current) {
+ return;
+ }
+ scrollableChildRef.current = event.target as HTMLElement;
+ };
+
+ element.addEventListener('touchstart', handleTouchStart);
+ element.addEventListener('touchend', handleTouchEnd);
+ element.addEventListener('scroll', handleScroll, true);
+
+ return () => {
+ element.removeEventListener('touchstart', handleTouchStart);
+ element.removeEventListener('touchend', handleTouchEnd);
+ element.removeEventListener('scroll', handleScroll);
+ };
+ }, [onSwipeDown, onSwipeUp]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+SwipeableView.displayName = 'SwipeableView';
+
+export default SwipeableView;
diff --git a/src/components/SwipeableView/types.ts b/src/components/SwipeableView/types.ts
index 560df7ef5a45..1f2fbcdc752c 100644
--- a/src/components/SwipeableView/types.ts
+++ b/src/components/SwipeableView/types.ts
@@ -1,11 +1,18 @@
import {ReactNode} from 'react';
+import {StyleProp, ViewStyle} from 'react-native';
type SwipeableViewProps = {
/** The content to be rendered within the SwipeableView */
children: ReactNode;
/** Callback to fire when the user swipes down on the child content */
- onSwipeDown: () => void;
+ onSwipeDown?: () => void;
+
+ /** Callback to fire when the user swipes up on the child content */
+ onSwipeUp?: () => void;
+
+ /** Style for the wrapper View, applied only for the web version. Not used by the native version, as it brakes the layout. */
+ style?: StyleProp;
};
export default SwipeableViewProps;
diff --git a/src/hooks/useBlockViewportScroll/index.native.ts b/src/hooks/useBlockViewportScroll/index.native.ts
new file mode 100644
index 000000000000..59ee34b1c9f6
--- /dev/null
+++ b/src/hooks/useBlockViewportScroll/index.native.ts
@@ -0,0 +1,15 @@
+/**
+ * A hook that blocks viewport scroll when the keyboard is visible.
+ * It does this by capturing the current scrollY position when the keyboard is shown, then scrolls back to this position smoothly on 'touchend' event.
+ * This scroll blocking is removed when the keyboard hides.
+ * This hook is doing nothing on native platforms.
+ *
+ * @example
+ * useBlockViewportScroll();
+ */
+function useBlockViewportScroll() {
+ // This hook is doing nothing on native platforms.
+ // Check index.ts for web implementation.
+}
+
+export default useBlockViewportScroll;
diff --git a/src/hooks/useBlockViewportScroll/index.ts b/src/hooks/useBlockViewportScroll/index.ts
new file mode 100644
index 000000000000..5766d59f2bdd
--- /dev/null
+++ b/src/hooks/useBlockViewportScroll/index.ts
@@ -0,0 +1,43 @@
+import {useEffect, useRef} from 'react';
+import Keyboard from '@libs/NativeWebKeyboard';
+
+/**
+ * A hook that blocks viewport scroll when the keyboard is visible.
+ * It does this by capturing the current scrollY position when the keyboard is shown, then scrolls back to this position smoothly on 'touchend' event.
+ * This scroll blocking is removed when the keyboard hides.
+ * This hook is doing nothing on native platforms.
+ *
+ * @example
+ * useBlockViewportScroll();
+ */
+function useBlockViewportScroll() {
+ const optimalScrollY = useRef(0);
+ const keyboardShowListenerRef = useRef(() => {});
+ const keyboardHideListenerRef = useRef(() => {});
+
+ useEffect(() => {
+ const handleTouchEnd = () => {
+ window.scrollTo({top: optimalScrollY.current, behavior: 'smooth'});
+ };
+
+ const handleKeybShow = () => {
+ optimalScrollY.current = window.scrollY;
+ window.addEventListener('touchend', handleTouchEnd);
+ };
+
+ const handleKeybHide = () => {
+ window.removeEventListener('touchend', handleTouchEnd);
+ };
+
+ keyboardShowListenerRef.current = Keyboard.addListener('keyboardDidShow', handleKeybShow);
+ keyboardHideListenerRef.current = Keyboard.addListener('keyboardDidHide', handleKeybHide);
+
+ return () => {
+ keyboardShowListenerRef.current();
+ keyboardHideListenerRef.current();
+ window.removeEventListener('touchend', handleTouchEnd);
+ };
+ }, []);
+}
+
+export default useBlockViewportScroll;
diff --git a/src/libs/DomUtils/index.native.ts b/src/libs/DomUtils/index.native.ts
index 0864f1a16ac0..8af83968e8d1 100644
--- a/src/libs/DomUtils/index.native.ts
+++ b/src/libs/DomUtils/index.native.ts
@@ -2,6 +2,21 @@ import GetActiveElement from './types';
const getActiveElement: GetActiveElement = () => null;
+/**
+ * Checks if there is a text selection within the currently focused input or textarea element.
+ *
+ * This function determines whether the currently focused element is an input or textarea,
+ * and if so, it checks whether there is a text selection (i.e., whether the start and end
+ * of the selection are at different positions). It assumes that only inputs and textareas
+ * can have text selections.
+ * Works only on web. Throws an error on native.
+ *
+ * @returns True if there is a text selection within the focused element, false otherwise.
+ */
+const isActiveTextSelection = () => {
+ throw new Error('Not implemented in React Native. Use only for web.');
+};
+
const requestAnimationFrame = (callback: () => void) => {
if (!callback) {
return;
@@ -12,5 +27,6 @@ const requestAnimationFrame = (callback: () => void) => {
export default {
getActiveElement,
+ isActiveTextSelection,
requestAnimationFrame,
};
diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts
index 6a2eed57fbe6..78c2cb37ccc8 100644
--- a/src/libs/DomUtils/index.ts
+++ b/src/libs/DomUtils/index.ts
@@ -2,7 +2,30 @@ import GetActiveElement from './types';
const getActiveElement: GetActiveElement = () => document.activeElement;
+/**
+ * Checks if there is a text selection within the currently focused input or textarea element.
+ *
+ * This function determines whether the currently focused element is an input or textarea,
+ * and if so, it checks whether there is a text selection (i.e., whether the start and end
+ * of the selection are at different positions). It assumes that only inputs and textareas
+ * can have text selections.
+ * Works only on web. Throws an error on native.
+ *
+ * @returns True if there is a text selection within the focused element, false otherwise.
+ */
+const isActiveTextSelection = (): boolean => {
+ const focused = document.activeElement as HTMLInputElement | HTMLTextAreaElement | null;
+ if (!focused) {
+ return false;
+ }
+ if (typeof focused.selectionStart === 'number' && typeof focused.selectionEnd === 'number') {
+ return focused.selectionStart !== focused.selectionEnd;
+ }
+ return false;
+};
+
export default {
getActiveElement,
+ isActiveTextSelection,
requestAnimationFrame: window.requestAnimationFrame.bind(window),
};
diff --git a/src/libs/NativeWebKeyboard/index.native.ts b/src/libs/NativeWebKeyboard/index.native.ts
new file mode 100644
index 000000000000..404bd58075d4
--- /dev/null
+++ b/src/libs/NativeWebKeyboard/index.native.ts
@@ -0,0 +1,3 @@
+import {Keyboard} from 'react-native';
+
+export default Keyboard;
diff --git a/src/libs/NativeWebKeyboard/index.ts b/src/libs/NativeWebKeyboard/index.ts
new file mode 100644
index 000000000000..45223d4d5b42
--- /dev/null
+++ b/src/libs/NativeWebKeyboard/index.ts
@@ -0,0 +1,136 @@
+import {Keyboard} from 'react-native';
+import CONST from '@src/CONST';
+
+type InputType = (typeof CONST.INPUT_TYPES_WITH_KEYBOARD)[number];
+type TCallbackFn = () => void;
+
+const isInputKeyboardType = (element: Element | null): boolean => {
+ if (element && ((element.tagName === 'INPUT' && CONST.INPUT_TYPES_WITH_KEYBOARD.includes((element as HTMLInputElement).type as InputType)) || element.tagName === 'TEXTAREA')) {
+ return true;
+ }
+ return false;
+};
+
+const isVisible = (): boolean => {
+ const focused = document.activeElement;
+ return isInputKeyboardType(focused);
+};
+
+const nullFn: () => null = () => null;
+
+let isKeyboardListenerRunning = false;
+let currentVisibleElement: Element | null = null;
+const showListeners: TCallbackFn[] = [];
+const hideListeners: TCallbackFn[] = [];
+const visualViewport = window.visualViewport ?? {
+ height: window.innerHeight,
+ width: window.innerWidth,
+ addEventListener: window.addEventListener.bind(window),
+ removeEventListener: window.removeEventListener.bind(window),
+};
+let previousVPHeight = visualViewport.height;
+
+const handleViewportResize = (): void => {
+ if (visualViewport.height < previousVPHeight) {
+ if (isInputKeyboardType(document.activeElement) && document.activeElement !== currentVisibleElement) {
+ showListeners.forEach((fn) => fn());
+ }
+ }
+
+ if (visualViewport.height > previousVPHeight) {
+ if (!isVisible()) {
+ hideListeners.forEach((fn) => fn());
+ }
+ }
+
+ previousVPHeight = visualViewport.height;
+ currentVisibleElement = document.activeElement;
+};
+
+const startKeboardListeningService = (): void => {
+ isKeyboardListenerRunning = true;
+ visualViewport.addEventListener('resize', handleViewportResize);
+};
+
+const addListener = (eventName: 'keyboardDidShow' | 'keyboardDidHide', callbackFn: TCallbackFn): (() => void) => {
+ if ((eventName !== 'keyboardDidShow' && eventName !== 'keyboardDidHide') || !callbackFn) {
+ throw new Error('Invalid eventName passed to addListener()');
+ }
+
+ if (eventName === 'keyboardDidShow') {
+ showListeners.push(callbackFn);
+ }
+
+ if (eventName === 'keyboardDidHide') {
+ hideListeners.push(callbackFn);
+ }
+
+ if (!isKeyboardListenerRunning) {
+ startKeboardListeningService();
+ }
+
+ return () => {
+ if (eventName === 'keyboardDidShow') {
+ showListeners.filter((fn) => fn !== callbackFn);
+ }
+
+ if (eventName === 'keyboardDidHide') {
+ hideListeners.filter((fn) => fn !== callbackFn);
+ }
+
+ if (isKeyboardListenerRunning && !showListeners.length && !hideListeners.length) {
+ visualViewport.removeEventListener('resize', handleViewportResize);
+ isKeyboardListenerRunning = false;
+ }
+ };
+};
+
+export default {
+ /**
+ * Whether the keyboard is last known to be visible.
+ */
+ isVisible,
+ /**
+ * Dismisses the active keyboard and removes focus.
+ */
+ dismiss: Keyboard.dismiss,
+ /**
+ * The `addListener` function connects a JavaScript function to an identified native
+ * keyboard notification event.
+ *
+ * This function then returns the reference to the listener.
+ *
+ * {string} eventName The `nativeEvent` is the string that identifies the event you're listening for. This
+ * can be any of the following:
+ *
+ * - `keyboardWillShow`
+ * - `keyboardDidShow`
+ * - `keyboardWillHide`
+ * - `keyboardDidHide`
+ * - `keyboardWillChangeFrame`
+ * - `keyboardDidChangeFrame`
+ *
+ * Note that if you set `android:windowSoftInputMode` to `adjustResize` or `adjustNothing`,
+ * only `keyboardDidShow` and `keyboardDidHide` events will be available on Android.
+ * `keyboardWillShow` as well as `keyboardWillHide` are generally not available on Android
+ * since there is no native corresponding event.
+ *
+ * On Web only two events are available:
+ *
+ * - `keyboardDidShow`
+ * - `keyboardDidHide`
+ *
+ * {function} callback function to be called when the event fires.
+ */
+ addListener,
+ /**
+ * Useful for syncing TextInput (or other keyboard accessory view) size of
+ * position changes with keyboard movements.
+ * Not working on web.
+ */
+ scheduleLayoutAnimation: nullFn,
+ /**
+ * Return the metrics of the soft-keyboard if visible. Currently not working on web.
+ */
+ metrics: nullFn,
+};
diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js
index e12cb5545240..7a2c61ea7b53 100644
--- a/src/libs/Navigation/Navigation.js
+++ b/src/libs/Navigation/Navigation.js
@@ -6,7 +6,7 @@ import Log from '@libs/Log';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
+import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS';
import getStateFromPath from './getStateFromPath';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
@@ -305,6 +305,57 @@ function setIsNavigationReady() {
resolveNavigationIsReadyPromise();
}
+/**
+ * Checks if the navigation state contains routes that are protected (over the auth wall).
+ *
+ * @function
+ * @param {Object} state - react-navigation state object
+ *
+ * @returns {Boolean}
+ */
+function navContainsProtectedRoutes(state) {
+ if (!state || !state.routeNames || !_.isArray(state.routeNames)) {
+ return false;
+ }
+
+ const protectedScreensName = _.values(PROTECTED_SCREENS);
+ const difference = _.difference(protectedScreensName, state.routeNames);
+
+ return !difference.length;
+}
+
+/**
+ * Waits for the navitgation state to contain protected routes specified in PROTECTED_SCREENS constant.
+ * If the navigation is in a state, where protected routes are avilable, the promise resolve immediately.
+ *
+ * @function
+ * @returns {Promise} A promise that resolves when the one of the PROTECTED_SCREENS screen is available in the nav tree.
+ *
+ * @example
+ * waitForProtectedRoutes()
+ * .then(()=> console.log('Protected routes are present!'))
+ */
+function waitForProtectedRoutes() {
+ return new Promise((resolve) => {
+ isNavigationReady().then(() => {
+ const currentState = navigationRef.current.getState();
+ if (navContainsProtectedRoutes(currentState)) {
+ resolve();
+ return;
+ }
+ let unsubscribe;
+ const handleStateChange = ({data}) => {
+ const state = lodashGet(data, 'state');
+ if (navContainsProtectedRoutes(state)) {
+ unsubscribe();
+ resolve();
+ }
+ };
+ unsubscribe = navigationRef.current.addListener('state', handleStateChange);
+ });
+ });
+}
+
export default {
setShouldPopAllStateOnUP,
canNavigate,
@@ -320,6 +371,8 @@ export default {
getTopmostReportId,
getRouteNameFromStateEvent,
getTopmostReportActionId,
+ waitForProtectedRoutes,
+ navContainsProtectedRoutes,
};
export {navigationRef};
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index ffb68203cad0..c225bdf5b65d 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -20,7 +20,6 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as Pusher from '@libs/Pusher/pusher';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
-import SidebarUtils from '@libs/SidebarUtils';
import * as UserUtils from '@libs/UserUtils';
import Visibility from '@libs/Visibility';
import CONFIG from '@src/CONFIG';
@@ -1986,7 +1985,6 @@ function toggleEmojiReaction(reportID, reportAction, reactionObject, existingRea
* @param {Boolean} isAuthenticated
*/
function openReportFromDeepLink(url, isAuthenticated) {
- const route = ReportUtils.getRouteFromLink(url);
const reportID = ReportUtils.getReportIDFromLink(url);
if (reportID && !isAuthenticated) {
@@ -2004,18 +2002,19 @@ function openReportFromDeepLink(url, isAuthenticated) {
// Navigate to the report after sign-in/sign-up.
InteractionManager.runAfterInteractions(() => {
- SidebarUtils.isSidebarLoadedReady().then(() => {
- if (route === ROUTES.CONCIERGE) {
- navigateToConciergeChat(true);
- return;
- }
- if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) {
- Navigation.isNavigationReady().then(() => {
+ Session.waitForUserSignIn().then(() => {
+ Navigation.waitForProtectedRoutes().then(() => {
+ const route = ReportUtils.getRouteFromLink(url);
+ if (route === ROUTES.CONCIERGE) {
+ navigateToConciergeChat(true);
+ return;
+ }
+ if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) {
Session.signOutAndRedirectToSignIn();
- });
- return;
- }
- Navigation.navigate(route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ return;
+ }
+ Navigation.navigate(route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ });
});
});
}
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index e884a4d7a6b3..ef998d6dac8d 100644
--- a/src/libs/actions/Task.js
+++ b/src/libs/actions/Task.js
@@ -222,7 +222,7 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail
{optimisticData, successData, failureData},
);
- Navigation.dismissModal(optimisticTaskReport.reportID);
+ Navigation.dismissModal(parentReportID);
}
/**
diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js
index dd541c68ecf4..b46d3aa0aa28 100644
--- a/src/pages/ReimbursementAccount/BankAccountManualStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js
@@ -28,6 +28,9 @@ function BankAccountManualStep(props) {
const styles = useThemeStyles();
const {translate, preferredLocale} = useLocalize();
const {reimbursementAccount, reimbursementAccountDraft} = props;
+
+ const shouldDisableInputs = Boolean(lodashGet(reimbursementAccount, 'achData.bankAccountID'));
+
/**
* @param {Object} values - form input values passed by the Form component
* @returns {Object}
@@ -41,7 +44,7 @@ function BankAccountManualStep(props) {
if (
values.accountNumber &&
!CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) &&
- !CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim())
+ !(shouldDisableInputs && CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim()))
) {
errors.accountNumber = 'bankAccount.error.accountNumber';
} else if (values.accountNumber && values.accountNumber === routingNumber) {
@@ -56,7 +59,7 @@ function BankAccountManualStep(props) {
return errors;
},
- [translate],
+ [translate, shouldDisableInputs],
);
const submit = useCallback(
@@ -71,8 +74,6 @@ function BankAccountManualStep(props) {
[reimbursementAccount, reimbursementAccountDraft],
);
- const shouldDisableInputs = Boolean(lodashGet(reimbursementAccount, 'achData.bankAccountID'));
-
return (
-
+
-
-
- {isLoading && }
+
+
+ {isLoading && (
+
+
+
+ )}
+
);
}
diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js
index 8a1db47aa742..1addabdd929d 100644
--- a/src/pages/iou/ReceiptSelector/index.native.js
+++ b/src/pages/iou/ReceiptSelector/index.native.js
@@ -205,14 +205,18 @@ function ReceiptSelector({route, report, iou, transactionID}) {
)}
{cameraPermissionStatus === RESULTS.GRANTED && device != null && (
-
+
+
+
+
+
)}
diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts
index a99d292e9dc6..e93d88c3eb53 100644
--- a/src/styles/StyleUtils.ts
+++ b/src/styles/StyleUtils.ts
@@ -462,7 +462,7 @@ function getBorderColorStyle(borderColor: string): ViewStyle {
/**
* Returns the width style for the wordmark logo on the sign in page
*/
-function getSignInWordmarkWidthStyle(environment: string, isSmallScreenWidth: boolean): ViewStyle {
+function getSignInWordmarkWidthStyle(isSmallScreenWidth: boolean, environment?: ValueOf): ViewStyle {
if (environment === CONST.ENVIRONMENT.DEV) {
return isSmallScreenWidth ? {width: variables.signInLogoWidthPill} : {width: variables.signInLogoWidthLargeScreenPill};
}
diff --git a/src/styles/styles.ts b/src/styles/styles.ts
index 63e219d7e953..efa346b55eb5 100644
--- a/src/styles/styles.ts
+++ b/src/styles/styles.ts
@@ -1367,7 +1367,6 @@ const styles = (theme: ThemeColors) =>
},
sidebarListContainer: {
- scrollbarWidth: 'none',
paddingBottom: 4,
},
diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js
index 21537a7a5db7..d52659ccce11 100644
--- a/tests/perf-test/ReportScreen.perf-test.js
+++ b/tests/perf-test/ReportScreen.perf-test.js
@@ -2,18 +2,18 @@ import {fireEvent, screen} from '@testing-library/react-native';
import React from 'react';
import Onyx from 'react-native-onyx';
import {measurePerformance} from 'reassure';
-import ComposeProviders from '../../src/components/ComposeProviders';
-import DragAndDropProvider from '../../src/components/DragAndDrop/Provider';
-import {LocaleContextProvider} from '../../src/components/LocaleContextProvider';
-import OnyxProvider from '../../src/components/OnyxProvider';
-import {CurrentReportIDContextProvider} from '../../src/components/withCurrentReportID';
-import {KeyboardStateProvider} from '../../src/components/withKeyboardState';
-import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions';
-import CONST from '../../src/CONST';
-import * as Localize from '../../src/libs/Localize';
-import ONYXKEYS from '../../src/ONYXKEYS';
-import {ReportAttachmentsProvider} from '../../src/pages/home/report/ReportAttachmentsContext';
-import ReportScreen from '../../src/pages/home/ReportScreen';
+import ComposeProviders from '@components/ComposeProviders';
+import DragAndDropProvider from '@components/DragAndDrop/Provider';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxProvider from '@components/OnyxProvider';
+import {CurrentReportIDContextProvider} from '@components/withCurrentReportID';
+import {KeyboardStateProvider} from '@components/withKeyboardState';
+import {WindowDimensionsProvider} from '@components/withWindowDimensions';
+import * as Localize from '@libs/Localize';
+import {ReportAttachmentsProvider} from '@pages/home/report/ReportAttachmentsContext';
+import ReportScreen from '@pages/home/ReportScreen';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import PusherHelper from '../utils/PusherHelper';
import * as ReportTestUtils from '../utils/ReportTestUtils';
diff --git a/tests/perf-test/SidebarLinks.perf-test.js b/tests/perf-test/SidebarLinks.perf-test.js
index f6819d40a48f..5601c588bb93 100644
--- a/tests/perf-test/SidebarLinks.perf-test.js
+++ b/tests/perf-test/SidebarLinks.perf-test.js
@@ -105,9 +105,9 @@ test('should scroll and click some of the items', () => {
expect(lhnOptionsList).toBeDefined();
fireEvent.scroll(lhnOptionsList, eventData);
-
- const button1 = await screen.findByTestId('1');
- const button2 = await screen.findByTestId('2');
+ // find elements that are currently visible in the viewport
+ const button1 = await screen.findByTestId('7');
+ const button2 = await screen.findByTestId('8');
fireEvent.press(button1);
fireEvent.press(button2);
};