\ No newline at end of file
diff --git a/assets/images/treasure-chest-green-with-sparkle.svg b/assets/images/treasure-chest-green-with-sparkle.svg
new file mode 100644
index 000000000000..6b98a1e74cf5
--- /dev/null
+++ b/assets/images/treasure-chest-green-with-sparkle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/user-minus.svg b/assets/images/user-minus.svg
new file mode 100644
index 000000000000..f819734464a8
--- /dev/null
+++ b/assets/images/user-minus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/codecov.yml b/codecov.yml
index 17cd8f9e567b..f526f04b2d41 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -13,8 +13,8 @@ coverage:
codecov:
notify:
- # We shard our tests into 3 chunks, so we want CodeCov to wait for 3 builds before commenting
- after_n_builds: 3
+ # We shard our tests into 8 chunks, so we want CodeCov to wait for 8 builds before commenting
+ after_n_builds: 8
comment:
require_changes: "coverage_drop OR uncovered_patch"
diff --git a/contributingGuides/philosophies/AI-REVIEWER.md b/contributingGuides/philosophies/AI-REVIEWER.md
new file mode 100644
index 000000000000..43fe9931e685
--- /dev/null
+++ b/contributingGuides/philosophies/AI-REVIEWER.md
@@ -0,0 +1,190 @@
+# AI Reviewer Philosophy
+This philosophy guides our approach to AI-assisted code and documentation review, explaining when to use each reviewer and how to respond to their feedback.
+
+#### Terminology
+- **AI Reviewer** - Automated agent that analyzes PRs or issues and provides feedback
+- **Holistic Reviewer** - A reviewer without predefined rules that provides general feedback
+- **Smart Linter** - The code-inline-reviewer; a rule-based reviewer with predefined patterns
+- **Rule Violation** - Specific pattern that triggers rule-based reviewer feedback
+
+## Why We Use AI Reviewers
+
+AI reviewers serve several key purposes in our development workflow:
+
+### Scale human reviewer capacity
+With a high volume of PRs, human reviewers can't catch every detail. AI reviewers provide consistent, automated first-pass review that catches common issues before human review, allowing human reviewers to focus on architectural decisions, business logic, and nuanced feedback.
+
+### Enforce institutional knowledge consistently
+Performance patterns, coding standards, and documentation guidelines are often tribal knowledge. AI reviewers codify this knowledge into repeatable checks, ensuring every PR benefits from the same expertise regardless of which human reviewer is assigned.
+
+### Reduce review turnaround time
+Contributors get immediate feedback on common issues without waiting for human reviewer availability. This enables faster iteration cycles and reduces the back-and-forth that slows down PR merges.
+
+### Maintain quality at scale
+As the codebase and contributor base grow, AI reviewers help maintain consistent quality standards without linearly increasing human reviewer burden.
+
+## Guiding Principles
+
+These are recommendations for working effectively with AI reviewers, not strict requirements.
+
+### Treat AI feedback as suggestions
+AI reviewers provide automated feedback to assist human reviewers, but their output is not infallible. Contributors and reviewers should evaluate each piece of feedback on its merits rather than blindly accepting or rejecting it.
+
+### Discuss on vague feedback
+When AI feedback is unclear or ambiguous, contributors will benefit from discussing it first with C+ reviewers before jumping to implementation. As mentioned in the first principle, reviewer feedback should be treated as suggestions only.
+
+### Report false positives to maintainers
+When AI feedback is incorrect or not applicable, reach out to the AI reviewer maintainers in the #expensify-open-source Slack channel to help improve the system. This feedback helps refine the reviewers and prevents the same issues from recurring.
+
+### Keep rule documentation in sync with AI reviewer prompts
+When adding or modifying rules in AI reviewer agent files, the corresponding documentation should be updated. The agent files in `.claude/agents/` are the source of truth for specific rules.
+
+## Reviewer Setup
+
+### Available AI Reviewers
+
+**code-inline-reviewer (Smart Linter)**
+- Reviews source code PRs for specific, predefined violations
+- Creates inline comments on lines that violate rules
+- See `.claude/agents/code-inline-reviewer.md` for current rule definitions
+
+**Holistic Reviewer**
+- Provides general code review without predefined rules
+- Catches issues that don't fit into specific rule categories
+- Acts as a counterweight to the Smart Linter
+- Outputs general code quality feedback and suggestions
+- Currently implemented using Codex, configured at the repository level
+
+**helpdot-inline-reviewer**
+- Reviews HelpDot documentation PRs for readability, AI readiness, and style compliance
+- Creates inline comments for specific violations
+- See `.claude/agents/helpdot-inline-reviewer.md` for criteria
+
+**helpdot-summary-reviewer**
+- Provides overall quality assessment with scoring for documentation PRs
+- Posts a top-level PR comment with summary and recommendations
+- See `.claude/agents/helpdot-summary-reviewer.md` for scoring criteria
+
+**deploy-blocker-investigator**
+- Investigates deploy blocker issues to identify the causing PR
+- Posts findings and recommendations on the issue
+- See `.claude/agents/deploy-blocker-investigator.md` for investigation process
+
+### Triggers and When Reviewers Run
+
+AI reviewers are triggered automatically based on contribution type and file changes. The diagram below shows the reviewer pipeline:
+
+```mermaid
+flowchart TD
+ subgraph triggers [GitHub Events]
+ T1[PR opened/ready_for_review]
+ T2[workflow_dispatch]
+ end
+
+ subgraph filters [Path Filters]
+ T1 --> F1{src/** changed?}
+ T1 --> F2{docs/**/*.md changed?}
+ end
+
+ F1 -->|Yes| B[Smart Linter]
+ F1 -->|Yes| C[Holistic Reviewer]
+ F2 -->|Yes| D[helpdot-inline-reviewer]
+ F2 -->|Yes| E[helpdot-summary-reviewer]
+ T2 -->|Manual trigger| F[deploy-blocker-investigator]
+
+ subgraph code [Code Review Output]
+ B --> G[Inline comments for violations]
+ C --> H[Quality feedback]
+ end
+
+ subgraph docs [Documentation Review Output]
+ D --> I[Line-specific feedback]
+ E --> J[Scores and recommendations]
+ end
+
+ subgraph deploy [Issue Investigation Output]
+ F --> K[Identify causing PR]
+ end
+```
+
+#### Code PRs
+
+**Trigger conditions:**
+- PR is opened or marked ready for review
+- PR modifies files in `src/**`
+- PR is not a draft
+- PR title does not contain "Revert"
+
+**How to re-run it?** Convert your PR to draft, then mark it ready for review again.
+
+Code PRs benefit from the **two-reviewer approach**:
+
+1. **Smart Linter (code-inline-reviewer)**: Catches specific, well-defined anti-patterns with consistent, rule-based feedback
+2. **Holistic Reviewer**: Catches general code quality issues, design concerns, and anything not covered by specific rules
+
+Together they balance precision (rules) with coverage (holistic review).
+
+#### Documentation PRs
+
+**Trigger conditions:**
+- PR is opened or marked ready for review
+- PR modifies files in `docs/**/*.md` or `docs/**/*.csv`
+- PR is not a draft
+- PR title does not contain "Revert"
+
+**How to re-run it?** Convert your PR to draft, then mark it ready for review again.
+
+Documentation PRs in the HelpDot system use two complementary reviewers:
+
+1. **helpdot-inline-reviewer**: Line-specific feedback on violations
+2. **helpdot-summary-reviewer**: Overall quality assessment with scores
+
+#### Deploy Blocker Issues
+
+**Trigger conditions:**
+- Manually triggered via `workflow_dispatch`
+- Issue must have the `DeployBlockerCash` label
+- Actor must have write access to the repository
+
+**How to re-run it?** Navigate to Actions → "Investigate Deploy Blocker" workflow → Run workflow with the issue URL.
+
+When a deploy blocker issue needs investigation:
+
+1. **deploy-blocker-investigator**: Analyzes the issue, identifies the likely causing PR, and recommends resolution
+
+## Working with AI Feedback
+
+### Addressing Valid Feedback
+When AI feedback is accurate:
+1. Make the suggested changes
+2. If the fix differs from the suggestion, explain your approach
+
+### Handling False Positives
+When AI feedback is incorrect or not applicable:
+1. Evaluate whether the feedback applies to your specific context
+2. Reach out to AI reviewer maintainers in the #expensify-open-source Slack channel
+3. Your feedback helps refine the reviewers and prevent recurring issues
+
+### Escalating to Human Reviewers
+Escalate to human reviewers when:
+- You're unsure whether AI feedback is valid
+- The AI feedback conflicts with other requirements
+- The suggested fix would require significant architectural changes
+
+### Examples
+
+#### Appropriate Response to Valid Feedback
+**AI Comment**: "PERF-4: This object passed as a prop should be memoized to prevent unnecessary re-renders."
+
+✅ **Good Response**: Wrap the object in `useMemo` or refactor to avoid creating new references.
+
+❌ **Bad Response**: Ignore the feedback without consideration.
+
+#### Appropriate Response to False Positive
+**AI Comment**: "PERF-4: This object passed as a prop should be memoized."
+
+**Context**: The parent component is already optimized by React Compiler.
+
+✅ **Good Response**: Reach out in the #expensify-open-source Slack channel with explanation of incorrect suggestion.
+
+❌ **Bad Response**: Apply the change anyway, adding unnecessary complexity.
diff --git a/contributingGuides/philosophies/INDEX.md b/contributingGuides/philosophies/INDEX.md
index 5e8ac3d7ace8..3150fa5cb10e 100644
--- a/contributingGuides/philosophies/INDEX.md
+++ b/contributingGuides/philosophies/INDEX.md
@@ -5,6 +5,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S
"OPTIONAL" are to be interpreted as described in [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119).
## Contents
+* [AI Reviewer Philosophy](/contributingGuides/philosophies/AI-REVIEWER.md)
* [Beta Usage Philosophy](/contributingGuides/philosophies/BETAS.md)
* [Cross-Platform Philosophy](/contributingGuides/philosophies/CROSS-PLATFORM.md)
* [Data Flow Philosophy](/contributingGuides/philosophies/DATA-FLOW.md)
diff --git a/cspell.json b/cspell.json
index 1c61490d481e..67c2c36081fb 100644
--- a/cspell.json
+++ b/cspell.json
@@ -294,6 +294,7 @@
"headshot",
"healthcheck",
"Heathrow",
+ "helpdot",
"helpsite",
"Highfive",
"Highlightable",
@@ -737,6 +738,8 @@
"Venmo",
"viewability",
"viewport",
+ "Unsharing",
+ "unsharing",
"viewports",
"VMPD",
"voidings",
@@ -782,6 +785,7 @@
"Yema",
"yourcompany",
"yourname",
+ "yalc",
"YYMM",
"zencdn",
"Zenefit",
@@ -805,7 +809,8 @@
"DYNAMICEXTERNAL",
"RNCORE",
"Wooo",
- "Splittable"
+ "Splittable",
+ "pgrep"
],
"ignorePaths": [
"src/languages/de.ts",
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Global-Reimbursement-EU.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Global-Reimbursement-EU.md
index b678fdd557ee..f21a8fa235cf 100644
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Global-Reimbursement-EU.md
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Global-Reimbursement-EU.md
@@ -77,7 +77,8 @@ Once the bank account is approved for global reimbursement:
3. Choose your verified EU account as the default reimbursement method.
4. Instruct employees to connect their deposit account:
- Go to **Settings > Account > Wallet**.
- - Click **Add deposit-only bank account** and input their account details.
+ - Click **Add Personal Bank Account** and input their account details.
+ - **Important:** The name on an employee’s bank account must exactly match the name entered in Expensify. This verification is required for every payment, and mismatches may cause delays or prevent processing.
---
diff --git a/docs/articles/expensify-classic/settings/Avoiding-common-scams.md b/docs/articles/expensify-classic/settings/Avoiding-common-scams.md
new file mode 100644
index 000000000000..2a72d315808a
--- /dev/null
+++ b/docs/articles/expensify-classic/settings/Avoiding-common-scams.md
@@ -0,0 +1,98 @@
+---
+title: Avoiding common scams
+description: Learn how to recognize and avoid common phishing scams that pretend to be from Expensify, including fake phone calls and emails asking for your Magic Code.
+keywords: [Expensify scam, phishing, Magic Code, security, login code, avoid fraud, suspicious email, fake call, fraud prevention]
+---
+
+
+# How to avoid scams pretending to be from Expensify
+
+Some scammers try to impersonate Expensify by sending fake emails or calling you directly, often asking for your **Magic Code** (your login code). These scams are designed to trick you into giving up access to your Expensify account.
+
+This guide explains what these scams look like, how to spot them, and what to do if you think you've been targeted.
+
+---
+
+## What is the Magic Code?
+
+The **Magic Code** is a one-time login code Expensify emails or texts to you when you sign in without a password. It should only be used by **you** during login.
+
+**Expensify will never call, email, or message you to ask for your Magic Code.**
+
+---
+
+## Common scams to watch out for
+
+Here are the most common types of scams we've seen targeting Expensify members:
+
+### 1. Phone call scams requesting your Magic Code
+
+- The scammer claims to be from Expensify and needs your Magic Code to verify your account.
+- They may spoof a legitimate phone number to make it seem more convincing.
+- Some scammers may create a sense of urgency (e.g., “We detected suspicious activity on your account.”)
+
+**If someone calls you asking for your Magic Code, hang up immediately.**
+
+---
+
+### 2. Phishing emails and texts requesting you use their link to login to Expensify
+
+- Emails and text messages often look like they’re from Expensify or Visa, but the sender address is likely not from an expensify.com email address.
+- These emails and text messages may have links that lead to a fake login page, prompt you to reset your password, or ask you to reply with your Magic Code.
+- Scammers sometimes use urgent language like “Your account will be closed if you don’t respond.”
+
+**Don’t click suspicious links, and never share your Magic Code in email. Expensify will only generate a magic code for you when you take an action in the app, such as logging in or making a high-risk change.**
+
+---
+
+## How to stay safe
+
+Here’s how you can protect your Expensify account:
+
+- **Never share your Magic Code** — not over the phone, not by email, not via text.
+- **Don’t click suspicious links** — always verify the sender’s email address.
+- **Use 2FA** (two-factor authentication) to add an extra layer of security.
+- **If in doubt, message Concierge** directly from within the Expensify app.
+
+---
+
+## What to do if you shared your Magic Code
+
+If you accidentally gave someone your Magic Code, act fast:
+
+1. **[Lock your Expensify account](https://help.expensify.com/articles/expensify-classic/settings/Report-Suspicious-Activity)** immediately to block unauthorized access.
+2. **Contact Concierge** from a safe device and explain the situation.
+3. **[Enable two-factor authentication](https://help.expensify.com/articles/expensify-classic/settings/Two-Factor-Authentication)** to protect your account going forward.
+
+---
+
+## How to report phishing and lock your account
+
+If you believe your account has been targeted or compromised:
+
+1. **[Lock your Expensify account](https://help.expensify.com/articles/expensify-classic/settings/Report-Suspicious-Activity)** immediately to block unauthorized access.
+2. **Message Concierge immediately** via [email](mailto:concierge@expensify.com), or from within the Expensify app (web or mobile).
+3. Let us know if you’ve received a suspicious message or phone call.
+4. Our team will work to secure your account.
+5. Once things are secure, we’ll guide you through restoring access safely.
+
+**Note:** You can also forward phishing emails to **abuse@expensify.com** so we can investigate and take action.
+
+---
+
+# FAQ
+
+## Why does Expensify use Magic Codes for login?
+
+Expensify uses Magic Codes instead of passwords to simplify login while keeping your account secure. These codes are valid for one-time use and expire quickly.
+
+## How do I know if an email is really from Expensify?
+
+Legitimate Expensify emails always come from an address ending in **@expensify.com** or **@team.expensify.com**. If you’re unsure, don’t click any links — just log into the Expensify app directly and message Concierge.
+
+## Can Expensify call me?
+
+Expensify will **never** call you asking for login codes, passwords, or payment information. If you receive such a call, it's a scam.
+
+
+
diff --git a/docs/articles/new-expensify/settings/Avoiding-common-scams.md b/docs/articles/new-expensify/settings/Avoiding-common-scams.md
new file mode 100644
index 000000000000..a1a78b8759fe
--- /dev/null
+++ b/docs/articles/new-expensify/settings/Avoiding-common-scams.md
@@ -0,0 +1,99 @@
+---
+title: Avoiding common scams
+description: Learn how to recognize and avoid common phishing scams that pretend to be from Expensify, including fake phone calls and emails asking for your Magic Code.
+keywords: [Expensify scam, phishing, Magic Code, security, login code, avoid fraud, suspicious email, fake call, fraud prevention]
+---
+
+
+
+
+# How to avoid scams pretending to be from Expensify
+
+Some scammers try to impersonate Expensify by sending fake emails or calling you directly, often asking for your **Magic Code** (your login code). These scams are designed to trick you into giving up access to your Expensify account.
+
+This guide explains what these scams look like, how to spot them, and what to do if you think you've been targeted.
+
+---
+
+## What is the Magic Code?
+
+The **Magic Code** is a one-time login code Expensify emails or texts to you when you sign in without a password. It should only be used by **you** during login.
+
+**Expensify will never call, email, or message you to ask for your Magic Code.**
+
+---
+
+## Common scams to watch out for
+
+Here are the most common types of scams we've seen targeting Expensify members:
+
+### 1. Phone call scams requesting your Magic Code
+
+- The scammer claims to be from Expensify and needs your Magic Code to verify your account.
+- They may spoof a legitimate phone number to make it seem more convincing.
+- Some scammers may create a sense of urgency (e.g., “We detected suspicious activity on your account.”)
+
+**If someone calls you asking for your Magic Code, hang up immediately.**
+
+---
+
+### 2. Phishing emails and texts requesting you use their link to login to Expensify
+
+- Emails and text messages often look like they’re from Expensify or Visa, but the sender address is likely not from an expensify.com email address.
+- These emails and text messages may have links that lead to a fake login page, prompt you to reset your password, or ask you to reply with your Magic Code.
+- Scammers sometimes use urgent language like “Your account will be closed if you don’t respond.”
+
+**Don’t click suspicious links, and never share your Magic Code in email. Expensify will only generate a magic code for you when you take an action in the app, such as logging in or making a high-risk change.**
+
+---
+
+## How to stay safe
+
+Here’s how you can protect your Expensify account:
+
+- **Never share your Magic Code** — not over the phone, not by email, not via text.
+- **Don’t click suspicious links** — always verify the sender’s email address.
+- **Use 2FA** (two-factor authentication) to add an extra layer of security.
+- **If in doubt, message Concierge** directly from within the Expensify app.
+
+---
+
+## What to do if you shared your Magic Code
+
+If you accidentally gave someone your Magic Code, act fast:
+
+1. **[Lock your Expensify account](https://help.expensify.com/articles/expensify-classic/settings/Report-Suspicious-Activity)** immediately to block unauthorized access.
+2. **Contact Concierge** from a safe device and explain the situation.
+3. **[Enable two-factor authentication](https://help.expensify.com/articles/new-expensify/settings/Two-Factor-Authentication)** to protect your account going forward.
+
+---
+
+## How to report phishing and lock your account
+
+If you believe your account has been targeted or compromised:
+
+1. **[Lock your Expensify account](https://help.expensify.com/articles/expensify-classic/settings/Report-Suspicious-Activity)** immediately to block unauthorized access.
+2. **Message Concierge immediately** via [email](mailto:concierge@expensify.com), or from within the Expensify app (web or mobile).
+3. Let us know if you’ve received a suspicious message or phone call.
+4. Our team will work to secure your account.
+5. Once things are secure, we’ll guide you through restoring access safely.
+
+**Note:** You can also forward phishing emails to **abuse@expensify.com** so we can investigate and take action.
+
+---
+
+# FAQ
+
+## Why does Expensify use Magic Codes for login?
+
+Expensify uses Magic Codes instead of passwords to simplify login while keeping your account secure. These codes are valid for one-time use and expire quickly.
+
+## How do I know if an email is really from Expensify?
+
+Legitimate Expensify emails always come from an address ending in **@expensify.com** or **@team.expensify.com**. If you’re unsure, don’t click any links — just log into the Expensify app directly and message Concierge.
+
+## Can Expensify call me?
+
+Expensify will **never** call you asking for login codes, passwords, or payment information. If you receive such a call, it's a scam.
+
+
diff --git a/docs/articles/new-expensify/wallet-and-payments/Unshare-a-Business-Bank-Account.md b/docs/articles/new-expensify/wallet-and-payments/Unshare-a-Business-Bank-Account.md
new file mode 100644
index 000000000000..b46a450e03ef
--- /dev/null
+++ b/docs/articles/new-expensify/wallet-and-payments/Unshare-a-Business-Bank-Account.md
@@ -0,0 +1,80 @@
+---
+title: Unshare a business bank account
+description: Learn how to unshare a business bank account from other admins in New Expensify to revoke access instantly.
+keywords: [unshare business bank account, remove bank access, New Expensify Wallet, Workspace Admin, revoke bank permission, Expensify Card settlement]
+---
+
+
+# Unshare a business bank account
+
+If other Workspace Admins have access to your business bank account in Expensify, you can unshare it at any time to revoke their access. Unsharing access is especially helpful when someone changes roles or leaves the company.
+
+Unsharing the account removes it from the other admin's Wallet immediately and prevents that admin from using it to reimburse reports, pay bills, or use it with the Expensify Card. We'll still complete any payments in process that the admin issued.
+
+---
+
+## Who can unshare a business bank account
+
+To unshare a business bank account, you must:
+- Be a **Workspace Admin**.
+- Have an active business bank account in your Wallet that at least one other admin can access.
+
+---
+
+## Where to find the unshare business bank account option
+
+1. Select **Wallet** from the navigation tabs on the left.
+2. Choose your business bank account and click the **More** (three-dot) menu.
+3. Select **Unshare**.
+
+---
+
+## How to unshare a business bank account
+
+1. Go to **Wallet** and open the **More** menu (three dots) on your business bank account.
+2. Click **Unshare**.
+3. A list of all admins with access to that bank account will appear.
+4. Click **Unshare** next to the admin you want to remove.
+5. Confirm in the pop-up modal.
+6. That admin will immediately lose access and receive a Concierge message letting them know.
+
+**Note:** Admins with access can unshare the business bank account from anyone else — even the person who originally added it.
+
+{:width="100%"}
+
+{:width="100%"}
+
+{:width="100%"}
+
+{:width="100%"}
+
+---
+
+## Unsharing when the account is set as Expensify Card settlement account
+
+If the admin's copy of the bank account is currently set as the **Expensify Card settlement account**, you'll see an error message and won't be able to unshare from that admin.
+
+To resolve this:
+- Click **Reach out to Concierge** in the error message to get help switching the settlement account.
+
+---
+
+# FAQ
+
+## Can I unshare a bank account I didn’t originally add?
+
+Yes. As long as your copy of the business bank account is active, you can unshare it from any admin — including the original sharer.
+
+## Will the other admin be notified when I unshare the bank account?
+
+Yes. They’ll receive a message from Concierge letting them know they no longer have access to the bank account.
+
+## What happens to the unshared bank account?
+
+The business bank account is removed from the admin's Wallet. The admin can no longer use the account for any payments or reimbursement features. We'll still complete any payments in process that the admin issued beforehand.
+
+
+
+
+
+
diff --git a/docs/articles/travel/booking-travel/Book-a-Flight.md b/docs/articles/travel/booking-travel/Book-a-Flight.md
index 6b1d1f485ba8..bf1183009815 100644
--- a/docs/articles/travel/booking-travel/Book-a-Flight.md
+++ b/docs/articles/travel/booking-travel/Book-a-Flight.md
@@ -1,19 +1,19 @@
---
title: Book a Flight in New Expensify
description: Learn how to book flights using Expensify’s integrated travel tool, from trip search to payment and confirmation.
-keywords: [New Expensify, book flight, travel tool, flight booking, trip room, itinerary, travel receipt]
+keywords: [New Expensify, book flight, flight search, travel booking, out of policy flight, travel approval, Expensify travel, create trip, travel receipt, travel tool, itinerary]
---
+# Book a flight in New Expensify
-Book flights easily through Expensify’s built-in travel booking tool. This guide walks you through finding flights, booking them, and accessing your travel details in New Expensify.
+Book flights easily with Expensify Travel. This guide walks you through finding flights, booking them, and accessing your travel details in New Expensify.
-# How to access the travel tool
+## Where to find Expensify Travel
-From the left-hand menu, select **Reports > Trips**.
-Click the green **+** button in the bottom-left corner, then choose **Book travel**.
+Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
-If you don’t see this option, reach out to your Account Manager or Concierge to schedule a travel demo and enable the feature for your account.
+If you don’t see **Book travel**, ask a Workspace Admin to [enable Expensify Travel](https://help.expensify.com/articles/travel/company-setup/Enable-Travel-on-a-Workspace) on the workspace.
# How to book a flight
@@ -36,7 +36,7 @@ If you don’t see this option, reach out to your Account Manager or Concierge t
7. Select an existing trip or create a new one to assign the booking.
8. Confirm your payment method and click **Book Flight**.
-# What happens next
+## What happens after booking with Expensify Travel
- Your company’s travel policy may require approval before final confirmation.
- You’ll receive a confirmation email after booking.
@@ -47,11 +47,11 @@ If you don’t see this option, reach out to your Account Manager or Concierge t
## Can I book flights for someone else?
-Yes, you can book flights for others if you’re an authorized arranger or guest booking is enabled. [Learn more about booking for others](https://docs.expensify.com).
+Yes, you can book flights for others if you’re an authorized arranger or guest booking is enabled. [Learn more](https://docs.expensify.com) about how guest and arranger travel works.
## Do I need to upload the flight receipt?
-Nope! Expensify automatically attaches the receipt to your report.
+No. Expensify attaches the receipt to your report automatically.
## Can I book international flights?
@@ -63,13 +63,15 @@ Yes! The receipt is SmartScanned and added to an expense report once the flight
## Can I cancel or change my flight later?
-Flight changes and cancellations depend on the airline’s policy and your specific fare.
-If changes are made through support, a $25 booking change fee applies.
-You can manage bookings by going to **Reports > Trips > My Trips**.
+Yes — but it depends on your fare and the airline’s policy. If changes are made through support, a $25 booking change fee applies.
+To modify or cancel a booking:
+1. Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
+2. In the window that opens, click the **Trips** tab.
+3. Find your trip and click **Modify or Cancel**
-## Where can I view my travel itinerary while on the go?
+## Where can I view my trip itinerary?
-When a traveler books a trip in Expensify Travel, a **trip itinerary** is automatically created in New Expensify.
+When a traveler books a trip in Expensify Travel, a **trip itinerary** is automatically created.
To view your trip:
1. Open the Expensify mobile app.
diff --git a/docs/articles/travel/company-setup/Enable-Travel-on-a-Workspace.md b/docs/articles/travel/company-setup/Enable-Travel-on-a-Workspace.md
new file mode 100644
index 000000000000..d75ecfbb24ed
--- /dev/null
+++ b/docs/articles/travel/company-setup/Enable-Travel-on-a-Workspace.md
@@ -0,0 +1,51 @@
+---
+title: Enable Expensify Travel on a workspace
+description: Learn how Workspace Admins can enable Expensify Travel to manage business travel bookings and expenses in one place.
+keywords: Expensify Travel, enable travel, workspace admin, business travel, travel management, travel policy, book travel, company travel
+---
+
+# Enable Expensify Travel on a workspace
+
+With Expensify Travel your team can book flights, hotels, cars, and more — all within Expensify. Once enabled, your company can centralize booking, expenses, approvals, and chat in one place.
+
+## Who can enable Expensify Travel
+
+Only **Workspace Admins** can enable Expensify Travel for a workspace.
+
+## How to enable Expensify Travel
+
+1. In the **navigation tabs** (on the left on web, and at the bottom on mobile), click **Workspaces**.
+2. Click your workspace name to access the settings for that workspace.
+3. In the left menu, click **More Features**.
+4. Toggle on **Travel**.
+
+## What happens after enabling Expensify Travel
+
+Once Expensify Travel is enabled:
+
+- Team members can book flights, hotels, cars, and trains
+- Travel bookings follow your workspace’s travel policy
+- Bookings and expenses stay connected from start to finish
+- Admins can book on behalf of others if needed
+- All travel activity is visible in one place
+
+Expensify Travel is available globally and included with every Expensify plan.
+
+## Learn more about setting up your travel policy
+
+To customize your company’s travel rules — like flight class, hotel limits, or approval routing — visit the
+[Expensify Travel policy setup hub](https://help.expensify.com/travel/hubs/company-setup/).
+
+# FAQ
+
+## Can I set up travel policy rules after enabling Expensify Travel?
+
+Yes! You can define rules for your travel policy after enabling Travel. Rules apply automatically to all bookings once Travel is active. You can learn more about setting restrictions [here](https://help.expensify.com/articles/travel/company-setup/Configuring-Booking-Restrictions).
+
+## Can a Workspace Admins book travel for others?
+
+Yes, a Workspace Admin can book on behalf of team members and retain full visibility.
+
+## Is Expensify Travel only available for Expensify Card users?
+
+No. Any workspace can use Expensify Travel, regardless of whether the Expensify Card is enabled.
diff --git a/eslint.config.mjs b/eslint.config.mjs
index d926204cddc8..f3be8b5704e1 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -4,7 +4,6 @@ import expensifyConfig from 'eslint-config-expensify';
import jsdoc from 'eslint-plugin-jsdoc';
import lodash from 'eslint-plugin-lodash';
import react from 'eslint-plugin-react';
-import reactCompiler from 'eslint-plugin-react-compiler';
import reactNativeA11Y from 'eslint-plugin-react-native-a11y';
import testingLibrary from 'eslint-plugin-testing-library';
import youDontNeedLodashUnderscore from 'eslint-plugin-you-dont-need-lodash-underscore';
@@ -180,7 +179,6 @@ const config = defineConfig([
'react-native-a11y': reactNativeA11Y,
react,
'testing-library': testingLibrary,
- 'react-compiler': reactCompiler,
lodash,
},
@@ -322,7 +320,6 @@ const config = defineConfig([
touchables: ['PressableWithoutFeedback', 'PressableWithFeedback'],
},
],
- 'react-compiler/react-compiler': 'error',
// Disallow usage of certain functions and imports
'no-restricted-syntax': [
@@ -606,6 +603,7 @@ const config = defineConfig([
'web/gtm.js',
'**/.expo/**/*',
'**/.rock/**/*',
+ '**/.yalc/**/*',
'src/libs/SearchParser/searchParser.js',
'src/libs/SearchParser/autocompleteParser.js',
'help/_scripts/**/*',
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 672cd260cdb6..168bca69a896 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
-
9.2.95
+
9.3.0
CFBundleSignature
????
CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
-
9.2.95.3
+
9.3.0.7
FullStory
OrgId
@@ -105,6 +105,7 @@
UIBackgroundModes
+ location
remote-notification
fetch
processing
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 915de0b8598d..2c5a4b5b4441 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.2.95
+ 9.3.0
CFBundleVersion
- 9.2.95.3
+ 9.3.0.7
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist
index 04689089eb42..98046e7dde50 100644
--- a/ios/ShareViewController/Info.plist
+++ b/ios/ShareViewController/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.2.95
+ 9.3.0
CFBundleVersion
- 9.2.95.3
+ 9.3.0.7
NSExtension
NSExtensionAttributes
diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts
index 51a5f85b3af5..bed82a6a6be4 100644
--- a/jest/setupMockFullstoryLib.ts
+++ b/jest/setupMockFullstoryLib.ts
@@ -24,6 +24,7 @@ export default function mockFSLibrary() {
consentAndIdentify: jest.fn(),
anonymize: jest.fn(),
getSessionId: jest.fn().mockResolvedValue(undefined),
+ getSessionURL: jest.fn().mockResolvedValue(undefined),
};
});
}
diff --git a/package-lock.json b/package-lock.json
index 7dee2c09c7cd..9051ffc1100f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.2.95-3",
+ "version": "9.3.0-7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.2.95-3",
+ "version": "9.3.0-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -118,7 +118,7 @@
"react-native-localize": "^3.5.4",
"react-native-nitro-modules": "0.29.4",
"react-native-nitro-sqlite": "9.2.0",
- "react-native-onyx": "3.0.26",
+ "react-native-onyx": "3.0.29",
"react-native-pager-view": "7.0.2",
"react-native-pdf": "7.0.2",
"react-native-performance": "^6.0.0",
@@ -241,12 +241,11 @@
"dotenv": "^16.0.3",
"eslint": "^9.36.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "2.0.101",
+ "eslint-config-expensify": "2.0.103",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^60.7.0",
"eslint-plugin-lodash": "^7.4.0",
- "eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^10.1.10",
"eslint-plugin-testing-library": "^7.11.0",
@@ -1801,16 +1800,6 @@
"@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@babel/generator/node_modules/jsesc": {
- "version": "3.0.2",
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/@babel/helper-annotate-as-pure": {
"version": "7.27.3",
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
@@ -22097,10 +22086,23 @@
"underscore": "^1.13.6"
}
},
+ "node_modules/eslint-config-airbnb-typescript/node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
"node_modules/eslint-config-expensify": {
- "version": "2.0.101",
- "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.101.tgz",
- "integrity": "sha512-cdCZWgd8FTa3XSqI9eyAXW0W5wiaonSOUxeqpqrVuBRs9xlHfFpeAg4v0f7T5BmP1+zXSo0iDjdxJe+HBp4Y6Q==",
+ "version": "2.0.103",
+ "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.103.tgz",
+ "integrity": "sha512-mfdTLcFf/pHtzBV2uuZYDqpYv2zqa+LUdzto0P42m8dFdcyIpR/fTOQsoqhpVTULQr8HMpSN/XhNzNATFtRr5Q==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -22115,7 +22117,7 @@
"eslint-plugin-jsdoc": "^60.2.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
- "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-rulesdir": "^0.2.2",
"eslint-plugin-unicorn": "^61.0.2",
"globals": "^15.14.0",
@@ -22529,35 +22531,34 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
- "node_modules/eslint-plugin-react-compiler": {
- "version": "19.1.0-rc.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.1.0-rc.2.tgz",
- "integrity": "sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw==",
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
- "@babel/plugin-proposal-private-methods": "^7.18.6",
"hermes-parser": "^0.25.1",
- "zod": "^3.22.4",
- "zod-validation-error": "^3.0.3"
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
},
"engines": {
- "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
+ "node": ">=18"
},
"peerDependencies": {
- "eslint": ">=7"
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
- "node_modules/eslint-plugin-react-compiler/node_modules/hermes-estree": {
+ "node_modules/eslint-plugin-react-hooks/node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
"integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
"dev": true,
"license": "MIT"
},
- "node_modules/eslint-plugin-react-compiler/node_modules/hermes-parser": {
+ "node_modules/eslint-plugin-react-hooks/node_modules/hermes-parser": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
"integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
@@ -22567,19 +22568,6 @@
"hermes-estree": "0.25.1"
}
},
- "node_modules/eslint-plugin-react-hooks": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
- "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
- }
- },
"node_modules/eslint-plugin-react-native-a11y": {
"version": "3.3.0",
"dev": true,
@@ -22726,9 +22714,9 @@
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
- "version": "16.4.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
- "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -22751,19 +22739,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint-plugin-unicorn/node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/eslint-plugin-unicorn/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -29356,6 +29331,18 @@
"node": ">=8"
}
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/json-bigint": {
"version": "1.0.0",
"dev": true,
@@ -33734,9 +33721,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "3.0.26",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.26.tgz",
- "integrity": "sha512-DQKzjNMKffiBPmP3HlDcr4esUaSN4VarCP1LqTWC6iJIqkK3QXy2MJtqMP1oeyHgvYqA9onM+dwjREhNWbFgPA==",
+ "version": "3.0.29",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.29.tgz",
+ "integrity": "sha512-JvVHioPgCLhcL9nETVv9TWq+Bk12+QEqgkXUTwODsycf+d4j/2b0gwoZNoRExc8HBMaJ9D2ucfA7AWmRsCnarg==",
"license": "MIT",
"dependencies": {
"ascii-table": "0.0.9",
diff --git a/package.json b/package.json
index 1566fcb0e9d2..a05c80ca53f0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.2.95-3",
+ "version": "9.3.0-7",
"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.",
@@ -43,7 +43,7 @@
"test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand",
"perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure",
"typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc",
- "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=110 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto",
+ "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=669 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto",
"lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh",
"check-lazy-loading": "ts-node scripts/checkLazyLoading.ts",
"lint-watch": "npx eslint-watch --watch --changed",
@@ -187,7 +187,7 @@
"react-native-localize": "^3.5.4",
"react-native-nitro-modules": "0.29.4",
"react-native-nitro-sqlite": "9.2.0",
- "react-native-onyx": "3.0.26",
+ "react-native-onyx": "3.0.29",
"react-native-pager-view": "7.0.2",
"react-native-pdf": "7.0.2",
"react-native-performance": "^6.0.0",
@@ -310,12 +310,11 @@
"dotenv": "^16.0.3",
"eslint": "^9.36.0",
"eslint-config-airbnb-typescript": "^18.0.0",
- "eslint-config-expensify": "2.0.101",
+ "eslint-config-expensify": "2.0.103",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^60.7.0",
"eslint-plugin-lodash": "^7.4.0",
- "eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^10.1.10",
"eslint-plugin-testing-library": "^7.11.0",
diff --git a/scripts/chatGPTCostEstimator.ts b/scripts/chatGPTCostEstimator.ts
new file mode 100644
index 000000000000..0467d6cd9cd4
--- /dev/null
+++ b/scripts/chatGPTCostEstimator.ts
@@ -0,0 +1,78 @@
+#!/usr/bin/env npx ts-node
+/**
+ * This script estimates the cost of OpenAI API calls based on input and output tokens.
+ */
+import CLI from './utils/CLI';
+import COLORS from './utils/COLORS';
+
+/**
+ * Utility class for estimating OpenAI API costs.
+ */
+class ChatGPTCostEstimator {
+ /** Cost per million input tokens for GPT 5.1 */
+ static readonly GPT_5_1_INPUT_COST_PER_MILLION = 1.25;
+
+ /** Cost per million output tokens for GPT 5.1 */
+ static readonly GPT_5_1_OUTPUT_COST_PER_MILLION = 10.0;
+
+ /** Average number of tokens per character (rule of thumb for English-ish text) */
+ static readonly TOKENS_PER_CHAR = 0.25;
+
+ /**
+ * Calculates the estimated cost for OpenAI API calls based on input and output tokens.
+ *
+ * @param inputTokens - Total number of input tokens
+ * @param outputTokens - Total number of output tokens
+ * @returns Estimated cost in USD
+ */
+ static getTotalEstimatedCost(inputTokens: number, outputTokens: number): number {
+ const inputCost = (inputTokens / 1_000_000) * ChatGPTCostEstimator.GPT_5_1_INPUT_COST_PER_MILLION;
+ const outputCost = (outputTokens / 1_000_000) * ChatGPTCostEstimator.GPT_5_1_OUTPUT_COST_PER_MILLION;
+ return inputCost + outputCost;
+ }
+}
+
+if (require.main === module) {
+ /* eslint-disable @typescript-eslint/naming-convention */
+ const cli = new CLI({
+ namedArgs: {
+ 'input-tokens': {
+ description: 'Total number of input tokens',
+ parse: (val: string): number => {
+ const parsed = parseInt(val, 10);
+ if (Number.isNaN(parsed) || parsed < 0) {
+ throw new Error('Input tokens must be a non-negative integer');
+ }
+ return parsed;
+ },
+ },
+ 'output-tokens': {
+ description: 'Total number of output tokens',
+ parse: (val: string): number => {
+ const parsed = parseInt(val, 10);
+ if (Number.isNaN(parsed) || parsed < 0) {
+ throw new Error('Output tokens must be a non-negative integer');
+ }
+ return parsed;
+ },
+ },
+ },
+ } as const);
+ /* eslint-enable @typescript-eslint/naming-convention */
+
+ const inputTokens = cli.namedArgs['input-tokens'];
+ const outputTokens = cli.namedArgs['output-tokens'];
+ const totalCost = ChatGPTCostEstimator.getTotalEstimatedCost(inputTokens, outputTokens);
+
+ console.log('\n=== ChatGPT Cost Estimator ===\n');
+ console.log('Pricing (GPT 5.1):');
+ console.log(` Input: $${ChatGPTCostEstimator.GPT_5_1_INPUT_COST_PER_MILLION.toFixed(2)}/million tokens`);
+ console.log(` Output: $${ChatGPTCostEstimator.GPT_5_1_OUTPUT_COST_PER_MILLION.toFixed(2)}/million tokens\n`);
+ console.log('Your estimate:');
+ console.log(` Input tokens: ${inputTokens.toLocaleString()}`);
+ console.log(` Output tokens: ${outputTokens.toLocaleString()}`);
+ console.log(` ${'─'.repeat(30)}`);
+ console.log(` Estimated cost: ${COLORS.BOLD}$${totalCost.toFixed(2)} USD${COLORS.RESET}\n`);
+}
+
+export default ChatGPTCostEstimator;
diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts
index 39214854aaaa..2eeaa6e53a0b 100755
--- a/scripts/generateTranslations.ts
+++ b/scripts/generateTranslations.ts
@@ -12,11 +12,15 @@ import ts from 'typescript';
import decodeUnicode from '@libs/StringUtils/decodeUnicode';
import dedent from '@libs/StringUtils/dedent';
import hashStr from '@libs/StringUtils/hash';
+import baseTranslationPrompt from '@prompts/translation/base';
+import contextPrompt from '@prompts/translation/context';
import {isTranslationTargetLocale, LOCALES, TRANSLATION_TARGET_LOCALES} from '@src/CONST/LOCALES';
import type {TranslationTargetLocale} from '@src/CONST/LOCALES';
import en from '@src/languages/en';
import type {TranslationPaths} from '@src/languages/types';
+import ChatGPTCostEstimator from './chatGPTCostEstimator';
import CLI from './utils/CLI';
+import COLORS from './utils/COLORS';
import Git from './utils/Git';
import Prettier from './utils/Prettier';
import PromisePool from './utils/PromisePool';
@@ -50,6 +54,11 @@ const GENERATED_FILE_PREFIX = dedent(`
const tsPrinter = ts.createPrinter();
+/**
+ * If the estimated cost of translation exceeds this threshold (in USD), prompt the user for confirmation before proceeding.
+ */
+const COST_CONFIRMATION_THRESHOLD = 1;
+
/**
* This class encapsulates most of the non-CLI logic to generate translations.
* The primary reason it exists as a class is so we can import this file with no side effects at the top level of the script.
@@ -117,6 +126,23 @@ class TranslationGenerator {
*/
private readonly isIncremental: boolean;
+ /**
+ * CLI instance for user prompts.
+ */
+ /* eslint-disable @typescript-eslint/naming-convention */
+ private readonly cli: CLI<{
+ flags: {
+ 'dry-run': {description: string};
+ verbose: {description: string};
+ };
+ namedArgs: {
+ locales: {description: string; default: TranslationTargetLocale[]; parse: (val: string) => TranslationTargetLocale[]};
+ 'compare-ref': {description: string; default: string; parse: (val: string) => string};
+ paths: {description: string; parse: (val: string) => Set; supersedes: string[]; required: false};
+ };
+ }>;
+ /* eslint-enable @typescript-eslint/naming-convention */
+
/**
* If a complex template expression comes from an existing translation file rather than ChatGPT, then the hashes of its spans will be serialized from the translated version of those spans.
* This map provides us a way to look up the English hash for each translated span hash, so that when we're transforming the English file and we encounter a translated expression hash,
@@ -130,26 +156,98 @@ class TranslationGenerator {
*/
private readonly dedentStringKeys = new Set();
- constructor(config: {
- targetLanguages: TranslationTargetLocale[];
- languagesDir: string;
- sourceFile: string;
- translator: Translator;
- compareRef: string;
- paths?: Set;
- verbose: boolean;
- }) {
- this.targetLanguages = config.targetLanguages;
- this.languagesDir = config.languagesDir;
- const sourceCode = fs.readFileSync(config.sourceFile, 'utf8');
- this.sourceFile = ts.createSourceFile(config.sourceFile, sourceCode, ts.ScriptTarget.Latest, true);
- this.translator = config.translator;
- this.compareRef = config.compareRef;
+ constructor() {
+ this.languagesDir = process.env.LANGUAGES_DIR ?? path.join(__dirname, '../src/languages');
+ const enSourceFile = path.join(this.languagesDir, 'en.ts');
+
+ /* eslint-disable @typescript-eslint/naming-convention */
+ this.cli = new CLI({
+ flags: {
+ 'dry-run': {
+ description: 'If true, just do local mocked translations rather than making real requests to an AI translator.',
+ },
+ verbose: {
+ description: 'Should we print verbose logs?',
+ },
+ },
+ namedArgs: {
+ locales: {
+ description: 'Locales to generate translations for.',
+ default: Object.values(TRANSLATION_TARGET_LOCALES).filter((locale) => locale !== LOCALES.ES),
+ parse: (val: string): TranslationTargetLocale[] => {
+ const rawLocales = val.split(',');
+ const validatedLocales: TranslationTargetLocale[] = [];
+ for (const locale of rawLocales) {
+ if (!isTranslationTargetLocale(locale)) {
+ throw new Error(`Invalid locale ${String(locale)}`);
+ }
+ validatedLocales.push(locale);
+ }
+ return validatedLocales;
+ },
+ },
+ 'compare-ref': {
+ description:
+ 'For incremental translations, this ref is the previous version of the codebase to compare to. Only strings that changed or had their context changed since this ref will be retranslated.',
+ default: '',
+ parse: (val: string): string => {
+ if (!val.trim()) {
+ return val;
+ }
+ if (!Git.isValidRef(val)) {
+ throw new Error(`Invalid git reference: "${val}". Please provide a valid branch, tag, or commit hash.`);
+ }
+ return val;
+ },
+ },
+ paths: {
+ description: 'Comma-separated list of specific translation paths to retranslate (e.g., "common.save,errors.generic").',
+ parse: (val: string): Set => {
+ const rawPaths = val.split(',').map((translationPath) => translationPath.trim());
+ const validatedPaths = new Set();
+ const invalidPaths: string[] = [];
+ for (const rawPath of rawPaths) {
+ if (get(en, rawPath)) {
+ validatedPaths.add(rawPath as TranslationPaths);
+ } else {
+ invalidPaths.push(rawPath);
+ }
+ }
+ if (invalidPaths.length > 0) {
+ throw new Error(`found the following invalid paths: ${JSON.stringify(invalidPaths)}`);
+ }
+ return validatedPaths;
+ },
+ supersedes: ['compare-ref'],
+ required: false,
+ },
+ },
+ } as const);
+ /* eslint-enable @typescript-eslint/naming-convention */
+
+ this.targetLanguages = this.cli.namedArgs.locales;
+ this.compareRef = this.cli.namedArgs['compare-ref'];
this.pathsToAdd = new Set();
- this.pathsToModify = config.paths ?? new Set();
+ this.pathsToModify = this.cli.namedArgs.paths ?? new Set();
this.pathsToRemove = new Set();
- this.verbose = config.verbose;
+ this.verbose = this.cli.flags.verbose;
this.isIncremental = this.pathsToModify.size > 0 || !!this.compareRef;
+
+ const sourceCode = fs.readFileSync(enSourceFile, 'utf8');
+ this.sourceFile = ts.createSourceFile(enSourceFile, sourceCode, ts.ScriptTarget.Latest, true);
+
+ if (this.cli.flags['dry-run']) {
+ console.log('🍸 Dry run enabled');
+ this.translator = new DummyTranslator();
+ } else {
+ if (!process.env.OPENAI_API_KEY) {
+ dotenv.config({path: path.resolve(__dirname, '../.env')});
+ if (!process.env.OPENAI_API_KEY) {
+ throw new Error('❌ OPENAI_API_KEY not found in environment.');
+ }
+ }
+ this.translator = new ChatGPTTranslator(process.env.OPENAI_API_KEY);
+ }
}
public async generateTranslations(): Promise {
@@ -170,14 +268,17 @@ class TranslationGenerator {
console.log(` pathsToRemove: ${Array.from(this.pathsToRemove).join(', ')}`);
}
+ // Extract strings to translate once (locale-independent)
+ const stringsToTranslate = new Map();
+ this.extractStringsToTranslate(this.sourceFile, stringsToTranslate);
+
+ // Estimate cost and prompt user if needed (respects --yes/--no flags)
+ await this.promptForCostApproval(stringsToTranslate);
+
for (const targetLanguage of this.targetLanguages) {
// Map of translations
const translationsForLocale = translations.get(targetLanguage) ?? new Map();
- // Extract strings to translate
- const stringsToTranslate = new Map();
- this.extractStringsToTranslate(this.sourceFile, stringsToTranslate);
-
// Translate all the strings in parallel (up to 8 at a time)
const translationPromises = [];
for (const [key, {text, context}] of stringsToTranslate) {
@@ -286,6 +387,72 @@ class TranslationGenerator {
}
}
+ /**
+ * Estimates the cost of translating the given strings and prompts the user for confirmation if the cost exceeds the threshold.
+ * If the user declines, the process exits.
+ * Skips prompting in dry-run mode since no real API calls are made.
+ */
+ private async promptForCostApproval(stringsToTranslate: Map): Promise {
+ // Skip cost check in dry-run mode since no real API calls are made (cost is $0)
+ if (this.cli.flags['dry-run']) {
+ return;
+ }
+
+ const numStrings = stringsToTranslate.size;
+ const numLocales = this.targetLanguages.length;
+
+ // Calculate base prompt tokens (use first target language as sample since length is similar across locales)
+ const basePromptTokens = Math.ceil(baseTranslationPrompt(TRANSLATION_TARGET_LOCALES.DE).length * ChatGPTCostEstimator.TOKENS_PER_CHAR);
+
+ // Calculate total input and output tokens for all strings
+ let totalInputTokens = numStrings * basePromptTokens;
+ let totalOutputTokens = 0;
+ for (const {text, context} of stringsToTranslate.values()) {
+ const tokensForString = Math.ceil(text.length * ChatGPTCostEstimator.TOKENS_PER_CHAR);
+
+ // The inputs and outputs for the string are assumed to be about the same length.
+ totalInputTokens += tokensForString;
+ totalOutputTokens += tokensForString;
+
+ // Add context prompt tokens if context exists
+ if (context) {
+ totalInputTokens += Math.ceil(contextPrompt(context).length * ChatGPTCostEstimator.TOKENS_PER_CHAR);
+ }
+ }
+
+ // Multiply total input and output tokens by the number of locales
+ totalInputTokens *= numLocales;
+ totalOutputTokens *= numLocales;
+
+ const estimatedCost = ChatGPTCostEstimator.getTotalEstimatedCost(totalInputTokens, totalOutputTokens);
+
+ if (estimatedCost > COST_CONFIRMATION_THRESHOLD) {
+ console.warn(
+ `${COLORS.YELLOW}${dedent(`
+ ⚠️ Warning: This translation will cost approximately $${estimatedCost.toFixed(2)} USD.
+ Strings to translate: ${stringsToTranslate.size.toLocaleString()}
+ Target locales: ${numLocales}
+ `)}${COLORS.RESET}`,
+ );
+
+ if (!this.isIncremental) {
+ const scriptPath = path.relative(process.cwd(), path.resolve(__dirname, 'generateTranslations.ts'));
+ console.log(
+ `Note: You are currently running a full retranslation of the entire \`en.ts\` file. To incrementally translate only what you changed on your branch, run: ${COLORS.BLUE}\`npx ts-node ${scriptPath} --compare-ref main\`${COLORS.RESET}\n`,
+ );
+ }
+
+ const userConfirmed = await this.cli.promptUserConfirmation(`Do you want to proceed? ${COLORS.BOLD}Estimated cost: $${estimatedCost.toFixed(2)} USD.${COLORS.RESET} (y/n) `);
+
+ if (!userConfirmed) {
+ console.log('\n❌ Translation cancelled by user.');
+ process.exit(0);
+ }
+
+ console.log('\n✅ Proceeding with translation...\n');
+ }
+ }
+
/**
* Each translation file should have an object called translations that's later default-exported.
* This function finds that object for a given SourceFile
@@ -1125,111 +1292,8 @@ class TranslationGenerator {
}
}
-/**
- * The main function mostly contains CLI and file I/O logic, while TS parsing and translation logic is encapsulated in TranslationGenerator.
- */
async function main(): Promise {
- const languagesDir = process.env.LANGUAGES_DIR ?? path.join(__dirname, '../src/languages');
- const enSourceFile = path.join(languagesDir, 'en.ts');
-
- /* eslint-disable @typescript-eslint/naming-convention */
- const cli = new CLI({
- flags: {
- 'dry-run': {
- description: 'If true, just do local mocked translations rather than making real requests to an AI translator.',
- },
- verbose: {
- description: 'Should we print verbose logs?',
- },
- },
- namedArgs: {
- // By default, generate translations for all supported languages. Can be overridden with the --locales flag
- locales: {
- description: 'Locales to generate translations for.',
- default: Object.values(TRANSLATION_TARGET_LOCALES).filter((locale) => locale !== LOCALES.ES),
- parse: (val: string): TranslationTargetLocale[] => {
- const rawLocales = val.split(',');
- const validatedLocales: TranslationTargetLocale[] = [];
- for (const locale of rawLocales) {
- if (!isTranslationTargetLocale(locale)) {
- throw new Error(`Invalid locale ${String(locale)}`);
- }
- validatedLocales.push(locale);
- }
- return validatedLocales;
- },
- },
- 'compare-ref': {
- description:
- 'For incremental translations, this ref is the previous version of the codebase to compare to. Only strings that changed or had their context changed since this ref will be retranslated.',
- default: '',
- parse: (val: string): string => {
- if (!val.trim()) {
- return val; // Empty string is valid (means no comparison)
- }
-
- // Validate that the ref exists using our Git utility
- if (!Git.isValidRef(val)) {
- throw new Error(`Invalid git reference: "${val}". Please provide a valid branch, tag, or commit hash.`);
- }
-
- return val;
- },
- },
- paths: {
- description: 'Comma-separated list of specific translation paths to retranslate (e.g., "common.save,errors.generic").',
- parse: (val: string): Set => {
- const rawPaths = val.split(',').map((translationPath) => translationPath.trim());
- const validatedPaths = new Set();
- const invalidPaths: string[] = [];
-
- for (const rawPath of rawPaths) {
- if (get(en, rawPath)) {
- validatedPaths.add(rawPath as TranslationPaths);
- } else {
- invalidPaths.push(rawPath);
- }
- }
-
- if (invalidPaths.length > 0) {
- throw new Error(`found the following invalid paths: ${JSON.stringify(invalidPaths)}`);
- }
-
- return validatedPaths;
- },
- supersedes: ['compare-ref'],
- required: false,
- },
- },
- } as const);
- /* eslint-enable @typescript-eslint/naming-convention */
-
- let translator: Translator;
- if (cli.flags['dry-run']) {
- console.log('🍸 Dry run enabled');
- translator = new DummyTranslator();
- } else {
- // Ensure OPEN_AI_KEY is set in environment
- if (!process.env.OPENAI_API_KEY) {
- // If not, try to load it from .env
- dotenv.config({path: path.resolve(__dirname, '../.env')});
- if (!process.env.OPENAI_API_KEY) {
- throw new Error('❌ OPENAI_API_KEY not found in environment.');
- }
- }
- translator = new ChatGPTTranslator(process.env.OPENAI_API_KEY);
- }
-
- const generator = new TranslationGenerator({
- targetLanguages: cli.namedArgs.locales,
- languagesDir,
- sourceFile: enSourceFile,
- translator,
- compareRef: cli.namedArgs['compare-ref'],
- paths: cli.namedArgs.paths,
- verbose: cli.flags.verbose,
- });
-
+ const generator = new TranslationGenerator();
await generator.generateTranslations();
}
diff --git a/scripts/react-compiler-compliance-check.ts b/scripts/react-compiler-compliance-check.ts
index 6d73d23ac923..716b466404e2 100644
--- a/scripts/react-compiler-compliance-check.ts
+++ b/scripts/react-compiler-compliance-check.ts
@@ -238,7 +238,7 @@ class ReactCompilerHealthcheck {
* Analyzes git diffs to filter compiler results to only changed lines.
*/
class DiffAnalyzer {
- private static readonly ESLINT_LINT_RULES = ['react-compiler/react-compiler', 'react-hooks'] as const;
+ private static readonly ESLINT_LINT_RULES = ['react-hooks'] as const;
/**
* Filter compiler results to only include errors for lines that were changed in the git diff.
diff --git a/scripts/utils/CLI.ts b/scripts/utils/CLI.ts
index 5dd54959d0e1..49b844637ab5 100644
--- a/scripts/utils/CLI.ts
+++ b/scripts/utils/CLI.ts
@@ -2,6 +2,7 @@
* This file contains a CLI utility class which can be used to declaratively implement a strongly-typed CLI.
* You provide a CLIConfig defining your arguments, then the class will handle parsing argv, type validation, error handling, and help messages.
*/
+import * as readline from 'readline';
import type {NonEmptyObject, NonEmptyTuple, ValueOf, Writable} from 'type-fest';
import SafeString from '@src/utils/SafeString';
@@ -135,11 +136,20 @@ type ParsedPositionalArgs =
* console.log(cli.namedArgs.time);
* ```
*/
+/**
+ * Built-in flags that are always available on any CLI.
+ */
+type BuiltInFlags = {
+ yes: boolean;
+ no: boolean;
+ help: boolean;
+};
+
class CLI {
/**
- * Flags after parsing.
+ * Flags after parsing (includes built-in flags like --yes, --no, and --help).
*/
- public readonly flags: ParsedFlags;
+ public readonly flags: ParsedFlags & BuiltInFlags;
/**
* Named args after parsing.
@@ -154,16 +164,15 @@ class CLI {
constructor(private readonly config: TConfig) {
const rawArgs = process.argv.slice(2);
- // Handle help command
- if (rawArgs.includes('help') || rawArgs.includes('--help')) {
- this.printHelp();
- process.exit(0);
- }
+ // Initialize all flags to false by default (including built-in flags)
+ this.flags = {
+ ...Object.fromEntries(Object.keys(config.flags ?? {}).map((key) => [key, false])),
+ yes: false,
+ no: false,
+ help: false,
+ } as typeof this.flags;
try {
- // Initialize all flags to false by default
- this.flags = Object.fromEntries(Object.keys(config.flags ?? {}).map((key) => [key, false])) as typeof this.flags;
-
const parsedNamedArgs: Partial> = {};
const parsedPositionalArgs: Partial> = {};
const providedNamedArgs = new Set();
@@ -180,7 +189,7 @@ class CLI {
const [rawArgName, rawArgValue] = rawArg.slice(2).split('=');
if (rawArgName in this.flags) {
// Arg is a flag
- this.flags[rawArgName as keyof typeof this.flags] = true;
+ (this.flags as Record)[rawArgName] = true;
} else if (config.namedArgs && rawArgName in config.namedArgs) {
// Arg is a named arg
providedNamedArgs.add(rawArgName);
@@ -213,6 +222,12 @@ class CLI {
}
}
+ // Handle help command
+ if (this.flags.help) {
+ this.printHelp();
+ process.exit(0);
+ }
+
// Handle supersession logic
const supersededArgs = new Set();
for (const [name, spec] of Object.entries(config.namedArgs ?? {})) {
@@ -259,6 +274,10 @@ class CLI {
this.namedArgs = parsedNamedArgs as typeof this.namedArgs;
this.positionalArgs = parsedPositionalArgs as unknown as typeof this.positionalArgs;
} catch (err) {
+ // If help flag was set, the error is from process.exit(0) in tests (where it's mocked to throw) - just rethrow it
+ if (this.flags.help) {
+ throw err;
+ }
if (err instanceof Error) {
console.error(err.message);
this.printHelp();
@@ -276,19 +295,19 @@ class CLI {
const namedArgUsage = Object.keys(namedArgs)
.map((key) => `[--${key} ]`)
.join(' ');
- const flagUsage = Object.keys(flags)
- .map((key) => `[--${key}]`)
- .join(' ');
+ const flagUsage = [...Object.keys(flags), '--yes', '--no', '--help'].map((key) => `[${key.startsWith('--') ? key : `--${key}`}]`).join(' ');
console.log(`\nUsage: npx ts-node ${scriptName} ${flagUsage} ${namedArgUsage} ${positionalUsage}\n`);
- if (Object.keys(flags).length > 0) {
- console.log('Flags:');
- for (const [name, spec] of Object.entries(flags)) {
- console.log(` --${name.padEnd(20)} ${spec.description}`);
- }
- console.log('');
+ console.log('Flags:');
+ for (const [name, spec] of Object.entries(flags)) {
+ console.log(` --${name.padEnd(20)} ${spec.description}`);
}
+ // Built-in flags
+ console.log(` --${'yes'.padEnd(20)} Automatically answer "yes" to all confirmation prompts.`);
+ console.log(` --${'no'.padEnd(20)} Automatically answer "no" to all confirmation prompts.`);
+ console.log(` --${'help'.padEnd(20)} Show this help message.`);
+ console.log('');
if (Object.keys(namedArgs).length > 0) {
console.log('Named Arguments:');
@@ -326,6 +345,34 @@ class CLI {
return rawString as InferStringArgParsedValue;
}
}
+
+ /**
+ * Prompts the user for confirmation and returns true if they confirm (y/yes), false otherwise.
+ * If --yes flag was passed, returns true immediately without prompting.
+ * If --no flag was passed, returns false immediately without prompting.
+ */
+ async promptUserConfirmation(message: string): Promise {
+ // Check for built-in flags first
+ if (this.flags.yes) {
+ return true;
+ }
+ if (this.flags.no) {
+ return false;
+ }
+
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+
+ return new Promise((resolve) => {
+ rl.question(message, (answer) => {
+ rl.close();
+ const normalizedAnswer = answer.trim().toLowerCase();
+ resolve(normalizedAnswer === 'y' || normalizedAnswer === 'yes');
+ });
+ });
+ }
}
export default CLI;
diff --git a/scripts/utils/COLORS.ts b/scripts/utils/COLORS.ts
new file mode 100644
index 000000000000..ce53f7e37f61
--- /dev/null
+++ b/scripts/utils/COLORS.ts
@@ -0,0 +1,11 @@
+/**
+ * ANSI color codes for terminal output formatting
+ */
+const COLORS = {
+ RESET: '\x1b[0m',
+ YELLOW: '\x1b[33m',
+ BLUE: '\x1b[34m',
+ BOLD: '\x1b[1m',
+} as const;
+
+export default COLORS;
diff --git a/scripts/utils/EslintUtils.ts b/scripts/utils/EslintUtils.ts
index bb2bb732f7a7..7d921f27a0ba 100644
--- a/scripts/utils/EslintUtils.ts
+++ b/scripts/utils/EslintUtils.ts
@@ -13,7 +13,7 @@ const EslintUtils = {
*
* @param content - The line content to check
* @param isFileLevel - Whether to check for file-level disable comments (true) or line-level (false)
- * @param rules - Array of ESLint rule names to check for (e.g., ['react-compiler/react-compiler', 'react-hooks'])
+ * @param rules - Array of ESLint rule names to check for (e.g., ['react-hooks'])
* @returns True if the line contains a matching eslint-disable comment
*/
hasEslintDisableComment(content: string, isFileLevel: boolean, rules: string[]): boolean {
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index cd678f72d1cd..79a6c40c91e0 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -32,6 +32,8 @@ const USE_EXPENSIFY_URL = 'https://use.expensify.com';
const EXPENSIFY_MOBILE_URL = 'https://expensify.com/mobile';
const EXPENSIFY_URL = 'https://www.expensify.com';
const UBER_CONNECT_URL = 'https://business-integrations.uber.com/connect';
+const XERO_PARTNER_LINK = 'https://xero5440.partnerlinks.io/uzfjy4uegog2-v0pj1v';
+const UBER_TERMS_LINK = 'https://www.uber.com/us/en/business/sign-up/terms/expense-partners/';
const PLATFORM_OS_MACOS = 'Mac OS';
const PLATFORM_IOS = 'iOS';
const ANDROID_PACKAGE_NAME = 'org.me.mobiexpensifyg';
@@ -445,6 +447,8 @@ const CONST = {
NEW_EXPENSIFY_URL: ACTIVE_EXPENSIFY_URL,
UBER_CONNECT_URL,
+ XERO_PARTNER_LINK,
+ UBER_TERMS_LINK,
APP_DOWNLOAD_LINKS: {
ANDROID: `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE_NAME}`,
IOS: 'https://apps.apple.com/us/app/expensify-travel-expense/id471713959',
@@ -725,11 +729,11 @@ const CONST = {
IS_TRAVEL_VERIFIED: 'isTravelVerified',
TRAVEL_INVOICING: 'travelInvoicing',
EXPENSIFY_CARD_EU_UK: 'expensifyCardEuUk',
+ TIME_TRACKING: 'timeTracking',
EUR_BILLING: 'eurBilling',
NO_OPTIMISTIC_TRANSACTION_THREADS: 'noOptimisticTransactionThreads',
UBER_FOR_BUSINESS: 'uberForBusiness',
CUSTOM_REPORT_NAMES: 'newExpensifyCustomReportNames',
- ZERO_EXPENSES: 'zeroExpenses',
NEW_DOT_DEW: 'newDotDEW',
GPS_MILEAGE: 'gpsMileage',
},
@@ -1226,6 +1230,7 @@ const CONST = {
CARD_ASSIGNED: 'CARDASSIGNED',
CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action
CHANGE_POLICY: 'CHANGEPOLICY',
+ CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS: 'CREATEDREPORTFORUNAPPROVEDTRANSACTIONS',
CHANGE_TYPE: 'CHANGETYPE', // OldDot Action
CHRONOS_OOO_LIST: 'CHRONOSOOOLIST',
CLOSED: 'CLOSED',
@@ -1245,6 +1250,7 @@ const CONST = {
HOLD: 'HOLD',
HOLD_COMMENT: 'HOLDCOMMENT',
INTEGRATION_SYNC_FAILED: 'INTEGRATIONSYNCFAILED',
+ COMPANY_CARD_CONNECTION_BROKEN: 'COMPANYCARDCONNECTIONBROKEN',
IOU: 'IOU',
INTEGRATIONS_MESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action
MANAGER_ATTACH_RECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action
@@ -1312,6 +1318,7 @@ const CONST = {
DELETE_APPROVER_RULE: 'POLICYCHANGELOG_DELETE_APPROVER_RULE',
DELETE_BUDGET: 'POLICYCHANGELOG_DELETE_BUDGET',
DELETE_CATEGORY: 'POLICYCHANGELOG_DELETE_CATEGORY',
+ DELETE_CATEGORIES: 'POLICYCHANGELOG_DELETE_CATEGORIES',
DELETE_CUSTOM_UNIT: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT',
DELETE_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT_RATE',
DELETE_CUSTOM_UNIT_SUB_RATE: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT_SUB_RATE',
@@ -1351,6 +1358,7 @@ const CONST = {
UPDATE_DISABLED_FIELDS: 'POLICYCHANGELOG_UPDATE_DISABLED_FIELDS',
UPDATE_EMPLOYEE: 'POLICYCHANGELOG_UPDATE_EMPLOYEE',
UPDATE_FIELD: 'POLICYCHANGELOG_UPDATE_FIELD',
+ UPDATE_ADDRESS: 'POLICYCHANGELOG_UPDATE_ADDRESS',
UPDATE_FEATURE_ENABLED: 'POLICYCHANGELOG_UPDATE_FEATURE_ENABLED',
UPDATE_IS_ATTENDEE_TRACKING_ENABLED: 'POLICYCHANGELOG_UPDATE_IS_ATTENDEE_TRACKING_ENABLED',
UPDATE_DEFAULT_APPROVER: 'POLICYCHANGELOG_UPDATE_DEFAULT_APPROVER',
@@ -1713,6 +1721,10 @@ const CONST = {
},
// Attribute names
ATTRIBUTE_IOU_TYPE: 'iou_type',
+ ATTRIBUTE_IS_ONE_TRANSACTION_REPORT: 'is_one_transaction_report',
+ ATTRIBUTE_IS_TRANSACTION_THREAD: 'is_transaction_thread',
+ ATTRIBUTE_REPORT_TYPE: 'report_type',
+ ATTRIBUTE_CHAT_TYPE: 'chat_type',
ATTRIBUTE_IOU_REQUEST_TYPE: 'iou_request_type',
ATTRIBUTE_REPORT_ID: 'report_id',
ATTRIBUTE_MESSAGE_LENGTH: 'message_length',
@@ -2881,6 +2893,8 @@ const CONST = {
// Note: These payment types are used when building IOU reportAction message values in the server and should
// not be changed.
LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS: 7,
+ // Maximum number of splits allowed for expenses
+ SPLITS_LIMIT: 30,
PAYMENT_TYPE: {
ELSEWHERE: 'Elsewhere',
EXPENSIFY: 'Expensify',
@@ -2913,6 +2927,8 @@ const CONST = {
DISTANCE_MAP: 'distance-map',
DISTANCE_MANUAL: 'distance-manual',
DISTANCE_GPS: 'distance-gps',
+ DISTANCE_ODOMETER: 'distance-odometer',
+ TIME: 'time',
},
EXPENSE_TYPE: {
DISTANCE: 'distance',
@@ -2924,7 +2940,10 @@ const CONST = {
DISTANCE_MAP: 'distance-map',
DISTANCE_MANUAL: 'distance-manual',
DISTANCE_GPS: 'distance-gps',
+ DISTANCE_ODOMETER: 'distance-odometer',
+ TIME: 'time',
},
+
REPORT_ACTION_TYPE: {
PAY: 'pay',
CREATE: 'create',
@@ -5505,6 +5524,8 @@ const CONST = {
DISTANCE_MAP: 'distance-map',
DISTANCE_MANUAL: 'distance-manual',
DISTANCE_GPS: 'distance-gps',
+ DISTANCE_ODOMETER: 'distance-odometer',
+ TIME: 'time',
},
STATUS_TEXT_MAX_LENGTH: 100,
@@ -5853,6 +5874,11 @@ const CONST = {
ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers},
ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes},
ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize},
+ ONBOARDING_RHP_VARIANT: {
+ RHP_CONCIERGE_DM: 'rhpConciergeDm',
+ RHP_ADMINS_ROOM: 'rhpAdminsRoom',
+ CONTROL: 'control',
+ },
ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?',
ONBOARDING_ACCOUNTING_MAPPING,
@@ -6648,6 +6674,7 @@ const CONST = {
MAX_TAX_RATE_DECIMAL_PLACES: 4,
MIN_TAX_RATE_DECIMAL_PLACES: 2,
DISTANCE_DECIMAL_PLACES: 2,
+ HOURS_DECIMAL_PLACES: 2,
DOWNLOADS_PATH: '/Downloads',
DOWNLOADS_TIMEOUT: 5000,
@@ -6935,7 +6962,6 @@ const CONST = {
BANK_ACCOUNT: 'bankAccount',
REPORT_ID: 'reportID',
BASE_62_REPORT_ID: 'base62ReportID',
- TAX: 'tax',
EXPORTED_TO: 'exportedto',
EXCHANGE_RATE: 'exchangeRate',
REIMBURSABLE_TOTAL: 'reimbursableTotal',
@@ -7080,6 +7106,54 @@ const CONST = {
return {
[this.TRANSACTION_TYPE.PER_DIEM]: 'per-diem',
[this.STATUS.EXPENSE.DRAFTS]: 'draft',
+ [this.TABLE_COLUMNS.RECEIPT]: 'receipt',
+ [this.TABLE_COLUMNS.DATE]: 'date',
+ [this.TABLE_COLUMNS.SUBMITTED]: 'submitted',
+ [this.TABLE_COLUMNS.APPROVED]: 'approved',
+ [this.TABLE_COLUMNS.POSTED]: 'posted',
+ [this.TABLE_COLUMNS.EXPORTED]: 'exported',
+ [this.TABLE_COLUMNS.MERCHANT]: 'merchant',
+ [this.TABLE_COLUMNS.DESCRIPTION]: 'description',
+ [this.TABLE_COLUMNS.FROM]: 'from',
+ [this.TABLE_COLUMNS.TO]: 'to',
+ [this.TABLE_COLUMNS.CATEGORY]: 'category',
+ [this.TABLE_COLUMNS.TAG]: 'tag',
+ [this.TABLE_COLUMNS.ORIGINAL_AMOUNT]: 'original-amount',
+ [this.TABLE_COLUMNS.REIMBURSABLE]: 'reimbursable',
+ [this.TABLE_COLUMNS.BILLABLE]: 'billable',
+ [this.TABLE_COLUMNS.TAX_RATE]: 'tax-rate',
+ [this.TABLE_COLUMNS.TOTAL_AMOUNT]: 'amount',
+ [this.TABLE_COLUMNS.TOTAL]: 'total',
+ [this.TABLE_COLUMNS.TYPE]: 'type',
+ [this.TABLE_COLUMNS.ACTION]: 'action',
+ [this.TABLE_COLUMNS.TAX_AMOUNT]: 'tax',
+ [this.TABLE_COLUMNS.TITLE]: 'title',
+ [this.TABLE_COLUMNS.ASSIGNEE]: 'assignee',
+ [this.TABLE_COLUMNS.IN]: 'in',
+ [this.TABLE_COLUMNS.COMMENTS]: 'comments',
+ [this.TABLE_COLUMNS.CARD]: 'card',
+ [this.TABLE_COLUMNS.POLICY_NAME]: 'policy-name',
+ [this.TABLE_COLUMNS.WITHDRAWAL_ID]: 'withdrawal-id',
+ [this.TABLE_COLUMNS.AVATAR]: 'avatar',
+ [this.TABLE_COLUMNS.STATUS]: 'status',
+ [this.TABLE_COLUMNS.EXPENSES]: 'expenses',
+ [this.TABLE_COLUMNS.FEED]: 'feed',
+ [this.TABLE_COLUMNS.WITHDRAWN]: 'withdrawn',
+ [this.TABLE_COLUMNS.BANK_ACCOUNT]: 'bank-account',
+ [this.TABLE_COLUMNS.REPORT_ID]: 'long-report-id',
+ [this.TABLE_COLUMNS.BASE_62_REPORT_ID]: 'report-id',
+ [this.TABLE_COLUMNS.EXPORTED_TO]: 'exported-to',
+ [this.TABLE_COLUMNS.EXCHANGE_RATE]: 'exchange-rate',
+ [this.TABLE_COLUMNS.REIMBURSABLE_TOTAL]: 'reimbursable-total',
+ [this.TABLE_COLUMNS.NON_REIMBURSABLE_TOTAL]: 'non-reimbursable-total',
+ [this.TABLE_COLUMNS.GROUP_FROM]: 'group-from',
+ [this.TABLE_COLUMNS.GROUP_EXPENSES]: 'group-expenses',
+ [this.TABLE_COLUMNS.GROUP_TOTAL]: 'group-total',
+ [this.TABLE_COLUMNS.GROUP_CARD]: 'group-card',
+ [this.TABLE_COLUMNS.GROUP_FEED]: 'group-feed',
+ [this.TABLE_COLUMNS.GROUP_BANK_ACCOUNT]: 'group-bank-account',
+ [this.TABLE_COLUMNS.GROUP_WITHDRAWN]: 'group-withdrawn',
+ [this.TABLE_COLUMNS.GROUP_WITHDRAWAL_ID]: 'group-withdrawal-id',
};
},
NOT_MODIFIER: 'Not',
@@ -7120,6 +7194,7 @@ const CONST = {
UNAPPROVED_CASH: 'unapprovedCash',
UNAPPROVED_CARD: 'unapprovedCard',
RECONCILIATION: 'reconciliation',
+ TOP_SPENDERS: 'topSpenders',
},
GROUP_PREFIX: 'group_',
ANIMATION: {
@@ -7901,6 +7976,13 @@ const CONST = {
HEADER_ACTION_BUTTON: 'Task-HeaderActionButton',
},
},
+
+ DOMAIN: {
+ /** Onyx prefix for domain admin account IDs */
+ EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_',
+ /** Onyx prefix for domain security groups */
+ DOMAIN_SECURITY_GROUP_PREFIX: 'domain_securityGroup_',
+ },
} as const;
const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index da34c896c8a5..4c995bab6dbc 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -233,11 +233,11 @@ function Expensify() {
useEffect(() => {
// Initialize Fullstory lib
FS.init(userMetadata);
- FS.getSessionId().then((sessionId) => {
- if (!sessionId) {
+ FS.getSessionURL().then((url) => {
+ if (!url) {
return;
}
- Sentry.setContext(CONST.TELEMETRY.CONTEXT_FULLSTORY, {sessionId});
+ Sentry.setContext(CONST.TELEMETRY.CONTEXT_FULLSTORY, {url});
});
}, [userMetadata]);
@@ -313,7 +313,7 @@ function Expensify() {
}
linkingChangeListener.current.remove();
};
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again
}, [sessionMetadata?.status]);
// This is being done since we want to play sound even when iOS device is on silent mode, to align with other platforms.
@@ -328,7 +328,7 @@ function Expensify() {
updateLastRoute('');
Navigation.navigate(lastRoute as Route);
// Disabling this rule because we only want it to run on the first render.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNavigationReady]);
useEffect(() => {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 91b60f5acff8..37cf5fb7cac2 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -125,6 +125,9 @@ const ONYXKEYS = {
/* Contains meta data for the call to the API to get the joinable policies */
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'validateUserAndGetAccessiblePolicies',
+ /** Stores details relating to unsharing a given bank account */
+ UNSHARE_BANK_ACCOUNT: 'unshareBankAccount',
+
/** Information about the current session (authToken, accountID, email, loading, error) */
SESSION: 'session',
STASHED_SESSION: 'stashedSession',
@@ -588,6 +591,9 @@ const ONYXKEYS = {
/** Stores the user's app review prompt state and response */
NVP_APP_REVIEW: 'nvp_appReview',
+ /** Stores the onboarding RHP variant for A/B/C testing */
+ NVP_ONBOARDING_RHP_VARIANT: 'nvp_onboardingRHPVariant',
+
/** Information about vacation delegate */
NVP_PRIVATE_VACATION_DELEGATE: 'nvp_private_vacationDelegate',
@@ -758,9 +764,6 @@ const ONYXKEYS = {
/** SAML login metadata for a domain */
SAML_METADATA: 'saml_metadata_',
- /** Stores domain admin account ID */
- EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_',
-
/** Pending actions for a domain */
DOMAIN_PENDING_ACTIONS: 'domainPendingActions_',
@@ -946,6 +949,8 @@ const ONYXKEYS = {
TEXT_PICKER_MODAL_FORM_DRAFT: 'textPickerModalFormDraft',
REPORTS_DEFAULT_TITLE_MODAL_FORM: 'ReportsDefaultTitleModalForm',
REPORTS_DEFAULT_TITLE_MODAL_FORM_DRAFT: 'ReportsDefaultTitleModalFormDraft',
+ RESET_DOMAIN_FORM: 'resetDomainForm',
+ RESET_DOMAIN_FORM_DRAFT: 'resetDomainFormDraft',
RULES_AUTO_APPROVE_REPORTS_UNDER_MODAL_FORM: 'rulesAutoApproveReportsUnderModalForm',
RULES_AUTO_APPROVE_REPORTS_UNDER_MODAL_FORM_DRAFT: 'rulesAutoApproveReportsUnderModalFormDraft',
RULES_RANDOM_REPORT_AUDIT_MODAL_FORM: 'rulesRandomReportAuditModalForm',
@@ -1040,6 +1045,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm;
[ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm;
[ONYXKEYS.FORMS.REPORT_FIELDS_EDIT_FORM]: FormTypes.ReportFieldsEditForm;
+ [ONYXKEYS.FORMS.RESET_DOMAIN_FORM]: FormTypes.ResetDomainForm;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm;
[ONYXKEYS.FORMS.ENTER_SINGER_INFO_FORM]: FormTypes.EnterSignerInfoForm;
[ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM]: FormTypes.PersonalBankAccountForm;
@@ -1157,7 +1163,6 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean;
[ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata;
- [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number;
[ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions;
[ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors;
};
@@ -1258,6 +1263,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.PURCHASE_LIST]: OnyxTypes.PurchaseList;
[ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount;
[ONYXKEYS.SHARE_BANK_ACCOUNT]: OnyxTypes.ShareBankAccount;
+ [ONYXKEYS.UNSHARE_BANK_ACCOUNT]: OnyxTypes.UnshareBankAccount;
[ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount;
[ONYXKEYS.REIMBURSEMENT_ACCOUNT_OPTION_PRESSED]: ValueOf;
[ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: number;
@@ -1355,6 +1361,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.BILLING_RECEIPT_DETAILS]: OnyxTypes.BillingReceiptDetails;
[ONYXKEYS.NVP_SIDE_PANEL]: OnyxTypes.SidePanel;
[ONYXKEYS.NVP_APP_REVIEW]: OnyxTypes.AppReview;
+ [ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT]: OnyxTypes.OnboardingRHPVariant;
[ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION]: boolean;
[ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE]: OnyxTypes.VacationDelegate;
[ONYXKEYS.SCHEDULE_CALL_DRAFT]: OnyxTypes.ScheduleCallDraft;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index f057aad29120..2ca954477aeb 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -187,6 +187,10 @@ const ROUTES = {
return `bank-account/enter-signer-info?policyID=${policyID}&bankAccountID=${bankAccountID}&isCompleted=${isCompleted}` as const;
},
},
+ BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT: {
+ route: 'bank-account/connect-existing-business-bank-account',
+ getRoute: (policyID: string) => `bank-account/connect-existing-business-bank-account?policyID=${policyID}` as const,
+ },
PUBLIC_CONSOLE_DEBUG: {
route: 'troubleshoot/console',
@@ -338,8 +342,13 @@ const ROUTES = {
},
SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account',
+ SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'settings/wallet/add-us-bank-account/entry-point',
SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`,
SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments',
+ SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT: {
+ route: 'settings/wallet/:bankAccountID/unshare-bank-account',
+ getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/unshare-bank-account` as const,
+ },
SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS: {
route: 'settings/wallet/:bankAccountID/enable-global-reimbursements',
getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/enable-global-reimbursements` as const,
@@ -410,13 +419,12 @@ const ROUTES = {
getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo),
},
SETTINGS_NEW_CONTACT_METHOD_CONFIRM_MAGIC_CODE: {
- route: 'settings/profile/contact-methods/new/:newContactMethod/confirm-magic-code',
- getRoute: (newContactMethod: string, backTo?: string) => {
- const encodedMethod = encodeURIComponent(newContactMethod);
+ route: 'settings/profile/contact-methods/new/confirm-magic-code',
+ getRoute: (backTo?: string) => {
// TODO this backTo comes from drilling it through settings screens
// should be removed once https://github.com/Expensify/App/pull/72219 is resolved
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
- return getUrlWithBackToParam(`settings/profile/contact-methods/new/${encodedMethod}/confirm-magic-code`, backTo);
+ return getUrlWithBackToParam(`settings/profile/contact-methods/new/confirm-magic-code`, backTo);
},
},
SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT: {
@@ -1171,6 +1179,17 @@ const ROUTES = {
return getUrlWithBackToParam(`${action as string}/${iouType as string}/distance-manual/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo);
},
},
+ MONEY_REQUEST_STEP_DISTANCE_ODOMETER: {
+ route: ':action/:iouType/distance-odometer/:transactionID/:reportID',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DISTANCE_ODOMETER route');
+ }
+
+ // eslint-disable-next-line no-restricted-syntax -- Legacy route generation
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/distance-odometer/${transactionID}/${reportID}`, backTo);
+ },
+ },
MONEY_REQUEST_STEP_DISTANCE_RATE: {
route: ':action/:iouType/distanceRate/:transactionID/:reportID/:reportActionID?',
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => {
@@ -1251,6 +1270,11 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) =>
`create/${iouType as string}/start/${transactionID}/${reportID}/per-diem/${backToReport ?? ''}` as const,
},
+ MONEY_REQUEST_CREATE_TAB_TIME: {
+ route: 'time/:backToReport?',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) =>
+ `create/${iouType as string}/start/${transactionID}/${reportID}/time/${backToReport ?? ''}` as const,
+ },
MONEY_REQUEST_RECEIPT_VIEW: {
route: 'receipt-view/:transactionID',
@@ -1290,6 +1314,11 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backToReport?: string) =>
`${action as string}/${iouType as string}/start/${transactionID}/${reportID}/distance-new${backToReport ? `/${backToReport}` : ''}/distance-gps` as const,
},
+ DISTANCE_REQUEST_CREATE_TAB_ODOMETER: {
+ route: 'distance-odometer',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) =>
+ `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/distance-new${backToReport ? `/${backToReport}` : ''}/distance-odometer` as const,
+ },
IOU_SEND_ADD_BANK_ACCOUNT: 'pay/new/add-bank-account',
IOU_SEND_ADD_DEBIT_CARD: 'pay/new/add-debit-card',
IOU_SEND_ENABLE_PAYMENTS: 'pay/new/enable-payments',
@@ -1674,7 +1703,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CLASSES: {
route: 'workspaces/:policyID/accounting/quickbooks-desktop/import/classes',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/quickbooks-desktop/import/classes` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CLASSES route');
+ }
+ return `workspaces/${policyID}/accounting/quickbooks-desktop/import/classes` as const;
+ },
},
POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CLASSES_DISPLAYED_AS: {
route: 'workspaces/:policyID/accounting/quickbooks-desktop/import/classes/displayed_as',
@@ -1682,7 +1716,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CUSTOMERS: {
route: 'workspaces/:policyID/accounting/quickbooks-desktop/import/customers',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/quickbooks-desktop/import/customers` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CUSTOMERS route');
+ }
+ return `workspaces/${policyID}/accounting/quickbooks-desktop/import/customers` as const;
+ },
},
POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS: {
route: 'workspaces/:policyID/accounting/quickbooks-desktop/import/customers/displayed_as',
@@ -1730,10 +1769,6 @@ const ROUTES = {
return `workspaces/${policyID}/workflows` as const;
},
},
- WORKSPACE_WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT: {
- route: 'workspaces/:policyID/workflows/connect-account',
- getRoute: (policyID: string) => `workspaces/${policyID}/workflows/connect-account` as const,
- },
WORKSPACE_WORKFLOWS_APPROVALS_NEW: {
route: 'workspaces/:policyID/workflows/approvals/new',
getRoute: (policyID: string) => `workspaces/${policyID}/workflows/approvals/new` as const,
@@ -2968,6 +3003,10 @@ const ROUTES = {
return `workspaces/${policyID}/accounting/xero/advanced` as const;
},
},
+ POLICY_ACCOUNTING_CLAIM_OFFER: {
+ route: 'workspaces/:policyID/accounting/claim-offer/:integration',
+ getRoute: (policyID: string, integration: string) => `workspaces/${policyID}/accounting/claim-offer/${integration}` as const,
+ },
POLICY_ACCOUNTING_XERO_AUTO_SYNC: {
route: 'workspaces/:policyID/accounting/xero/advanced/autosync',
getRoute: (policyID: string | undefined, backTo?: string) => {
@@ -3024,7 +3063,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES: {
route: 'workspaces/:policyID/accounting/quickbooks-online/import/classes',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/quickbooks-online/import/classes` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES route');
+ }
+ return `workspaces/${policyID}/accounting/quickbooks-online/import/classes` as const;
+ },
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS: {
route: 'workspaces/:policyID/accounting/quickbooks-online/import/classes/displayed-as',
@@ -3032,7 +3076,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS: {
route: 'workspaces/:policyID/accounting/quickbooks-online/import/customers',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/quickbooks-online/import/customers` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS route');
+ }
+ return `workspaces/${policyID}/accounting/quickbooks-online/import/customers` as const;
+ },
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS: {
route: 'workspaces/:policyID/accounting/quickbooks-online/import/customers/displayed-as',
@@ -3040,7 +3089,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS: {
route: 'workspaces/:policyID/accounting/quickbooks-online/import/locations',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/quickbooks-online/import/locations` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS route');
+ }
+ return `workspaces/${policyID}/accounting/quickbooks-online/import/locations` as const;
+ },
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS: {
route: 'workspaces/:policyID/accounting/quickbooks-online/import/locations/displayed-as',
@@ -3075,7 +3129,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_NETSUITE_IMPORT: {
route: 'workspaces/:policyID/accounting/netsuite/import',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/netsuite/import` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_NETSUITE_IMPORT route');
+ }
+ return `workspaces/${policyID}/accounting/netsuite/import` as const;
+ },
},
POLICY_ACCOUNTING_NETSUITE_IMPORT_MAPPING: {
route: 'workspaces/:policyID/accounting/netsuite/import/mapping/:importField',
@@ -3115,7 +3174,12 @@ const ROUTES = {
},
POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: {
route: 'workspaces/:policyID/accounting/netsuite/import/customer-projects',
- getRoute: (policyID: string) => `workspaces/${policyID}/accounting/netsuite/import/customer-projects` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS route');
+ }
+ return `workspaces/${policyID}/accounting/netsuite/import/customer-projects` as const;
+ },
},
POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: {
route: 'workspaces/:policyID/accounting/netsuite/import/customer-projects/select',
@@ -3581,6 +3645,18 @@ const ROUTES = {
route: 'domain/:domainAccountID/admins/invite',
getRoute: (domainAccountID: number) => `domain/${domainAccountID}/admins/invite` as const,
},
+ DOMAIN_MEMBERS: {
+ route: 'domain/:domainAccountID/members',
+ getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members` as const,
+ },
+ DOMAIN_MEMBER_DETAILS: {
+ route: 'domain/:domainAccountID/members/:accountID',
+ getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}` as const,
+ },
+ DOMAIN_RESET_DOMAIN: {
+ route: 'domain/:domainAccountID/admins/:accountID/reset-domain',
+ getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/admins/${accountID}/reset-domain` as const,
+ },
} as const;
/**
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 4643456897ca..b8dd170e1554 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -106,6 +106,7 @@ const SCREENS = {
ADD_BANK_ACCOUNT_VERIFY_ACCOUNT: 'Settings_Add_Bank_Account_Verify_Account',
ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account',
ADD_US_BANK_ACCOUNT: 'Settings_Add_US_Bank_Account',
+ ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'Settings_Add_US_Bank_Account_Entry_Point',
ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: 'Settings_Add_Bank_Account_Select_Country_Verify_Account',
CLOSE: 'Settings_Close',
REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged',
@@ -166,6 +167,7 @@ const SCREENS = {
REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_ReportVirtualCardFraud_ConfirmMagicCode',
REPORT_VIRTUAL_CARD_FRAUD_CONFIRMATION: 'Settings_Wallet_ReportVirtualCardFraudConfirmation',
CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address',
+ UNSHARE_BANK_ACCOUNT: 'Settings_Wallet_Unshare_Bank_Account',
ENABLE_GLOBAL_REIMBURSEMENTS: 'Settings_Wallet_Enable_Global_Reimbursements',
SHARE_BANK_ACCOUNT: 'Settings_Wallet_Share_Bank_Account',
},
@@ -329,6 +331,7 @@ const SCREENS = {
STEP_DISTANCE_MAP: 'Money_Request_Step_Distance_Map',
STEP_DISTANCE_MANUAL: 'Money_Request_Step_Distance_Manual',
STEP_DISTANCE_GPS: 'Money_Request_Step_Distance_GPS',
+ STEP_DISTANCE_ODOMETER: 'Money_Request_Step_Distance_Odometer',
RECEIPT_PREVIEW: 'Money_Request_Receipt_preview',
},
@@ -496,6 +499,7 @@ const SCREENS = {
QUICKBOOKS_DESKTOP_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Desktop_Import_Customers_Displayed_As',
QUICKBOOKS_DESKTOP_ITEMS: 'Policy_Accounting_Quickbooks_Desktop_Import_Items',
XERO_IMPORT: 'Policy_Accounting_Xero_Import',
+ CLAIM_OFFER: 'Policy_Accounting_Claim_Offer',
XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers',
XERO_CHART_OF_ACCOUNTS: 'Policy_Accounting_Xero_Import_Chart_Of_Accounts',
XERO_CUSTOMER: 'Policy_Accounting_Xero_Import_Customer',
@@ -673,7 +677,6 @@ const SCREENS = {
WORKFLOWS_APPROVALS_OVER_LIMIT_APPROVER: 'Workspace_Workflows_Approvals_Over_Limit_Approver',
WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency',
WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset',
- WORKFLOWS_CONNECT_EXISTING_BANK_ACCOUNT: 'Workspace_Workflows_Connect_Existing_Bank_Account',
DESCRIPTION: 'Workspace_Overview_Description',
SHARE: 'Workspace_Overview_Share',
NAME: 'Workspace_Overview_Name',
@@ -799,6 +802,7 @@ const SCREENS = {
ADD_PERSONAL_BANK_ACCOUNT_ROOT: 'AddPersonalBankAccount_Root',
REIMBURSEMENT_ACCOUNT_ROOT: 'Reimbursement_Account_Root',
REIMBURSEMENT_ACCOUNT_VERIFY_ACCOUNT: 'Reimbursement_Account_Verify_Account',
+ CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT_ROOT: 'Connect_Existing_Business_Bank_Account_Root',
WALLET_STATEMENT_ROOT: 'WalletStatement_Root',
SIGN_IN_ROOT: 'SignIn_Root',
DETAILS_ROOT: 'Details_Root',
@@ -872,6 +876,9 @@ const SCREENS = {
ADMINS_SETTINGS: 'Admins_Settings',
ADD_PRIMARY_CONTACT: 'Add_Primary_Contact',
ADD_ADMIN: 'Domain_Add_Admin',
+ MEMBERS: 'Domain_Members',
+ MEMBER_DETAILS: 'Member_Details',
+ RESET_DOMAIN: 'Domain_Reset',
},
} as const;
diff --git a/src/components/ActionSheetAwareScrollView/useActionSheetAwareScrollViewRef.ts b/src/components/ActionSheetAwareScrollView/useActionSheetAwareScrollViewRef.ts
index 66029f470ff2..7690aa5c1bb4 100644
--- a/src/components/ActionSheetAwareScrollView/useActionSheetAwareScrollViewRef.ts
+++ b/src/components/ActionSheetAwareScrollView/useActionSheetAwareScrollViewRef.ts
@@ -12,7 +12,7 @@ function useActionSheetAwareScrollViewRef(ref: Ref {
diff --git a/src/components/AnimatedFlatListWithCellRenderer.tsx b/src/components/AnimatedFlatListWithCellRenderer.tsx
index f9ff4a18b61f..f223a7820b29 100644
--- a/src/components/AnimatedFlatListWithCellRenderer.tsx
+++ b/src/components/AnimatedFlatListWithCellRenderer.tsx
@@ -69,7 +69,6 @@ function FlatListForwardRefRender- (props: AnimatedFlatListWithCellRen
// We set it to 1, so we have peace until
// there are 960 fps screens.
if (!('scrollEventThrottle' in restProps)) {
- // eslint-disable-next-line react-compiler/react-compiler
restProps.scrollEventThrottle = 1;
}
diff --git a/src/components/AnimatedSubmitButton/index.tsx b/src/components/AnimatedSubmitButton/index.tsx
index 7647ebc30635..128806d5c36f 100644
--- a/src/components/AnimatedSubmitButton/index.tsx
+++ b/src/components/AnimatedSubmitButton/index.tsx
@@ -107,7 +107,6 @@ function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunn
return () => clearTimeout(timer);
}, [isAnimationRunning, isShowingLoading]);
- // eslint-disable-next-line react-compiler/react-compiler
const showLoading = isShowingLoading || (!viewRef.current && isAnimationRunning);
return (
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index d6c35237a71d..7729ecfee1f3 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -321,7 +321,6 @@ function AttachmentPicker({
* @param onCanceledHandler A callback that will be called without a selected attachment
*/
const open = (onPickedHandler: (files: FileObject[]) => void, onCanceledHandler: () => void = () => {}, onClosedHandler: () => void = () => {}) => {
- // eslint-disable-next-line react-compiler/react-compiler
completeAttachmentSelection.current = onPickedHandler;
onCanceled.current = onCanceledHandler;
onClosed.current = onClosedHandler;
@@ -498,7 +497,6 @@ function AttachmentPicker({
}}
isVisible={isVisible}
anchorRef={popoverRef}
- // eslint-disable-next-line react-compiler/react-compiler
onModalHide={() => onModalHide.current?.()}
>
@@ -514,7 +512,6 @@ function AttachmentPicker({
))}
- {/* eslint-disable-next-line react-compiler/react-compiler */}
{renderChildren()}
>
);
diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx
index e303f948e438..7be7f552a172 100644
--- a/src/components/AttachmentPicker/index.tsx
+++ b/src/components/AttachmentPicker/index.tsx
@@ -75,7 +75,6 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
// Cleanup after selecting a file to start from a fresh state
if (fileInput.current) {
- // eslint-disable-next-line react-compiler/react-compiler
fileInput.current.value = '';
}
}}
@@ -108,7 +107,6 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
accept={acceptedFileTypes ? getAcceptableFileTypesFromAList(acceptedFileTypes) : getAcceptableFileTypes(type)}
multiple={allowMultiple}
/>
- {/* eslint-disable-next-line react-compiler/react-compiler */}
{children({
openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => {
if (isPickingRef.current) {
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
index 6b62483991ba..95df58bfe1a3 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
@@ -216,7 +216,6 @@ function AttachmentCarouselView({
isPagerScrolling.set(false);
scrollTo(scrollRef, newIndex * cellWidth, 0, true);
})
- // eslint-disable-next-line react-compiler/react-compiler
.withRef(pagerRef as RefObject),
[attachments.length, canUseTouchScreen, cellWidth, page, isScrollEnabled, scrollRef, isPagerScrolling],
);
@@ -229,7 +228,7 @@ function AttachmentCarouselView({
scrollRef.current.scrollToIndex({index: page, animated: false});
// The hook is not supposed to run on page change, so we keep the page out of the dependencies
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [cellWidth]);
return (
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
index ed195fd943f1..a7ce0f93114b 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
@@ -45,7 +45,7 @@ function useCarouselArrows() {
useEffect(() => {
autoHideArrows();
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows};
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
index e9c7e7029119..235e42a31f72 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
@@ -24,7 +24,7 @@ function BaseAttachmentViewPdf({
return;
}
attachmentCarouselPagerContext.onScaleChanged?.(1);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we just want to call this function when component is mounted
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted
}, []);
/**
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
index c304187b8876..225dd84e9ff5 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx
@@ -41,7 +41,6 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
// enable the pager scroll so that the user
// can swipe to the next attachment otherwise disable it.
if (translateX > translateY && translateX > SCROLL_THRESHOLD && allowEnablingScroll) {
- // eslint-disable-next-line react-compiler/react-compiler
isScrollEnabled.set(true);
} else if (translateY > SCROLL_THRESHOLD) {
isScrollEnabled.set(false);
diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx
index 3480dc991b92..2cb83d5ea5e7 100644
--- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx
+++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx
@@ -22,7 +22,6 @@ function TransparentOverlay({onPress: onPressProp}: TransparentOverlayProps) {
const dropZone = useRef(null);
const {isDraggingOver} = useDragAndDrop({
- // eslint-disable-next-line react-compiler/react-compiler
dropZone: htmlDivElementRef(dropZone),
onDrop: () => {},
});
@@ -52,7 +51,6 @@ function TransparentOverlay({onPress: onPressProp}: TransparentOverlayProps) {
({ref, ...props}: ButtonWithDropdownM
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct popover styles
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
- // eslint-disable-next-line react-compiler/react-compiler
const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, dropdownAnchor);
const selectedItem = options.at(selectedItemIndex) ?? options.at(0);
const areAllOptionsDisabled = options.every((option) => option.disabled);
@@ -276,7 +275,6 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM
}}
anchorPosition={popoverAnchorPosition}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
- // eslint-disable-next-line react-compiler/react-compiler
anchorRef={nullCheckRef(dropdownAnchor)}
scrollContainerStyle={!shouldUseModalPaddingStyle && isSmallScreenWidth && {...styles.pt4, paddingBottom}}
anchorAlignment={anchorAlignment}
diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index 1f7d890e6c31..6251fd7d97d7 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -59,7 +59,7 @@ function Composer({
return () => clearTimeout(timeoutID);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isComposerFullSize]);
/**
@@ -67,7 +67,6 @@ function Composer({
* @param {Element} el
*/
const setTextInputRef = useCallback((el: AnimatedMarkdownTextInputRef | null) => {
- // eslint-disable-next-line react-compiler/react-compiler
textInput.current = el;
if (typeof ref !== 'function' || textInput.current === null) {
return;
@@ -78,7 +77,7 @@ function Composer({
// this.textInput = el} /> this will not
// return a ref to the component, but rather the HTML element by default
ref(textInput.current);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onClear = useCallback(
diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 0af6381864de..7d181775ed4e 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -82,7 +82,7 @@ function Composer({
return;
}
setSelection(selectionProp);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectionProp]);
/**
@@ -251,9 +251,8 @@ function Composer({
if (!textInput.current || prevScroll === undefined || prevHeight === undefined) {
return;
}
- // eslint-disable-next-line react-compiler/react-compiler
textInput.current.scrollTop = prevScroll + prevHeight - textInput.current.clientHeight;
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isComposerFullSize]);
const isActive = useIsFocused();
diff --git a/src/components/ConnectToNetSuiteFlow/index.tsx b/src/components/ConnectToNetSuiteFlow/index.tsx
index 069856e2bfbe..3b681d7e39e5 100644
--- a/src/components/ConnectToNetSuiteFlow/index.tsx
+++ b/src/components/ConnectToNetSuiteFlow/index.tsx
@@ -57,7 +57,7 @@ function ConnectToNetSuiteFlow({policyID}: ConnectToNetSuiteFlowProps) {
return;
}
setIsReuseConnectionsPopoverOpen(true);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (threeDotsMenuContainerRef) {
diff --git a/src/components/ConnectToQuickbooksDesktopFlow/index.native.tsx b/src/components/ConnectToQuickbooksDesktopFlow/index.native.tsx
index e4c5a94df7fd..425bd35f9e8f 100644
--- a/src/components/ConnectToQuickbooksDesktopFlow/index.native.tsx
+++ b/src/components/ConnectToQuickbooksDesktopFlow/index.native.tsx
@@ -6,7 +6,7 @@ import type {ConnectToQuickbooksDesktopFlowProps} from './types';
function ConnectToQuickbooksDesktopFlow({policyID}: ConnectToQuickbooksDesktopFlowProps) {
useEffect(() => {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_SETUP_REQUIRED_DEVICE_MODAL.getRoute(policyID));
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
diff --git a/src/components/ConnectToQuickbooksOnlineFlow/index.native.tsx b/src/components/ConnectToQuickbooksOnlineFlow/index.native.tsx
index c994560944fd..d15e7dc0d505 100644
--- a/src/components/ConnectToQuickbooksOnlineFlow/index.native.tsx
+++ b/src/components/ConnectToQuickbooksOnlineFlow/index.native.tsx
@@ -26,7 +26,7 @@ function ConnectToQuickbooksOnlineFlow({policyID}: ConnectToQuickbooksOnlineFlow
// Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO
enablePolicyTaxes(policyID, false);
setIsWebViewOpen(true);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/ConnectToQuickbooksOnlineFlow/index.tsx b/src/components/ConnectToQuickbooksOnlineFlow/index.tsx
index a08979f30cd5..b9229f86b404 100644
--- a/src/components/ConnectToQuickbooksOnlineFlow/index.tsx
+++ b/src/components/ConnectToQuickbooksOnlineFlow/index.tsx
@@ -12,7 +12,7 @@ function ConnectToQuickbooksOnlineFlow({policyID}: ConnectToQuickbooksOnlineFlow
// Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO
PolicyAction.enablePolicyTaxes(policyID, false);
Link.openLink(getQuickbooksOnlineSetupLink(policyID), environmentURL);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
diff --git a/src/components/ConnectToSageIntacctFlow/index.tsx b/src/components/ConnectToSageIntacctFlow/index.tsx
index 0c87640d4314..ef68e503d37e 100644
--- a/src/components/ConnectToSageIntacctFlow/index.tsx
+++ b/src/components/ConnectToSageIntacctFlow/index.tsx
@@ -26,7 +26,7 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) {
return;
}
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.getRoute(policyID));
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
diff --git a/src/components/ConnectToXeroFlow/index.native.tsx b/src/components/ConnectToXeroFlow/index.native.tsx
index d8a7a0219d2c..605dc0084fa0 100644
--- a/src/components/ConnectToXeroFlow/index.native.tsx
+++ b/src/components/ConnectToXeroFlow/index.native.tsx
@@ -36,7 +36,7 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
return;
}
setIsWebViewOpen(true);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/ConnectToXeroFlow/index.tsx b/src/components/ConnectToXeroFlow/index.tsx
index 061648267a84..acfedfcf8e4f 100644
--- a/src/components/ConnectToXeroFlow/index.tsx
+++ b/src/components/ConnectToXeroFlow/index.tsx
@@ -27,7 +27,7 @@ function ConnectToXeroFlow({policyID}: ConnectToXeroFlowProps) {
return;
}
openLink(getXeroSetupLink(policyID), environmentURL);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!is2FAEnabled) {
diff --git a/src/components/ContactPermissionModal/index.native.tsx b/src/components/ContactPermissionModal/index.native.tsx
index 541cc05ac341..9ee25223dba6 100644
--- a/src/components/ContactPermissionModal/index.native.tsx
+++ b/src/components/ContactPermissionModal/index.native.tsx
@@ -32,7 +32,7 @@ function ContactPermissionModal({onDeny, onGrant, onFocusTextInput}: ContactPerm
}
setIsModalVisible(true);
});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleGrantPermission = () => {
diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx
index 02c20d0c9e82..9c64311a1aea 100644
--- a/src/components/CountrySelector.tsx
+++ b/src/components/CountrySelector.tsx
@@ -65,7 +65,7 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = ()
// This helps prevent issues where the component might not update correctly if the country is controlled by both the parent and the URL.
Navigation.setParams({country: undefined});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [countryFromUrl, isFocused, onBlur]);
return (
diff --git a/src/components/CurrencySelector.tsx b/src/components/CurrencySelector.tsx
index 542de4ecdd8c..9bb6e80a50bb 100644
--- a/src/components/CurrencySelector.tsx
+++ b/src/components/CurrencySelector.tsx
@@ -69,7 +69,7 @@ function CurrencySelector({
useEffect(() => {
// This will cause the form to revalidate and remove any error related to currency
onInputChange(currency);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currency]);
return (
diff --git a/src/components/DestinationPicker.tsx b/src/components/DestinationPicker.tsx
index d32d594a38ea..8ffee6e20d3f 100644
--- a/src/components/DestinationPicker.tsx
+++ b/src/components/DestinationPicker.tsx
@@ -71,6 +71,7 @@ function DestinationPicker({selectedDestination, policyID, onSubmit}: Destinatio
return (
);
}
diff --git a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
index 7dbef23f38b0..88cd002eab5c 100644
--- a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
+++ b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
@@ -64,7 +64,7 @@ function DisplayNamesTooltipItem({
if (!childRefs.current?.at(index) || !el) {
return;
}
- // eslint-disable-next-line react-compiler/react-compiler, no-param-reassign
+ // eslint-disable-next-line no-param-reassign
childRefs.current[index] = el;
}}
style={[textStyles, styles.pre]}
diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
index e569b02aecbc..8a6024275c86 100644
--- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
+++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
@@ -24,7 +24,6 @@ function DisplayNamesWithToolTip({
const styles = useThemeStyles();
const containerRef = useRef(null);
const childRefs = useRef([]);
- // eslint-disable-next-line react-compiler/react-compiler
const isEllipsisActive = !!containerRef.current?.offsetWidth && !!containerRef.current?.scrollWidth && containerRef.current.offsetWidth < containerRef.current.scrollWidth;
/**
diff --git a/src/components/Domain/DomainMenuItem.tsx b/src/components/Domain/DomainMenuItem.tsx
index 2e0a5356b9a2..c8f207b9a43a 100644
--- a/src/components/Domain/DomainMenuItem.tsx
+++ b/src/components/Domain/DomainMenuItem.tsx
@@ -8,7 +8,9 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
+import {clearDomainErrors} from '@src/libs/actions/Domain';
import ROUTES from '@src/ROUTES';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
import DomainsListRow from './DomainsListRow';
type DomainMenuItemProps = {
@@ -37,6 +39,9 @@ type DomainItem = {
/** Whether the row's domain is validated (aka verified) */
isValidated: boolean;
+
+ /** Current errors for domain */
+ errors?: Errors;
} & Pick;
function DomainMenuItem({item, index}: DomainMenuItemProps) {
@@ -68,12 +73,14 @@ function DomainMenuItem({item, index}: DomainMenuItemProps) {
clearDomainErrors(item.accountID)}
>
{({hovered}) => (
diff --git a/src/components/DragAndDrop/NoDropZone/index.tsx b/src/components/DragAndDrop/NoDropZone/index.tsx
index 69113ac9b9ea..b870339a4d2f 100644
--- a/src/components/DragAndDrop/NoDropZone/index.tsx
+++ b/src/components/DragAndDrop/NoDropZone/index.tsx
@@ -11,14 +11,12 @@ function NoDropZone({children}: NoDropZoneProps) {
const noDropZone = useRef(null);
useDragAndDrop({
- // eslint-disable-next-line react-compiler/react-compiler
dropZone: htmlDivElementRef(noDropZone),
shouldAllowDrop: false,
});
return (
diff --git a/src/components/DragAndDrop/Provider/index.tsx b/src/components/DragAndDrop/Provider/index.tsx
index 666402b24a95..e534c4bcc4db 100644
--- a/src/components/DragAndDrop/Provider/index.tsx
+++ b/src/components/DragAndDrop/Provider/index.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-compiler/react-compiler */
import {PortalHost} from '@gorhom/portal';
import {Str} from 'expensify-common';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
diff --git a/src/components/DropZone/DropZoneWrapper.tsx b/src/components/DropZone/DropZoneWrapper.tsx
index 83be6904bb9f..717c592a84c0 100644
--- a/src/components/DropZone/DropZoneWrapper.tsx
+++ b/src/components/DropZone/DropZoneWrapper.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-compiler/react-compiler */
import type {ReactNode} from 'react';
import React, {useRef} from 'react';
import {View} from 'react-native';
diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx
index 20f8ad521a3d..8d759fc14f86 100644
--- a/src/components/EmojiPicker/EmojiPicker.tsx
+++ b/src/components/EmojiPicker/EmojiPicker.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-compiler/react-compiler */
import React, {useCallback, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react';
import type {ForwardedRef, RefObject} from 'react';
import {Dimensions, View} from 'react-native';
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
index 4a18692c8c75..386c0b33f715 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
+++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
@@ -14,7 +14,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
const useEmojiPickerMenu = () => {
const emojiListRef = useRef>(null);
const [frequentlyUsedEmojis] = useOnyx(ONYXKEYS.FREQUENTLY_USED_EMOJIS, {canBeMissing: true});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
const allEmojis = useMemo(() => mergeEmojisWithFrequentlyUsedEmojis(emojis, processFrequentlyUsedEmojis(frequentlyUsedEmojis)), [frequentlyUsedEmojis]);
const headerEmojis = useMemo(() => getHeaderEmojis(allEmojis), [allEmojis]);
const headerRowIndices = useMemo(() => headerEmojis.map((headerEmoji) => headerEmoji.index), [headerEmojis]);
diff --git a/src/components/EmojiPicker/EmojiSkinToneList.tsx b/src/components/EmojiPicker/EmojiSkinToneList.tsx
index 1fcbc89c525e..81e35eec1930 100644
--- a/src/components/EmojiPicker/EmojiSkinToneList.tsx
+++ b/src/components/EmojiPicker/EmojiSkinToneList.tsx
@@ -38,7 +38,7 @@ function EmojiSkinToneList() {
return;
}
toggleIsSkinToneListVisible();
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- only run when preferredSkinTone updates
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only run when preferredSkinTone updates
}, [preferredSkinTone]);
const currentSkinTone = getSkinToneEmojiFromIndex(preferredSkinTone);
diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx
index 931034a585ef..965682e2cbcd 100644
--- a/src/components/FeedbackSurvey.tsx
+++ b/src/components/FeedbackSurvey.tsx
@@ -73,7 +73,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe
}
setReason(draft.reason);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- only sync with draft data when it is loaded
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only sync with draft data when it is loaded
}, [isLoadingDraft]);
const handleOptionSelect = (value: string) => {
diff --git a/src/components/FilePicker/index.native.tsx b/src/components/FilePicker/index.native.tsx
index 3a2da022861a..8efd8b1dbd72 100644
--- a/src/components/FilePicker/index.native.tsx
+++ b/src/components/FilePicker/index.native.tsx
@@ -139,7 +139,6 @@ function FilePicker({children}: FilePickerProps) {
openPicker: ({onPicked, onCanceled: newOnCanceled}) => open(onPicked, newOnCanceled),
});
- // eslint-disable-next-line react-compiler/react-compiler
return <>{renderChildren()}>;
}
diff --git a/src/components/FilePicker/index.tsx b/src/components/FilePicker/index.tsx
index f394a1b972f0..3d3b16518821 100644
--- a/src/components/FilePicker/index.tsx
+++ b/src/components/FilePicker/index.tsx
@@ -34,7 +34,6 @@ function FilePicker({children, acceptableFileTypes = ''}: FilePickerProps): Reac
// Cleanup after selecting a file to start from a fresh state
if (fileInput.current) {
- // eslint-disable-next-line react-compiler/react-compiler
fileInput.current.value = '';
}
}}
@@ -65,7 +64,6 @@ function FilePicker({children, acceptableFileTypes = ''}: FilePickerProps): Reac
}}
accept={acceptableFileTypes}
/>
- {/* eslint-disable-next-line react-compiler/react-compiler */}
{children({
openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => {
onPicked.current = newOnPicked;
diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx
index afe3bac07ad6..9ac8f70b2d53 100644
--- a/src/components/FlatList/index.android.tsx
+++ b/src/components/FlatList/index.android.tsx
@@ -24,7 +24,7 @@ function CustomFlatList({ref, enableAnimatedKeyboardDismissal = false, onMome
}
}, [ref]);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
const handleScrollEnd = useCallback(
(event: NativeSyntheticEvent) => {
onMomentumScrollEnd?.(event);
diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx
index 4b8e1852c92f..70e72f174fc7 100644
--- a/src/components/FlatList/index.tsx
+++ b/src/components/FlatList/index.tsx
@@ -62,7 +62,6 @@ function MVCPFlatList({
const lastScrollOffsetRef = useRef(0);
const isListRenderedRef = useRef(false);
const mvcpAutoscrollToTopThresholdRef = useRef(mvcpAutoscrollToTopThreshold);
- // eslint-disable-next-line react-compiler/react-compiler
mvcpAutoscrollToTopThresholdRef.current = mvcpAutoscrollToTopThreshold;
const getScrollOffset = useCallback((): number => {
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index 32cb0c0c48a3..2050822941c3 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -221,7 +221,7 @@ function FormProvider({
onValidate(trimmedStringValues, !hasServerError);
// Only run when locales change
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [preferredLocale]);
/** @param inputID - The inputID of the input being touched */
@@ -247,6 +247,10 @@ function FormProvider({
touchedInputs.current[inputID] = true;
}
+ if (hasServerError) {
+ return;
+ }
+
// Validate form and return early if any errors are found
if (!isEmptyObject(onValidate(trimmedStringValues))) {
return;
@@ -258,7 +262,7 @@ function FormProvider({
}
KeyboardUtils.dismiss().then(() => onSubmit(trimmedStringValues));
- }, [enabledWhenOffline, formState?.isLoading, inputValues, isLoading, network?.isOffline, onSubmit, onValidate, shouldTrimValues]),
+ }, [enabledWhenOffline, formState?.isLoading, inputValues, isLoading, network?.isOffline, onSubmit, onValidate, shouldTrimValues, hasServerError]),
1000,
{leading: true, trailing: false},
);
@@ -323,7 +327,6 @@ function FormProvider({
inputRefs.current[inputID] = newRef;
}
if (inputProps.value !== undefined) {
- // eslint-disable-next-line react-compiler/react-compiler
inputValues[inputID] = inputProps.value;
} else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) {
inputValues[inputID] = draftValues[inputID];
diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx
index ecc6debdc040..b37719693db8 100644
--- a/src/components/Form/InputWrapper.tsx
+++ b/src/components/Form/InputWrapper.tsx
@@ -82,7 +82,6 @@ function InputWrapper({
const {registerInput} = useContext(FormContext);
const {shouldSetTouchedOnBlurOnly, submitBehavior, shouldSubmitForm} = computeComponentSpecificRegistrationParams(props as InputComponentBaseProps);
- // eslint-disable-next-line react-compiler/react-compiler
const {key, ...registerInputProps} = registerInput(inputID, shouldSubmitForm, {ref, valueType, ...rest, shouldSetTouchedOnBlurOnly, submitBehavior});
return (
diff --git a/src/components/FormElement/index.tsx b/src/components/FormElement/index.tsx
index 4382b25affea..408794d95e24 100644
--- a/src/components/FormElement/index.tsx
+++ b/src/components/FormElement/index.tsx
@@ -17,7 +17,6 @@ const preventFormDefault = (event: SubmitEvent) => {
function FormElement({ref, ...props}: FormElementProps) {
const formRef = useRef(null);
- // eslint-disable-next-line react-compiler/react-compiler
const mergedRef = mergeRefs(formRef, ref);
useEffect(() => {
diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx
index 43b820eec9ed..f6c32f821088 100644
--- a/src/components/Hoverable/ActiveHoverable.tsx
+++ b/src/components/Hoverable/ActiveHoverable.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-compiler/react-compiler */
import {cloneElement, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {DeviceEventEmitter} from 'react-native';
import mergeRefs from '@libs/mergeRefs';
diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx
index b710354b7b97..14e73343a77e 100644
--- a/src/components/Hoverable/index.tsx
+++ b/src/components/Hoverable/index.tsx
@@ -15,7 +15,6 @@ function Hoverable({isDisabled, ref, ...props}: HoverableProps) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (isDisabled || !hasHoverSupport()) {
const child = getReturnValue(props.children, false);
- // eslint-disable-next-line react-compiler/react-compiler
return cloneElement(child, {ref: mergeRefs(ref, child.props.ref)} as React.HTMLAttributes);
}
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index 3c2e66df07a6..02e26682bc7a 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -215,6 +215,7 @@ 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 TreasureChestGreenWithSparkle from '@assets/images/treasure-chest-green-with-sparkle.svg';
import TreasureChest from '@assets/images/treasure-chest.svg';
import Unlock from '@assets/images/unlock.svg';
import UploadAlt from '@assets/images/upload-alt.svg';
@@ -222,6 +223,7 @@ import Upload from '@assets/images/upload.svg';
import UserCheck from '@assets/images/user-check.svg';
import UserEye from '@assets/images/user-eye.svg';
import UserLock from '@assets/images/user-lock.svg';
+import UserMinus from '@assets/images/user-minus.svg';
import UserPlus from '@assets/images/user-plus.svg';
import User from '@assets/images/user.svg';
import Users from '@assets/images/users.svg';
@@ -342,6 +344,7 @@ const Expensicons = {
LinkCopy,
Location,
Lock,
+ UserMinus,
Luggage,
MagnifyingGlass,
Mail,
@@ -469,6 +472,7 @@ const Expensicons = {
XeroExport,
ArrowCircleClockwise,
LuggageWithLines,
+ TreasureChestGreenWithSparkle,
};
// Create the ExpensifyIcons object from the imported Expensicons
diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts
index f1c505d7e1d7..ce7cf4c06406 100644
--- a/src/components/Icon/chunks/illustrations.chunk.ts
+++ b/src/components/Icon/chunks/illustrations.chunk.ts
@@ -156,6 +156,7 @@ import TrashCan from '@assets/images/simple-illustrations/simple-illustration__t
import TravelAlerts from '@assets/images/simple-illustrations/simple-illustration__travelalerts.svg';
import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__twocards-horizontal.svg';
+import UserShield from '@assets/images/simple-illustrations/simple-illustration__user-shield.svg';
import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg';
import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg';
import ExpensifyApprovedLogo from '@assets/images/subscription-details__approvedlogo.svg';
@@ -329,6 +330,7 @@ const Illustrations = {
ShieldYellow,
Clock,
Members,
+ UserShield,
};
/**
diff --git a/src/components/Image/BaseImage.android.tsx b/src/components/Image/BaseImage.android.tsx
index 6475eb5ca33a..e272ceeb9051 100644
--- a/src/components/Image/BaseImage.android.tsx
+++ b/src/components/Image/BaseImage.android.tsx
@@ -15,9 +15,14 @@ function BaseImage({onLoad, source, ...props}: BaseImageProps) {
return;
}
setAttachmentLoaded(source as AttachmentSource, false);
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
+ // Reset isLoadedRef when source changes to allow onLoad to fire again for new images (e.g., after rotation)
+ useEffect(() => {
+ isLoadedRef.current = false;
+ }, [source]);
+
const imageLoadedSuccessfully = useCallback(
(event: ImageLoadEventData) => {
setAttachmentLoaded(source as AttachmentSource, true);
diff --git a/src/components/Image/BaseImage.ios.tsx b/src/components/Image/BaseImage.ios.tsx
index 5fd7bfa5a9b1..ca90ef3d1768 100644
--- a/src/components/Image/BaseImage.ios.tsx
+++ b/src/components/Image/BaseImage.ios.tsx
@@ -15,9 +15,14 @@ function BaseImage({onLoad, source, ...props}: BaseImageProps) {
return;
}
setAttachmentLoaded(source as AttachmentSource, false);
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+
+ // Reset isLoadedRef when source changes to allow onLoad to fire again for new images (e.g., after rotation)
+ useEffect(() => {
+ isLoadedRef.current = false;
+ }, [source]);
+
const imageLoadedSuccessfully = useCallback(
(event: ImageLoadEventData) => {
setAttachmentLoaded(source as AttachmentSource, true);
diff --git a/src/components/Image/BaseImage.tsx b/src/components/Image/BaseImage.tsx
index 8fcf192b9064..ca07c09323b2 100644
--- a/src/components/Image/BaseImage.tsx
+++ b/src/components/Image/BaseImage.tsx
@@ -13,7 +13,6 @@ function BaseImage({onLoad, source, ...props}: BaseImageProps) {
}
setAttachmentLoaded?.(source as AttachmentSource, false);
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const imageLoadedSuccessfully = useCallback(
diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx
index bb0b10857d6e..fe5d8f5040e9 100644
--- a/src/components/Image/index.tsx
+++ b/src/components/Image/index.tsx
@@ -138,7 +138,7 @@ function Image({
// The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034.
// but we still need the image to reload sometimes (example : when the current session is expired)
// by forcing a recalculation of the source (which value could indeed change) through the modification of the variable validSessionAge
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [propsSource, isAuthTokenRequired, validSessionAge]);
useEffect(() => {
if (!isAuthTokenRequired || source !== undefined) {
diff --git a/src/components/ImportColumn.tsx b/src/components/ImportColumn.tsx
index 5bf5a29048ab..3f6451882dd9 100644
--- a/src/components/ImportColumn.tsx
+++ b/src/components/ImportColumn.tsx
@@ -175,7 +175,7 @@ function ImportColumn({column, columnName, columnRoles, columnIndex, shouldShowD
return;
}
setColumnName(columnIndex, colName);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again
}, []);
const columnHeader = containsHeader ? column.at(0) : translate('spreadsheet.column', {name: columnName});
diff --git a/src/components/ImportSpreadsheet.tsx b/src/components/ImportSpreadsheet.tsx
index 60b498b3a239..084e08ef47a4 100644
--- a/src/components/ImportSpreadsheet.tsx
+++ b/src/components/ImportSpreadsheet.tsx
@@ -65,7 +65,9 @@ function ImportSpreadsheet({backTo, goTo, isImportingMultiLevelTags}: ImportSpre
const validateFile = (file: FileObject) => {
const {fileExtension} = splitExtensionFromFileName(file?.name ?? '');
- if (!CONST.ALLOWED_SPREADSHEET_EXTENSIONS.includes(fileExtension.toLowerCase() as TupleToUnion)) {
+ const allowedExtensions: readonly string[] = isImportingMultiLevelTags ? CONST.MULTILEVEL_TAG_ALLOWED_SPREADSHEET_EXTENSIONS : CONST.ALLOWED_SPREADSHEET_EXTENSIONS;
+
+ if (!allowedExtensions.includes(fileExtension.toLowerCase())) {
setUploadFileError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension');
return false;
}
@@ -161,7 +163,7 @@ function ImportSpreadsheet({backTo, goTo, isImportingMultiLevelTags}: ImportSpre
{isImportingMultiLevelTags ? translate('spreadsheet.import') : translate('spreadsheet.upload')}
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index ac4814c1a8f5..be3641aa3508 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-compiler/react-compiler */
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {Dimensions} from 'react-native';
import type {EmitterSubscription, View} from 'react-native';
@@ -10,13 +9,16 @@ import useOnyx from '@hooks/useOnyx';
import useParentReportAction from '@hooks/useParentReportAction';
import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts';
import {completePaymentOnboarding, savePreferredPaymentMethod} from '@libs/actions/IOU';
+import {navigateToBankAccountRoute} from '@libs/actions/ReimbursementAccount';
import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report';
+import {isBankAccountPartiallySetup} from '@libs/BankAccountUtils';
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
import Log from '@libs/Log';
import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue';
import Navigation from '@libs/Navigation/Navigation';
import {hasExpensifyPaymentMethod} from '@libs/PaymentUtils';
import {getBankAccountRoute, isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils';
+import {getEligibleExistingBusinessBankAccounts, getOpenConnectedToPolicyBusinessBankAccounts} from '@libs/WorkflowUtils';
import {createWorkspaceFromIOUPayment} from '@userActions/Policy/Policy';
import {setKYCWallSource} from '@userActions/Wallet';
import CONST from '@src/CONST';
@@ -51,15 +53,17 @@ function KYCWall({
source,
shouldShowPersonalBankAccountOption = false,
ref,
+ currency,
}: KYCWallProps) {
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true});
const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true});
const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true});
const [bankAccountList = getEmptyObject()] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
- const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true});
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, {canBeMissing: true});
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true});
+
const {formatPhoneNumber} = useLocalize();
const currentUserDetails = useCurrentUserPersonalDetails();
const currentUserEmail = currentUserDetails.email ?? '';
@@ -115,6 +119,8 @@ function KYCWall({
setPositionAddPaymentMenu(position);
}, [getAnchorPosition]);
+ const canLinkExistingBusinessBankAccount = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true).length > 0;
+
const selectPaymentMethod = useCallback(
(paymentMethod?: PaymentMethod, policy?: Policy) => {
if (paymentMethod) {
@@ -169,6 +175,22 @@ function KYCWall({
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID));
return;
}
+
+ // If user has a setup in progress for we redirect to the flow where setup can be finished
+ // Setup is in progress in 2 cases:
+ // - account already present on policy is partially setup
+ // - account is being connected 'on the spot' while trying to pay for an expense (it won't be linked to policy yet but will appear as reimbursementAccount)
+ if (policy !== undefined && (isBankAccountPartiallySetup(policy?.achAccount?.state) || isBankAccountPartiallySetup(reimbursementAccount?.achData?.state))) {
+ navigateToBankAccountRoute(policy.id);
+ return;
+ }
+
+ // If user has existing bank accounts that he can connect we show the list of these accounts
+ if (policy !== undefined && canLinkExistingBusinessBankAccount) {
+ Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policy?.id));
+ return;
+ }
+
const bankAccountRoute = addBankAccountRoute ?? getBankAccountRoute(chatReport);
Navigation.navigate(bankAccountRoute);
}
@@ -177,6 +199,8 @@ function KYCWall({
onSelectPaymentMethod,
iouReport,
addDebitCardRoute,
+ reimbursementAccount?.achData?.state,
+ canLinkExistingBusinessBankAccount,
addBankAccountRoute,
chatReport,
policies,
@@ -218,11 +242,14 @@ function KYCWall({
const isExpenseReport = isExpenseReportReportUtils(iouReport);
const paymentCardList = fundList ?? {};
+ const hasOpenConnectedBusinessBankAccount = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policy).length > 0;
const hasValidPaymentMethod = hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard);
const isFromWalletPage = source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET || source === CONST.KYC_WALL_SOURCE.TRANSFER_BALANCE;
- // Check to see if user has a valid payment method on file and display the add payment popover if they don't
- if ((isExpenseReport && reimbursementAccount?.achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) || (!isExpenseReport && bankAccountList !== null && !hasValidPaymentMethod)) {
+ // Check if the user needs to add or select a payment method before continuing.
+ // - For expense reports: Proceeds if no accounts that are connected are valid and usable (`OPEN`)
+ // - For other expenses: Proceeds if the user lacks a valid personal bank account or debit card
+ if ((isExpenseReport && !hasOpenConnectedBusinessBankAccount) || (!isExpenseReport && bankAccountList !== null && !hasValidPaymentMethod)) {
Log.info('[KYC Wallet] User does not have valid payment method');
if (!shouldIncludeDebitCard || (isFromWalletPage && !hasValidPaymentMethod)) {
@@ -286,7 +313,6 @@ function KYCWall({
getAnchorPosition,
iouReport,
onSuccessfulKYC,
- reimbursementAccount?.achData?.state,
selectPaymentMethod,
shouldIncludeDebitCard,
shouldShowAddPaymentMenu,
diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts
index 59f3485442d6..80f6be6afbc9 100644
--- a/src/components/KYCWall/types.ts
+++ b/src/components/KYCWall/types.ts
@@ -77,6 +77,9 @@ type KYCWallProps = {
/** Reference to the KYCWall component */
ref: ForwardedRef;
+
+ /** Currency associated with the payment */
+ currency?: string;
};
type KYCWallRef = {
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index 27f1009824dd..362146554059 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -77,19 +77,16 @@ function OptionRowLHNData({
movedFromReport,
movedToReport,
});
- // eslint-disable-next-line react-compiler/react-compiler
if (deepEqual(item, optionItemRef.current)) {
- // eslint-disable-next-line react-compiler/react-compiler
return optionItemRef.current;
}
- // eslint-disable-next-line react-compiler/react-compiler
optionItemRef.current = item;
return item;
// Listen parentReportAction to update title of thread report when parentReportAction changed
// Listen to transaction to update title of transaction report when transaction changed
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
fullReport,
reportAttributes?.brickRoadStatus,
diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx
index 1b9ffe100dea..ee6329027e1f 100644
--- a/src/components/Lightbox/index.tsx
+++ b/src/components/Lightbox/index.tsx
@@ -12,6 +12,7 @@ import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCa
import type {OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types';
import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils';
import useNetwork from '@hooks/useNetwork';
+import usePrevious from '@hooks/usePrevious';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {isLocalFile} from '@libs/fileDownload/FileUtils';
@@ -148,6 +149,24 @@ function Lightbox({attachmentID, isAuthTokenRequired = false, uri, onScaleChange
const [isFallbackVisible, setFallbackVisible] = useState(!isLightboxVisible);
const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false);
+ const previousUri = usePrevious(uri);
+
+ // Clear cached dimensions and reset loading states when URI changes to ensure the new image get fresh dimensions
+ useEffect(() => {
+ if (previousUri === uri || !previousUri || !uri) {
+ return;
+ }
+ // Clear the content size state to force recalculation of dimensions
+ // This ensures that when an image is rotated and gets a new URI,
+ // we don't use stale cached dimensions from the previous image
+ setInternalContentSize(undefined);
+ setLightboxImageLoaded(false);
+ setFallbackImageLoaded(false);
+ setIsLoading(true);
+ // Don't delete from cache here as other components might still need it
+ // The new URI will get its own cache entry when loaded
+ }, [uri, previousUri]);
+
const fallbackSize = useMemo(() => {
if (!hasSiblingCarouselItems || !contentSize || isCanvasLoading) {
return undefined;
@@ -276,7 +295,7 @@ function Lightbox({attachmentID, isAuthTokenRequired = false, uri, onScaleChange
)}
{/* Show activity indicator while the lightbox is still loading the image. */}
- {!isImageLoaded && !shouldShowOfflineIndicator && (
+ {(!isImageLoaded || previousUri !== uri) && !shouldShowOfflineIndicator && (
{
if (shouldShow) {
- // eslint-disable-next-line react-compiler/react-compiler
left.set(0);
width.set(0);
opacity.set(withTiming(1, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}));
@@ -59,7 +58,7 @@ function LoadingBar({shouldShow}: LoadingBarProps) {
);
}
// we want to update only when shouldShow changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldShow]);
const animatedIndicatorStyle = useAnimatedStyle(() => ({
diff --git a/src/components/LocationPermissionModal/index.android.tsx b/src/components/LocationPermissionModal/index.android.tsx
index 3cec7b004ce2..f1acc55d8f71 100644
--- a/src/components/LocationPermissionModal/index.android.tsx
+++ b/src/components/LocationPermissionModal/index.android.tsx
@@ -33,7 +33,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe
setShowModal(true);
setHasError(status === RESULTS.BLOCKED);
});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes
}, [startPermissionFlow]);
const handledBlockedPermission = (cb: () => void) => () => {
diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx
index 6b144b369780..2cfcaff4cb7a 100644
--- a/src/components/LocationPermissionModal/index.tsx
+++ b/src/components/LocationPermissionModal/index.tsx
@@ -70,7 +70,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe
setShowModal(true);
setHasError(status === RESULTS.BLOCKED);
});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes
}, [startPermissionFlow]);
const handledBlockedPermission = (cb: () => void) => () => {
diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx
index 1c4f2102afd6..a5c1833541bb 100644
--- a/src/components/Lottie/index.tsx
+++ b/src/components/Lottie/index.tsx
@@ -48,7 +48,7 @@ function Lottie({source, webStyle, shouldLoadAfterInteractions, ref, ...props}:
return () => {
interactionTask.cancel();
};
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const aspectRatioStyle = styles.aspectRatioLottie(source);
diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx
index d01ba73d5e41..6741825dd2d2 100644
--- a/src/components/MagicCodeInput.tsx
+++ b/src/components/MagicCodeInput.tsx
@@ -249,7 +249,7 @@ function MagicCodeInput({
// We have not added:
// + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code.
// + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, shouldSubmitOnComplete]);
/**
@@ -270,7 +270,6 @@ function MagicCodeInput({
*/
const tapGesture = Gesture.Tap()
.runOnJS(true)
- // eslint-disable-next-line react-compiler/react-compiler
.onBegin((event) => {
const index = Math.floor(event.x / (inputWidth.current / maxLength));
shouldFocusLast.current = false;
@@ -434,14 +433,14 @@ function MagicCodeInput({
// We have not added:
// + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastPressedDigit, isDisableKeyboard]);
const cursorOpacity = useSharedValue(1);
useEffect(() => {
cursorOpacity.set(withRepeat(withSequence(withDelay(500, withTiming(0, {duration: 0})), withDelay(500, withTiming(1, {duration: 0}))), -1, false));
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const animatedCursorStyle = useAnimatedStyle(() => ({
diff --git a/src/components/MapView/MapViewImpl.website.tsx b/src/components/MapView/MapViewImpl.website.tsx
index c11f0cba91aa..e4d9714f64f6 100644
--- a/src/components/MapView/MapViewImpl.website.tsx
+++ b/src/components/MapView/MapViewImpl.website.tsx
@@ -173,7 +173,7 @@ function MapViewImpl({
resetBoundaries();
setShouldResetBoundaries(false);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced
}, [shouldResetBoundaries]);
useEffect(() => {
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index f11abee49c16..6d6d3a807f98 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -87,6 +87,9 @@ type MenuItemBaseProps = ForwardedFSClassProps &
/** Whether the badge should be shown as success */
badgeSuccess?: boolean;
+ /** Callback to fire when the badge is pressed */
+ onBadgePress?: (event?: GestureResponderEvent | KeyboardEvent) => void;
+
/** Used to apply offline styles to child text components */
style?: StyleProp;
@@ -397,6 +400,12 @@ type MenuItemBaseProps = ForwardedFSClassProps &
/** Whether the screen containing the item is focused */
isFocused?: boolean;
+
+ /** Whether to show the badge in a separate row */
+ shouldShowBadgeInSeparateRow?: boolean;
+
+ /** Whether to show the badge below the title */
+ shouldShowBadgeBelow?: boolean;
};
type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
@@ -416,6 +425,9 @@ function MenuItem({
badgeText,
badgeIcon,
badgeSuccess,
+ onBadgePress,
+ shouldShowBadgeInSeparateRow = false,
+ shouldShowBadgeBelow = false,
style,
wrapperStyle,
titleWrapperStyle,
@@ -914,18 +926,30 @@ function MenuItem({
)}
+ {!!badgeText && shouldShowBadgeBelow && (
+
+ )}
{furtherDetailsComponent}
{titleComponent}
- {!!badgeText && (
+ {!!badgeText && !shouldShowBadgeInSeparateRow && !shouldShowBadgeBelow && (
)}
{/* Since subtitle can be of type number, we should allow 0 to be shown */}
@@ -1007,6 +1031,16 @@ function MenuItem({
)}
+ {!!badgeText && shouldShowBadgeInSeparateRow && (
+
+ )}
{!!errorText && (
(
new Keyframe(getModalOutAnimation(animationOut))
.duration(animationOutTiming)
- // eslint-disable-next-line react-compiler/react-compiler
.withCallback(() => onCloseCallbackRef.current())
// on web the callbacks are not called when animations are disabled with the reduced motion setting on
// we enable the animations to make sure they are called
diff --git a/src/components/Modal/ReanimatedModal/index.tsx b/src/components/Modal/ReanimatedModal/index.tsx
index d80649752863..d43aae9bc604 100644
--- a/src/components/Modal/ReanimatedModal/index.tsx
+++ b/src/components/Modal/ReanimatedModal/index.tsx
@@ -106,7 +106,7 @@ function ReanimatedModal({
setIsVisibleState(false);
setIsContainerOpen(false);
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
@@ -127,7 +127,7 @@ function ReanimatedModal({
setIsVisibleState(false);
setIsTransitioning(true);
}
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, isContainerOpen, isTransitioning]);
const backdropStyle: ViewStyle = useMemo(() => {
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
index 926e48e1c699..25a1ad6b10f4 100644
--- a/src/components/Modal/index.tsx
+++ b/src/components/Modal/index.tsx
@@ -22,6 +22,9 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
const hideModal = () => {
onModalHide();
+ if ((window.history.state as WindowState)?.shouldGoBack && shouldHandleNavigationBack) {
+ window.history.back();
+ }
};
const handlePopStateRef = useRef(() => {
@@ -34,7 +37,6 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
handlePopStateRef.current = () => {
rest.onClose?.();
};
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rest.onClose]);
@@ -83,9 +85,6 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
const onModalWillHide = () => {
setStatusBarColor(previousStatusBarColor);
rest.onModalWillHide?.();
- if ((window.history.state as WindowState)?.shouldGoBack && shouldHandleNavigationBack) {
- window.history.back();
- }
};
return (
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 0751d9a2e57b..251f50f2e9b2 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -105,7 +105,6 @@ import {
cancelPayment,
canIOUBePaid as canIOUBePaidAction,
dismissRejectUseExplanation,
- duplicateExpenseTransaction as duplicateTransactionAction,
getNavigationUrlOnMoneyRequestDelete,
initSplitExpense,
markRejectViolationAsResolved,
@@ -117,6 +116,7 @@ import {
submitReport,
unapproveExpenseReport,
} from '@userActions/IOU';
+import {duplicateExpenseTransaction as duplicateTransactionAction} from '@userActions/IOU/DuplicateAction';
import {markAsCash as markAsCashAction} from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -209,6 +209,7 @@ function MoneyReportHeader({
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
const [reportPDFFilename] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDF_FILENAME}${moneyRequestReport?.reportID}`, {canBeMissing: true}) ?? null;
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
const activePolicy = usePolicy(activePolicyID);
const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true});
@@ -385,8 +386,8 @@ function MoneyReportHeader({
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${moneyRequestReport?.reportID}`, {canBeMissing: true});
const getCanIOUBePaid = useCallback(
- (onlyShowPayElsewhere = false) => canIOUBePaidAction(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere),
- [moneyRequestReport, chatReport, policy, transaction],
+ (onlyShowPayElsewhere = false) => canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, transaction ? [transaction] : undefined, onlyShowPayElsewhere),
+ [moneyRequestReport, chatReport, policy, bankAccountList, transaction],
);
const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport);
@@ -761,12 +762,13 @@ function MoneyReportHeader({
const primaryAction = useMemo(() => {
return getReportPrimaryAction({
- currentUserEmail: currentUserLogin ?? '',
+ currentUserLogin: currentUserLogin ?? '',
currentUserAccountID: accountID,
report: moneyRequestReport,
chatReport,
reportTransactions: transactions,
violations,
+ bankAccountList,
policy,
reportNameValuePairs,
reportActions,
@@ -793,6 +795,7 @@ function MoneyReportHeader({
invoiceReceiverPolicy,
currentUserLogin,
accountID,
+ bankAccountList,
]);
const confirmExport = useCallback(() => {
@@ -1070,13 +1073,14 @@ function MoneyReportHeader({
return [];
}
return getSecondaryReportActions({
- currentUserEmail: currentUserLogin ?? '',
+ currentUserLogin: currentUserLogin ?? '',
currentUserAccountID: accountID,
report: moneyRequestReport,
chatReport,
reportTransactions: transactions,
originalTransaction: originalIOUTransaction,
violations,
+ bankAccountList,
policy,
reportNameValuePairs,
reportActions,
@@ -1098,14 +1102,15 @@ function MoneyReportHeader({
reportMetadata,
policies,
isChatReportArchived,
+ bankAccountList,
]);
const secondaryExportActions = useMemo(() => {
if (!moneyRequestReport) {
return [];
}
- return getSecondaryExportReportActions(accountID, email ?? '', moneyRequestReport, policy, exportTemplates);
- }, [moneyRequestReport, accountID, email, policy, exportTemplates]);
+ return getSecondaryExportReportActions(accountID, email ?? '', moneyRequestReport, bankAccountList, policy, exportTemplates);
+ }, [moneyRequestReport, accountID, email, policy, exportTemplates, bankAccountList]);
const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : '';
const unapproveWarningText = useMemo(
@@ -1409,7 +1414,7 @@ function MoneyReportHeader({
Navigation.goBack(backToRoute);
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
- deleteAppReport(moneyRequestReport?.reportID, email ?? '', reportTransactions, violations);
+ deleteAppReport(moneyRequestReport?.reportID, email ?? '', reportTransactions, violations, bankAccountList);
});
},
},
@@ -1505,7 +1510,6 @@ function MoneyReportHeader({
}
clearSelectedTransactions(true);
// We don't need to run the effect on change of clearSelectedTransactions since it can cause the infinite loop.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionThreadReportID]);
@@ -1513,9 +1517,9 @@ function MoneyReportHeader({
if (!hasFinishedPDFDownload || !canTriggerAutomaticPDFDownload.current) {
return;
}
- downloadReportPDF(reportPDFFilename, moneyRequestReport?.reportName ?? '', translate);
+ downloadReportPDF(reportPDFFilename, moneyRequestReport?.reportName ?? '', translate, currentUserLogin ?? '');
canTriggerAutomaticPDFDownload.current = false;
- }, [hasFinishedPDFDownload, reportPDFFilename, moneyRequestReport?.reportName, translate]);
+ }, [hasFinishedPDFDownload, reportPDFFilename, moneyRequestReport?.reportName, translate, currentUserLogin]);
const shouldShowBackButton = shouldDisplayBackButton || shouldUseNarrowLayout;
@@ -1741,7 +1745,6 @@ function MoneyReportHeader({
paymentType={paymentType}
chatReport={chatReport}
moneyRequestReport={moneyRequestReport}
- hasNonHeldExpenses={!hasOnlyHeldExpenses}
startAnimation={() => {
if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
startApprovedAnimation();
@@ -1825,7 +1828,7 @@ function MoneyReportHeader({
if (!hasFinishedPDFDownload) {
setIsPDFModalVisible(false);
} else {
- downloadReportPDF(reportPDFFilename, moneyRequestReport?.reportName ?? '', translate);
+ downloadReportPDF(reportPDFFilename, moneyRequestReport?.reportName ?? '', translate, currentUserLogin ?? '');
}
}}
text={hasFinishedPDFDownload ? translate('common.download') : translate('common.cancel')}
diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx
index 55a8192f985b..f69f60be5dc0 100644
--- a/src/components/MoneyRequestAmountInput.tsx
+++ b/src/components/MoneyRequestAmountInput.tsx
@@ -184,7 +184,7 @@ function MoneyRequestAmountInput({
}
// we want to re-initialize the state only when the amount changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [amount, shouldKeepUserInput]);
const formatAmount = useCallback(() => {
@@ -217,7 +217,6 @@ function MoneyRequestAmountInput({
// eslint-disable-next-line no-param-reassign
ref.current = newRef;
}
- // eslint-disable-next-line react-compiler/react-compiler
textInput.current = newRef;
}}
disabled={disabled}
@@ -225,7 +224,7 @@ function MoneyRequestAmountInput({
if (typeof moneyRequestAmountInputRef === 'function') {
moneyRequestAmountInputRef(newRef);
} else if (moneyRequestAmountInputRef && 'current' in moneyRequestAmountInputRef) {
- // eslint-disable-next-line react-compiler/react-compiler, no-param-reassign
+ // eslint-disable-next-line no-param-reassign
moneyRequestAmountInputRef.current = newRef;
}
numberFormRef.current = newRef;
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 76c09481b5d8..4da78c1ab818 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -152,9 +152,15 @@ type MoneyRequestConfirmationListProps = {
/** Whether the expense is a manual distance expense */
isManualDistanceRequest: boolean;
+ /** Whether the expense is an odometer distance expense */
+ isOdometerDistanceRequest?: boolean;
+
/** Whether the expense is a per diem expense */
isPerDiemRequest?: boolean;
+ /** Whether the expense is a time expense */
+ isTimeRequest?: boolean;
+
/** Whether we're editing a split expense */
isEditingSplitBill?: boolean;
@@ -213,6 +219,7 @@ function MoneyRequestConfirmationList({
iouAmount,
isDistanceRequest,
isManualDistanceRequest,
+ isOdometerDistanceRequest = false,
isPerDiemRequest = false,
isPolicyExpenseChat = false,
iouCategory = '',
@@ -245,6 +252,7 @@ function MoneyRequestConfirmationList({
iouIsReimbursable = true,
onToggleReimbursable,
showRemoveExpenseConfirmModal,
+ isTimeRequest = false,
}: MoneyRequestConfirmationListProps) {
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true});
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true});
@@ -343,7 +351,7 @@ function MoneyRequestConfirmationList({
const policyTagLists = useMemo(() => getTagLists(policyTags), [policyTags]);
- const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
+ const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest, isTimeRequest);
// Update the tax code when the default changes (for example, because the transaction currency changed)
const defaultTaxCode = getDefaultTaxCode(policy, transaction) ?? '';
@@ -428,7 +436,7 @@ function MoneyRequestConfirmationList({
// reset the form error whenever the screen gains or loses focus
setFormError('');
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
}, [isFocused, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
useEffect(() => {
@@ -544,12 +552,9 @@ function MoneyRequestConfirmationList({
if (iouAmount !== 0) {
text = translate('iou.createExpenseWithAmount', {amount: formattedAmount});
}
- } else if (isTypeSplit) {
- text = translate('iou.splitAmount', {amount: formattedAmount});
- } else if (iouAmount === 0) {
- text = translate('iou.createExpense');
} else {
- text = translate('iou.createExpenseWithAmount', {amount: formattedAmount});
+ const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.createExpenseWithAmount';
+ text = translate(translationKey, {amount: formattedAmount});
}
return [
{
@@ -841,7 +846,7 @@ function MoneyRequestConfirmationList({
}
setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy);
// Keep 'transaction' out to ensure that we auto select the option only once
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldShowCategories, policyCategories, isCategoryRequired, policy?.id]);
// Auto select the tag if there is only one enabled tag and it is required
@@ -866,7 +871,7 @@ function MoneyRequestConfirmationList({
setMoneyRequestTag(transactionID, updatedTagsString);
}
// Keep 'transaction' out to ensure that we auto select the option only once
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionID, policyTagLists, policyTags]);
/**
@@ -1144,6 +1149,7 @@ function MoneyRequestConfirmationList({
isCategoryRequired={isCategoryRequired}
isDistanceRequest={isDistanceRequest}
isManualDistanceRequest={isManualDistanceRequest}
+ isOdometerDistanceRequest={isOdometerDistanceRequest}
isPerDiemRequest={isPerDiemRequest}
isMerchantEmpty={isMerchantEmpty}
isMerchantRequired={isMerchantRequired}
@@ -1231,5 +1237,6 @@ export default memo(
prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed &&
prevProps.reportActionID === nextProps.reportActionID &&
prevProps.action === nextProps.action &&
- prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt,
+ prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt &&
+ prevProps.isTimeRequest === nextProps.isTimeRequest,
);
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 4e4acba9a7df..bce89a190df1 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -41,6 +41,7 @@ import CONST from '@src/CONST';
import type {IOUAction, IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {Unit} from '@src/types/onyx/Policy';
@@ -114,6 +115,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if it is a manual distance request */
isManualDistanceRequest: boolean;
+ /** Flag indicating if it is an odometer distance request */
+ isOdometerDistanceRequest?: boolean;
+
/** Flag indicating if it is a per diem request */
isPerDiemRequest: boolean;
@@ -231,6 +235,7 @@ function MoneyRequestConfirmationListFooter({
isCategoryRequired,
isDistanceRequest,
isManualDistanceRequest,
+ isOdometerDistanceRequest = false,
isPerDiemRequest,
isMerchantEmpty,
isMerchantRequired,
@@ -301,8 +306,8 @@ function MoneyRequestConfirmationListFooter({
const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction);
const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints);
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const shouldShowMap = isDistanceRequest && !isManualDistanceRequest && !!(hasErrors || hasPendingWaypoints || iouType !== CONST.IOU.TYPE.SPLIT || !isReadOnly);
+ const shouldShowMap =
+ isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest && [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean);
const isFromGlobalCreate = !!transaction?.isFromGlobalCreate;
const senderWorkspace = useMemo(() => {
@@ -504,6 +509,11 @@ function MoneyRequestConfirmationListFooter({
return;
}
+ if (isOdometerDistanceRequest) {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()) as Route);
+ return;
+ }
+
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
disabled={didConfirm}
@@ -1031,7 +1041,7 @@ function MoneyRequestConfirmationListFooter({
>
)}
- {(!shouldShowMap || isManualDistanceRequest) && (
+ {(!shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest) && (
{hasReceiptImageOrThumbnail
? receiptThumbnailContent
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index ce3f0351f5b7..07c01272f3c1 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -53,7 +53,8 @@ import {
shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils,
} from '@libs/TransactionUtils';
import variables from '@styles/variables';
-import {dismissRejectUseExplanation, duplicateExpenseTransaction as duplicateTransactionAction} from '@userActions/IOU';
+import {dismissRejectUseExplanation} from '@userActions/IOU';
+import {duplicateExpenseTransaction as duplicateTransactionAction} from '@userActions/IOU/DuplicateAction';
import {markAsCash as markAsCashAction} from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -137,7 +138,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
const defaultExpensePolicy = useDefaultExpensePolicy();
const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id);
const isOnHold = isOnHoldTransactionUtils(transaction);
- const isDuplicate = isDuplicateTransactionUtils(transaction, email ?? '', accountID, report, policy);
+ const isDuplicate = isDuplicateTransactionUtils(transaction, email ?? '', accountID, report, policy, transactionViolations);
const reportID = report?.reportID;
const {removeTransaction, currentSearchHash} = useSearchContext();
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
index 564886595c31..7de13fef2bfb 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
@@ -302,7 +302,7 @@ function MoneyRequestReportActionsList({
useEffect(() => {
setUnreadMarkerTime(reportLastReadTime);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [report.reportID]);
useEffect(() => {
@@ -331,7 +331,7 @@ function MoneyRequestReportActionsList({
readActionSkipped.current = true;
}
}
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible]);
useEffect(() => {
@@ -363,7 +363,7 @@ function MoneyRequestReportActionsList({
// is changed to visible(meaning user switched to app/web, while user was previously using different tab or application).
// We will mark the report as read in the above case which marks the LHN report item as read while showing the new message
// marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocused, isVisible]);
/**
@@ -406,7 +406,6 @@ function MoneyRequestReportActionsList({
prevUnreadMarkerReportActionID: prevUnreadMarkerReportActionID.current,
});
- // eslint-disable-next-line react-compiler/react-compiler
if (shouldDisplayNewMarker) {
return [reportAction.reportActionID, index];
}
@@ -529,7 +528,7 @@ function MoneyRequestReportActionsList({
};
// This effect handles subscribing to events, so we only want to run it on mount, and in case reportID changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [report.reportID]);
useEffect(() => {
@@ -647,8 +646,8 @@ function MoneyRequestReportActionsList({
didLayout.current = true;
- markOpenReportEnd(reportID);
- }, [reportID]);
+ markOpenReportEnd(report);
+ }, [report]);
const isSelectAllChecked = selectedTransactionIDs.length > 0 && selectedTransactionIDs.length === transactionsWithoutPendingDelete.length;
// Wrapped into useCallback to stabilize children re-renders
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
index ae673d1a88c2..c2a0d096c1f6 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
@@ -35,6 +35,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo
});
const [cardFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
const archivedReportsIdSet = useArchivedReportsIdSet();
@@ -48,12 +49,14 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo
currentUserEmail: currentUserDetails.email ?? '',
translate,
formatPhoneNumber,
+ bankAccountList,
groupBy,
reportActions: exportReportActions,
currentSearch: lastSearchQuery?.searchKey,
archivedReportsIDList: archivedReportsIdSet,
isActionLoadingSet,
cardFeeds,
+ shouldSkipActionFiltering: true,
});
results = getSortedSections(type, status ?? '', searchData, localeCompare, translate, sortBy, sortOrder, groupBy).map((value) => value.reportID);
}
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
index 442a87064b9c..c16a16bfce5f 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
@@ -50,6 +50,18 @@ const columnConfig: ColumnConfig[] = [
translationKey: 'common.tag',
canBeMissing: true,
},
+ {
+ columnName: CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE,
+ translationKey: 'common.reimbursable',
+ canBeMissing: true,
+ isColumnSortable: false,
+ },
+ {
+ columnName: CONST.SEARCH.TABLE_COLUMNS.BILLABLE,
+ translationKey: 'common.billable',
+ canBeMissing: true,
+ isColumnSortable: false,
+ },
{
columnName: CONST.SEARCH.TABLE_COLUMNS.COMMENTS,
translationKey: undefined, // comments have no title displayed
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index f4c9c81c0cfa..5f8d6ac0eaa9 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -28,6 +28,7 @@ import {setOptimisticTransactionThread} from '@libs/actions/Report';
import {getReportLayoutGroupBy} from '@libs/actions/ReportLayout';
import {setActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
import {convertToDisplayString} from '@libs/CurrencyUtils';
+import {hasNonReimbursableTransactions, isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
import {navigationRef} from '@libs/Navigation/Navigation';
import Parser from '@libs/Parser';
import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
@@ -254,7 +255,6 @@ function MoneyRequestReportTransactionList({
useEffect(() => {
clearSelectedTransactions(true);
// We don't want to run the effect on change of clearSelectedTransactions since it can cause an infinite loop.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportID]);
@@ -276,8 +276,18 @@ function MoneyRequestReportTransactionList({
// Always use default columns for money request report view (don't use user-customized search columns)
const columnsToShow = useMemo(() => {
- return getColumnsToShow(currentUserDetails?.accountID, transactions, [], true, undefined, undefined, isIOUReport(report));
- }, [transactions, currentUserDetails?.accountID, report]);
+ return getColumnsToShow(
+ currentUserDetails?.accountID,
+ transactions,
+ [],
+ true,
+ undefined,
+ undefined,
+ isIOUReport(report),
+ isBillableEnabledOnPolicy(policy),
+ hasNonReimbursableTransactions(transactions),
+ );
+ }, [transactions, currentUserDetails?.accountID, report, policy]);
const currentGroupBy = getReportLayoutGroupBy(reportLayoutGroupBy);
@@ -289,7 +299,7 @@ function MoneyRequestReportTransactionList({
return groupTransactionsByTag(sortedTransactions, report, localeCompare);
}
return groupTransactionsByCategory(sortedTransactions, report, localeCompare);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedTransactions, currentGroupBy, report?.reportID, localeCompare, shouldShowGroupedTransactions]);
const visualOrderTransactionIDs = useMemo(() => {
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
index f447710206c0..d8438a3bf253 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
@@ -12,7 +12,6 @@ import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils
import Navigation from '@navigation/Navigation';
import navigationRef from '@navigation/navigationRef';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import getEmptyArray from '@src/types/utils/getEmptyArray';
@@ -142,7 +141,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
navigationParams.reportID = transactionThreadReport?.reportID;
}
// Wait for the next frame to ensure Onyx has processed the optimistic data updates from setOptimisticTransactionThread or createTransactionThreadReport before navigating
- requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(navigationParams), {forceReplace: true}));
+ requestAnimationFrame(() => Navigation.setParams(navigationParams));
};
const onPrevious = (e: GestureResponderEvent | KeyboardEvent | undefined) => {
@@ -170,7 +169,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
navigationParams.reportID = transactionThreadReport?.reportID;
}
// Wait for the next frame to ensure Onyx has processed the optimistic data updates from setOptimisticTransactionThread or createTransactionThreadReport before navigating
- requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(navigationParams), {forceReplace: true}));
+ requestAnimationFrame(() => Navigation.setParams(navigationParams));
};
return (
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
index bc5845fbf0a3..1d821678276d 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
@@ -166,7 +166,9 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
// We need to cancel telemetry span when user leaves the screen before full report data is loaded
useEffect(() => {
- return () => cancelSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`);
+ return () => {
+ cancelSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`);
+ };
}, [reportID]);
if (!!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions) {
diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
index 985c607124dc..35d413dbff79 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
@@ -91,6 +91,7 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false,
.map((field): EnrichedPolicyReportField => {
const fieldValue = field.value ?? field.defaultValue;
const isFieldDisabled = isReportFieldDisabledForUser(report, field, policy);
+ const isDeletedFormulaField = field.type === CONST.REPORT_FIELD_TYPES.FORMULA && field.deletable;
const fieldKey = getReportFieldKey(field.fieldID);
const violation = isFieldDisabled ? undefined : getFieldViolation(violations, field);
@@ -99,7 +100,7 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false,
return {
...field,
fieldValue,
- isFieldDisabled,
+ isFieldDisabled: isFieldDisabled && !isDeletedFormulaField,
fieldKey,
violation,
violationTranslation,
diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx
index b1aa97cb7e04..c5ac395fe35d 100644
--- a/src/components/MultiGestureCanvas/index.tsx
+++ b/src/components/MultiGestureCanvas/index.tsx
@@ -127,7 +127,7 @@ function MultiGestureCanvas({
if (!isUsedInCarousel) {
return;
}
- // eslint-disable-next-line react-compiler/react-compiler, no-param-reassign
+ // eslint-disable-next-line no-param-reassign
isPagerScrollEnabled.set(!current);
},
);
@@ -195,7 +195,6 @@ function MultiGestureCanvas({
onTap,
shouldDisableTransformationGestures,
});
- // eslint-disable-next-line react-compiler/react-compiler
const singleTapGesture = baseSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef);
const panGestureSimultaneousList = useMemo(
@@ -218,7 +217,6 @@ function MultiGestureCanvas({
onSwipeDown,
})
.simultaneousWithExternalGesture(...panGestureSimultaneousList)
- // eslint-disable-next-line react-compiler/react-compiler
.withRef(panGestureRef);
const pinchGesture = usePinchGesture({
diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts
index c3558c95498c..1af69dbc4e2f 100644
--- a/src/components/MultiGestureCanvas/usePanGesture.ts
+++ b/src/components/MultiGestureCanvas/usePanGesture.ts
@@ -122,7 +122,6 @@ const usePanGesture = ({
// If the (absolute) velocity is 0, we don't need to run an animation
if (Math.abs(panVelocityX.get()) !== 0) {
// Phase out the pan animation
- // eslint-disable-next-line react-compiler/react-compiler
offsetX.set(
withDecay({
velocity: panVelocityX.get(),
diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts
index 62de12f0d8d4..ed5b3426540f 100644
--- a/src/components/MultiGestureCanvas/usePinchGesture.ts
+++ b/src/components/MultiGestureCanvas/usePinchGesture.ts
@@ -69,7 +69,6 @@ const usePinchGesture = ({
useAnimatedReaction(
() => [pinchTranslateX.get(), pinchTranslateY.get(), pinchBounceTranslateX.get(), pinchBounceTranslateY.get()],
([translateX, translateY, bounceX, bounceY]) => {
- // eslint-disable-next-line react-compiler/react-compiler
totalPinchTranslateX.set(translateX + bounceX);
totalPinchTranslateY.set(translateY + bounceY);
},
diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx
index bc41aa9045b4..b218de77b702 100644
--- a/src/components/NumberWithSymbolForm.tsx
+++ b/src/components/NumberWithSymbolForm.tsx
@@ -288,7 +288,7 @@ function NumberWithSymbolForm({
setNewNumber(stripDecimalsFromAmount(currentNumber));
// we want to update only when decimals change.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [decimals]);
/**
diff --git a/src/components/Onfido/BaseOnfidoWeb.tsx b/src/components/Onfido/BaseOnfidoWeb.tsx
index c579563613c2..5ebbefcbd031 100644
--- a/src/components/Onfido/BaseOnfidoWeb.tsx
+++ b/src/components/Onfido/BaseOnfidoWeb.tsx
@@ -154,7 +154,7 @@ function Onfido({sdkToken, onSuccess, onError, onUserExit, ref}: OnfidoProps) {
window.addEventListener('userAnalyticsEvent', logOnFidoEvent);
return () => window.removeEventListener('userAnalyticsEvent', logOnFidoEvent);
// Onfido should be initialized only once on mount
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/Onfido/index.native.tsx b/src/components/Onfido/index.native.tsx
index dae7504a9d90..917a5186317b 100644
--- a/src/components/Onfido/index.native.tsx
+++ b/src/components/Onfido/index.native.tsx
@@ -99,7 +99,7 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) {
}
});
// Onfido should be initialized only once on mount
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ;
diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx
index 3a108beef173..29e47b3256b0 100644
--- a/src/components/OptionListContextProvider.tsx
+++ b/src/components/OptionListContextProvider.tsx
@@ -237,7 +237,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
});
// This effect is used to update the options list when personal details change so we ignore all dependencies except personalDetails
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [personalDetails]);
const initializeOptions = useCallback(() => {
@@ -258,7 +258,7 @@ function OptionsListContextProvider({children}: OptionsListProviderProps) {
}, []);
return (
- ({options, initializeOptions, areOptionsInitialized: areOptionsInitialized.current, resetOptions}), [options, initializeOptions, resetOptions])}
>
{children}
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx
index 0c48d7aa86d0..3605758e620a 100644
--- a/src/components/PDFView/index.tsx
+++ b/src/components/PDFView/index.tsx
@@ -72,7 +72,7 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, sty
useEffect(() => {
retrieveCanvasLimits();
// This rule needs to be applied so that this effect is executed only when the component is mounted
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx
index 159504703837..301b09e06ceb 100644
--- a/src/components/ParentNavigationSubtitle.tsx
+++ b/src/components/ParentNavigationSubtitle.tsx
@@ -121,7 +121,7 @@ function ParentNavigationSubtitle({
}
// If the parent report is already displayed underneath RHP, simply dismiss the modal
- if (Navigation.getTopmostReportId() === parentReportID) {
+ if (Navigation.getTopmostReportId() === parentReportID && currentFullScreenRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
Navigation.dismissModal();
return;
}
@@ -169,6 +169,7 @@ function ParentNavigationSubtitle({
{`${translate('threads.from')} `}
{hasAccessToParentReport ? (
({
onInputChange(item.value, 0);
}
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [items]);
const context = useScrollContext();
diff --git a/src/components/PlaidLink/BaseNativePlaidLink.tsx b/src/components/PlaidLink/BaseNativePlaidLink.tsx
index 853331d1b3df..3680d3f3054e 100644
--- a/src/components/PlaidLink/BaseNativePlaidLink.tsx
+++ b/src/components/PlaidLink/BaseNativePlaidLink.tsx
@@ -28,7 +28,7 @@ function BaseNativePlaidLink({token, onSuccess = () => {}, onExit = () => {}, on
};
// We generally do not need to include the token as a dependency here as it is only provided once via props and should not change
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 742cfa872acb..eea11c2081a7 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -486,7 +486,7 @@ function BasePopoverMenu({
setFocusedIndex(getSelectedItemIndex(menuItems));
}
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [menuItems, setFocusedIndex]);
const menuContainerStyle = useMemo(() => {
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index fb4e20acfb91..96bdc875947d 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -146,7 +146,6 @@ function PopoverContextProvider(props: PopoverContextProps) {
onOpen,
setActivePopoverExtraAnchorRef,
close: closePopover,
- // eslint-disable-next-line react-compiler/react-compiler
popover: activePopoverRef.current,
popoverAnchor: activePopoverAnchor,
isOpen,
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index 9f7ecaa53d39..817d7adf6153 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -71,7 +71,7 @@ function PopoverWithoutOverlay({
removeOnClose();
};
// We want this effect to run strictly ONLY when isVisible prop changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible]);
const modalPaddingStyles = useMemo(
diff --git a/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
index 8cac1a955108..513e3df69bb0 100644
--- a/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
+++ b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
@@ -154,7 +154,6 @@ function GenericPressable({
onLayout={shouldUseAutoHitSlop ? onLayout : undefined}
ref={ref as ForwardedRef}
disabled={fullDisabled}
- // eslint-disable-next-line react-compiler/react-compiler
onPress={!isDisabled ? singleExecution(onPressHandler) : undefined}
onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined}
onKeyDown={!isDisabled ? onKeyDown : undefined}
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index aa21dce5eef4..c34f11928ddc 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -47,14 +47,11 @@ type ProcessMoneyReportHoldMenuProps = {
/** Callback for displaying payment animation on IOU preview component */
startAnimation?: () => void;
-
- /** Whether the report has non held expenses */
- hasNonHeldExpenses?: boolean;
};
function ProcessMoneyReportHoldMenu({
requestType,
- nonHeldAmount = '0',
+ nonHeldAmount,
fullAmount,
onClose,
isVisible,
@@ -63,7 +60,6 @@ function ProcessMoneyReportHoldMenu({
moneyRequestReport,
transactionCount,
startAnimation,
- hasNonHeldExpenses,
}: ProcessMoneyReportHoldMenuProps) {
const {translate} = useLocalize();
const isApprove = requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE;
@@ -106,11 +102,11 @@ function ProcessMoneyReportHoldMenu({
};
const promptText = useMemo(() => {
- if (hasNonHeldExpenses) {
+ if (nonHeldAmount) {
return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount');
}
return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {count: transactionCount});
- }, [hasNonHeldExpenses, transactionCount, translate, isApprove]);
+ }, [nonHeldAmount, transactionCount, translate, isApprove]);
return (
onSubmit(false)}
onSecondOptionSubmit={() => onSubmit(true)}
diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
index a676e1fef761..ac64d50c1f1f 100644
--- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -87,7 +87,7 @@ function ExportWithDropdownMenu({
}
return options;
// We do not include exportMethods not to re-render the component when the preferred export method changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [canBeExported, iconToDisplay, connectionName, report?.policyID, translate]);
const confirmExport = useCallback(() => {
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
index 877f10778e8a..c2819921fd91 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
@@ -1,4 +1,3 @@
-import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState} from 'react';
import {FlatList, View} from 'react-native';
import type {ListRenderItemInfo, ViewToken} from 'react-native';
@@ -97,7 +96,6 @@ const reportAttributesSelector = (c: OnyxEntry) =>
function MoneyRequestReportPreviewContent({
iouReportID,
- newTransactionIDs,
chatReportID,
action,
containerStyles,
@@ -158,7 +156,7 @@ function MoneyRequestReportPreviewContent({
hasNonReimbursableTransactions: hasNonReimbursableTransactionsReportUtils(iouReportID),
}),
// When transactions get updated these values may have changed, so that is a case where we also want to recompute them
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[transactions, iouReportID, action],
);
@@ -173,13 +171,14 @@ function MoneyRequestReportPreviewContent({
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const {isBetaEnabled} = usePermissions();
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW);
const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations, currentUserAccountID, currentUserEmail);
const getCanIOUBePaid = useCallback(
- (shouldShowOnlyPayElsewhere = false) => canIOUBePaidIOUActions(iouReport, chatReport, policy, transactions, shouldShowOnlyPayElsewhere),
- [iouReport, chatReport, policy, transactions],
+ (shouldShowOnlyPayElsewhere = false) => canIOUBePaidIOUActions(iouReport, chatReport, policy, bankAccountList, transactions, shouldShowOnlyPayElsewhere),
+ [iouReport, chatReport, policy, bankAccountList, transactions],
);
const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
@@ -221,7 +220,6 @@ function MoneyRequestReportPreviewContent({
const currentReportName = iouReport?.reportID ? reportAttributes?.[iouReport.reportID]?.reportName : undefined;
const reportPreviewName = useMemo(() => {
return getMoneyReportPreviewName(action, iouReport, isInvoice, reportAttributes);
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [action, iouReport, isInvoice, currentReportName]);
@@ -403,7 +401,7 @@ function MoneyRequestReportPreviewContent({
}),
);
// We only want to animate the text when the text changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [previewMessage, previewMessageOpacity]);
useEffect(() => {
@@ -422,7 +420,7 @@ function MoneyRequestReportPreviewContent({
thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBS_UP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBS_UP_DURATION})) : 1);
}, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
- const carouselTransactions = useMemo(() => (shouldShowAccessPlaceHolder ? [] : transactions.slice(0, 11)), [shouldShowAccessPlaceHolder, transactions]);
+ const carouselTransactions = shouldShowAccessPlaceHolder ? [] : transactions.slice(0, 11);
const prevCarouselTransactionLength = useRef(0);
useEffect(() => {
@@ -448,49 +446,7 @@ function MoneyRequestReportPreviewContent({
const viewabilityConfig = useMemo(() => {
return {itemVisiblePercentThreshold: 100};
}, []);
- const numberOfScrollToIndexFailed = useRef(0);
- const onScrollToIndexFailed: (info: {index: number; highestMeasuredFrameIndex: number; averageItemLength: number}) => void = ({index}) => {
- // There is a probability of infinite loop so we want to make sure that it is not called more than 5 times.
- if (numberOfScrollToIndexFailed.current > 4) {
- return;
- }
-
- // Sometimes scrollToIndex might be called before the item is rendered so we will re-call scrollToIndex after a small delay.
- setTimeout(() => {
- carouselRef.current?.scrollToIndex({index, animated: true, viewOffset: 2 * styles.gap2.gap});
- }, 100);
- numberOfScrollToIndexFailed.current++;
- };
-
- const carouselTransactionsRef = useRef(carouselTransactions);
-
- useEffect(() => {
- carouselTransactionsRef.current = carouselTransactions;
- }, [carouselTransactions]);
- useFocusEffect(
- useCallback(() => {
- const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.includes(transaction.transactionID));
-
- if (index < 0) {
- return;
- }
- const newTransaction = carouselTransactions.at(index);
- setTimeout(() => {
- // If the new transaction is not available at the index it was on before the delay, avoid the scrolling
- // because we are scrolling to either a wrong or unavailable transaction (which can cause crash).
- if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) {
- return;
- }
- numberOfScrollToIndexFailed.current = 0;
- carouselRef.current?.scrollToIndex({index, viewOffset: 2 * styles.gap2.gap, animated: true});
- }, CONST.ANIMATED_TRANSITION);
-
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [newTransactionIDs]),
- );
-
- // eslint-disable-next-line react-compiler/react-compiler
const onViewableItemsChanged = useRef(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
const newIndex = viewableItems.at(0)?.index;
if (typeof newIndex === 'number') {
@@ -561,6 +517,7 @@ function MoneyRequestReportPreviewContent({
name: 'MoneyRequestReportPreviewContent',
op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
});
+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
}, [iouReportID]);
@@ -570,10 +527,11 @@ function MoneyRequestReportPreviewContent({
return getReportPreviewAction({
isReportArchived: isIouReportArchived || isChatReportArchived,
currentUserAccountID: currentUserDetails.accountID,
- currentUserEmail: currentUserDetails.email ?? '',
+ currentUserLogin: currentUserDetails.login ?? '',
report: iouReport,
policy,
transactions,
+ bankAccountList,
invoiceReceiverPolicy,
isPaidAnimationRunning,
isApprovedAnimationRunning,
@@ -582,10 +540,11 @@ function MoneyRequestReportPreviewContent({
violationsData: transactionViolations,
});
}, [
+ bankAccountList,
isIouReportArchived,
isChatReportArchived,
currentUserDetails.accountID,
- currentUserDetails.email,
+ currentUserDetails.login,
iouReport,
policy,
transactions,
@@ -861,7 +820,6 @@ function MoneyRequestReportPreviewContent({
) : (
(Number(transaction?.modifiedAmount) || transaction?.amount) < 0);
+ return transactions.some((transaction) => (transaction?.modifiedAmount || transaction?.amount) < 0);
}, [transactions, action.childType, iouReport]);
const openReportFromPreview = useCallback(() => {
@@ -114,9 +112,6 @@ function MoneyRequestReportPreview({
});
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
}, [iouReportID]);
- const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`, {canBeMissing: true});
- const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, transactions);
- const newTransactionIDs = newTransactions.map((transaction) => transaction.transactionID);
const renderItem: ListRenderItem = ({item}) => (
);
return (
void;
-
- /** IDs of newly added transactions */
- newTransactionIDs?: string[];
};
export type {MoneyRequestReportPreviewContentProps, MoneyRequestReportPreviewProps, MoneyRequestReportPreviewStyleType};
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 98a40b4b5263..c2e31941a049 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -32,7 +32,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
import type {ViolationField} from '@hooks/useViolations';
import useViolations from '@hooks/useViolations';
-import {filterPersonalCards, getCompanyCardDescription} from '@libs/CardUtils';
+import {filterPersonalCards, getCompanyCardDescription, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -90,6 +90,7 @@ import {
isDistanceRequest as isDistanceRequestTransactionUtils,
isExpenseUnreported as isExpenseUnreportedTransactionUtils,
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
+ isOdometerDistanceRequest as isOdometerDistanceRequestTransactionUtils,
isPerDiemRequest as isPerDiemRequestTransactionUtils,
isScanning,
isTimeRequest as isTimeRequestTransactionUtils,
@@ -227,6 +228,8 @@ function MoneyRequestView({
const allPolicyTags = usePolicyTags();
const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`];
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
+ const [companyCardList] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
+ const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(companyCardList ?? CONST.EMPTY_OBJECT, cardList), [companyCardList, cardList]);
const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true});
const transactionViolations = useTransactionViolations(transaction?.transactionID);
@@ -236,7 +239,6 @@ function MoneyRequestView({
const currentUserEmailParam = currentUserPersonalDetails.login ?? '';
const {isBetaEnabled} = usePermissions();
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
- const isZeroExpensesBetaEnabled = isBetaEnabled(CONST.BETAS.ZERO_EXPENSES);
const moneyRequestReport = parentReport;
const isApproved = isReportApproved({report: moneyRequestReport});
@@ -270,12 +272,12 @@ function MoneyRequestView({
tag: transactionTag,
originalCurrency: transactionOriginalCurrency,
postedDate: transactionPostedDate,
+ convertedAmount: transactionConvertedAmount,
} = getTransactionDetails(transaction, undefined, undefined, allowNegativeAmount, false, currentUserPersonalDetails) ?? {};
- const isZeroTransactionAmount = transactionAmount === 0;
- const isEmptyMerchant =
- transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transactionMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT;
+ const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction, !!mergeTransactionID);
+ const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction);
const isMapDistanceRequest = isDistanceRequest && !isManualDistanceRequest;
const isTransactionScanning = isScanning(updatedTransaction ?? transaction);
const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest);
@@ -285,15 +287,14 @@ function MoneyRequestView({
// Use the updated transaction amount in merge flow to have correct positive/negative sign
const actualAmount = isFromMergeTransaction && updatedTransaction ? updatedTransaction.amount : transactionAmount;
const actualCurrency = updatedTransaction ? getCurrency(updatedTransaction) : transactionCurrency;
- const shouldDisplayTransactionAmount = (isDistanceRequest && hasRoute) || !isDistanceRequest;
+ const shouldDisplayTransactionAmount = ((isDistanceRequest && hasRoute) || !!actualAmount) && actualAmount !== undefined;
const formattedTransactionAmount = shouldDisplayTransactionAmount ? convertToDisplayString(actualAmount, actualCurrency) : '';
- const formattedPerAttendeeAmount =
- shouldDisplayTransactionAmount && actualAmount !== undefined ? convertToDisplayString(actualAmount / (transactionAttendees?.length ?? 1), actualCurrency) : '';
+ const formattedPerAttendeeAmount = shouldDisplayTransactionAmount ? convertToDisplayString(actualAmount / (actualAttendees?.length ?? 1), actualCurrency) : '';
const transactionOriginalAmount = transaction && getOriginalAmountForDisplay(transaction, isExpenseReport(moneyRequestReport));
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isManagedCardTransaction = isCardTransactionTransactionUtils(transaction);
- const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, cardList);
+ const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, allCards);
const shouldShowCard = isManagedCardTransaction && cardProgramName;
const taxRates = policy?.taxRates;
@@ -321,7 +322,7 @@ function MoneyRequestView({
const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(transactionThreadReport?.policyID)}`;
const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`, {canBeMissing: true});
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
- const isSplitAvailable = moneyRequestReport && transaction && isSplitAction(moneyRequestReport, [transaction], originalTransaction, policy);
+ const isSplitAvailable = moneyRequestReport && transaction && isSplitAction(moneyRequestReport, [transaction], originalTransaction, currentUserPersonalDetails.login ?? '', policy);
const canEditTaxFields = canEdit && !isDistanceRequest;
const canEditAmount =
@@ -382,6 +383,8 @@ function MoneyRequestView({
getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0,
[getViolationsForField],
);
+ // Need to return undefined when we have pendingAction to avoid the duplicate pending action
+ const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => (pendingAction ? undefined : transaction?.pendingFields?.[fieldPath]);
let amountDescription = `${translate('iou.amount')}`;
let dateDescription = `${translate('common.date')}`;
@@ -394,20 +397,28 @@ function MoneyRequestView({
let rateToDisplay = isCustomUnitOutOfPolicy ? translate('common.rateOutOfPolicy') : DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate);
let merchantTitle = isEmptyMerchant ? '' : transactionMerchant;
- let amountTitle = formattedTransactionAmount?.toString() || '';
+ let amountTitle = formattedTransactionAmount ? formattedTransactionAmount.toString() : '';
if (isTransactionScanning) {
merchantTitle = translate('iou.receiptStatusTitle');
amountTitle = translate('iou.receiptStatusTitle');
}
const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy;
+
const updatedTransactionDescription = getDescription(updatedTransaction) || undefined;
- const isEmptyUpdatedMerchant =
- updatedTransaction?.modifiedMerchant === '' ||
- updatedTransaction?.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ||
- updatedTransaction?.modifiedMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT;
+ const isEmptyUpdatedMerchant = updatedTransaction?.modifiedMerchant === '' || updatedTransaction?.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const updatedMerchantTitle = isEmptyUpdatedMerchant ? '' : (updatedTransaction?.modifiedMerchant ?? merchantTitle);
+ const shouldShowConvertedAmount =
+ transactionConvertedAmount &&
+ currency !== moneyRequestReport?.currency &&
+ !isManagedCardTransaction &&
+ transaction?.reportID !== CONST.REPORT.UNREPORTED_REPORT_ID &&
+ !isFromMergeTransaction &&
+ !isFromReviewDuplicates &&
+ !getPendingFieldAction('amount') &&
+ !pendingAction;
+
const saveBillable = (newBillable: boolean) => {
// If the value hasn't changed, don't request to save changes on the server and just close the modal
if (newBillable === getBillable(transaction) || !transaction?.transactionID || !transactionThreadReport?.reportID) {
@@ -415,7 +426,8 @@ function MoneyRequestView({
}
updateMoneyRequestBillable(
transaction.transactionID,
- transactionThreadReport?.reportID,
+ transactionThreadReport,
+ parentReport,
newBillable,
policy,
policyTagList,
@@ -433,7 +445,8 @@ function MoneyRequestView({
}
updateMoneyRequestReimbursable(
transaction.transactionID,
- transactionThreadReport?.reportID,
+ transactionThreadReport,
+ parentReport,
newReimbursable,
policy,
policyTagList,
@@ -464,6 +477,9 @@ function MoneyRequestView({
if (isExpenseSplit) {
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.split')}`;
}
+ if (shouldShowConvertedAmount) {
+ amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('common.converted')} ${convertToDisplayString(transactionConvertedAmount, moneyRequestReport?.currency)}`;
+ }
if (isFromMergeTransaction) {
// Because we lack the necessary data in policy.customUnits to determine the rate in merge flow,
@@ -473,15 +489,13 @@ function MoneyRequestView({
}
const hasErrors = hasMissingSmartscanFields(transaction);
- // Need to return undefined when we have pendingAction to avoid the duplicate pending action
- const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => (pendingAction ? undefined : transaction?.pendingFields?.[fieldPath]);
const getErrorForField = (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => {
// Checks applied when creating a new expense
// NOTE: receipt field can return multiple violations, so we need to handle it separately
const fieldChecks: Partial> = {
amount: {
- isError: isZeroTransactionAmount && !isZeroExpensesBetaEnabled,
+ isError: transactionAmount === 0,
translationPath: canEditAmount ? 'common.error.enterAmount' : 'common.error.missingAmount',
},
merchant: {
@@ -553,6 +567,19 @@ function MoneyRequestView({
return;
}
+ if (isOdometerDistanceRequest) {
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction.transactionID,
+ transactionThreadReport.reportID,
+ getReportRHPActiveRoute(),
+ ),
+ );
+ return;
+ }
+
if (isManualDistanceRequest) {
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_DISTANCE_MANUAL.getRoute(
@@ -832,7 +859,7 @@ function MoneyRequestView({
copyable={!!descriptionCopyValue}
/>
- {isManualDistanceRequest || (isMapDistanceRequest && transaction?.comment?.waypoints) ? (
+ {isManualDistanceRequest || isOdometerDistanceRequest || (isMapDistanceRequest && transaction?.comment?.waypoints) ? (
distanceRequestFields
) : (
diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
index 03ac28b53a6f..a417b3917126 100644
--- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
+++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
@@ -1,7 +1,6 @@
import truncate from 'lodash/truncate';
import React, {useMemo} from 'react';
import {View} from 'react-native';
-import Animated from 'react-native-reanimated';
import Button from '@components/Button';
import Icon from '@components/Icon';
// eslint-disable-next-line no-restricted-imports
@@ -12,7 +11,6 @@ import ReportActionItemImages from '@components/ReportActionItem/ReportActionIte
import UserInfoCellsWithArrow from '@components/SelectionListWithSections/Search/UserInfoCellsWithArrow';
import Text from '@components/Text';
import TransactionPreviewSkeletonView from '@components/TransactionPreviewSkeletonView';
-import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -64,7 +62,6 @@ function TransactionPreviewContent({
shouldShowPayerAndReceiver,
navigateToReviewFields,
isReviewDuplicateTransactionPage = false,
- shouldHighlight = false,
}: TransactionPreviewContentProps) {
const icons = useMemoizedLazyExpensifyIcons(['Folder', 'Tag']);
const theme = useTheme();
@@ -224,17 +221,10 @@ function TransactionPreviewContent({
const previewTextViewGap = (shouldShowCategoryOrTag || !shouldWrapDisplayAmount) && styles.gap2;
const previewTextMargin = shouldShowIOUHeader && shouldShowMerchantOrDescription && !isBillSplit && !shouldShowCategoryOrTag && styles.mbn1;
- const animatedHighlightStyle = useAnimatedHighlightStyle({
- shouldHighlight,
- highlightColor: theme.messageHighlightBG,
- backgroundColor: theme.cardBG,
- shouldApplyOtherStyles: false,
- });
-
const transactionWrapperStyles = [styles.border, styles.moneyRequestPreviewBox, (isIOUSettled || isApproved) && isSettlementOrApprovalPartial && styles.offlineFeedbackPending];
return (
-
+
offlineWithFeedbackOnClose}
@@ -245,7 +235,7 @@ function TransactionPreviewContent({
shouldDisableOpacity={isDeleted}
shouldHideOnDelete={shouldHideOnDelete}
>
-
+
-
+
);
}
diff --git a/src/components/ReportActionItem/TransactionPreview/index.tsx b/src/components/ReportActionItem/TransactionPreview/index.tsx
index 56d7892fecee..5a50a494d99a 100644
--- a/src/components/ReportActionItem/TransactionPreview/index.tsx
+++ b/src/components/ReportActionItem/TransactionPreview/index.tsx
@@ -41,7 +41,6 @@ function TransactionPreview(props: TransactionPreviewProps) {
iouReportID,
transactionID: transactionIDFromProps,
onPreviewPressed,
- shouldHighlight,
reportPreviewAction,
contextAction,
} = props;
@@ -94,7 +93,7 @@ function TransactionPreview(props: TransactionPreviewProps) {
// See description of `transactionRawAmount` prop for more context
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const transactionRawAmount = (Number(transaction?.modifiedAmount) || transaction?.amount) ?? 0;
+ const transactionRawAmount = (transaction?.modifiedAmount || transaction?.amount) ?? 0;
const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction);
const isTransactionMadeWithCard = isManagedCardTransaction(transaction);
@@ -131,7 +130,6 @@ function TransactionPreview(props: TransactionPreviewProps) {
walletTermsErrors={walletTerms?.errors}
routeName={route.name}
isReviewDuplicateTransactionPage={isReviewDuplicateTransactionPage}
- shouldHighlight={shouldHighlight}
/>
);
@@ -156,7 +154,6 @@ function TransactionPreview(props: TransactionPreviewProps) {
walletTermsErrors={walletTerms?.errors}
routeName={route.name}
reportPreviewAction={reportPreviewAction}
- shouldHighlight={shouldHighlight}
isReviewDuplicateTransactionPage={isReviewDuplicateTransactionPage}
/>
);
diff --git a/src/components/ReportActionItem/TransactionPreview/types.ts b/src/components/ReportActionItem/TransactionPreview/types.ts
index 2a94f8701261..886dcf25a185 100644
--- a/src/components/ReportActionItem/TransactionPreview/types.ts
+++ b/src/components/ReportActionItem/TransactionPreview/types.ts
@@ -72,9 +72,6 @@ type TransactionPreviewProps = {
/** In case we want to override context menu action */
contextAction?: OnyxEntry;
-
- /** Whether the item should be highlighted */
- shouldHighlight?: boolean;
};
type TransactionPreviewContentProps = {
@@ -144,9 +141,6 @@ type TransactionPreviewContentProps = {
/** Is this component used during duplicate review flow */
isReviewDuplicateTransactionPage?: boolean;
-
- /** Whether the item should be highlighted */
- shouldHighlight?: boolean;
};
export type {TransactionPreviewContentProps, TransactionPreviewProps, TransactionPreviewStyleType};
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index 49155a42d501..707944fb0f77 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -121,7 +121,10 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) {
return translate('reportActionsView.sayHello');
}, [isChatRoom, isInvoiceRoom, isPolicyExpenseChat, isSelfDM, isSystemChat, translate, policyName, reportName]);
- const participantAccountIDsExcludeCurrentUser = getParticipantsAccountIDsForDisplay(report, undefined, undefined, true);
+
+ // If we are the only participant (e.g. solo group chat) then keep the current user personal details so the welcome message does not show up empty.
+ const shouldExcludeCurrentUser = participantAccountIDs.length > 0;
+ const participantAccountIDsExcludeCurrentUser = getParticipantsAccountIDsForDisplay(report, undefined, undefined, shouldExcludeCurrentUser);
const participantPersonalDetailListExcludeCurrentUser = Object.values(
getPersonalDetailsForAccountIDs(participantAccountIDsExcludeCurrentUser, personalDetails as OnyxInputOrEntry),
);
diff --git a/src/components/RequireTwoFactorAuthenticationModal.tsx b/src/components/RequireTwoFactorAuthenticationModal.tsx
index fe156c15080f..37350c8f958f 100644
--- a/src/components/RequireTwoFactorAuthenticationModal.tsx
+++ b/src/components/RequireTwoFactorAuthenticationModal.tsx
@@ -32,7 +32,9 @@ type RequireTwoFactorAuthenticationModalProps = {
};
function RequireTwoFactorAuthenticationModal({onCancel = () => {}, description, isVisible, onSubmit, shouldEnableNewFocusManagement}: RequireTwoFactorAuthenticationModalProps) {
- const {shouldUseNarrowLayout} = useResponsiveLayout();
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
const styles = useThemeStyles();
const {translate} = useLocalize();
const StyleUtils = useStyleUtils();
@@ -41,7 +43,7 @@ function RequireTwoFactorAuthenticationModal({onCancel = () => {}, description,
diff --git a/src/components/ScreenWrapper/ScreenWrapperContainer.tsx b/src/components/ScreenWrapper/ScreenWrapperContainer.tsx
index 3b9c46729ac3..d49619020b86 100644
--- a/src/components/ScreenWrapper/ScreenWrapperContainer.tsx
+++ b/src/components/ScreenWrapper/ScreenWrapperContainer.tsx
@@ -205,7 +205,7 @@ function ScreenWrapperContainer({
ref={ref}
// This style gives the background for the screens. Stack cards are transparent to make different width screens in RHP possible.
style={[styles.flex1, styles.appBG, styles.screenWrapperContainerMinHeight(minHeight)]}
- // eslint-disable-next-line react/jsx-props-no-spreading, react-compiler/react-compiler
+ // eslint-disable-next-line react/jsx-props-no-spreading
{...panResponder.panHandlers}
testID={testID}
fsClass={forwardedFSClass}
@@ -213,7 +213,7 @@ function ScreenWrapperContainer({
{
diff --git a/src/components/Search/FilterComponents/DatePresetFilterBase.tsx b/src/components/Search/FilterComponents/DatePresetFilterBase.tsx
index 4667e08c0b0f..bb0465ea2019 100644
--- a/src/components/Search/FilterComponents/DatePresetFilterBase.tsx
+++ b/src/components/Search/FilterComponents/DatePresetFilterBase.tsx
@@ -74,7 +74,7 @@ function DatePresetFilterBase({defaultDateValues, selectedDateModifier, onSelect
return;
}
setDateValues(defaultDateValues);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSearchAdvancedFiltersFormLoading]);
const setDateValue = useCallback((dateModifier: SearchDateModifier, value: string | undefined) => {
diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx
index 9f1ce5560040..e9fc33df0b20 100644
--- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx
+++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx
@@ -7,7 +7,7 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider';
import {useOptionsList} from '@components/OptionListContextProvider';
import SelectionList from '@components/SelectionList';
import UserSelectionListItem from '@components/SelectionList/ListItem/UserSelectionListItem';
-import type {SelectionListHandle} from '@components/SelectionList/types';
+import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -52,7 +52,7 @@ type UserSelectPopupProps = {
};
function UserSelectPopup({value, closeOverlay, onChange, isSearchable}: UserSelectPopupProps) {
- const selectionListRef = useRef(null);
+ const selectionListRef = useRef | null>(null);
const styles = useThemeStyles();
const {translate} = useLocalize();
const {options} = useOptionsList();
diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx
index aa20315d8a50..9665142e3d08 100644
--- a/src/components/Search/SearchFiltersParticipantsSelector.tsx
+++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx
@@ -195,7 +195,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}:
});
setSelectedOptions(preSelectedOptions);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this should react only to changes in form data
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this should react only to changes in form data
}, [initialAccountIDs, personalDetails]);
const handleParticipantSelection = useCallback(
diff --git a/src/components/Search/SearchList/BaseSearchList/index.tsx b/src/components/Search/SearchList/BaseSearchList/index.tsx
index 05553d602645..678a73793d2a 100644
--- a/src/components/Search/SearchList/BaseSearchList/index.tsx
+++ b/src/components/Search/SearchList/BaseSearchList/index.tsx
@@ -52,7 +52,6 @@ function BaseSearchList({
onFocusedIndexChange: (index: number) => {
scrollToIndex?.(index);
},
- // eslint-disable-next-line react-compiler/react-compiler
...(!hasKeyBeenPressed.current && {setHasKeyBeenPressed}),
isFocused,
});
diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
index 46a3f3c99a5b..4f5fbc652ced 100644
--- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
+++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx
@@ -308,14 +308,18 @@ function SearchFiltersBar({
updatedFilterFormValues.columns = [];
}
- const queryString = buildQueryStringFromFilterFormValues(updatedFilterFormValues);
+ // Preserve the current sortBy and sortOrder from queryJSON when updating filters
+ const queryString = buildQueryStringFromFilterFormValues(updatedFilterFormValues, {
+ sortBy: queryJSON.sortBy,
+ sortOrder: queryJSON.sortOrder,
+ });
close(() => {
- // We want to explicitly clear stale rawQuery since it’s only used for manually typed-in queries.
+ // We want to explicitly clear stale rawQuery since it's only used for manually typed-in queries.
Navigation.setParams({q: queryString, rawQuery: undefined});
});
},
- [searchAdvancedFiltersForm],
+ [searchAdvancedFiltersForm, queryJSON.sortBy, queryJSON.sortOrder],
);
const openAdvancedFilters = useCallback(() => {
diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
index 0475b21d0dcb..4ecd440d4a51 100644
--- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
+++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx
@@ -96,7 +96,6 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
return;
}
textInputRef.current.blur();
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchRouterListVisible]);
@@ -125,7 +124,6 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
return;
}
setShowPopupButton(true);
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchRouterListVisible]);
@@ -133,7 +131,6 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
onSearchRouterFocus?.();
listRef.current?.updateAndScrollToFocusedIndex(0);
setShowPopupButton(false);
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx
index 9b796b5be248..1dad9fc115d3 100644
--- a/src/components/Search/SearchRouter/SearchButton.tsx
+++ b/src/components/Search/SearchRouter/SearchButton.tsx
@@ -36,7 +36,6 @@ function SearchButton({style, shouldUseAutoHitSlop = false}: SearchButtonProps)
style={[styles.flexRow, styles.touchableButtonImage, style]}
shouldUseAutoHitSlop={shouldUseAutoHitSlop}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.SEARCH_BUTTON}
- // eslint-disable-next-line react-compiler/react-compiler
onPress={callFunctionIfActionIsAllowed(() => {
pressableRef?.current?.blur();
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index c02c8db81fb6..4c6c8414bb77 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -58,7 +58,7 @@ import {
shouldShowYear as shouldShowYearUtil,
} from '@libs/SearchUIUtils';
import {cancelSpan, endSpan, startSpan} from '@libs/telemetry/activeSpans';
-import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isOnHold, isTransactionPendingDelete, mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils';
+import {getOriginalTransactionWithSplitInfo, isOnHold, isTransactionPendingDelete, mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils';
import Navigation, {navigationRef} from '@navigation/Navigation';
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
import EmptySearchView from '@pages/Search/EmptySearchView';
@@ -100,7 +100,6 @@ function mapTransactionItemToSelectedEntry(
): [string, SelectedTransactionInfo] {
const {canHoldRequest, canUnholdRequest} = canHoldUnholdReportAction(item.report, item.reportAction, item.holdReportAction, item, item.policy);
const canRejectRequest = item.report ? canRejectReportAction(currentUserLogin, item.report, item.policy) : false;
-
return [
item.keyForList,
{
@@ -109,7 +108,7 @@ function mapTransactionItemToSelectedEntry(
canHold: canHoldRequest,
isHeld: isOnHold(item),
canUnhold: canUnholdRequest,
- canSplit: isSplitAction(item.report, [itemTransaction], originalItemTransaction, item.policy),
+ canSplit: isSplitAction(item.report, [itemTransaction], originalItemTransaction, currentUserLogin, item.policy),
hasBeenSplit: getOriginalTransactionWithSplitInfo(itemTransaction, originalItemTransaction).isExpenseSplit,
canChangeReport: canEditFieldOfMoneyRequest(
item.reportAction,
@@ -126,7 +125,7 @@ function mapTransactionItemToSelectedEntry(
groupExchangeRate: item.groupExchangeRate,
reportID: item.reportID,
policyID: item.report?.policyID,
- amount: hasValidModifiedAmount(item) ? Number(item.modifiedAmount) : item.amount,
+ amount: item.modifiedAmount ?? item.amount,
groupAmount: item.groupAmount,
currency: item.currency,
isFromOneTransactionReport: isOneTransactionReport(item.report),
@@ -161,7 +160,7 @@ function prepareTransactionsList(
canHold: canHoldRequest,
isHeld: isOnHold(item),
canUnhold: canUnholdRequest,
- canSplit: isSplitAction(item.report, [itemTransaction], originalItemTransaction, item.policy),
+ canSplit: isSplitAction(item.report, [itemTransaction], originalItemTransaction, currentUserLogin, item.policy),
hasBeenSplit: getOriginalTransactionWithSplitInfo(itemTransaction, originalItemTransaction).isExpenseSplit,
canChangeReport: canEditFieldOfMoneyRequest(
item.reportAction,
@@ -176,7 +175,8 @@ function prepareTransactionsList(
action: item.action,
reportID: item.reportID,
policyID: item.policyID,
- amount: Math.abs(hasValidModifiedAmount(item) ? Number(item.modifiedAmount) : item.amount),
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ amount: Math.abs(item.modifiedAmount || item.amount),
groupAmount: item.groupAmount,
groupCurrency: item.groupCurrency,
groupExchangeRate: item.groupExchangeRate,
@@ -244,7 +244,7 @@ function Search({
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true});
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
- const {accountID, email} = useCurrentUserPersonalDetails();
+ const {accountID, email, login} = useCurrentUserPersonalDetails();
const [isActionLoadingSet = new Set()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSetSelector});
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true, selector: columnsSelector});
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, {canBeMissing: true});
@@ -296,6 +296,7 @@ function Search({
});
const [cardFeeds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
const {defaultCardFeed} = useCardFeedsForDisplay();
const suggestedSearches = useMemo(() => getSuggestedSearches(accountID, defaultCardFeed?.id), [defaultCardFeed?.id, accountID]);
@@ -320,7 +321,7 @@ function Search({
clearTransactionsAndSetHashAndKey();
// Trigger once on mount (e.g., on page reload), when RHP is open and screen is not focused
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const validGroupBy = groupBy && Object.values(CONST.SEARCH.GROUP_BY).includes(groupBy) ? groupBy : undefined;
@@ -345,7 +346,6 @@ function Search({
}
// We don't want to run the effect on isFocused change as we only need it to early return when it is false.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTransactions, isMobileSelectionModeEnabled, shouldTurnOffSelectionMode]);
@@ -366,19 +366,18 @@ function Search({
}
// We only want this effect to handle the switching of mobile selection mode state when screen size changes.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSmallScreenWidth]);
useEffect(() => {
- openSearch();
+ openSearch({includePartiallySetupBankAccounts: true});
}, []);
useEffect(() => {
if (!prevIsOffline || isOffline) {
return;
}
- openSearch();
+ openSearch({includePartiallySetupBankAccounts: true});
}, [isOffline, prevIsOffline]);
const {newSearchResultKeys, handleSelectionListScroll, newTransactions} = useSearchHighlightAndScroll({
@@ -424,6 +423,7 @@ function Search({
currentUserEmail: email ?? '',
translate,
formatPhoneNumber,
+ bankAccountList,
groupBy: validGroupBy,
reportActions: exportReportActions,
currentSearch: searchKey,
@@ -431,6 +431,7 @@ function Search({
queryJSON,
isActionLoadingSet,
cardFeeds,
+ allTransactionViolations: violations,
});
return [filteredData1, filteredData1.length, allLength];
}, [
@@ -448,6 +449,8 @@ function Search({
email,
isActionLoadingSet,
cardFeeds,
+ bankAccountList,
+ violations,
]);
useEffect(() => {
@@ -467,7 +470,6 @@ function Search({
handleSearch({queryJSON, searchKey, offset, shouldCalculateTotals, prevReportsLength: filteredDataLength, isLoading: !!searchResults?.search?.isLoading});
// We don't need to run the effect on change of isFocused.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleSearch, isOffline, offset, queryJSON, searchKey, shouldCalculateTotals]);
@@ -519,7 +521,7 @@ function Search({
canHold: canHoldRequest,
isHeld: isOnHold(transactionItem),
canUnhold: canUnholdRequest,
- canSplit: isSplitAction(transactionItem.report, [itemTransaction], originalItemTransaction, transactionItem.policy),
+ canSplit: isSplitAction(transactionItem.report, [itemTransaction], originalItemTransaction, login ?? '', transactionItem.policy),
hasBeenSplit: getOriginalTransactionWithSplitInfo(itemTransaction, originalItemTransaction).isExpenseSplit,
canChangeReport: canEditFieldOfMoneyRequest(
transactionItem.reportAction,
@@ -536,7 +538,7 @@ function Search({
canReject: canRejectRequest,
reportID: transactionItem.reportID,
policyID: transactionItem.report?.policyID,
- amount: hasValidModifiedAmount(transactionItem) ? Number(transactionItem.modifiedAmount) : transactionItem.amount,
+ amount: transactionItem.modifiedAmount ?? transactionItem.amount,
groupAmount: transactionItem.groupAmount,
groupCurrency: transactionItem.groupCurrency,
groupExchangeRate: transactionItem.groupExchangeRate,
@@ -572,7 +574,7 @@ function Search({
canHold: canHoldRequest,
isHeld: isOnHold(transactionItem),
canUnhold: canUnholdRequest,
- canSplit: isSplitAction(transactionItem.report, [itemTransaction], originalItemTransaction, transactionItem.policy),
+ canSplit: isSplitAction(transactionItem.report, [itemTransaction], originalItemTransaction, login ?? '', transactionItem.policy),
hasBeenSplit: getOriginalTransactionWithSplitInfo(itemTransaction, originalItemTransaction).isExpenseSplit,
canChangeReport: canEditFieldOfMoneyRequest(
transactionItem.reportAction,
@@ -589,7 +591,7 @@ function Search({
canReject: canRejectRequest,
reportID: transactionItem.reportID,
policyID: transactionItem.report?.policyID,
- amount: hasValidModifiedAmount(transactionItem) ? Number(transactionItem.modifiedAmount) : transactionItem.amount,
+ amount: transactionItem.modifiedAmount ?? transactionItem.amount,
groupAmount: transactionItem.groupAmount,
groupCurrency: transactionItem.groupCurrency,
groupExchangeRate: transactionItem.groupExchangeRate,
@@ -606,7 +608,7 @@ function Search({
setSelectedTransactions(newTransactionList, filteredData);
isRefreshingSelection.current = true;
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredData, setSelectedTransactions, areAllMatchingItemsSelected, isFocused, outstandingReportsByPolicyID, isExpenseReportType]);
useEffect(() => {
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 289d2a889634..cc1fddf94507 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -236,11 +236,11 @@ type SearchQueryAST = {
status: SearchStatus;
sortBy: SearchColumnType;
sortOrder: SortOrder;
- columns?: SearchCustomColumnIds[];
groupBy?: SearchGroupBy;
filters: ASTNode;
policyID?: string[];
rawFilterList?: RawQueryFilter[];
+ columns?: SearchCustomColumnIds | SearchCustomColumnIds[];
};
type SearchQueryJSON = {
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index f6a40a6dc6cd..840f3227fc8e 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -41,6 +41,7 @@ function BaseSelectionList({
initiallyFocusedItemKey,
onSelectRow,
onSelectAll,
+ onLongPressRow,
onCheckboxPress,
onScrollBeginDrag,
onDismissError,
@@ -56,7 +57,7 @@ function BaseSelectionList({
listFooterContent,
rightHandSideComponent,
alternateNumberOfSupportedLines,
- selectedItems = CONST.EMPTY_ARRAY,
+ selectedItems = CONST.EMPTY_ARRAY as unknown as string[],
style,
isSelected,
isDisabled = false,
@@ -73,7 +74,9 @@ function BaseSelectionList({
shouldUseUserSkeletonView,
shouldShowTooltips = true,
shouldIgnoreFocus = false,
+ shouldShowRightCaret = false,
shouldStopPropagation = false,
+ shouldHeaderBeInsideList = false,
shouldScrollToFocusedIndex = true,
shouldDebounceScrolling = false,
shouldUpdateFocusedIndex = false,
@@ -121,10 +124,10 @@ function BaseSelectionList({
if (isItemSelected(item) && (canSelectMultiple || acc.selectedOptions.length === 0)) {
acc.selectedOptions.push(item);
}
- if (isItemDisabled) {
+ if (isItemDisabled || item?.isDisabledCheckbox) {
acc.disabledIndexes.push(idx);
- if (!item?.isDisabledCheckbox) {
+ if (isItemDisabled) {
acc.disabledArrowKeyIndexes.push(idx);
}
}
@@ -150,11 +153,21 @@ function BaseSelectionList({
const scrollToIndex = useCallback(
(index: number) => {
+ // Bounds check: ensure index is valid for current data
+ if (index < 0 || index >= data.length) {
+ return;
+ }
const item = data.at(index);
- if (!listRef.current || !item || index === -1) {
+ if (!listRef.current || !item) {
return;
}
- listRef.current.scrollToIndex({index});
+ try {
+ listRef.current.scrollToIndex({index});
+ } catch (error) {
+ // FlashList may throw if layout for this index doesn't exist yet
+ // This can happen when data changes rapidly (e.g., during search filtering)
+ // The layout will be computed on next render, so we can safely ignore this
+ }
},
[data],
);
@@ -182,6 +195,11 @@ function BaseSelectionList({
onArrowUpDownCallback,
});
+ // extraData helps FlashList detect when data changes significantly (e.g., during filtering)
+ // Including data.length ensures FlashList resets its layout cache when the list size changes
+ // This prevents "index out of bounds" errors when filtering reduces the list size
+ const extraData = useMemo(() => [data.length], [data.length]);
+
const selectRow = useCallback(
(item: TItem, indexToFocus?: number) => {
if (!isFocused) {
@@ -312,6 +330,7 @@ function BaseSelectionList({
const isItemDisabled = isDisabled || item.isDisabled;
const selected = isItemSelected(item);
const isItemFocused = (!isDisabled || selected) && focusedIndex === index;
+ const isItemHighlighted = !!itemsToHighlight?.has(item.keyForList);
return (
({
selectRow={selectRow}
keyForList={item.keyForList}
showTooltip={shouldShowTooltips}
- item={item}
+ item={{
+ shouldAnimateInHighlight: isItemHighlighted,
+ isSelected: selected,
+ ...item,
+ }}
setFocusedIndex={setFocusedIndex}
index={index}
normalizedIndex={index}
@@ -335,6 +358,8 @@ function BaseSelectionList({
isDisabled={isItemDisabled}
canSelectMultiple={canSelectMultiple}
onDismissError={onDismissError}
+ onLongPressRow={onLongPressRow}
+ onCheckboxPress={onCheckboxPress}
shouldSingleExecuteRowSelect={shouldSingleExecuteRowSelect}
shouldUseDefaultRightHandSideCheckmark={shouldUseDefaultRightHandSideCheckmark}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
@@ -343,13 +368,15 @@ function BaseSelectionList({
isAlternateTextMultilineSupported={(alternateNumberOfSupportedLines ?? 0) > 1}
alternateTextNumberOfLines={alternateNumberOfSupportedLines}
shouldIgnoreFocus={shouldIgnoreFocus}
- wrapperStyle={style?.listItemWrapperStyle}
titleStyles={style?.listItemTitleStyles}
+ wrapperStyle={style?.listItemWrapperStyle}
+ titleContainerStyles={style?.listItemTitleContainerStyles}
singleExecution={singleExecution}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldSyncFocus={!isTextInputFocusedRef.current && hasKeyBeenPressed.current}
shouldDisableHoverStyle={shouldDisableHoverStyle}
shouldStopMouseLeavePropagation={false}
+ shouldShowRightCaret={shouldShowRightCaret}
/>
);
@@ -364,6 +391,28 @@ function BaseSelectionList({
}
};
+ const scrollTimeoutRef = useRef(null);
+
+ // The function scrolls to the focused input to prevent keyboard occlusion.
+ // It ensures the entire list item is visible, not just the input field.
+ // Added specifically for SplitExpensePage
+ const scrollToFocusedInput = useCallback((item: TItem) => {
+ if (!listRef.current) {
+ return;
+ }
+
+ // Clear any existing timer before starting a new one
+ if (scrollTimeoutRef.current) {
+ clearTimeout(scrollTimeoutRef.current);
+ }
+
+ // Delay scrolling by 300ms to allow the keyboard to open.
+ // This ensures FlashList calculates the correct window size.
+ setTimeout(() => {
+ listRef.current?.scrollToItem({item, viewPosition: 1, animated: true, viewOffset: 4});
+ }, CONST.ANIMATED_TRANSITION);
+ }, []);
+
const scrollAndHighlightItem = useCallback(
(items: string[]) => {
const newItemsToHighlight = new Set(items);
@@ -406,7 +455,7 @@ function BaseSelectionList({
return;
}
setFocusedIndex(selectedItemIndex);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedItemIndex]);
const prevSearchValue = usePrevious(textInputOptions?.value);
@@ -474,13 +523,26 @@ function BaseSelectionList({
}
}, [onSelectAll, shouldShowTextInput, shouldPreventDefaultFocusOnSelectRow]);
- useImperativeHandle(ref, () => ({scrollAndHighlightItem, scrollToIndex, updateFocusedIndex, focusTextInput}), [
+ useImperativeHandle(ref, () => ({scrollAndHighlightItem, scrollToIndex, updateFocusedIndex, scrollToFocusedInput, focusTextInput}), [
focusTextInput,
scrollAndHighlightItem,
scrollToIndex,
+ scrollToFocusedInput,
updateFocusedIndex,
]);
+ const header = (
+
+ );
+
return (
{textInputComponent({shouldBeInsideList: false})}
@@ -488,19 +550,13 @@ function BaseSelectionList({
renderListEmptyContent()
) : (
<>
-
+ {!shouldHeaderBeInsideList && header}
item.keyForList}
+ extraData={extraData}
ListFooterComponent={listFooterContent}
scrollEnabled={scrollEnabled}
indicatorStyle="white"
@@ -516,6 +572,7 @@ function BaseSelectionList({
<>
{customListHeaderContent}
{textInputComponent({shouldBeInsideList: true})}
+ {shouldHeaderBeInsideList && header}
>
}
/>
diff --git a/src/components/SelectionList/ListItem/BaseListItem.tsx b/src/components/SelectionList/ListItem/BaseListItem.tsx
index b711705cc968..6cc84c3304be 100644
--- a/src/components/SelectionList/ListItem/BaseListItem.tsx
+++ b/src/components/SelectionList/ListItem/BaseListItem.tsx
@@ -7,6 +7,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useHover from '@hooks/useHover';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import {useMouseContext} from '@hooks/useMouseContext';
import useStyleUtils from '@hooks/useStyleUtils';
import useSyncFocus from '@hooks/useSyncFocus';
@@ -45,12 +46,14 @@ function BaseListItem({
shouldHighlightSelectedItem = true,
shouldDisableHoverStyle,
shouldStopMouseLeavePropagation = true,
+ shouldShowRightCaret = false,
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {hovered, bind} = useHover();
const {isMouseDownOnInput, setMouseUp} = useMouseContext();
+ const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
const pressableRef = useRef(null);
@@ -167,6 +170,17 @@ function BaseListItem({
)}
{rightHandSideComponentRender()}
+ {shouldShowRightCaret && (
+
+
+
+ )}
{FooterComponent}
diff --git a/src/components/SelectionList/ListItem/ListItemRenderer.tsx b/src/components/SelectionList/ListItem/ListItemRenderer.tsx
index 0a28837594d6..494e2edeacb8 100644
--- a/src/components/SelectionList/ListItem/ListItemRenderer.tsx
+++ b/src/components/SelectionList/ListItem/ListItemRenderer.tsx
@@ -49,6 +49,7 @@ function ListItemRenderer({
shouldHighlightSelectedItem,
shouldDisableHoverStyle,
shouldStopMouseLeavePropagation,
+ shouldShowRightCaret,
}: ListItemRendererProps) {
const handleOnCheckboxPress = () => {
if (isTransactionGroupListItemType(item)) {
@@ -102,6 +103,7 @@ function ListItemRenderer({
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldDisableHoverStyle={shouldDisableHoverStyle}
shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation}
+ shouldShowRightCaret={shouldShowRightCaret}
/>
{item.footerContent && item.footerContent}
>
diff --git a/src/components/SelectionListWithSections/SplitListItem.tsx b/src/components/SelectionList/ListItem/SplitListItem.tsx
similarity index 95%
rename from src/components/SelectionListWithSections/SplitListItem.tsx
rename to src/components/SelectionList/ListItem/SplitListItem.tsx
index 52353aebacdc..fbf5215d7869 100644
--- a/src/components/SelectionListWithSections/SplitListItem.tsx
+++ b/src/components/SelectionList/ListItem/SplitListItem.tsx
@@ -1,6 +1,7 @@
import React, {useCallback, useLayoutEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
+import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
@@ -14,9 +15,9 @@ import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
-import SplitAmountDisplay from './SplitExpense/SplitAmountDisplay';
-import SplitListItemInput from './SplitListItemInput';
-import type {ListItem, SplitListItemProps, SplitListItemType} from './types';
+import SplitAmountDisplay from './SplitListItem/SplitAmountDisplay';
+import SplitListItemInput from './SplitListItem/SplitListItemInput';
+import type {SplitListItemProps, SplitListItemType} from './types';
function SplitListItem({
item,
@@ -27,7 +28,6 @@ function SplitListItem({
shouldPreventEnterKeySubmit,
rightHandSideComponent,
onFocus,
- index,
onInputFocus,
onInputBlur,
}: SplitListItemProps) {
@@ -59,15 +59,8 @@ function SplitListItem({
const contentWidth = (formattedOriginalAmount.length + 1) * CONST.CHARACTER_WIDTH;
const [percentageDraft, setPercentageDraft] = useState();
const focusHandler = useCallback(() => {
- if (!onInputFocus) {
- return;
- }
-
- if (!index && index !== 0) {
- return;
- }
- onInputFocus(index);
- }, [onInputFocus, index]);
+ onInputFocus?.(item);
+ }, [onInputFocus, item]);
// Auto-focus input when item is selected and screen transition ends
useLayoutEffect(() => {
diff --git a/src/components/SelectionListWithSections/SplitExpense/SplitAmountDisplay.tsx b/src/components/SelectionList/ListItem/SplitListItem/SplitAmountDisplay.tsx
similarity index 95%
rename from src/components/SelectionListWithSections/SplitExpense/SplitAmountDisplay.tsx
rename to src/components/SelectionList/ListItem/SplitListItem/SplitAmountDisplay.tsx
index c781fd39effc..5120e1e6ca73 100644
--- a/src/components/SelectionListWithSections/SplitExpense/SplitAmountDisplay.tsx
+++ b/src/components/SelectionList/ListItem/SplitListItem/SplitAmountDisplay.tsx
@@ -1,6 +1,6 @@
import React, {useState} from 'react';
import {View} from 'react-native';
-import type {SplitListItemType} from '@components/SelectionListWithSections/types';
+import type {SplitListItemType} from '@components/SelectionList/ListItem/types';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils';
diff --git a/src/components/SelectionListWithSections/SplitExpense/SplitAmountInput.tsx b/src/components/SelectionList/ListItem/SplitListItem/SplitAmountInput.tsx
similarity index 97%
rename from src/components/SelectionListWithSections/SplitExpense/SplitAmountInput.tsx
rename to src/components/SelectionList/ListItem/SplitListItem/SplitAmountInput.tsx
index 79a322e41ecb..adf09095df74 100644
--- a/src/components/SelectionListWithSections/SplitExpense/SplitAmountInput.tsx
+++ b/src/components/SelectionList/ListItem/SplitListItem/SplitAmountInput.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import type {BlurEvent} from 'react-native';
import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput';
-import type {SplitListItemType} from '@components/SelectionListWithSections/types';
+import type {SplitListItemType} from '@components/SelectionList/ListItem/types';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useThemeStyles from '@hooks/useThemeStyles';
import SplitAmountDisplay from './SplitAmountDisplay';
diff --git a/src/components/SelectionListWithSections/SplitListItemInput.tsx b/src/components/SelectionList/ListItem/SplitListItem/SplitListItemInput.tsx
similarity index 92%
rename from src/components/SelectionListWithSections/SplitListItemInput.tsx
rename to src/components/SelectionList/ListItem/SplitListItem/SplitListItemInput.tsx
index 5eebfe960ca7..03fbae0f065e 100644
--- a/src/components/SelectionListWithSections/SplitListItemInput.tsx
+++ b/src/components/SelectionList/ListItem/SplitListItem/SplitListItemInput.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import type {BlurEvent} from 'react-native';
+import type {SplitListItemType} from '@components/SelectionList/ListItem/types';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
-import SplitAmountInput from './SplitExpense/SplitAmountInput';
-import SplitPercentageInput from './SplitExpense/SplitPercentageInput';
-import type {SplitListItemType} from './types';
+import SplitAmountInput from './SplitAmountInput';
+import SplitPercentageInput from './SplitPercentageInput';
type SplitListItemInputProps = {
/** Whether the list is percentage mode (for scroll offset calculation) */
diff --git a/src/components/SelectionListWithSections/SplitExpense/SplitPercentageDisplay.tsx b/src/components/SelectionList/ListItem/SplitListItem/SplitPercentageDisplay.tsx
similarity index 91%
rename from src/components/SelectionListWithSections/SplitExpense/SplitPercentageDisplay.tsx
rename to src/components/SelectionList/ListItem/SplitListItem/SplitPercentageDisplay.tsx
index 532296ecbc5d..a635a3b1b468 100644
--- a/src/components/SelectionListWithSections/SplitExpense/SplitPercentageDisplay.tsx
+++ b/src/components/SelectionList/ListItem/SplitListItem/SplitPercentageDisplay.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {View} from 'react-native';
-import type {SplitListItemType} from '@components/SelectionListWithSections/types';
+import type {SplitListItemType} from '@components/SelectionList/ListItem/types';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
diff --git a/src/components/SelectionListWithSections/SplitExpense/SplitPercentageInput.tsx b/src/components/SelectionList/ListItem/SplitListItem/SplitPercentageInput.tsx
similarity index 93%
rename from src/components/SelectionListWithSections/SplitExpense/SplitPercentageInput.tsx
rename to src/components/SelectionList/ListItem/SplitListItem/SplitPercentageInput.tsx
index 6f2ac3efebca..dfa224bad555 100644
--- a/src/components/SelectionListWithSections/SplitExpense/SplitPercentageInput.tsx
+++ b/src/components/SelectionList/ListItem/SplitListItem/SplitPercentageInput.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import type {BlurEvent} from 'react-native';
import PercentageForm from '@components/PercentageForm';
-import type {SplitListItemType} from '@components/SelectionListWithSections/types';
+import type {SplitListItemType} from '@components/SelectionList/ListItem/types';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import SplitPercentageDisplay from './SplitPercentageDisplay';
@@ -38,7 +38,7 @@ function SplitPercentageInput({splitItem, contentWidth, percentageDraft, onSplit
}}
value={inputValue}
textInputContainerStyles={StyleUtils.splitPercentageInputStyles(styles)}
- containerStyles={styles.optionRowPercentInputContainer}
+ containerStyles={[styles.optionRowPercentInputContainer, styles.ml3]}
inputStyle={[styles.optionRowPercentInput, styles.lineHeightUndefined]}
onFocus={focusHandler}
onBlur={(event) => {
diff --git a/src/components/SelectionList/ListItem/TableListItem.tsx b/src/components/SelectionList/ListItem/TableListItem.tsx
new file mode 100644
index 000000000000..4835cc0a5b5f
--- /dev/null
+++ b/src/components/SelectionList/ListItem/TableListItem.tsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import ReportActionAvatars from '@components/ReportActionAvatars';
+import TextWithTooltip from '@components/TextWithTooltip';
+import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import BaseListItem from './BaseListItem';
+import type {ListItem, TableListItemProps} from './types';
+
+function TableListItem({
+ item,
+ isFocused,
+ showTooltip,
+ isDisabled,
+ canSelectMultiple,
+ onSelectRow,
+ onCheckboxPress,
+ onDismissError,
+ rightHandSideComponent,
+ onFocus,
+ onLongPressRow,
+ shouldSyncFocus,
+ titleContainerStyles,
+ shouldUseDefaultRightHandSideCheckmark,
+ shouldShowRightCaret,
+}: TableListItemProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const StyleUtils = useStyleUtils();
+ const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
+
+ const animatedHighlightStyle = useAnimatedHighlightStyle({
+ borderRadius: styles.selectionListPressableItemWrapper.borderRadius,
+ shouldHighlight: !!item.shouldAnimateInHighlight,
+ highlightColor: theme.messageHighlightBG,
+ backgroundColor: theme.highlightBG,
+ });
+
+ const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
+ const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
+
+ const handleCheckboxPress = () => {
+ if (onCheckboxPress) {
+ onCheckboxPress(item);
+ } else {
+ onSelectRow(item);
+ }
+ };
+
+ return (
+
+ {(hovered) => (
+ <>
+ {!!canSelectMultiple && (
+
+
+ {!!item.isSelected && (
+
+ )}
+
+
+ )}
+ {!!item.accountID && (
+
+ )}
+
+
+ {!!item.alternateText && (
+
+ )}
+
+ {!!item.rightElement && item.rightElement}
+ >
+ )}
+
+ );
+}
+
+TableListItem.displayName = 'TableListItem';
+
+export default TableListItem;
diff --git a/src/components/SelectionList/ListItem/types.ts b/src/components/SelectionList/ListItem/types.ts
index eb6b328631b8..f2d0cb6f8362 100644
--- a/src/components/SelectionList/ListItem/types.ts
+++ b/src/components/SelectionList/ListItem/types.ts
@@ -1,10 +1,13 @@
import type {ReactElement, ReactNode} from 'react';
-import type {AccessibilityState, NativeSyntheticEvent, StyleProp, TargetedEvent, TextStyle, ViewStyle} from 'react-native';
+import type {AccessibilityState, BlurEvent, NativeSyntheticEvent, StyleProp, TargetedEvent, TextStyle, ViewStyle} from 'react-native';
import type {AnimatedStyle} from 'react-native-reanimated';
+import type {ValueOf} from 'type-fest';
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
// eslint-disable-next-line no-restricted-imports
import type CursorStyles from '@styles/utils/cursor/types';
+import type CONST from '@src/CONST';
+import type {SplitExpense} from '@src/types/onyx/IOU';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
import type BaseListItem from './BaseListItem';
@@ -12,6 +15,7 @@ import type MultiSelectListItem from './MultiSelectListItem';
import type RadioListItem from './RadioListItem';
import type SingleSelectListItem from './SingleSelectListItem';
import type SpendCategorySelectorListItem from './SpendCategorySelectorListItem';
+import type SplitListItem from './SplitListItem';
import type TravelDomainListItem from './TravelDomainListItem';
type ListItem = {
@@ -180,6 +184,9 @@ type CommonListItemProps = {
/** Accessibility State tells a person using either VoiceOver on iOS or TalkBack on Android the state of the element currently focused on */
accessibilityState?: AccessibilityState;
+
+ /** Whether to show the right caret icon */
+ shouldShowRightCaret?: boolean;
} & TRightHandSideComponent;
type ListItemFocusEventHandler = (event: NativeSyntheticEvent) => void;
@@ -225,6 +232,9 @@ type ListItemProps = CommonListItemProps & {
/** Whether to show RBR */
shouldDisplayRBR?: boolean;
+ /** Boolean whether to display the right icon */
+ shouldShowRightCaret?: boolean;
+
/** Styles applied for the title */
titleStyles?: StyleProp;
@@ -237,6 +247,15 @@ type ListItemProps = CommonListItemProps & {
/** Whether to highlight the selected item */
shouldHighlightSelectedItem?: boolean;
+ /** Index of the item in the list */
+ index?: number;
+
+ /** Callback when the input inside the item is focused (if input exists) */
+ onInputFocus?: (item: TItem) => void;
+
+ /** Callback when the input inside the item is blurred (if input exists) */
+ onInputBlur?: (e: BlurEvent) => void;
+
/** Whether to disable the hover style of the item */
shouldDisableHoverStyle?: boolean;
@@ -250,7 +269,8 @@ type ValidListItem =
| typeof MultiSelectListItem
| typeof SingleSelectListItem
| typeof SpendCategorySelectorListItem
- | typeof TravelDomainListItem;
+ | typeof TravelDomainListItem
+ | typeof SplitListItem;
type BaseListItemProps = CommonListItemProps & {
item: TItem;
@@ -281,6 +301,46 @@ type BaseListItemProps = CommonListItemProps & {
/** Whether to call stopPropagation on the mouseleave event in BaseListItem */
shouldStopMouseLeavePropagation?: boolean;
};
+
+type SplitListItemType = ListItem &
+ SplitExpense & {
+ /** Item header text */
+ headerText: string;
+
+ /** Merchant or vendor name */
+ merchant: string;
+
+ /** Currency code */
+ currency: string;
+
+ /** ID of split expense */
+ transactionID: string;
+
+ /** Currency symbol */
+ currencySymbol: string;
+
+ /** Original amount before split */
+ originalAmount: number;
+
+ /** Indicates whether a split wasn't approved, paid etc. when report.statusNum < CONST.REPORT.STATUS_NUM.CLOSED */
+ isEditable: boolean;
+
+ /** Current mode for the split editor: amount or percentage */
+ mode: ValueOf;
+
+ /** Percentage value to show when in percentage mode (0-100) */
+ percentage: number;
+
+ /**
+ * Function for updating value (amount or percentage based on mode)
+ */
+ onSplitExpenseValueChange: (transactionID: string, value: number, mode: ValueOf) => void;
+
+ onInputFocus?: (item: SplitListItemType) => void;
+ };
+
+type SplitListItemProps = ListItemProps;
+
type RadioListItemProps = ListItemProps;
type SingleSelectListItemProps = ListItemProps;
@@ -291,6 +351,8 @@ type SpendCategorySelectorListItemProps = ListItemProps<
type UserListItemProps = ListItemProps & ForwardedFSClassProps;
+type TableListItemProps = ListItemProps;
+
type InviteMemberListItemProps = UserListItemProps & {
/** Whether product training tooltips can be displayed */
canShowProductTrainingTooltip?: boolean;
@@ -331,6 +393,9 @@ export type {
SpendCategorySelectorListItemProps,
UserListItemProps,
InviteMemberListItemProps,
+ SplitListItemType,
+ SplitListItemProps,
+ TableListItemProps,
WorkspaceListItemType,
UserSelectionListItemProps,
};
diff --git a/src/components/SelectionList/components/ListHeader.tsx b/src/components/SelectionList/components/ListHeader.tsx
index 004378cd9143..13315469aee1 100644
--- a/src/components/SelectionList/components/ListHeader.tsx
+++ b/src/components/SelectionList/components/ListHeader.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import Checkbox from '@components/Checkbox';
import {PressableWithFeedback} from '@components/Pressable';
@@ -18,6 +19,9 @@ type ListHeaderProps = {
/** Whether multiple items can be selected */
canSelectMultiple: boolean;
+ /** Styles for the list header wrapper */
+ headerStyle?: StyleProp;
+
/** Function called when the select all button is pressed */
onSelectAll: () => void;
@@ -33,6 +37,7 @@ function ListHeader({
customListHeader,
canSelectMultiple,
onSelectAll,
+ headerStyle,
shouldShowSelectAllButton,
shouldPreventDefaultFocusOnSelectRow,
}: ListHeaderProps) {
@@ -54,11 +59,12 @@ function ListHeader({
return (
= Partial & {
data: TItem[];
/** Reference to the SelectionList component */
- ref?: React.Ref;
+ ref?: React.Ref>;
/** Component to render for each list item */
ListItem: ValidListItem;
@@ -26,6 +26,9 @@ type SelectionListProps = Partial & {
/** Called when "Select All" button is pressed */
onSelectAll?: () => void;
+ /** Callback to fire when the item is long pressed */
+ onLongPressRow?: (item: TItem) => void;
+
/** Called when a checkbox is pressed */
onCheckboxPress?: (item: TItem) => void;
@@ -72,17 +75,23 @@ type SelectionListProps = Partial & {
selectedItems?: readonly string[];
style?: {
- /** Styles to apply to the list */
+ /** Styles for the list */
listStyle?: StyleProp;
- /** Styles applied for the title of the list item */
+ /** Styles for the list container */
+ containerStyle?: StyleProp;
+
+ /** Styles for the title of the list item */
listItemTitleStyles?: StyleProp;
/** Styles for the list item wrapper */
listItemWrapperStyle?: StyleProp;
- /** Styles to apply to the list container */
- containerStyle?: StyleProp;
+ /** Styles for the list header wrapper */
+ listHeaderWrapperStyle?: StyleProp;
+
+ /** Styles for the title container of the list item */
+ listItemTitleContainerStyles?: StyleProp;
};
/** Function that determines if an item is selected */
@@ -130,9 +139,15 @@ type SelectionListProps = Partial & {
/** Whether to ignore focus events */
shouldIgnoreFocus?: boolean;
+ /** Whether to show the right caret icon */
+ shouldShowRightCaret?: boolean;
+
/** Whether to stop automatic propagation on pressing enter key */
shouldStopPropagation?: boolean;
+ /** Whether to place customListHeader in the list so it scrolls with data */
+ shouldHeaderBeInsideList?: boolean;
+
/** Whether to scroll to the focused item */
shouldScrollToFocusedIndex?: boolean;
@@ -222,7 +237,7 @@ type ConfirmButtonOptions = {
type ButtonOrCheckBoxRoles = 'button' | 'checkbox';
-type SelectionListHandle = {
+type SelectionListHandle = {
/** Scrolls to and highlights the specified items */
scrollAndHighlightItem: (items: string[]) => void;
@@ -232,6 +247,9 @@ type SelectionListHandle = {
/** Updates the focused index and optionally scrolls to it */
updateFocusedIndex: (newFocusedIndex: number, shouldScroll?: boolean) => void;
+ /** Scrolls to the focused input on SplitExpensePage */
+ scrollToFocusedInput: (item: TItem) => void;
+
/** Sets the focus to the textInput component */
focusTextInput: () => void;
};
diff --git a/src/components/SelectionListWithSections/ListItemRightCaretWithLabel.tsx b/src/components/SelectionListWithModal/ListItemRightCaretWithLabel.tsx
similarity index 100%
rename from src/components/SelectionListWithSections/ListItemRightCaretWithLabel.tsx
rename to src/components/SelectionListWithModal/ListItemRightCaretWithLabel.tsx
index 7876efb761fa..3db1d5e21ad1 100644
--- a/src/components/SelectionListWithSections/ListItemRightCaretWithLabel.tsx
+++ b/src/components/SelectionListWithModal/ListItemRightCaretWithLabel.tsx
@@ -13,10 +13,10 @@ type ListItemRightCaretWithLabelProps = {
};
function ListItemRightCaretWithLabel({labelText, shouldShowCaret = false}: ListItemRightCaretWithLabelProps) {
- const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();
+ const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
return (
diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx
index e5b622b3f170..821735849147 100644
--- a/src/components/SelectionListWithModal/index.tsx
+++ b/src/components/SelectionListWithModal/index.tsx
@@ -1,11 +1,11 @@
import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
-import React, {useMemo, useState} from 'react';
+import React, {useEffect, useMemo, useState} from 'react';
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
-// eslint-disable-next-line no-restricted-imports
-import SelectionList from '@components/SelectionListWithSections';
-import type {ListItem, SelectionListHandle, SelectionListProps} from '@components/SelectionListWithSections/types';
+import SelectionList from '@components/SelectionList';
+import type {ListItem, SelectionListHandle, SelectionListProps} from '@components/SelectionList/types';
+import useDebouncedState from '@hooks/useDebouncedState';
import useHandleSelectionMode from '@hooks/useHandleSelectionMode';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -17,16 +17,14 @@ import CONST from '@src/CONST';
type SelectionListWithModalProps = SelectionListProps & {
turnOnSelectionModeOnLongPress?: boolean;
onTurnOnSelectionMode?: (item: TItem | null) => void;
- isScreenFocused?: boolean;
- ref?: ForwardedRef;
+ ref?: ForwardedRef>;
};
function SelectionListWithModal({
turnOnSelectionModeOnLongPress,
onTurnOnSelectionMode,
onLongPressRow,
- isScreenFocused = false,
- sections,
+ data,
isSelected,
selectedItems: selectedItemsProp,
ref,
@@ -44,25 +42,37 @@ function SelectionListWithModal({
const isMobileSelectionModeEnabled = useMobileSelectionMode();
- const sectionData = sections[0]?.data;
+ // Debounce the data prop to prevent rapid updates that cause FlashList layout errors
+ // This gives FlashList time to properly update its layout cache when searching/filtering
+ const [, debouncedData, setDataState] = useDebouncedState(data, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
+
+ // Determine if this is changed by filtering (to limit multiple rerenders)
+ const isFiltering = data.length < debouncedData.length;
+
+ useEffect(() => {
+ setDataState(data);
+ }, [data, setDataState]);
+
+ const displayData = isFiltering ? debouncedData : data;
+
const selectedItems = useMemo(
() =>
selectedItemsProp ??
- sectionData?.filter((item) => {
+ displayData.filter((item) => {
if (isSelected) {
return isSelected(item);
}
return !!item.isSelected;
}) ??
[],
- [isSelected, sectionData, selectedItemsProp],
+ [isSelected, displayData, selectedItemsProp],
);
useHandleSelectionMode(selectedItems);
const handleLongPressRow = (item: TItem) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (!turnOnSelectionModeOnLongPress || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox || (!isFocused && !isScreenFocused)) {
+ if (!turnOnSelectionModeOnLongPress || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox || !isFocused) {
return;
}
if (isSmallScreenWidth && isMobileSelectionModeEnabled) {
@@ -91,11 +101,12 @@ function SelectionListWithModal({
<>
diff --git a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
index 7eb2b96aaaa9..cb49ef3ac3f5 100644
--- a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
+++ b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
@@ -365,7 +365,7 @@ function BaseSelectionListWithSections({
pendingScrollIndexRef.current = null;
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[flattenedSections.allOptions, currentPage],
);
@@ -397,7 +397,7 @@ function BaseSelectionListWithSections({
}
setDisabledArrowKeyIndexes(flattenedSections.disabledArrowKeyOptionsIndexes);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [flattenedSections.disabledArrowKeyOptionsIndexes]);
/** Check whether there is a need to scroll to an item and if all items are loaded */
@@ -461,7 +461,7 @@ function BaseSelectionListWithSections({
return;
}
setFocusedIndex(selectedItemIndex);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedItemIndex]);
const clearInputAfterSelect = useCallback(() => {
@@ -750,7 +750,7 @@ function BaseSelectionListWithSections({
if (typeof textInputRef === 'function') {
textInputRef(element as RNTextInput);
} else {
- // eslint-disable-next-line no-param-reassign, react-compiler/react-compiler
+ // eslint-disable-next-line no-param-reassign
textInputRef.current = element as RNTextInput;
}
}}
diff --git a/src/components/SelectionListWithSections/Search/ActionCell.tsx b/src/components/SelectionListWithSections/Search/ActionCell.tsx
index 916309688d53..a13f40bf41e9 100644
--- a/src/components/SelectionListWithSections/Search/ActionCell.tsx
+++ b/src/components/SelectionListWithSections/Search/ActionCell.tsx
@@ -77,9 +77,10 @@ function ActionCell({
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID);
const policy = usePolicy(policyID);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`, {canBeMissing: true});
- const canBePaid = canIOUBePaid(iouReport, chatReport, policy, transactions, false);
- const shouldOnlyShowElsewhere = !canBePaid && canIOUBePaid(iouReport, chatReport, policy, transactions, true);
+ const canBePaid = canIOUBePaid(iouReport, chatReport, policy, bankAccountList, transactions, false);
+ const shouldOnlyShowElsewhere = !canBePaid && canIOUBePaid(iouReport, chatReport, policy, bankAccountList, transactions, true);
const text = isChildListItem ? translate(actionTranslationsMap[CONST.SEARCH.ACTION_TYPES.VIEW]) : translate(actionTranslationsMap[action]);
const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID);
diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx
index 161362cd38da..5435fe69448a 100644
--- a/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx
+++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItemRow.tsx
@@ -130,7 +130,7 @@ function ExpenseReportListItemRow({
)}
@@ -141,7 +141,7 @@ function ExpenseReportListItemRow({
)}
diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
index ed015ba4491d..bf5d45394495 100644
--- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
@@ -99,6 +99,7 @@ function TransactionGroupListItem({
const [transactionsVisibleLimit, setTransactionsVisibleLimit] = useState(CONST.TRANSACTION.RESULTS_PAGE_SIZE as number);
const [isExpanded, setIsExpanded] = useState(false);
const [isActionLoadingSet = new Set()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}`, {canBeMissing: true, selector: isActionLoadingSetSelector});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
const transactions = useMemo(() => {
if (isExpenseReportType) {
@@ -114,6 +115,7 @@ function TransactionGroupListItem({
currentUserEmail: currentUserDetails.email ?? '',
translate,
formatPhoneNumber,
+ bankAccountList,
isActionLoadingSet,
}) as [TransactionListItemType[], number];
return sectionData.map((transactionItem) => ({
@@ -130,6 +132,7 @@ function TransactionGroupListItem({
selectedTransactionIDsSet,
currentUserDetails.email,
isActionLoadingSet,
+ bankAccountList,
]);
const selectedItemsLength = useMemo(() => {
diff --git a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx
index 4d514f30a1fb..dc993d368b8a 100644
--- a/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx
+++ b/src/components/SelectionListWithSections/Search/UserInfoAndActionButtonRow.tsx
@@ -2,7 +2,6 @@ import React from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import type {TransactionListItemType, TransactionReportGroupListItemType} from '@components/SelectionListWithSections/types';
-import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -29,13 +28,12 @@ function UserInfoAndActionButtonRow({
}) {
const styles = useThemeStyles();
const {isLargeScreenWidth} = useResponsiveLayout();
- const {translate} = useLocalize();
const transactionItem = item as unknown as TransactionListItemType;
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector});
const hasFromSender = !!item?.from && !!item?.from?.accountID && !!item?.from?.displayName;
const hasToRecipient = !!item?.to && !!item?.to?.accountID && !!item?.to?.displayName;
- const participantFromDisplayName = item?.from?.displayName ?? item?.from?.login ?? translate('common.hidden');
- const participantToDisplayName = item?.to?.displayName ?? item?.to?.login ?? translate('common.hidden');
+ const participantFromDisplayName = item.formattedFrom ?? item?.from?.displayName ?? '';
+ const participantToDisplayName = item.formattedTo ?? item?.to?.displayName ?? '';
const shouldShowToRecipient = hasFromSender && hasToRecipient && !!item?.to?.accountID && !!isCorrectSearchUserName(participantToDisplayName);
return (
({
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();
+ const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
const animatedHighlightStyle = useAnimatedHighlightStyle({
borderRadius: styles.selectionListPressableItemWrapper.borderRadius,
@@ -44,13 +45,13 @@ function TableListItem({
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
- const handleCheckboxPress = useCallback(() => {
+ const handleCheckboxPress = () => {
if (onCheckboxPress) {
onCheckboxPress(item);
} else {
onSelectRow(item);
}
- }, [item, onCheckboxPress, onSelectRow]);
+ };
return (
({
{!!item.isSelected && (
= ListItemProps &
FooterComponent?: ReactElement;
};
-type SplitListItemType = ListItem &
- SplitExpense & {
- /** Item header text */
- headerText: string;
-
- /** Merchant or vendor name */
- merchant: string;
-
- /** Currency code */
- currency: string;
-
- /** ID of split expense */
- transactionID: string;
-
- /** Currency symbol */
- currencySymbol: string;
-
- /** Original amount before split */
- originalAmount: number;
-
- /** Indicates whether a split wasn't approved, paid etc. when report.statusNum < CONST.REPORT.STATUS_NUM.CLOSED */
- isEditable: boolean;
-
- /** Current mode for the split editor: amount or percentage */
- mode: ValueOf;
-
- /** Percentage value to show when in percentage mode (0-100) */
- percentage: number;
-
- /**
- * Function for updating value (amount or percentage based on mode)
- */
- onSplitExpenseValueChange: (transactionID: string, value: number, mode: ValueOf) => void;
- };
-
-type SplitListItemProps = ListItemProps;
-
type TransactionSelectionListItem = ListItemProps & Transaction;
type InviteMemberListItemProps = UserListItemProps & {
@@ -1181,8 +1143,6 @@ export type {
ReportActionListItemType,
ChatListItemProps,
SortableColumnName,
- SplitListItemProps,
- SplitListItemType,
SearchListItem,
UnreportedExpenseListItemType,
};
diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx
index b220f2aa752b..3315559894c4 100644
--- a/src/components/SelectionScreen.tsx
+++ b/src/components/SelectionScreen.tsx
@@ -17,10 +17,10 @@ import HeaderWithBackButton from './HeaderWithBackButton';
import OfflineWithFeedback from './OfflineWithFeedback';
import ScreenWrapper from './ScreenWrapper';
// eslint-disable-next-line no-restricted-imports
-import SelectionList from './SelectionListWithSections';
-import type RadioListItem from './SelectionListWithSections/RadioListItem';
-import type TableListItem from './SelectionListWithSections/TableListItem';
-import type {ListItem, SectionListDataType} from './SelectionListWithSections/types';
+import SelectionList from './SelectionList';
+import type RadioListItem from './SelectionList/ListItem/RadioListItem';
+import type TableListItem from './SelectionList/ListItem/TableListItem';
+import type {ListItem} from './SelectionList/types';
import type UserListItem from './SelectionListWithSections/UserListItem';
type SelectorType = ListItem & {
@@ -46,7 +46,7 @@ type SelectionScreenProps = {
listFooterContent?: React.JSX.Element | null;
/** Sections for the section list */
- sections: Array>>;
+ data: Array>;
/** Default renderer for every item in the list */
listItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem;
@@ -55,10 +55,10 @@ type SelectionScreenProps = {
listItemWrapperStyle?: StyleProp;
/** Item `keyForList` to focus initially */
- initiallyFocusedOptionKey?: string | null | undefined;
+ initiallyFocusedOptionKey?: string | undefined;
/** Callback to fire when a row is pressed */
- onSelectRow: (selection: SelectorType) => void;
+ onSelectRow: (item: SelectorType) => void;
/** Callback to fire when back button is pressed */
onBackButtonPress?: () => void;
@@ -102,14 +102,16 @@ type SelectionScreenProps = {
/** Whether to show the text input */
shouldShowTextInput?: boolean;
- /** Label for the text input */
- textInputLabel?: string;
+ textInputOptions?: {
+ /** Label for the text input */
+ label?: string;
- /** Value for the text input */
- textInputValue?: string;
+ /** Value for the text input */
+ value?: string;
- /** Callback to fire when the text input changes */
- onChangeText?: (text: string) => void;
+ /** Callback to fire when the text input changes */
+ onChangeText?: (text: string) => void;
+ };
};
function SelectionScreen({
@@ -118,7 +120,7 @@ function SelectionScreen({
headerContent,
listEmptyContent,
listFooterContent,
- sections,
+ data,
listItem,
listItemWrapperStyle,
initiallyFocusedOptionKey,
@@ -135,10 +137,8 @@ function SelectionScreen({
onClose,
shouldSingleExecuteRowSelect,
headerTitleAlreadyTranslated,
- textInputLabel,
- textInputValue,
- onChangeText,
shouldShowTextInput,
+ textInputOptions,
shouldUpdateFocusedIndex = false,
}: SelectionScreenProps) {
const {translate} = useLocalize();
@@ -167,26 +167,23 @@ function SelectionScreen({
pendingAction={pendingAction}
style={[styles.flex1]}
contentContainerStyle={[styles.flex1]}
- shouldDisableOpacity={!sections.length}
+ shouldDisableOpacity={!data.length}
>
1;
const formattedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles, translate);
- const hasIntentToPay = ((formattedPaymentMethods.length === 1 && isIOUReport(iouReport)) || !!policy?.achAccount) && !lastPaymentMethod;
+ const hasIntentToPay = ((formattedPaymentMethods.length === 1 && isIOUReport(iouReport)) || policy?.achAccount?.state === CONST.BANK_ACCOUNT.STATE.OPEN) && !lastPaymentMethod;
const {isBetaEnabled} = usePermissions();
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
@@ -165,7 +165,9 @@ function SettlementButton({
if (!policy?.achAccount?.bankAccountID) {
return;
}
- const policyBankAccounts = formattedPaymentMethods.filter((method) => method.methodID === policy?.achAccount?.bankAccountID);
+ const policyBankAccounts = formattedPaymentMethods.filter(
+ (method) => method.methodID === policy?.achAccount?.bankAccountID && (method.accountData as AccountData)?.state === CONST.BANK_ACCOUNT.STATE.OPEN,
+ );
return policyBankAccounts.map((formattedPaymentMethod) => {
const {icon, iconStyles, iconSize, title, description, methodID} = formattedPaymentMethod ?? {};
@@ -418,7 +420,7 @@ function SettlementButton({
return buttonOptions;
// We don't want to reorder the options when the preferred payment method changes while the button is still visible except for component initialization when the last payment method is not initialized yet.
// We need to be sure that onPress should be wrapped in an useCallback to prevent unnecessary updates.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
paymentMethods,
isLoadingLastPaymentMethod,
@@ -513,33 +515,38 @@ function SettlementButton({
return lastPaymentPolicy.name;
}
- const bankAccountToDisplay = hasIntentToPay ? (formattedPaymentMethods.at(0) as BankAccount) : bankAccount;
+ const bankAccountToDisplay = hasIntentToPay
+ ? ((formattedPaymentMethods.find((method) => method.methodID === policy?.achAccount?.bankAccountID) ?? formattedPaymentMethods.at(0)) as BankAccount)
+ : bankAccount;
- if (lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || (hasIntentToPay && (isExpenseReport || isInvoiceReport))) {
- if (isInvoiceReport) {
- const isBusinessBankAccount = bankAccountToDisplay?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS;
- return translate(isBusinessBankAccount ? 'iou.invoiceBusinessBank' : 'iou.invoicePersonalBank', bankAccountToDisplay?.accountData?.accountNumber?.slice(-4) ?? '');
+ // Handle bank account payments first (expense reports require bank account, never wallet)
+ if ((lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.VBBA || (hasIntentToPay && isExpenseReport)) && !!policy?.achAccount) {
+ if (policy?.achAccount?.accountNumber) {
+ return translate('paymentMethodList.bankAccountLastFour', policy?.achAccount?.accountNumber?.slice(-4));
}
- if (!personalBankAccountList.length) {
+
+ if (!bankAccountToDisplay?.accountData?.accountNumber) {
return;
}
- return translate('common.wallet');
+ return translate('paymentMethodList.bankAccountLastFour', bankAccountToDisplay?.accountData?.accountNumber?.slice(-4));
}
- if ((lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.VBBA || hasIntentToPay) && !!policy?.achAccount) {
- if (policy?.achAccount?.accountNumber) {
- return translate('paymentMethodList.bankAccountLastFour', policy?.achAccount?.accountNumber?.slice(-4));
+ // Handle wallet payments for IOUs and bank account display for invoices
+ if (lastPaymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || (hasIntentToPay && isInvoiceReport)) {
+ if (isInvoiceReport) {
+ const isBusinessBankAccount = bankAccountToDisplay?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS;
+ return translate(isBusinessBankAccount ? 'iou.invoiceBusinessBank' : 'iou.invoicePersonalBank', bankAccountToDisplay?.accountData?.accountNumber?.slice(-4) ?? '');
}
- if (!bankAccountToDisplay?.accountData?.accountNumber) {
+ if (!personalBankAccountList.length) {
return;
}
- return translate('paymentMethodList.bankAccountLastFour', bankAccountToDisplay?.accountData?.accountNumber?.slice(-4));
+ return translate('common.wallet');
}
- if (bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && isExpenseReportUtil(iouReport)) {
+ if (bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && bankAccount?.methodID === policy?.achAccount?.bankAccountID && isExpenseReportUtil(iouReport)) {
return translate('paymentMethodList.bankAccountLastFour', bankAccount?.accountData?.accountNumber?.slice(-4) ?? '');
}
@@ -551,9 +558,13 @@ function SettlementButton({
return;
}
- const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(selectedOption, activeAdminPolicies, latestBankItem);
+ const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(selectedOption, activeAdminPolicies, latestBankItem, policyIDKey);
+
+ // Payment type for 'Pay via workspace' option is "Elsewhere" but selected option points to one of workspaces where user is admin
+ const isPayingViaWorkspace = paymentType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE && activeAdminPolicies.find((activeAdminPolicy) => activeAdminPolicy.id === selectedOption);
+ const isPayingWithMethod = paymentType !== CONST.IOU.PAYMENT_TYPE.ELSEWHERE;
- if (!!selectedPolicy || shouldSelectPaymentMethod) {
+ if ((!!selectedPolicy || shouldSelectPaymentMethod) && (isPayingWithMethod || isPayingViaWorkspace)) {
selectPaymentMethod(event, paymentType, triggerKYCFlow, selectedOption as PaymentMethod, selectedPolicy);
return;
}
@@ -597,6 +608,7 @@ function SettlementButton({
policy={lastPaymentPolicy}
anchorAlignment={kycWallAnchorAlignment}
shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption}
+ currency={currency}
>
{(triggerKYCFlow, buttonRef) => (
diff --git a/src/components/SidePanel/RHPVariantTest/index.native.ts b/src/components/SidePanel/RHPVariantTest/index.native.ts
new file mode 100644
index 000000000000..466080a1b07d
--- /dev/null
+++ b/src/components/SidePanel/RHPVariantTest/index.native.ts
@@ -0,0 +1,13 @@
+import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types';
+
+/**
+ * Side Panel is not supported on native platforms, so we always return false.
+ */
+const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => false;
+
+/**
+ * No-op on native platforms since Side Panel is not supported.
+ */
+const handleRHPVariantNavigation: HandleRHPVariantNavigation = () => {};
+
+export {shouldOpenRHPVariant, handleRHPVariantNavigation};
diff --git a/src/components/SidePanel/RHPVariantTest/index.ts b/src/components/SidePanel/RHPVariantTest/index.ts
new file mode 100644
index 000000000000..b301a5c8e89b
--- /dev/null
+++ b/src/components/SidePanel/RHPVariantTest/index.ts
@@ -0,0 +1,55 @@
+import Onyx from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import SidePanelActions from '@libs/actions/SidePanel';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {OnboardingRHPVariant} from '@src/types/onyx';
+import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types';
+
+let onboardingRHPVariant: OnyxEntry;
+let onboardingCompanySize: OnyxEntry;
+
+// We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied directly to the UI.
+Onyx.connectWithoutView({
+ key: ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT,
+ callback: (value) => {
+ onboardingRHPVariant = value;
+ },
+});
+
+Onyx.connectWithoutView({
+ key: ONYXKEYS.ONBOARDING_COMPANY_SIZE,
+ callback: (value) => {
+ onboardingCompanySize = value;
+ },
+});
+
+/**
+ * Determines if the user should be navigated to the RHP variant side panel after onboarding.
+ * The RHP variant is only shown to micro companies that are part of the RHP experiment.
+ */
+const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => {
+ const isMicroCompany = onboardingCompanySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO;
+ const isRHPConciergeDM = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_CONCIERGE_DM;
+ const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM;
+
+ return isMicroCompany && (isRHPConciergeDM || isRHPAdminsRoom);
+};
+
+/**
+ * Handles navigation for RHP experiment:
+ * - Control: navigate to the last accessed report on small screens, do not open side panel
+ * - RHP Concierge DM: navigate to the workspace overview and open the side panel with the Concierge DM
+ * - RHP Admins Room: navigate to the workspace overview and open the side panel with the Admins Room
+ */
+const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID) => {
+ Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(onboardingPolicyID));
+ SidePanelActions.openSidePanel(true);
+ Navigation.isNavigationReady().then(() => {
+ Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route);
+ });
+};
+
+export {shouldOpenRHPVariant, handleRHPVariantNavigation};
diff --git a/src/components/SidePanel/RHPVariantTest/types.ts b/src/components/SidePanel/RHPVariantTest/types.ts
new file mode 100644
index 000000000000..7efd441c0526
--- /dev/null
+++ b/src/components/SidePanel/RHPVariantTest/types.ts
@@ -0,0 +1,4 @@
+type ShouldOpenRHPVariant = () => boolean;
+type HandleRHPVariantNavigation = (onboardingPolicyID?: string) => void;
+
+export type {ShouldOpenRHPVariant, HandleRHPVariantNavigation};
diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx
index cfea33384493..3318bc08906f 100644
--- a/src/components/SidePanel/SidePanelContextProvider.tsx
+++ b/src/components/SidePanel/SidePanelContextProvider.tsx
@@ -3,14 +3,18 @@ import React, {createContext, useCallback, useEffect, useMemo, useRef, useState}
// Import Animated directly from 'react-native' as animations are used with navigation.
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
+import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSidePanelDisplayStatus from '@hooks/useSidePanelDisplayStatus';
import useWindowDimensions from '@hooks/useWindowDimensions';
import SidePanelActions from '@libs/actions/SidePanel';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
+import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import variables from '@styles/variables';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {emailSelector} from '@src/selectors/Session';
import type {SidePanel} from '@src/types/onyx';
type SidePanelContextProps = {
@@ -25,6 +29,7 @@ type SidePanelContextProps = {
openSidePanel: () => void;
closeSidePanel: () => void;
sidePanelNVP?: SidePanel;
+ reportID?: string;
};
const SidePanelContext = createContext({
@@ -56,6 +61,28 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
const sidePanelOffset = useRef(new Animated.Value(shouldApplySidePanelOffset ? variables.sidePanelWidth : 0));
const sidePanelTranslateX = useRef(new Animated.Value(shouldHideSidePanel ? sidePanelWidth : 0));
+ const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
+ const [onboardingRHPVariant] = useOnyx(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, {canBeMissing: true});
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
+ const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true});
+ const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {
+ canBeMissing: true,
+ selector: emailSelector,
+ });
+
+ const reportID = useMemo(() => {
+ const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM;
+ const isUserAdmin = isPolicyAdmin(activePolicy, sessionEmail);
+ const isPolicyActive = shouldShowPolicy(activePolicy, false, sessionEmail ?? '');
+ const adminsChatReportID = activePolicy?.chatReportIDAdmins?.toString();
+
+ if (isRHPAdminsRoom && isUserAdmin && isPolicyActive && adminsChatReportID) {
+ return adminsChatReportID;
+ }
+
+ return conciergeReportID;
+ }, [onboardingRHPVariant, activePolicy, sessionEmail, conciergeReportID]);
+
useEffect(() => {
setIsSidePanelTransitionEnded(false);
Animated.parallel([
@@ -71,7 +98,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
}),
]).start(() => setIsSidePanelTransitionEnded(true));
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- sidePanelWidth dependency caused the help panel content to slide in on window resize
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- sidePanelWidth dependency caused the help panel content to slide in on window resize
}, [shouldHideSidePanel, shouldApplySidePanelOffset]);
const closeSidePanel = useCallback(
@@ -103,6 +130,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
openSidePanel: () => SidePanelActions.openSidePanel(!isExtraLargeScreenWidth),
closeSidePanel,
sidePanelNVP,
+ reportID,
}),
[
closeSidePanel,
@@ -114,6 +142,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
shouldHideSidePanelBackdrop,
shouldHideToolTip,
sidePanelNVP,
+ reportID,
],
);
diff --git a/src/components/SidePanel/SidePanelModal/index.tsx b/src/components/SidePanel/SidePanelModal/index.tsx
index d7daaeb4eef7..467da113af98 100644
--- a/src/components/SidePanel/SidePanelModal/index.tsx
+++ b/src/components/SidePanel/SidePanelModal/index.tsx
@@ -54,7 +54,7 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi
return () => {
ComposerFocusManager.setReadyToFocus(uniqueModalId);
};
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/components/SidePanel/Concierge/index.tsx b/src/components/SidePanel/SidePanelReport/index.tsx
similarity index 61%
rename from src/components/SidePanel/Concierge/index.tsx
rename to src/components/SidePanel/SidePanelReport/index.tsx
index d9fc9eb8f7b7..ba6e527107ac 100644
--- a/src/components/SidePanel/Concierge/index.tsx
+++ b/src/components/SidePanel/SidePanelReport/index.tsx
@@ -1,22 +1,17 @@
import {NavigationRouteContext} from '@react-navigation/native';
import React from 'react';
-import useOnyx from '@hooks/useOnyx';
import type {ExtraContentProps, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
import ReportScreen from '@pages/home/ReportScreen';
-import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
-const CONCIERGE_REPORT_KEY = 'Report-Concierge-Key';
+type SidePanelReportProps = Pick & {
+ reportID: string;
+};
-function Concierge({navigation}: Pick) {
- const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
+function SidePanelReport({navigation, reportID}: SidePanelReportProps) {
// eslint-disable-next-line react/jsx-no-constructed-context-values
- const route = !!conciergeReportID && ({name: SCREENS.REPORT, params: {reportID: conciergeReportID}, key: CONCIERGE_REPORT_KEY} as const);
-
- if (!route) {
- return null;
- }
+ const route = {name: SCREENS.REPORT, params: {reportID}, key: `Report-SidePanel-${reportID}`} as const;
return (
@@ -29,4 +24,4 @@ function Concierge({navigation}: Pick) {
);
}
-export default Concierge;
+export default SidePanelReport;
diff --git a/src/components/SidePanel/index.tsx b/src/components/SidePanel/index.tsx
index 168a2035999f..677f3c9e6686 100644
--- a/src/components/SidePanel/index.tsx
+++ b/src/components/SidePanel/index.tsx
@@ -1,12 +1,12 @@
import React from 'react';
import useSidePanel from '@hooks/useSidePanel';
import type {ExtraContentProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import Concierge from './Concierge';
import SidePanelModal from './SidePanelModal';
+import SidePanelReport from './SidePanelReport';
import useSyncSidePanelWithHistory from './useSyncSidePanelWithHistory';
function SidePanel({navigation}: Pick) {
- const {sidePanelNVP, isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel} = useSidePanel();
+ const {sidePanelNVP, isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel, reportID} = useSidePanel();
// Hide side panel once animation ends
// This hook synchronizes the side panel visibility with the browser history when it is displayed as RHP.
@@ -15,7 +15,7 @@ function SidePanel({navigation}: Pick) {
useSyncSidePanelWithHistory();
// Side panel can't be displayed if NVP is undefined
- if (!sidePanelNVP) {
+ if (!sidePanelNVP || !reportID) {
return null;
}
@@ -30,7 +30,10 @@ function SidePanel({navigation}: Pick) {
closeSidePanel={closeSidePanel}
shouldHideSidePanelBackdrop={shouldHideSidePanelBackdrop}
>
-
+
);
}
diff --git a/src/components/SidePanel/isSidePanelReportSupported/index.native.ts b/src/components/SidePanel/isSidePanelReportSupported/index.native.ts
new file mode 100644
index 000000000000..dde4cf7b9bc8
--- /dev/null
+++ b/src/components/SidePanel/isSidePanelReportSupported/index.native.ts
@@ -0,0 +1,5 @@
+import type IsSidePanelReportSupported from './types';
+
+const isSidePanelReportSupported: IsSidePanelReportSupported = false;
+
+export default isSidePanelReportSupported;
diff --git a/src/components/SidePanel/isSidePanelReportSupported/index.ts b/src/components/SidePanel/isSidePanelReportSupported/index.ts
new file mode 100644
index 000000000000..5a807f44d232
--- /dev/null
+++ b/src/components/SidePanel/isSidePanelReportSupported/index.ts
@@ -0,0 +1,5 @@
+import type IsSidePanelReportSupported from './types';
+
+const isSidePanelReportSupported: IsSidePanelReportSupported = true;
+
+export default isSidePanelReportSupported;
diff --git a/src/components/SidePanel/isSidePanelReportSupported/types.ts b/src/components/SidePanel/isSidePanelReportSupported/types.ts
new file mode 100644
index 000000000000..c037d7d6a756
--- /dev/null
+++ b/src/components/SidePanel/isSidePanelReportSupported/types.ts
@@ -0,0 +1,3 @@
+type IsSidePanelReportSupported = boolean;
+
+export default IsSidePanelReportSupported;
diff --git a/src/components/SpacerView.tsx b/src/components/SpacerView.tsx
index 6869de3a2809..eeb90fa9294f 100644
--- a/src/components/SpacerView.tsx
+++ b/src/components/SpacerView.tsx
@@ -38,7 +38,7 @@ function SpacerView({shouldShow, style}: SpacerViewProps) {
marginVertical.set(values.marginVertical);
borderBottomWidth.set(values.borderBottomWidth);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we only need to trigger when shouldShow prop is changed
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we only need to trigger when shouldShow prop is changed
}, [shouldShow, prevShouldShow]);
return ;
diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx
index da5547d9c1c8..77d1e56ca4e3 100644
--- a/src/components/StateSelector.tsx
+++ b/src/components/StateSelector.tsx
@@ -71,7 +71,7 @@ function StateSelector({errorText, onBlur, value: stateCode, label, onInputChang
// This helps prevent issues where the component might not update correctly if the state is controlled by both the parent and the URL.
Navigation.setParams({state: undefined});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [stateFromUrl, onBlur, isFocused]);
const title = stateCode && stateCode in COMMON_CONST.STATES ? translate(`allStates.${stateCode}.stateName`) : '';
diff --git a/src/components/SwipeableView/index.native.tsx b/src/components/SwipeableView/index.native.tsx
index 5315a83bb60b..d92f3302362f 100644
--- a/src/components/SwipeableView/index.native.tsx
+++ b/src/components/SwipeableView/index.native.tsx
@@ -7,7 +7,6 @@ function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
const minimumPixelDistance = CONST.COMPOSER_MAX_HEIGHT;
const oldYRef = useRef(0);
const panResponder = useRef(
- // eslint-disable-next-line react-compiler/react-compiler
PanResponder.create({
// The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
@@ -23,7 +22,7 @@ function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
}),
).current;
- // eslint-disable-next-line react/jsx-props-no-spreading, react-compiler/react-compiler
+ // eslint-disable-next-line react/jsx-props-no-spreading
return {children} ;
}
diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx
index 53364fafb1e3..22d36d92ae64 100644
--- a/src/components/TabSelector/TabSelector.tsx
+++ b/src/components/TabSelector/TabSelector.tsx
@@ -37,7 +37,22 @@ type IconTitleAndTestID = {
testID?: string;
};
-const MEMOIZED_LAZY_TAB_SELECTOR_ICONS = ['CalendarSolid', 'UploadAlt', 'User', 'Car', 'Hashtag', 'Map', 'Pencil', 'ReceiptScan', 'Receipt', 'MoneyCircle', 'Percent', 'Crosshair'] as const;
+const MEMOIZED_LAZY_TAB_SELECTOR_ICONS = [
+ 'CalendarSolid',
+ 'UploadAlt',
+ 'User',
+ 'Car',
+ 'Hashtag',
+ 'Map',
+ 'Pencil',
+ 'ReceiptScan',
+ 'Receipt',
+ 'MoneyCircle',
+ 'Percent',
+ 'Crosshair',
+ 'Meter',
+ 'Clock',
+] as const;
function getIconTitleAndTestID(
icons: Record, IconAsset>,
@@ -73,12 +88,16 @@ function getIconTitleAndTestID(
return {icon: icons.Pencil, title: translate('tabSelector.manual'), testID: 'distanceManual'};
case CONST.TAB_REQUEST.DISTANCE_GPS:
return {icon: icons.Crosshair, title: translate('tabSelector.gps'), testID: 'distanceGPS'};
+ case CONST.TAB_REQUEST.DISTANCE_ODOMETER:
+ return {icon: icons.Meter, title: translate('tabSelector.odometer'), testID: 'distanceOdometer'};
case CONST.TAB.SPLIT.AMOUNT:
return {icon: icons.MoneyCircle, title: translate('iou.amount'), testID: 'split-amount'};
case CONST.TAB.SPLIT.PERCENTAGE:
return {icon: icons.Percent, title: translate('iou.percent'), testID: 'split-percentage'};
case CONST.TAB.SPLIT.DATE:
return {icon: icons.CalendarSolid, title: translate('iou.date'), testID: 'split-date'};
+ case CONST.TAB_REQUEST.TIME:
+ return {icon: icons.Clock, title: translate('iou.time'), testID: 'time'};
default:
throw new Error(`Route ${route} has no icon nor title set.`);
}
diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx
index d79957d9085a..3e88e5ce281c 100644
--- a/src/components/Table/Table.tsx
+++ b/src/components/Table/Table.tsx
@@ -175,18 +175,35 @@ function Table;
});
+ const originalDataLength = data?.length ?? 0;
+
+ // Check if filters are applied (not default values)
+ const hasActiveFilters = filters
+ ? (Object.keys(currentFilters) as FilterKey[]).some((key) => {
+ const filterValue = currentFilters[key];
+ const defaultValue = filters[key]?.default;
+ return filterValue !== defaultValue;
+ })
+ : false;
+
+ const hasSearchString = activeSearchString.trim().length > 0;
+ const isEmptyResult = processedData.length === 0 && originalDataLength > 0 && (hasSearchString || hasActiveFilters);
+
// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue: TableContextValue = {
listRef,
listProps,
processedData,
- originalDataLength: data?.length ?? 0,
+ originalDataLength,
columns,
filterConfig: filters,
activeFilters: currentFilters,
activeSorting,
activeSearchString,
tableMethods,
+ hasActiveFilters,
+ hasSearchString,
+ isEmptyResult,
};
return }>{children} ;
diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx
index 893c6624107f..7db8400bd37f 100644
--- a/src/components/Table/TableBody.tsx
+++ b/src/components/Table/TableBody.tsx
@@ -46,21 +46,9 @@ type TableBodyProps = ViewProps & {
function TableBody({contentContainerStyle, ...props}: TableBodyProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {processedData: filteredAndSortedData, originalDataLength, activeSearchString, activeFilters, filterConfig, listProps} = useTableContext();
+ const {processedData: filteredAndSortedData, activeSearchString, listProps, hasActiveFilters, hasSearchString, isEmptyResult} = useTableContext();
const {ListEmptyComponent, contentContainerStyle: listContentContainerStyle, ...restListProps} = listProps ?? {};
- // Check if filters are applied (not default values)
- const hasActiveFilters = filterConfig
- ? Object.keys(activeFilters).some((key) => {
- const filterValue = activeFilters[key];
- const defaultValue = filterConfig?.[key]?.default;
- return filterValue !== defaultValue;
- })
- : false;
-
- const hasSearchString = activeSearchString.trim().length > 0;
- const isEmptyResult = filteredAndSortedData.length === 0 && originalDataLength > 0 && (hasSearchString || hasActiveFilters);
-
// Determine the message based on what caused the empty result
const getEmptyMessage = () => {
if (hasSearchString) {
diff --git a/src/components/Table/TableContext.tsx b/src/components/Table/TableContext.tsx
index e03127714067..136b21f43413 100644
--- a/src/components/Table/TableContext.tsx
+++ b/src/components/Table/TableContext.tsx
@@ -41,6 +41,15 @@ type TableContextValue;
+
+ /** Whether any filters differ from their default values. */
+ hasActiveFilters: boolean;
+
+ /** Whether search string is not empty. */
+ hasSearchString: boolean;
+
+ /** Whether the table has an empty result caused by search or filters. */
+ isEmptyResult: boolean;
};
const defaultTableContextValue: TableContextValue = {
@@ -57,6 +66,9 @@ const defaultTableContextValue: TableContextValue = {
tableMethods: {} as TableMethods,
filterConfig: undefined,
listProps: {} as SharedListProps,
+ hasActiveFilters: false,
+ hasSearchString: false,
+ isEmptyResult: false,
};
const TableContext = createContext(defaultTableContextValue);
diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx
index f105898a3379..093a9000ea64 100644
--- a/src/components/Table/TableHeader.tsx
+++ b/src/components/Table/TableHeader.tsx
@@ -20,7 +20,10 @@ const NUMBER_OF_TOGGLES_BEFORE_RESET = 2;
/**
* Props for the TableHeader component.
*/
-type TableHeaderProps = ViewProps;
+type TableHeaderProps = ViewProps & {
+ /** Hide table header when search returns no results. */
+ shouldHideHeaderWhenEmptySearch?: boolean;
+};
/**
* Renders the table header row with sortable column headers.
@@ -45,9 +48,13 @@ type TableHeaderProps = ViewProps;
*
* ```
*/
-function TableHeader({style, ...props}: TableHeaderProps) {
+function TableHeader({style, shouldHideHeaderWhenEmptySearch = true, ...props}: TableHeaderProps) {
const styles = useThemeStyles();
- const {columns} = useTableContext();
+ const {columns, isEmptyResult} = useTableContext();
+
+ if (shouldHideHeaderWhenEmptySearch && isEmptyResult) {
+ return null;
+ }
return (
{
if (!isAdminRoom(onboardingReport)) {
diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index cb8072f2c22b..d31450a02250 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import useIsAuthenticated from '@hooks/useIsAuthenticated';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
+import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports';
import useThemeStyles from '@hooks/useThemeStyles';
import {isUsingStagingApi} from '@libs/ApiUtils';
import {setShouldFailAllRequests, setShouldForceOffline, setShouldSimulatePoorConnection} from '@userActions/Network';
@@ -23,6 +24,7 @@ function TestToolMenu() {
const [isDebugModeEnabled = false] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED, {canBeMissing: true});
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {clearLHNCache} = useSidebarOrderedReports();
// Check if the user is authenticated to show options that require authentication
const isAuthenticated = useIsAuthenticated();
@@ -72,6 +74,15 @@ function TestToolMenu() {
onPress={() => expireSessionWithDelay()}
/>
+
+ {/* Clears the useSidebarOrderedReports cache to re-compute from latest onyx values */}
+
+
+
>
)}
diff --git a/src/components/TextInput/BaseTextInput/implementation/index.tsx b/src/components/TextInput/BaseTextInput/implementation/index.tsx
index 8b3c0959c6f8..91e097dffd3c 100644
--- a/src/components/TextInput/BaseTextInput/implementation/index.tsx
+++ b/src/components/TextInput/BaseTextInput/implementation/index.tsx
@@ -129,7 +129,7 @@ function BaseTextInput({
input.current.focus();
// We only want this to run on mount
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const animateLabel = useCallback(
diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx
index 043aa87fc6bb..b1d13979e43f 100644
--- a/src/components/TextInput/TextInputLabel/index.tsx
+++ b/src/components/TextInput/TextInputLabel/index.tsx
@@ -16,7 +16,7 @@ function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale,
return;
}
labelRef.current.setAttribute('for', inputId);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const animatedStyle = useAnimatedStyle(() => styles.textInputLabelTransformation(labelTranslateY, labelScale));
@@ -25,7 +25,6 @@ function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale,
{
diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx
index 348ae788e13d..5fd01f2b5f50 100644
--- a/src/components/ThemeProvider.tsx
+++ b/src/components/ThemeProvider.tsx
@@ -32,7 +32,7 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr
DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input');
// staticThemePreference as it is a property that does not change we don't need it in the dependency array
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme.text]);
return {children} ;
diff --git a/src/components/TimePicker/TimePicker.tsx b/src/components/TimePicker/TimePicker.tsx
index 04d59222e5b7..9ebdcbe9205f 100644
--- a/src/components/TimePicker/TimePicker.tsx
+++ b/src/components/TimePicker/TimePicker.tsx
@@ -616,7 +616,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}, shou
handleMillisecondsChange(insertAtPosition(milliseconds, trimmedKey, selectionMillisecond.start, selectionMillisecond.end));
}
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[minutes, hours, seconds, milliseconds, selectionMinute, selectionHour, selectionSecond, selectionMillisecond],
);
@@ -651,7 +651,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}, shou
focusSecondInputOnLastCharacter();
}
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[selectionHour, selectionMinute.start],
);
const arrowRightCallback = useCallback(
@@ -672,7 +672,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}, shou
focusMillisecondInputOnFirstCharacter();
}
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[selectionHour.start, selectionMinute.start, selectionSecond.start, selectionMillisecond],
);
@@ -697,7 +697,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}, shou
focusSecondInputOnLastCharacter();
}
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[
selectionMinute.start,
selectionMinute.end,
@@ -728,7 +728,7 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}, shou
useEffect(() => {
onInputChange(showFullFormat ? `${hours}:${minutes}:${seconds}.${milliseconds} ${amPmValue}` : `${hours}:${minutes} ${amPmValue}`);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [hours, minutes, amPmValue]);
const handleSubmit = () => {
@@ -804,7 +804,6 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}, shou
onChangeAmount={handleHourChange}
ref={(textInputRef) => {
updateRefs('hourRef', textInputRef);
- // eslint-disable-next-line react-compiler/react-compiler
hourInputRef.current = textInputRef as TextInput | null;
}}
onSelectionChange={(e) => {
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
index eba5d7274af2..de40deed1f6c 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
@@ -57,7 +57,6 @@ function BaseGenericTooltip({
const {rootWrapperStyle, textStyle, pointerWrapperStyle, pointerStyle} = useMemo(
() =>
StyleUtils.getTooltipStyles({
- // eslint-disable-next-line react-compiler/react-compiler
tooltip: rootWrapper.current,
windowWidth,
xOffset,
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx
index ec212dfee5de..41341d7dbf2c 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react-compiler/react-compiler */
import React, {useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import {View} from 'react-native';
diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
index 159062362cd3..87799a7ec53c 100644
--- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
+++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx
@@ -159,7 +159,6 @@ function BaseEducationalTooltip({children, shouldRender = false, shouldHideOnNav
>
{(genericTooltipState) => {
const {updateTargetBounds, showTooltip} = genericTooltipState;
- // eslint-disable-next-line react-compiler/react-compiler
genericTooltipStateRef.current = genericTooltipState;
return React.cloneElement(children as React.ReactElement<{onLayout?: (e: LayoutChangeEventWithTarget) => void}>, {
onLayout: (e: LayoutChangeEventWithTarget) => {
diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx
index e61bee9fb07e..34f31d768ee7 100644
--- a/src/components/Tooltip/GenericTooltip.tsx
+++ b/src/components/Tooltip/GenericTooltip.tsx
@@ -138,7 +138,6 @@ function GenericTooltip({
cancelAnimation(animation);
if (TooltipSense.isActive() && !isTooltipSenseInitiator.get()) {
- // eslint-disable-next-line react-compiler/react-compiler
animation.set(0);
} else {
// Hide the first tooltip which initiated the TooltipSense with animation
@@ -159,7 +158,6 @@ function GenericTooltip({
// Skip the tooltip and return the children if the text is empty, we don't have a render function.
if (StringUtils.isEmptyString(text) && renderTooltipContent == null) {
- // eslint-disable-next-line react-compiler/react-compiler
return children({isVisible, showTooltip, hideTooltip, updateTargetBounds});
}
@@ -168,7 +166,6 @@ function GenericTooltip({
{shouldRender && isRendered && (
)}
- {/* eslint-disable-next-line react-compiler/react-compiler */}
{children({isVisible, showTooltip, hideTooltip, updateTargetBounds})}
>
);
diff --git a/src/components/Tooltip/PopoverAnchorTooltip.tsx b/src/components/Tooltip/PopoverAnchorTooltip.tsx
index cff87e48323b..5768fbdeaa2f 100644
--- a/src/components/Tooltip/PopoverAnchorTooltip.tsx
+++ b/src/components/Tooltip/PopoverAnchorTooltip.tsx
@@ -9,7 +9,7 @@ function PopoverAnchorTooltip({shouldRender = true, children, ...props}: Tooltip
const tooltipRef = useRef(null);
const isPopoverRelatedToTooltipOpen = useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/dot-notation, react-compiler/react-compiler
+ // eslint-disable-next-line @typescript-eslint/dot-notation
const tooltipNode = (tooltipRef.current?.['_childNode'] as Node | undefined) ?? null;
if (isOpen && popoverAnchor && tooltipNode && ((popoverAnchor instanceof Node && tooltipNode.contains(popoverAnchor)) || tooltipNode === popoverAnchor)) {
diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx
index 31b49735fa4d..14cf550fac18 100644
--- a/src/components/TransactionItemRow/index.tsx
+++ b/src/components/TransactionItemRow/index.tsx
@@ -33,6 +33,7 @@ import {
getMerchant,
getOriginalAmountForDisplay,
getOriginalCurrencyForDisplay,
+ getReimbursable,
getTaxName,
getCreated as getTransactionCreated,
hasMissingSmartscanFields,
@@ -144,7 +145,7 @@ function getMerchantName(transactionItem: TransactionWithOptionalSearchFields, t
}
const merchantName = StringUtils.getFirstLine(merchant);
- return merchantName !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchantName !== CONST.TRANSACTION.DEFAULT_MERCHANT ? merchantName : '';
+ return merchantName !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? merchantName : '';
}
function TransactionItemRow({
@@ -364,7 +365,7 @@ function TransactionItemRow({
key={CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE}
style={[StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE)]}
>
- {transactionItem.reimbursable ? translate('common.yes') : translate('common.no')}
+ {getReimbursable(transactionItem) ? translate('common.yes') : translate('common.no')}
),
[CONST.SEARCH.TABLE_COLUMNS.BILLABLE]: (
diff --git a/src/components/ValidateCodeActionForm/index.tsx b/src/components/ValidateCodeActionForm/index.tsx
index a717934632c0..bd179370cf8a 100644
--- a/src/components/ValidateCodeActionForm/index.tsx
+++ b/src/components/ValidateCodeActionForm/index.tsx
@@ -32,7 +32,7 @@ function ValidateCodeActionForm({
return () => {
isUnmounted.current = true;
};
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldSkipInitialValidation]);
useEffect(() => {
diff --git a/src/components/ValidateCodeActionModal/ValidateCodeActionContent.tsx b/src/components/ValidateCodeActionModal/ValidateCodeActionContent.tsx
index b81bf98dc2a1..df7918866293 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeActionContent.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeActionContent.tsx
@@ -40,7 +40,6 @@ function ValidateCodeActionContent({
sendValidateCode();
// We only want to send validate code on first render not on change of validateCodeSent, so we don't add it as a dependency.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sendValidateCode]);
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index 2199e7e64764..828f62b61887 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -204,7 +204,6 @@ function BaseVideoPlayer({
if (videoResumeTryNumberRef.current === 1) {
playVideo();
}
- // eslint-disable-next-line react-compiler/react-compiler
videoResumeTryNumberRef.current -= 1;
},
[playVideo, videoResumeTryNumberRef],
@@ -263,7 +262,7 @@ function BaseVideoPlayer({
videoStateRef.current = status;
onPlaybackStatusUpdate?.(status);
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this when isPlaying changes because isPlaying is only used inside shouldReplayVideo
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to trigger this when isPlaying changes because isPlaying is only used inside shouldReplayVideo
[onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration, isEnded],
);
diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/index.tsx
index 9d74d0af0396..aaac27553d92 100644
--- a/src/components/VideoPlayer/VideoPlayerControls/index.tsx
+++ b/src/components/VideoPlayer/VideoPlayerControls/index.tsx
@@ -77,7 +77,6 @@ function VideoPlayerControls({
};
const enterFullScreenMode = useCallback(() => {
- // eslint-disable-next-line react-compiler/react-compiler
isFullScreenRef.current = true;
updateCurrentURLAndReportID(url, reportID);
videoPlayerRef.current?.presentFullscreenPlayer();
diff --git a/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts b/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts
index 1c682ef73f71..202937988d66 100644
--- a/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts
+++ b/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts
@@ -20,7 +20,7 @@ const useHandleNativeVideoControls: UseHandleNativeVideoControl = ({videoPlayerR
} else {
videoElement.removeAttribute('controlsList');
}
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOffline, isLocalFile]);
};
diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
index cd273167a74c..15833fbcc98c 100644
--- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
+++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx
@@ -42,7 +42,6 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
const items: PopoverMenuItem[] = [];
if (!isOffline && !isLocalFile) {
- // eslint-disable-next-line react-compiler/react-compiler
items.push({
icon: icons.Download,
text: translate('common.download'),
diff --git a/src/components/WideRHPContextProvider/default.ts b/src/components/WideRHPContextProvider/default.ts
index 08b70c3a08f1..7add883bb4a1 100644
--- a/src/components/WideRHPContextProvider/default.ts
+++ b/src/components/WideRHPContextProvider/default.ts
@@ -13,6 +13,7 @@ const defaultWideRHPContextValue: WideRHPContextType = {
isReportIDMarkedAsExpense: () => false,
isReportIDMarkedAsMultiTransactionExpense: () => false,
isWideRHPFocused: false,
+ isSuperWideRHPFocused: false,
shouldRenderTertiaryOverlay: false,
superWideRHPRouteKeys: [],
showSuperWideRHPVersion: () => {},
diff --git a/src/components/WideRHPContextProvider/index.tsx b/src/components/WideRHPContextProvider/index.tsx
index 47d71499f2fd..941c3d20b6df 100644
--- a/src/components/WideRHPContextProvider/index.tsx
+++ b/src/components/WideRHPContextProvider/index.tsx
@@ -81,6 +81,20 @@ function removeWideRHPRoute(route: NavigationRoute, setAllRHPRouteKeys: React.Di
setAllRHPRouteKeys((prev) => (prev.includes(keyToRemove) ? prev.filter((key) => key !== keyToRemove) : prev));
}
+// Set the rhp width based on the super wide / wide rhp route keys
+function setExpandedRHPProgress(superWideRHPRouteKeys: string[], wideRHPRouteKeys: string[]) {
+ const numberOfSuperWideRoutes = superWideRHPRouteKeys.length;
+ const numberOfWideRoutes = wideRHPRouteKeys.length;
+
+ if (numberOfSuperWideRoutes > 0) {
+ expandedRHPProgress.setValue(2);
+ } else if (numberOfWideRoutes > 0) {
+ expandedRHPProgress.setValue(1);
+ } else {
+ expandedRHPProgress.setValue(0);
+ }
+}
+
function WideRHPContextProvider({children}: React.PropsWithChildren) {
// We have a separate containers for allWideRHPRouteKeys and wideRHPRouteKeys because we may have two or more RHPs on the stack.
// For convenience and proper overlay logic wideRHPRouteKeys will show only the keys existing in the last RHP.
@@ -112,6 +126,10 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
return !!focusedRoute?.key && allWideRHPRouteKeys.includes(focusedRoute.key);
}, [focusedRoute?.key, allWideRHPRouteKeys]);
+ const isSuperWideRHPFocused = useMemo(() => {
+ return !!focusedRoute?.key && allSuperWideRHPRouteKeys.includes(focusedRoute.key);
+ }, [focusedRoute?.key, allSuperWideRHPRouteKeys]);
+
const isRHPFocused = focusedNavigator === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
// Whether Wide RHP is displayed below the currently displayed screen
@@ -122,11 +140,13 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
const {visibleSuperWideRHPRouteKeys, visibleWideRHPRouteKeys} = getVisibleRHPKeys(allSuperWideRHPRouteKeys, allWideRHPRouteKeys);
setWideRHPRouteKeys(visibleWideRHPRouteKeys);
setSuperWideRHPRouteKeys(visibleSuperWideRHPRouteKeys);
+ setExpandedRHPProgress(visibleSuperWideRHPRouteKeys, visibleWideRHPRouteKeys);
}, [allSuperWideRHPRouteKeys, allWideRHPRouteKeys]);
const clearWideRHPKeys = useCallback(() => {
setWideRHPRouteKeys([]);
setSuperWideRHPRouteKeys([]);
+ expandedRHPProgress.setValue(0);
}, []);
// Once we have updated the array of all Super Wide RHP keys, we should sync it with the array of RHP keys visible on the screen
@@ -157,21 +177,6 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
*/
const shouldRenderTertiaryOverlay = useShouldRenderOverlay(isRHPFocused && isWideRHPBelow && isSuperWideRHPBelow, thirdOverlayProgress);
- /**
- * Effect that shows/hides the expanded RHP progress based on the number of wide RHP routes.
- */
- useEffect(() => {
- const numberOfSuperWideRoutes = superWideRHPRouteKeys.length;
- const numberOfWideRoutes = wideRHPRouteKeys.length;
- if (numberOfSuperWideRoutes > 0) {
- expandedRHPProgress.setValue(2);
- } else if (numberOfWideRoutes > 0) {
- expandedRHPProgress.setValue(1);
- } else {
- expandedRHPProgress.setValue(0);
- }
- }, [superWideRHPRouteKeys.length, wideRHPRouteKeys.length]);
-
/**
* Removes a route from the super wide RHP route keys list, disabling wide RHP display for that route.
*/
@@ -325,6 +330,7 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
isReportIDMarkedAsExpense,
isReportIDMarkedAsMultiTransactionExpense,
isWideRHPFocused,
+ isSuperWideRHPFocused,
syncRHPKeys,
clearWideRHPKeys,
}),
@@ -345,6 +351,7 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
isReportIDMarkedAsExpense,
isReportIDMarkedAsMultiTransactionExpense,
isWideRHPFocused,
+ isSuperWideRHPFocused,
syncRHPKeys,
clearWideRHPKeys,
],
diff --git a/src/components/WideRHPContextProvider/types.ts b/src/components/WideRHPContextProvider/types.ts
index db6c01379cb0..8ffb8e7dca87 100644
--- a/src/components/WideRHPContextProvider/types.ts
+++ b/src/components/WideRHPContextProvider/types.ts
@@ -49,6 +49,9 @@ type WideRHPContextType = {
// Whether the currently focused route is inside the wide RHP set
isWideRHPFocused: boolean;
+ // Whether the currently focused route is inside the super wide RHP set
+ isSuperWideRHPFocused: boolean;
+
// Sync super wide and wide RHP keys with the visible RHP screens
syncRHPKeys: () => void;
diff --git a/src/components/WideRHPContextProvider/useShowSuperWideRHPVersion/index.ts b/src/components/WideRHPContextProvider/useShowSuperWideRHPVersion/index.ts
index f2b7301f36d0..2b5853f7c613 100644
--- a/src/components/WideRHPContextProvider/useShowSuperWideRHPVersion/index.ts
+++ b/src/components/WideRHPContextProvider/useShowSuperWideRHPVersion/index.ts
@@ -1,8 +1,8 @@
import {useRoute} from '@react-navigation/native';
import {useCallback, useContext, useEffect} from 'react';
-import {InteractionManager} from 'react-native';
-import useBeforeRemove from '@hooks/useBeforeRemove';
-import {WideRHPContext} from '..';
+import {navigationRef} from '@libs/Navigation/Navigation';
+import NAVIGATORS from '@src/NAVIGATORS';
+import {expandedRHPProgress, WideRHPContext} from '..';
/**
* Hook that manages super wide RHP display for a screen based on condition or optimistic state.
@@ -25,14 +25,18 @@ function useShowSuperWideRHPVersion(condition: boolean) {
} = useContext(WideRHPContext);
const onSuperWideRHPClose = useCallback(() => {
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
- removeWideRHPRouteKey(route);
- removeSuperWideRHPRouteKey(route);
- });
+ removeWideRHPRouteKey(route);
+ removeSuperWideRHPRouteKey(route);
+ // When the RHP has been closed, expandedRHPProgress should be set to 0.
+ if (navigationRef?.getRootState()?.routes?.at(-1)?.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ expandedRHPProgress.setValue(0);
+ }
}, [removeSuperWideRHPRouteKey, removeWideRHPRouteKey, route]);
- useBeforeRemove(onSuperWideRHPClose);
+ /**
+ * Effect that sets up cleanup when the screen is unmounted.
+ */
+ useEffect(() => () => onSuperWideRHPClose(), [onSuperWideRHPClose]);
/**
* Effect that determines whether to show wide RHP based on condition or optimistic state.
diff --git a/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts b/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts
index 27a6ce16fdad..4adf2fef8f4b 100644
--- a/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts
+++ b/src/components/WideRHPContextProvider/useShowWideRHPVersion/index.ts
@@ -1,8 +1,8 @@
import {useRoute} from '@react-navigation/native';
import {useCallback, useContext, useEffect} from 'react';
-import {InteractionManager} from 'react-native';
-import useBeforeRemove from '@hooks/useBeforeRemove';
-import {WideRHPContext} from '..';
+import {navigationRef} from '@libs/Navigation/Navigation';
+import NAVIGATORS from '@src/NAVIGATORS';
+import {expandedRHPProgress, WideRHPContext} from '..';
/**
* Hook that manages wide RHP display for a screen based on condition or optimistic state.
@@ -16,22 +16,18 @@ function useShowWideRHPVersion(condition: boolean) {
const reportID = route.params && 'reportID' in route.params && typeof route.params.reportID === 'string' ? route.params.reportID : '';
const {showWideRHPVersion, removeWideRHPRouteKey, isReportIDMarkedAsExpense} = useContext(WideRHPContext);
- // beforeRemove event is not called when closing nested Wide RHP using the browser back button.
- // This hook removes the route key from the array in the following case.
- useEffect(() => () => removeWideRHPRouteKey(route), [removeWideRHPRouteKey, route]);
-
const onWideRHPClose = useCallback(() => {
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
- removeWideRHPRouteKey(route);
- });
+ removeWideRHPRouteKey(route);
+ // When the RHP has been closed, expandedRHPProgress should be set to 0.
+ if (navigationRef?.getRootState()?.routes?.at(-1)?.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ expandedRHPProgress.setValue(0);
+ }
}, [removeWideRHPRouteKey, route]);
/**
- * Effect that sets up cleanup when the screen is about to be removed.
- * Uses InteractionManager to ensure cleanup happens after closing animation.
+ * Effect that sets up cleanup when the screen is unmounted.
*/
- useBeforeRemove(onWideRHPClose);
+ useEffect(() => () => onWideRHPClose(), [onWideRHPClose]);
/**
* Effect that determines whether to show wide RHP based on condition or optimistic state.
diff --git a/src/components/WideRHPOverlayWrapper/index.tsx b/src/components/WideRHPOverlayWrapper/index.tsx
index e1ffa54fb33b..f70c91495f9c 100644
--- a/src/components/WideRHPOverlayWrapper/index.tsx
+++ b/src/components/WideRHPOverlayWrapper/index.tsx
@@ -1,5 +1,5 @@
-import {useFocusEffect, useRoute} from '@react-navigation/native';
-import React, {useCallback, useContext} from 'react';
+import {useRoute} from '@react-navigation/native';
+import React, {useContext} from 'react';
import {
animatedReceiptPaneRHPWidth,
modalStackOverlaySuperWideRHPPositionLeft,
@@ -7,12 +7,10 @@ import {
secondOverlayRHPOnSuperWideRHPProgress,
secondOverlayRHPOnWideRHPProgress,
secondOverlayWideRHPProgress,
- thirdOverlayProgress,
WideRHPContext,
} from '@components/WideRHPContextProvider';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay';
-import {navigationRef} from '@libs/Navigation/Navigation';
-import NAVIGATORS from '@src/NAVIGATORS';
function SecondaryOverlay() {
const {shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys, wideRHPRouteKeys} =
@@ -70,26 +68,6 @@ function SecondaryOverlay() {
return null;
}
-function TertiaryOverlay() {
- const {shouldRenderTertiaryOverlay, wideRHPRouteKeys} = useContext(WideRHPContext);
- const route = useRoute();
-
- const isWide = route?.key && wideRHPRouteKeys.includes(route.key);
-
- // This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen
- // There is a special case where three different RHP widths are displayed at the same time. In this case, an overlay under RHP should be rendered from Wide RHP.
- if (isWide && shouldRenderTertiaryOverlay) {
- return (
-
- );
- }
-
- return null;
-}
-
type WideRHPOverlayWrapperProps = {
children: React.ReactNode;
shouldWrap?: boolean;
@@ -97,30 +75,11 @@ type WideRHPOverlayWrapperProps = {
// This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen.
export default function WideRHPOverlayWrapper({children, shouldWrap = true}: WideRHPOverlayWrapperProps) {
- const {syncRHPKeys} = useContext(WideRHPContext);
-
- // This hook handles the case when a wider RHP is displayed above a narrower one.
- // In this situation, we need to synchronize the keys, as superWideRHPKeys and wideRHPKeys store the keys of the screens that are visible.
- useFocusEffect(
- useCallback(
- () => () => {
- if (!shouldWrap) {
- return;
- }
-
- // Synchronization after RHP unmount is handled in RightModalNavigator.tsx.
- const isRHPOpened = navigationRef?.getRootState()?.routes?.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
- if (!isRHPOpened) {
- return;
- }
-
- syncRHPKeys();
- },
- [shouldWrap, syncRHPKeys],
- ),
- );
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
+ const shouldUseOverlayWrapper = !isSmallScreenWidth && shouldWrap;
- if (!shouldWrap) {
+ if (!shouldUseOverlayWrapper) {
return children;
}
@@ -128,7 +87,6 @@ export default function WideRHPOverlayWrapper({children, shouldWrap = true}: Wid
<>
{children}
-
>
);
}
diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx
index 299e47e64f1d..e65a1b9da2f9 100644
--- a/src/components/WorkspaceConfirmationForm.tsx
+++ b/src/components/WorkspaceConfirmationForm.tsx
@@ -130,7 +130,6 @@ function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButto
{
diff --git a/src/components/withNavigationTransitionEnd.tsx b/src/components/withNavigationTransitionEnd.tsx
index ac0350c67c8d..5c45f2b7ad5f 100644
--- a/src/components/withNavigationTransitionEnd.tsx
+++ b/src/components/withNavigationTransitionEnd.tsx
@@ -18,7 +18,7 @@ export default function (WrappedComponent: ComponentType): React
});
return unsubscribeTransitionEnd;
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts
index cbb744e164d9..158b6fa667c0 100644
--- a/src/hooks/useAnimatedHighlightStyle/index.ts
+++ b/src/hooks/useAnimatedHighlightStyle/index.ts
@@ -33,9 +33,6 @@ type Props = {
/** Whether the item should be highlighted */
shouldHighlight: boolean;
- /** Whether it should return height and border radius styles */
- shouldApplyOtherStyles?: boolean;
-
/** The base backgroundColor used for the highlight animation, defaults to theme.appBG
* @default theme.appBG
*/
@@ -66,7 +63,6 @@ export default function useAnimatedHighlightStyle({
height,
highlightColor,
backgroundColor,
- shouldApplyOtherStyles = true,
skipInitialFade = false,
}: Props) {
const [startHighlight, setStartHighlight] = useState(false);
@@ -84,8 +80,9 @@ export default function useAnimatedHighlightStyle({
return {
backgroundColor: interpolateColor(repeatableValue, [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]),
+ height: height ? interpolate(nonRepeatableValue, [0, 1], [0, height]) : 'auto',
opacity: interpolate(nonRepeatableValue, [0, 1], [0, 1]),
- ...(shouldApplyOtherStyles && {height: height ? interpolate(nonRepeatableValue, [0, 1], [0, height]) : 'auto', borderRadius}),
+ borderRadius,
};
}, [borderRadius, height, backgroundColor, highlightColor, theme.appBG, theme.border]);
@@ -97,7 +94,7 @@ export default function useAnimatedHighlightStyle({
// We only need to add shouldHighlight as a dependency and adding startHighlight as deps will cause a loop because
// if shouldHighlight stays at true the above early return will not be executed and this useEffect will be run
// as long as shouldHighlight is true as we set startHighlight to false in the below useEffect.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldHighlight]);
React.useEffect(() => {
diff --git a/src/hooks/useArrowKeyFocusManager.ts b/src/hooks/useArrowKeyFocusManager.ts
index 74877438f0a9..a28eff02d5b8 100644
--- a/src/hooks/useArrowKeyFocusManager.ts
+++ b/src/hooks/useArrowKeyFocusManager.ts
@@ -80,7 +80,7 @@ export default function useArrowKeyFocusManager({
return;
}
onFocusedIndexChange(focusedIndex);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [focusedIndex, prevIsFocusedIndex]);
const arrowUpCallback = useCallback(() => {
diff --git a/src/hooks/useAutoUpdateTimezone.ts b/src/hooks/useAutoUpdateTimezone.ts
index 7961f09b0cd0..4005ac3c369b 100644
--- a/src/hooks/useAutoUpdateTimezone.ts
+++ b/src/hooks/useAutoUpdateTimezone.ts
@@ -19,7 +19,7 @@ const useAutoUpdateTimezone = () => {
currentUserPersonalDetails.accountID,
);
}
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [timezone?.automatic, timezone?.selected]);
};
diff --git a/src/hooks/useBeforeRemove.tsx b/src/hooks/useBeforeRemove.tsx
index 835d5a30babe..908bb659960c 100644
--- a/src/hooks/useBeforeRemove.tsx
+++ b/src/hooks/useBeforeRemove.tsx
@@ -3,13 +3,16 @@ import type {EventListenerCallback, EventMapCore, NavigationState} from '@react-
import {useEffect} from 'react';
// beforeRemove have some limitations. When the react-navigation is upgraded to 7.x, update this to use usePreventRemove hook.
-const useBeforeRemove = (onBeforeRemove: EventListenerCallback, 'beforeRemove'>) => {
+const useBeforeRemove = (onBeforeRemove: EventListenerCallback, 'beforeRemove'>, isEnabled = true) => {
const navigation = useNavigation();
useEffect(() => {
+ if (!isEnabled) {
+ return undefined;
+ }
const unsubscribe = navigation.addListener('beforeRemove', onBeforeRemove);
return unsubscribe;
- }, [navigation, onBeforeRemove]);
+ }, [navigation, onBeforeRemove, isEnabled]);
};
export default useBeforeRemove;
diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts
index 83b102aafad2..d97b8643c620 100644
--- a/src/hooks/useCancellationType.ts
+++ b/src/hooks/useCancellationType.ts
@@ -27,7 +27,6 @@ function useCancellationType(): CancellationType | undefined {
}
// There are no new items in the cancellation details NVP
- // eslint-disable-next-line react-compiler/react-compiler
if (previousCancellationDetails.current?.length === cancellationDetails?.length) {
return;
}
diff --git a/src/hooks/useCheckIfRouteHasRemainedUnchanged.ts b/src/hooks/useCheckIfRouteHasRemainedUnchanged.ts
index 0c6c2bd5794e..a36e6936363f 100644
--- a/src/hooks/useCheckIfRouteHasRemainedUnchanged.ts
+++ b/src/hooks/useCheckIfRouteHasRemainedUnchanged.ts
@@ -59,7 +59,6 @@ function useCheckIfRouteHasRemainedUnchanged(videoUrl: string) {
// Thus, it can be considered as still being on the rendered route.
isOnInitialRenderedRouteRef.current = navigation.isFocused() || route?.name === SCREENS.REPORT_ATTACHMENTS;
});
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/hooks/useCurrentReportID.tsx b/src/hooks/useCurrentReportID.tsx
index 4630749ed79c..74d81be8fb43 100644
--- a/src/hooks/useCurrentReportID.tsx
+++ b/src/hooks/useCurrentReportID.tsx
@@ -51,7 +51,6 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro
props.onSetCurrentReportID?.(reportID);
setCurrentReportID(reportID);
},
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to re-render when onSetCurrentReportID changes
[setCurrentReportID, currentReportID],
);
diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts
index b2914d86907a..a5438785295a 100644
--- a/src/hooks/useDebounce.ts
+++ b/src/hooks/useDebounce.ts
@@ -42,6 +42,5 @@ export default function useDebounce(func: T, wait: nu
}
}, []);
- // eslint-disable-next-line react-compiler/react-compiler
return debounceCallback as T;
}
diff --git a/src/hooks/useDebounceNonReactive.ts b/src/hooks/useDebounceNonReactive.ts
index 2c22658f826c..7464b925e114 100644
--- a/src/hooks/useDebounceNonReactive.ts
+++ b/src/hooks/useDebounceNonReactive.ts
@@ -52,6 +52,5 @@ export default function useDebounceNonReactive(func:
debouncedFnRef.current?.(...args);
}, []);
- // eslint-disable-next-line react-compiler/react-compiler
return debounceCallback as T;
}
diff --git a/src/hooks/useDebouncedState.ts b/src/hooks/useDebouncedState.ts
index 96ea0e37e72f..459dde42652f 100644
--- a/src/hooks/useDebouncedState.ts
+++ b/src/hooks/useDebouncedState.ts
@@ -20,7 +20,6 @@ import CONST from '@src/CONST';
function useDebouncedState(initialValue: T, delay: number = CONST.TIMING.USE_DEBOUNCED_STATE_DELAY): [T, T, (value: T) => void] {
const [value, setValue] = useState(initialValue);
const [debouncedValue, setDebouncedValue] = useState(initialValue);
- // eslint-disable-next-line react-compiler/react-compiler
const debouncedSetDebouncedValue = useRef(debounce(setDebouncedValue, delay)).current;
useEffect(() => () => debouncedSetDebouncedValue.cancel(), [debouncedSetDebouncedValue]);
diff --git a/src/hooks/useDebugShortcut.tsx b/src/hooks/useDebugShortcut.tsx
index 38b2abccce0c..7876d5dbc53f 100644
--- a/src/hooks/useDebugShortcut.tsx
+++ b/src/hooks/useDebugShortcut.tsx
@@ -19,7 +19,7 @@ function useDebugShortcut() {
};
// Rule disabled because this effect is only for component did mount & will component unmount lifecycle event
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}
diff --git a/src/hooks/useDeepCompareRef.ts b/src/hooks/useDeepCompareRef.ts
index 46318ab88675..d5aa85d5398d 100644
--- a/src/hooks/useDeepCompareRef.ts
+++ b/src/hooks/useDeepCompareRef.ts
@@ -17,11 +17,8 @@ import {useRef} from 'react';
*/
export default function useDeepCompareRef(value: T): T | undefined {
const ref = useRef(undefined);
- // eslint-disable-next-line react-compiler/react-compiler
if (!deepEqual(value, ref.current)) {
- // eslint-disable-next-line react-compiler/react-compiler
ref.current = value;
}
- // eslint-disable-next-line react-compiler/react-compiler
return ref.current;
}
diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts
index fde6ef70f855..a45163910231 100644
--- a/src/hooks/useDeleteTransactions.ts
+++ b/src/hooks/useDeleteTransactions.ts
@@ -1,6 +1,7 @@
import {useCallback} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
-import {deleteMoneyRequest, getIOUActionForTransactions, getIOURequestPolicyID, initSplitExpenseItemData, updateSplitTransactions} from '@libs/actions/IOU';
+import {deleteMoneyRequest, getIOURequestPolicyID, initSplitExpenseItemData, updateSplitTransactions} from '@libs/actions/IOU';
+import {getIOUActionForTransactions} from '@libs/actions/IOU/DuplicateAction';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getChildTransactions, getOriginalTransactionWithSplitInfo} from '@libs/TransactionUtils';
@@ -34,6 +35,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES, {canBeMissing: true});
+ const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true});
const {isBetaEnabled} = usePermissions();
const archivedReportsIdSet = useArchivedReportsIdSet();
@@ -146,6 +148,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
currentUserPersonalDetails,
transactionViolations,
policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
+ quickAction,
});
}
@@ -184,15 +187,16 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
allTransactions,
allReports,
report,
- allReportNameValuePairs,
allPolicyRecentlyUsedCategories,
+ allReportNameValuePairs,
policyCategories,
policy,
- archivedReportsIdSet,
isBetaEnabled,
currentUserPersonalDetails,
transactionViolations,
policyRecentlyUsedCurrencies,
+ quickAction,
+ archivedReportsIdSet,
],
);
diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/const.ts b/src/hooks/useDisplayFocusedInputUnderKeyboard/const.ts
deleted file mode 100644
index b5db59275f03..000000000000
--- a/src/hooks/useDisplayFocusedInputUnderKeyboard/const.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-const MARGIN_FROM_INPUT_IOS = 28;
-const MARGIN_FROM_INPUT_ANDROID = 20;
-const FOOTER_BOTTOM_MARGIN = 20;
-
-export {MARGIN_FROM_INPUT_IOS, MARGIN_FROM_INPUT_ANDROID, FOOTER_BOTTOM_MARGIN};
diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts
deleted file mode 100644
index 288ff88d8810..000000000000
--- a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import {useRef} from 'react';
-import type {View} from 'react-native';
-import {Dimensions, Platform} from 'react-native';
-import {useKeyboardHandler} from 'react-native-keyboard-controller';
-import {useSharedValue} from 'react-native-reanimated';
-import SplitListItem from '@components/SelectionListWithSections/SplitListItem';
-import type {SelectionListHandle} from '@components/SelectionListWithSections/types';
-import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
-import {FOOTER_BOTTOM_MARGIN, MARGIN_FROM_INPUT_ANDROID, MARGIN_FROM_INPUT_IOS} from './const';
-import type UseDisplayFocusedInputUnderKeyboardType from './types';
-
-const useDisplayFocusedInputUnderKeyboard = (): UseDisplayFocusedInputUnderKeyboardType => {
- const screenHeight = Dimensions.get('window').height;
- const viewRef = useRef(null);
- const bottomOffset = useRef(0);
- const footerRef = useRef(null);
- const keyboardHeight = useSharedValue(0);
- const safeAreaPaddings = useSafeAreaPaddings();
- const listRef = useRef(null);
-
- const changeKeyboardHeight = ({height}: {height: number}) => {
- 'worklet';
-
- keyboardHeight.set(height);
- };
-
- useKeyboardHandler({
- onStart: changeKeyboardHeight,
- onMove: changeKeyboardHeight,
- onEnd: changeKeyboardHeight,
- });
-
- const scrollToFocusedInput = () => {
- if (!viewRef.current) {
- return;
- }
-
- viewRef.current.measureInWindow((_x, _y, _width, height) => {
- footerRef.current?.measureInWindow((_footerX, _footerY, _footerWidth, footerHeight) => {
- if (keyboardHeight.get() >= 1.0) {
- return;
- }
- bottomOffset.current =
- screenHeight -
- safeAreaPaddings.paddingBottom -
- safeAreaPaddings.paddingTop -
- height +
- footerHeight +
- Platform.select({ios: MARGIN_FROM_INPUT_IOS, default: MARGIN_FROM_INPUT_ANDROID}) +
- FOOTER_BOTTOM_MARGIN;
- });
- });
- };
-
- return {
- listRef,
- viewRef,
- footerRef,
- bottomOffset,
- scrollToFocusedInput,
- SplitListItem,
- };
-};
-
-export default useDisplayFocusedInputUnderKeyboard;
diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx
deleted file mode 100644
index b6c570f7a34b..000000000000
--- a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import type {View} from 'react-native';
-import SplitListItemFocus from '@components/SelectionListWithSections/SplitListItem';
-import type {SelectionListHandle} from '@components/SelectionListWithSections/types';
-import useDebouncedState from '@hooks/useDebouncedState';
-import type UseDisplayFocusedInputUnderKeyboardType from './types';
-
-type SplitListItemProps = React.ComponentProps;
-
-const useDisplayFocusedInputUnderKeyboard = (): UseDisplayFocusedInputUnderKeyboardType => {
- const listRef = useRef(null);
- const [inputIndexIsFocused, setInputIndexIsFocused] = useState(-1);
- const viewRef = useRef(null);
- const footerRef = useRef(null);
- const bottomOffset = useRef(0);
- const [scrollTrigger, debouncedScrollTrigger, setScrollTrigger] = useDebouncedState(0);
-
- useEffect(() => {
- if (debouncedScrollTrigger <= 0) {
- return;
- }
-
- listRef.current?.scrollToFocusedInput(inputIndexIsFocused);
-
- // We only want this effect to run when debouncedScrollTrigger changes, not when inputIndexIsFocused changes
- // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [debouncedScrollTrigger]);
-
- const scrollToFocusedInput = () => {
- setScrollTrigger(scrollTrigger + 1);
- };
-
- useEffect(() => {
- scrollToFocusedInput();
-
- // we doesn't need scrollToFocusedInput in deps, because we want it to run only after inputIndexIsFocused changes
- // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [inputIndexIsFocused]);
-
- // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const SplitListItemWithFocus = useCallback(
- ((props: SplitListItemProps) => (
- setInputIndexIsFocused(-1)}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...props}
- />
- )) as typeof SplitListItemFocus,
- [],
- );
-
- return {
- viewRef,
- footerRef,
- bottomOffset,
- listRef,
- scrollToFocusedInput,
- SplitListItem: SplitListItemWithFocus,
- };
-};
-
-export default useDisplayFocusedInputUnderKeyboard;
diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts b/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts
deleted file mode 100644
index 197793c13666..000000000000
--- a/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type {View} from 'react-native';
-import type SplitListItem from '@components/SelectionListWithSections/SplitListItem';
-import type {SelectionListHandle} from '@components/SelectionListWithSections/types';
-
-type UseDisplayFocusedInputUnderKeyboardType = {
- listRef: React.RefObject;
- viewRef: React.RefObject;
- footerRef: React.RefObject;
- bottomOffset: React.RefObject;
- scrollToFocusedInput: () => void;
- SplitListItem: typeof SplitListItem;
-};
-
-export default UseDisplayFocusedInputUnderKeyboardType;
diff --git a/src/hooks/useFlatListScrollKey.ts b/src/hooks/useFlatListScrollKey.ts
index 0065a97dd566..1099bece9506 100644
--- a/src/hooks/useFlatListScrollKey.ts
+++ b/src/hooks/useFlatListScrollKey.ts
@@ -107,7 +107,7 @@ export default function useFlatListScrollKey({
return;
}
handleStartReached({distanceFromStart: 0});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [shouldPreserveVisibleContentPosition, setShouldPreserveVisibleContentPosition] = useState(true);
diff --git a/src/hooks/useHandleSelectionMode.ts b/src/hooks/useHandleSelectionMode.ts
index 10ad7d3c68a9..051d59c38460 100644
--- a/src/hooks/useHandleSelectionMode.ts
+++ b/src/hooks/useHandleSelectionMode.ts
@@ -1,11 +1,11 @@
import {useIsFocused} from '@react-navigation/native';
import {useEffect, useRef} from 'react';
-import type {ListItem} from '@components/SelectionListWithSections/types';
+import type {ListItem} from '@components/SelectionList/types';
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import useMobileSelectionMode from './useMobileSelectionMode';
import useResponsiveLayout from './useResponsiveLayout';
-function useHandleSelectionMode(selectedItems: string[] | TItem[]) {
+function useHandleSelectionMode(selectedItems: readonly string[] | TItem[]) {
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const isFocused = useIsFocused();
diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index 1bb5a65ea964..59762bd600cd 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -83,7 +83,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, isActive
// eslint-disable-next-line no-empty
} catch (e) {}
// We only need to set the callback once.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
},
[maxLength, textInputRef],
);
@@ -171,7 +171,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, isActive
}
handlePastePlainText(event);
},
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[handlePastedHTML, handlePastePlainText, preHtmlPasteCallback],
);
diff --git a/src/hooks/useInitial.ts b/src/hooks/useInitial.ts
index 3afceaeba109..9c9a93456397 100644
--- a/src/hooks/useInitial.ts
+++ b/src/hooks/useInitial.ts
@@ -8,13 +8,11 @@ import {useRef} from 'react';
*/
function useInitial(value: T | undefined): T | undefined {
const initialValueRef = useRef(undefined);
- /* eslint-disable react-compiler/react-compiler */
if (initialValueRef.current === undefined && value !== undefined) {
initialValueRef.current = value;
}
return initialValueRef.current;
- /* eslint-enable react-compiler/react-compiler */
}
export default useInitial;
diff --git a/src/hooks/useIsBlockedToAddFeed.ts b/src/hooks/useIsBlockedToAddFeed.ts
index de15855aea3a..1910081f9340 100644
--- a/src/hooks/useIsBlockedToAddFeed.ts
+++ b/src/hooks/useIsBlockedToAddFeed.ts
@@ -1,5 +1,5 @@
-import {useMemo} from 'react';
-import {getCompanyFeeds, isCSVFeedOrExpensifyCard} from '@libs/CardUtils';
+import {useEffect, useState} from 'react';
+import {getCompanyFeeds} from '@libs/CardUtils';
import {isCollectPolicy} from '@libs/PolicyUtils';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import useCardFeeds from './useCardFeeds';
@@ -10,7 +10,6 @@ import usePolicy from './usePolicy';
*
* Collect plan workspaces are limited to one company card feed. This hook checks if the workspace already has
* a feed and returns whether users should be blocked from adding more feeds.
- * CSV uploads from Classic and Expensify Cards should not count toward this limit.
*
* @param policyID - The ID of the workspace/policy to check
* @returns An object containing:
@@ -20,27 +19,24 @@ import usePolicy from './usePolicy';
function useIsBlockedToAddFeed(policyID?: string) {
const policy = usePolicy(policyID);
const [cardFeeds, allFeedsResult, defaultFeed] = useCardFeeds(policyID);
- // Include pending feeds in the count to prevent users from adding multiple feeds
- // Pending feeds count toward the limit because the backend checks before adding
- const companyFeeds = getCompanyFeeds(cardFeeds, true, false);
+ const companyFeeds = getCompanyFeeds(cardFeeds, true);
const isCollect = isCollectPolicy(policy);
const isAllFeedsResultLoading = isLoadingOnyxValue(allFeedsResult);
+ const [prevCompanyFeedsLength, setPrevCompanyFeedsLength] = useState(0);
const isLoading = !cardFeeds || !!defaultFeed?.isLoading;
- // Count feeds excluding CSV uploads from Classic and Expensify Cards
- // Include pending feeds in the count to enforce the limit
- const connectedFeedsCount = useMemo(() => {
+ useEffect(() => {
if (isLoading) {
- return 0;
+ return;
}
- const feeds = companyFeeds ?? {};
- const nonCSVFeeds = Object.keys(feeds).filter((feedKey) => !isCSVFeedOrExpensifyCard(feedKey));
- return nonCSVFeeds.length;
- }, [isLoading, companyFeeds]);
+ const connectedFeeds = Object.entries(companyFeeds)?.length;
+ setPrevCompanyFeedsLength(connectedFeeds);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again
+ }, [isLoading]);
return {
- isBlockedToAddNewFeeds: isCollect && !isLoading && connectedFeedsCount >= 1,
+ isBlockedToAddNewFeeds: isCollect && !isLoading && prevCompanyFeedsLength >= 1,
isAllFeedsResultLoading: isCollect && (isLoading || isAllFeedsResultLoading),
};
}
diff --git a/src/hooks/useKeyboardShortcut.ts b/src/hooks/useKeyboardShortcut.ts
index 2a64a90cc8fa..2256481687b9 100644
--- a/src/hooks/useKeyboardShortcut.ts
+++ b/src/hooks/useKeyboardShortcut.ts
@@ -63,6 +63,6 @@ export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: G
return () => {
unsubscribe();
};
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, callback, captureOnInputs, excludedNodes, priority, shortcut.descriptionKey, shortcut.modifiers.join(), shortcut.shortcutKey, shouldBubble, shouldPreventDefault]);
}
diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts
index 30787b788202..a18e836d169f 100644
--- a/src/hooks/useNetwork.ts
+++ b/src/hooks/useNetwork.ts
@@ -12,7 +12,6 @@ type UseNetwork = {isOffline: boolean; lastOfflineAt?: string};
export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {}): UseNetwork {
const callback = useRef(onReconnect);
- // eslint-disable-next-line react-compiler/react-compiler
callback.current = onReconnect;
const [network] = useOnyx(ONYXKEYS.NETWORK, {
diff --git a/src/hooks/useNewTransactions.ts b/src/hooks/useNewTransactions.ts
index e79ae1925b2c..2a10522991fc 100644
--- a/src/hooks/useNewTransactions.ts
+++ b/src/hooks/useNewTransactions.ts
@@ -24,7 +24,6 @@ function useNewTransactions(hasOnceLoadedReportActions: boolean | undefined, tra
}
return transactions.filter((transaction) => !prevTransactions?.some((prevTransaction) => prevTransaction.transactionID === transaction.transactionID));
// Depending only on transactions is enough because prevTransactions is a helper object.
- // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactions]);
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 08a9c6a9555f..2f1134ffd8a8 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -4,6 +4,7 @@ import {emailSelector} from '@selectors/Session';
import {useEffect, useMemo, useRef} from 'react';
import {InteractionManager} from 'react-native';
import {startOnboardingFlow} from '@libs/actions/Welcome/OnboardingFlow';
+import Log from '@libs/Log';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
@@ -120,6 +121,7 @@ function useOnboardingFlowRouter() {
// This is a special case when user created an account from NewDot without finishing the onboarding flow and then logged in from OldDot
if (isHybridAppOnboardingCompleted === true && isOnboardingCompleted === false && !startedOnboardingFlowRef.current) {
startedOnboardingFlowRef.current = true;
+ Log.info('[Onboarding] Hybrid app onboarding is completed, but NewDot onboarding is not completed, starting NewDot onboarding flow');
startOnboardingFlow({
onboardingValuesParam: onboardingValues,
isUserFromPublicDomain: !!account?.isFromPublicDomain,
@@ -135,6 +137,7 @@ function useOnboardingFlowRouter() {
// If the user is not transitioning from OldDot to NewDot, we should start NewDot onboarding flow if it's not completed yet
if (!CONFIG.IS_HYBRID_APP && isOnboardingCompleted === false && !startedOnboardingFlowRef.current) {
startedOnboardingFlowRef.current = true;
+ Log.info('[Onboarding] Not a hybrid app, NewDot onboarding is not completed, starting NewDot onboarding flow');
startOnboardingFlow({
onboardingValuesParam: onboardingValues,
isUserFromPublicDomain: !!account?.isFromPublicDomain,
diff --git a/src/hooks/usePaymentOptions.ts b/src/hooks/usePaymentOptions.ts
index b76d59afef3b..c260240d767d 100644
--- a/src/hooks/usePaymentOptions.ts
+++ b/src/hooks/usePaymentOptions.ts
@@ -105,7 +105,7 @@ function usePaymentOptions({
return;
}
lastPaymentMethodRef.current = lastPaymentMethod;
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingLastPaymentMethod]);
const isInvoiceReport = (!isEmptyObject(iouReport) && isInvoiceReportUtil(iouReport)) || false;
@@ -230,7 +230,7 @@ function usePaymentOptions({
return buttonOptions;
// We don't want to reorder the options when the preferred payment method changes while the button is still visible except for component initialization when the last payment method is not initialized yet.
// We need to be sure that onPress should be wrapped in an useCallback to prevent unnecessary updates.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isLoadingLastPaymentMethod,
iouReport,
diff --git a/src/hooks/usePersonalBankAccountDetailsFormSubmit.ts b/src/hooks/usePersonalBankAccountDetailsFormSubmit.ts
new file mode 100644
index 000000000000..f65276112a8f
--- /dev/null
+++ b/src/hooks/usePersonalBankAccountDetailsFormSubmit.ts
@@ -0,0 +1,27 @@
+import type {FormOnyxKeys} from '@components/Form/types';
+import type {OnyxFormKey} from '@src/ONYXKEYS';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useStepFormSubmit from './useStepFormSubmit';
+import type {SubStepProps} from './useSubStep/types';
+
+type UsePersonalBankAccountDetailsFormSubmit = Pick & {
+ formId?: OnyxFormKey;
+ fieldIds: Array>;
+ shouldSaveDraft: boolean;
+};
+
+/**
+ * Hook for handling submit method in Personal Bank account Details substeps.
+ * When user is in editing mode, we should save values only when user confirms the change
+ * @param onNext - callback
+ * @param fieldIds - field IDs for particular step
+ * @param shouldSaveDraft - if we should save draft values
+ */
+export default function usePersonalBankAccountDetailsFormSubmit({onNext, fieldIds, shouldSaveDraft}: UsePersonalBankAccountDetailsFormSubmit) {
+ return useStepFormSubmit({
+ formId: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM,
+ onNext,
+ fieldIds,
+ shouldSaveDraft,
+ });
+}
diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts
index e5db9bffd39c..279e8e4a3bf4 100644
--- a/src/hooks/usePrevious.ts
+++ b/src/hooks/usePrevious.ts
@@ -8,6 +8,5 @@ export default function usePrevious(value: T): T {
useEffect(() => {
ref.current = value;
}, [value]);
- // eslint-disable-next-line react-compiler/react-compiler
return ref.current;
}
diff --git a/src/hooks/useRestartOnReceiptFailure.ts b/src/hooks/useRestartOnReceiptFailure.ts
index 5b50c13d408b..6d76695b8d7e 100644
--- a/src/hooks/useRestartOnReceiptFailure.ts
+++ b/src/hooks/useRestartOnReceiptFailure.ts
@@ -45,7 +45,7 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry, reportI
});
// We want this hook to run on mounting only
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts
index 7196fa0a9ea5..cd87176342b2 100644
--- a/src/hooks/useSearchSelector.base.ts
+++ b/src/hooks/useSearchSelector.base.ts
@@ -271,6 +271,7 @@ function useSearchSelectorBase({
searchString: computedSearchTerm,
includeUserToInvite,
includeCurrentUser,
+ shouldAcceptName: true,
});
default:
return getEmptyOptions();
diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts
index a50965e1e33f..0291873e3e24 100644
--- a/src/hooks/useSelectedTransactionsActions.ts
+++ b/src/hooks/useSelectedTransactionsActions.ts
@@ -27,6 +27,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
import useAllTransactions from './useAllTransactions';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useDeleteTransactions from './useDeleteTransactions';
import useDuplicateTransactionsAndViolations from './useDuplicateTransactionsAndViolations';
import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
@@ -71,11 +72,13 @@ function useSelectedTransactionsActions({
const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true});
const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true});
const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true});
+ const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'Trashcan', 'ArrowRight', 'Table', 'DocumentMerge', 'Export', 'ArrowCollapse', 'ArrowSplit', 'ThumbsDown']);
const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs);
const isReportArchived = useReportIsArchived(report?.reportID);
const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy});
+ const {login} = useCurrentUserPersonalDetails();
const selectedTransactionsList = useMemo(
() =>
selectedTransactionIDs.reduce((acc, transactionID) => {
@@ -215,14 +218,14 @@ function useSelectedTransactionsActions({
if (!action?.childReportID) {
continue;
}
- unholdRequest(transactionID, action?.childReportID);
+ unholdRequest(transactionID, action?.childReportID, policy);
}
clearSelectedTransactions(true);
},
});
}
- const hasNoRejectedTransaction = selectedTransactionIDs.every((id) => !hasTransactionBeenRejected(id));
+ const hasNoRejectedTransaction = selectedTransactionIDs.every((id) => !hasTransactionBeenRejected(allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + id] ?? []));
const canRejectTransactions =
selectedTransactionsList.length > 0 && isMoneyRequestReport && !!session?.email && !!report && canRejectReportAction(session.email, report, policy) && hasNoRejectedTransaction;
if (canRejectTransactions) {
@@ -329,7 +332,7 @@ function useSelectedTransactionsActions({
const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${firstTransaction?.comment?.originalTransactionID}`];
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(firstTransaction, originalTransaction);
- const canSplitTransaction = selectedTransactionsList.length === 1 && report && !isExpenseSplit && isSplitAction(report, [firstTransaction], originalTransaction, policy);
+ const canSplitTransaction = selectedTransactionsList.length === 1 && report && !isExpenseSplit && isSplitAction(report, [firstTransaction], originalTransaction, login ?? '', policy);
if (canSplitTransaction) {
options.push({
@@ -387,6 +390,7 @@ function useSelectedTransactionsActions({
allReports,
session?.accountID,
showDeleteModal,
+ allTransactionViolations,
expensifyIcons.Stopwatch,
expensifyIcons.ThumbsDown,
expensifyIcons.Table,
@@ -398,6 +402,7 @@ function useSelectedTransactionsActions({
expensifyIcons.Trashcan,
localeCompare,
isOnSearch,
+ login,
]);
return {
diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx
index 52f19cb9153d..8307061e1353 100644
--- a/src/hooks/useSidebarOrderedReports.tsx
+++ b/src/hooks/useSidebarOrderedReports.tsx
@@ -31,6 +31,7 @@ type SidebarOrderedReportsContextValue = {
orderedReportIDs: string[];
currentReportID: string | undefined;
policyMemberAccountIDs: number[];
+ clearLHNCache: () => void;
};
type ReportsToDisplayInLHN = Record;
@@ -40,6 +41,7 @@ const SidebarOrderedReportsContext = createContext {},
});
const policySelector = (policy: OnyxEntry): PartialPolicyForSidebar =>
@@ -81,6 +83,10 @@ function SidebarOrderedReportsContextProvider({
const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID;
const prevDerivedCurrentReportID = usePrevious(derivedCurrentReportID);
+ // we need to force reportsToDisplayInLHN to re-compute when we clear currentReportsToDisplay, but the way it currently works relies on not having currentReportsToDisplay as a memo dependency, so we just need something we can change to trigger it
+ // I don't like it either, but clearing the cache is only a hack for the debug modal and I will endeavor to make it better as I work to improve the cache correctness of the LHN more broadly
+ const [clearCacheDummyCounter, setClearCacheDummyCounter] = useState(0);
+
const policyMemberAccountIDs = useMemo(() => getPolicyEmployeeListByIdWithoutCurrentUser(policies, undefined, accountID), [policies, accountID]);
const prevBetas = usePrevious(betas);
const prevPriorityMode = usePrevious(priorityMode);
@@ -185,6 +191,7 @@ function SidebarOrderedReportsContextProvider({
draftComments: reportsDrafts,
});
} else {
+ Log.info('[useSidebarOrderedReports] building reportsToDisplay from scratch');
reportsToDisplay = SidebarUtils.getReportsToDisplayInLHN(
derivedCurrentReportID,
chatReports,
@@ -200,8 +207,20 @@ function SidebarOrderedReportsContextProvider({
return reportsToDisplay;
// Rule disabled intentionally — triggering a re-render on currentReportsToDisplay would cause an infinite loop
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [getUpdatedReports, chatReports, derivedCurrentReportID, priorityMode, betas, policies, transactionViolations, reportNameValuePairs, reportAttributes, reportsDrafts]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ getUpdatedReports,
+ chatReports,
+ derivedCurrentReportID,
+ priorityMode,
+ betas,
+ policies,
+ transactionViolations,
+ reportNameValuePairs,
+ reportAttributes,
+ reportsDrafts,
+ clearCacheDummyCounter,
+ ]);
const deepComparedReportsToDisplayInLHN = useDeepCompareRef(reportsToDisplayInLHN);
const deepComparedReportsDrafts = useDeepCompareRef(reportsDrafts);
@@ -213,7 +232,7 @@ function SidebarOrderedReportsContextProvider({
const getOrderedReportIDs = useCallback(
() => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, deepComparedReportsDrafts, reportNameValuePairs, reportAttributes),
// Rule disabled intentionally - reports should be sorted only when the reportsToDisplayInLHN changes
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[deepComparedReportsToDisplayInLHN, localeCompare, deepComparedReportsDrafts],
);
@@ -232,6 +251,12 @@ function SidebarOrderedReportsContextProvider({
const orderedReports = useMemo(() => getOrderedReports(orderedReportIDs), [getOrderedReports, orderedReportIDs]);
+ const clearLHNCache = useCallback(() => {
+ Log.info('[useSidebarOrderedReports] Clearing sidebar cache manually via debug modal');
+ setCurrentReportsToDisplay({});
+ setClearCacheDummyCounter((current) => current + 1);
+ }, []);
+
const contextValue: SidebarOrderedReportsContextValue = useMemo(() => {
// We need to make sure the current report is in the list of reports, but we do not want
// to have to re-generate the list every time the currentReportID changes. To do that
@@ -256,6 +281,7 @@ function SidebarOrderedReportsContextProvider({
orderedReportIDs: updatedReportIDs,
currentReportID: derivedCurrentReportID,
policyMemberAccountIDs,
+ clearLHNCache,
};
}
@@ -264,8 +290,9 @@ function SidebarOrderedReportsContextProvider({
orderedReportIDs,
currentReportID: derivedCurrentReportID,
policyMemberAccountIDs,
+ clearLHNCache,
};
- }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID, policyMemberAccountIDs, shouldUseNarrowLayout, getOrderedReports, orderedReports]);
+ }, [getOrderedReportIDs, orderedReportIDs, derivedCurrentReportID, policyMemberAccountIDs, shouldUseNarrowLayout, getOrderedReports, orderedReports, clearLHNCache]);
const currentDeps = {
priorityMode,
diff --git a/src/hooks/useSingleExecution/index.native.ts b/src/hooks/useSingleExecution/index.native.ts
index c9d501e38bcc..cec1b26b30b5 100644
--- a/src/hooks/useSingleExecution/index.native.ts
+++ b/src/hooks/useSingleExecution/index.native.ts
@@ -10,7 +10,6 @@ export default function useSingleExecution() {
const [isExecuting, setIsExecuting] = useState(false);
const isExecutingRef = useRef(undefined);
- // eslint-disable-next-line react-compiler/react-compiler
isExecutingRef.current = isExecuting;
const singleExecution = useCallback(
diff --git a/src/hooks/useSubStep/index.ts b/src/hooks/useSubStep/index.ts
index f4c219152a98..a71b173f68b9 100644
--- a/src/hooks/useSubStep/index.ts
+++ b/src/hooks/useSubStep/index.ts
@@ -84,11 +84,9 @@ export default function useSubStep({bodyContent, on
setScreenIndex(lastScreenIndex);
}, [lastScreenIndex]);
- // eslint-disable-next-line react-compiler/react-compiler
return {
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
componentToRender: bodyContent.at(screenIndex) as ComponentType,
- // eslint-disable-next-line react-compiler/react-compiler
isEditing: isEditing.current,
screenIndex,
prevScreen,
diff --git a/src/hooks/useSyncFocus/useSyncFocusImplementation.ts b/src/hooks/useSyncFocus/useSyncFocusImplementation.ts
index 08c9a9497419..df6bfe6fadb8 100644
--- a/src/hooks/useSyncFocus/useSyncFocusImplementation.ts
+++ b/src/hooks/useSyncFocus/useSyncFocusImplementation.ts
@@ -31,7 +31,7 @@ const useSyncFocusImplementation = (ref: RefObject, i
}
ref.current?.focus({preventScroll: true});
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [didScreenTransitionEnd, isFocused, ref]);
};
diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts
index cc5859a3c6d4..485bea8bbf2c 100644
--- a/src/hooks/useTodos.ts
+++ b/src/hooks/useTodos.ts
@@ -12,7 +12,8 @@ export default function useTodos() {
const [allReportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: false});
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: false});
- const {email = '', accountID} = useCurrentUserPersonalDetails();
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
+ const {login = '', accountID} = useCurrentUserPersonalDetails();
return useMemo(() => {
const reportsToSubmit: Report[] = [];
@@ -50,14 +51,14 @@ export default function useTodos() {
if (isApproveAction(report, reportTransactions, policy)) {
reportsToApprove.push(report);
}
- if (isPrimaryPayAction(report, accountID, email, policy, reportNameValuePair)) {
+ if (isPrimaryPayAction(report, accountID, login, bankAccountList, policy, reportNameValuePair)) {
reportsToPay.push(report);
}
- if (isExportAction(report, policy, reportActions)) {
+ if (isExportAction(report, login, policy, reportActions)) {
reportsToExport.push(report);
}
}
return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport};
- }, [allReports, allTransactions, allPolicies, allReportNameValuePairs, allReportActions, accountID, email]);
+ }, [allReports, allTransactions, allPolicies, allReportNameValuePairs, allReportActions, accountID, login, bankAccountList]);
}
diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts
index fb8c3fd8ace1..b3ad06386774 100644
--- a/src/hooks/useWindowDimensions/index.ts
+++ b/src/hooks/useWindowDimensions/index.ts
@@ -93,7 +93,7 @@ export default function (useCachedViewportHeight = false): WindowDimensions {
return;
}
setCachedViewportHeight(windowHeight);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowHeight, isCachedViewportHeight]);
useEffect(() => {
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 1cec84db8ddd..921b34ec35d8 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -21,28 +21,13 @@ import type en from './en';
import type {
ChangeFieldParams,
ConnectionNameParams,
- CustomersOrJobsLabelParams,
+ CreatedReportForUnapprovedTransactionsParams,
DelegateRoleParams,
DeleteActionParams,
DeleteConfirmationParams,
- DeleteTransactionParams,
- DemotedFromWorkspaceParams,
- DidSplitAmountMessageParams,
- EarlyDiscountSubtitleParams,
- EarlyDiscountTitleParams,
EditActionParams,
- EditDestinationSubtitleParams,
- ElectronicFundsParams,
- EmployeeInviteMessageParams,
- EmptyCategoriesSubtitleWithAccountingParams,
- EmptyTagsSubtitleWithAccountingParams,
- EnableContinuousReconciliationParams,
- EnterMagicCodeParams,
- ErrorODIntegrationParams,
ExportAgainModalDescriptionParams,
- ExportedToIntegrationParams,
ExportIntegrationSelectedParams,
- FeatureNameParams,
FileLimitParams,
FileTypeParams,
FiltersAmountBetweenParams,
@@ -97,6 +82,7 @@ import type {
OptionalParam,
OurEmailProviderParams,
OwnerOwesAmountParams,
+ PaidElsewhereParams,
ParentNavigationSummaryParams,
PayAndDowngradeDescriptionParams,
PayerOwesParams,
@@ -123,7 +109,6 @@ import type {
ReportFieldParams,
ReportPolicyNameParams,
RequestAmountParams,
- RequestedAmountMessageParams,
RequiredFieldParams,
ResolutionConstraintsParams,
ReviewParams,
@@ -270,6 +255,7 @@ const translations: TranslationDeepObject = {
dismiss: 'Schließen',
// @context Used on a button to continue an action or workflow, not the formal or procedural sense of “to proceed.”
proceed: 'Fortfahren',
+ unshare: 'Nicht teilen',
yes: 'Ja',
no: 'Nein',
// @context Universal confirmation button. Keep the UI-standard term “OK” unless the locale strongly prefers an alternative.
@@ -669,6 +655,7 @@ const translations: TranslationDeepObject = {
reimbursableTotal: 'Erstattungsfähiger Gesamtbetrag',
nonReimbursableTotal: 'Nicht erstattungsfähiger Gesamtbetrag',
originalAmount: 'Ursprünglicher Betrag',
+ insights: 'Einblicke',
},
supportalNoAccess: {
title: 'Nicht so schnell',
@@ -935,6 +922,8 @@ const translations: TranslationDeepObject = {
asCopilot: 'als Copilot für',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`hat diesen Bericht erstellt, um alle Ausgaben aus ${reportName} aufzunehmen, die mit der von dir gewählten Frequenz nicht eingereicht werden konnten`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `hat diesen Bericht für alle zurückgehaltenen Ausgaben aus ${reportName} erstellt`,
},
mentionSuggestions: {
hereAlternateText: 'Alle in dieser Unterhaltung benachrichtigen',
@@ -992,15 +981,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abonnement',
domains: 'Domänen',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Raum',
- distance: 'Entfernung',
- manual: 'Manuell',
- scan: 'Scannen',
- map: 'Karte',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Raum', distance: 'Entfernung', manual: 'Manuell', scan: 'Scannen', map: 'Karte', gps: 'GPS', odometer: 'Kilometerzähler'},
spreadsheet: {
upload: 'Eine Tabellenkalkulation hochladen',
import: 'Tabellenkalkulation importieren',
@@ -1149,11 +1130,10 @@ const translations: TranslationDeepObject = {
posted: 'Gebucht',
deleteReceipt: 'Beleg löschen',
findExpense: 'Ausgabe finden',
- deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `hat eine Ausgabe gelöscht (${amount} für ${merchant})`,
+ deletedTransaction: (amount: string, merchant: string) => `hat eine Ausgabe gelöscht (${amount} für ${merchant})`,
movedFromReport: ({reportName}: MovedFromReportParams) => `hat eine Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `hat diese Ausgabe verschoben${reportName ? `zu ${reportName} ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `hat diese Ausgabe verschoben${reportName ? `von ${reportName} ` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `hat diese Ausgabe aus deinem Persönlichen Bereich verschoben`,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `hat diese Ausgabe in deinen persönlichen Bereich verschoben`,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1244,13 +1224,13 @@ const translations: TranslationDeepObject = {
finished: 'Fertig',
flip: 'Drehen',
sendInvoice: ({amount}: RequestAmountParams) => `${amount} Rechnung senden`,
- expenseAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? `für ${comment}` : ''}`,
+ expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `für ${comment}` : ''}`,
submitted: ({memo}: SubmittedWithMemoParams) => `Eingereicht${memo ? `, mit dem Hinweis ${memo}` : ''}`,
automaticallySubmitted: `eingereicht über verspätete Einreichungen `,
queuedToSubmitViaDEW: 'in die Warteschlange gestellt zur Einreichung über benutzerdefinierten Genehmigungsworkflow',
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `Verfolgung von ${formattedAmount}${comment ? `für ${comment}` : ''}`,
+ trackedAmount: (formattedAmount: string, comment?: string) => `Verfolgung von ${formattedAmount}${comment ? `für ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `Split ${amount}`,
- didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `aufteilen ${formattedAmount}${comment ? `für ${comment}` : ''}`,
+ didSplitAmount: (formattedAmount: string, comment: string) => `aufteilen ${formattedAmount}${comment ? `für ${comment}` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `Dein Anteil ${amount}`,
payerOwesAmount: (amount: number | string, payer: string, comment?: string) => `${payer} schuldet ${amount}${comment ? `für ${comment}` : ''}`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} schuldet:`,
@@ -1275,7 +1255,7 @@ const translations: TranslationDeepObject = {
`hat die Zahlung über ${amount} storniert, weil ${submitterDisplayName} seine Expensify Wallet nicht innerhalb von 30 Tagen aktiviert hat`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
`${submitterDisplayName} hat ein Bankkonto hinzugefügt. Die Zahlung über ${amount} wurde durchgeführt.`,
- paidElsewhere: (payer?: string) => `${payer ? `${payer} ` : ''}als bezahlt markiert`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}als bezahlt markiert${comment ? `, mit dem Kommentar "${comment}"` : ''}`,
paidWithExpensify: (payer?: string) => `${payer ? `${payer} ` : ''}mit Wallet bezahlt`,
automaticallyPaidWithExpensify: (payer?: string) =>
`${payer ? `${payer} ` : ''}mit Expensify über Workspace-Regeln bezahlt`,
@@ -1331,6 +1311,10 @@ const translations: TranslationDeepObject = {
invalidRate: 'Satz für diesen Workspace ungültig. Bitte wählen Sie einen verfügbaren Satz aus dem Workspace aus.',
endDateBeforeStartDate: 'Das Enddatum darf nicht vor dem Startdatum liegen',
endDateSameAsStartDate: 'Das Enddatum darf nicht mit dem Startdatum identisch sein',
+ manySplitsProvided: `Die maximale Anzahl zulässiger Aufteilungen beträgt ${CONST.IOU.SPLITS_LIMIT}.`,
+ dateRangeExceedsMaxDays: `Der Datumsbereich darf ${CONST.IOU.SPLITS_LIMIT} Tage nicht überschreiten.`,
+ invalidReadings: 'Bitte geben Sie sowohl Anfangs- als auch Endstand ein',
+ negativeDistanceNotAllowed: 'Endablesung muss größer als Startablesung sein',
},
dismissReceiptError: 'Fehler ausblenden',
dismissReceiptErrorConfirmation: 'Achtung! Wenn du diesen Fehler verwirfst, wird dein hochgeladener Beleg vollständig entfernt. Bist du sicher?',
@@ -1482,6 +1466,7 @@ const translations: TranslationDeepObject = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} bis ${endDate} (${count} Tage)`,
splitByDate: 'Nach Datum aufteilen',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `bericht aufgrund eines benutzerdefinierten Genehmigungsworkflows an ${to} weitergeleitet`,
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'Stunde' : 'Stunden'} @ ${rate} / Stunde`, hrs: 'Std.'},
},
transactionMerge: {
listPage: {
@@ -1726,8 +1711,7 @@ const translations: TranslationDeepObject = {
`Füge weitere Möglichkeiten hinzu, dich anzumelden und Belege an Expensify zu senden. Füge eine E‑Mail-Adresse hinzu, um Belege an ${email} weiterzuleiten, oder füge eine Telefonnummer hinzu, um Belege per SMS an 47777 zu senden (nur US-Nummern).`,
pleaseVerify: 'Bitte verifiziere diese Kontaktmethode.',
getInTouch: 'Wir verwenden diese Methode, um Sie zu kontaktieren.',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
- `Bitte gib den magischen Code ein, der an ${contactMethod} gesendet wurde. Er sollte innerhalb ein bis zwei Minuten ankommen.`,
+ enterMagicCode: (contactMethod: string) => `Bitte gib den magischen Code ein, der an ${contactMethod} gesendet wurde. Er sollte innerhalb ein bis zwei Minuten ankommen.`,
setAsDefault: 'Als Standard festlegen',
yourDefaultContactMethod:
'Dies ist Ihre aktuelle Standardkontaktmethode. Bevor Sie sie löschen können, müssen Sie eine andere Kontaktmethode auswählen und auf „Als Standard festlegen“ klicken.',
@@ -1846,6 +1830,8 @@ const translations: TranslationDeepObject = {
sentryDebugDescription: 'Sentry-Anfragen in der Konsole protokollieren',
sentryHighlightedSpanOps: 'Hervorgehobene Span-Namen',
sentryHighlightedSpanOpsPlaceholder: 'ui.interaction.click, navigation, ui.load',
+ leftHandNavCache: 'Cache für linke Seitenleiste',
+ clearleftHandNavCache: 'Löschen',
},
debugConsole: {
saveLog: 'Protokoll speichern',
@@ -2135,6 +2121,12 @@ const translations: TranslationDeepObject = {
shareBankAccountEmptyTitle: 'Keine Administratoren verfügbar',
shareBankAccountEmptyDescription: 'Es gibt keine Workspace-Administratoren, mit denen Sie dieses Bankkonto teilen können.',
shareBankAccountNoAdminsSelected: 'Bitte wählen Sie einen Administrator aus, bevor Sie fortfahren',
+ unshareBankAccount: 'Bankkonto freigeben',
+ unshareBankAccountDescription:
+ 'Alle unten aufgeführten Personen haben Zugriff auf dieses Bankkonto. Sie können den Zugriff jederzeit entfernen. Laufende Zahlungen werden weiterhin ausgeführt.',
+ unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} verliert den Zugriff auf dieses Geschäftskonto. Laufende Zahlungen werden weiterhin ausgeführt.`,
+ reachOutForHelp: 'Dieses Konto wird mit der Expensify Card verwendet. Wenden Sie sich an den Concierge , wenn Sie die Freigabe aufheben möchten.',
+ unshareErrorModalTitle: 'Bankkonto kann nicht freigegeben werden',
},
cardPage: {
expensifyCard: 'Expensify Card',
@@ -2178,7 +2170,7 @@ const translations: TranslationDeepObject = {
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `Zum ${platform}-Wallet hinzugefügt`,
cardDetailsLoadingFailure: 'Beim Laden der Kartendetails ist ein Fehler aufgetreten. Bitte überprüfe deine Internetverbindung und versuche es erneut.',
validateCardTitle: 'Stellen wir sicher, dass du es bist',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
+ enterMagicCode: (contactMethod: string) =>
`Bitte gib den magischen Code ein, der an ${contactMethod} gesendet wurde, um deine Kartendaten anzusehen. Er sollte innerhalb von ein bis zwei Minuten ankommen.`,
missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) =>
`Bitte füge deine persönlichen Daten hinzu und versuche es dann erneut.`,
@@ -3121,6 +3113,7 @@ ${
currencyHeader: 'Was ist die Währung deines Bankkontos?',
confirmationStepHeader: 'Überprüfen Sie Ihre Angaben.',
confirmationStepSubHeader: 'Überprüfen Sie die untenstehenden Angaben und aktivieren Sie das Kontrollkästchen für die Bedingungen, um zu bestätigen.',
+ toGetStarted: 'Fügen Sie ein persönliches Bankkonto hinzu, um Erstattungen zu erhalten, Rechnungen zu bezahlen oder die Expensify Wallet zu aktivieren.',
},
addPersonalBankAccountPage: {
enterPassword: 'Expensify-Passwort eingeben',
@@ -3235,7 +3228,7 @@ ${
sendingFundsDetails: 'Es fällt keine Gebühr an, wenn du mit deinem Guthaben, Bankkonto oder deiner Debitkarte Geld an einen anderen Kontoinhaber sendest.',
electronicFundsStandardDetails:
'Für Überweisungen von deinem Expensify Wallet auf dein Bankkonto mit der Standardoption fällt keine Gebühr an. Diese Überweisung wird in der Regel innerhalb von 1–3 Werktagen abgeschlossen.',
- electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
+ electronicFundsInstantDetails: (percentage: string, amount: string) =>
'Für Überweisungen von deinem Expensify Wallet auf deine verknüpfte Debitkarte per Sofortüberweisung fällt eine Gebühr an. Diese Überweisung wird in der Regel innerhalb weniger Minuten abgeschlossen.' +
`Die Gebühr beträgt ${percentage}% des Überweisungsbetrags (mit einer Mindestgebühr von ${amount}).`,
fdicInsuranceBancorp: ({amount}: TermsParams) =>
@@ -3866,9 +3859,9 @@ ${
lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} – Zuletzt synchronisiert am ${formattedDate}`,
authenticationError: (connectionName: string) => `Verbindung mit ${connectionName} aufgrund eines Authentifizierungsfehlers nicht möglich.`,
learnMore: 'Mehr erfahren',
- memberAlternateText: 'Mitglieder können Berichte einreichen und genehmigen.',
- adminAlternateText: 'Admins haben vollen Bearbeitungszugriff auf alle Berichte und Workspace-Einstellungen.',
- auditorAlternateText: 'Prüfer können Berichte anzeigen und kommentieren.',
+ memberAlternateText: 'Berichte einreichen und genehmigen.',
+ adminAlternateText: 'Berichte und Workspace-Einstellungen verwalten.',
+ auditorAlternateText: 'Berichte anzeigen und kommentieren.',
roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
@@ -3952,8 +3945,8 @@ ${
importPerDiemRates: 'Tagespauschalen importieren',
editPerDiemRate: 'Tagessatz bearbeiten',
editPerDiemRates: 'Tagessätze bearbeiten',
- editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Wenn dieses Ziel aktualisiert wird, ändert es sich für alle ${destination}-Tagessatz-Untersätze.`,
- editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Wenn Sie diese Währung aktualisieren, wird sie für alle ${destination} Tagegeld-Teilbeträge geändert.`,
+ editDestinationSubtitle: (destination: string) => `Wenn dieses Ziel aktualisiert wird, ändert es sich für alle ${destination}-Tagessatz-Untersätze.`,
+ editCurrencySubtitle: (destination: string) => `Wenn Sie diese Währung aktualisieren, wird sie für alle ${destination} Tagegeld-Teilbeträge geändert.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Legen Sie fest, wie Auslagen in QuickBooks Desktop exportiert werden.',
@@ -4564,7 +4557,7 @@ ${
importJobs: 'Projekte importieren',
customers: 'Kunden',
jobs: 'Projekte',
- label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join('und')}, ${importType}`,
+ label: (importFields: string[], importType: string) => `${importFields.join('und')}, ${importType}`,
},
importTaxDescription: 'Steuergruppen aus NetSuite importieren.',
importCustomFields: {
@@ -4977,7 +4970,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
emptyCategories: {
title: 'Du hast noch keine Kategorien erstellt',
subtitle: 'Fügen Sie eine Kategorie hinzu, um Ihre Ausgaben zu organisieren.',
- subtitleWithAccounting: ({accountingPageURL}: EmptyCategoriesSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Ihre Kategorien werden derzeit über eine Buchhaltungsanbindung importiert. Gehen Sie zu Buchhaltung , um Änderungen vorzunehmen. `,
},
updateFailureMessage: 'Beim Aktualisieren der Kategorie ist ein Fehler aufgetreten. Bitte versuche es erneut.',
@@ -5291,7 +5284,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
// We need to remove the subtitle and use the below one when we remove the canUseMultiLevelTags beta
subtitle: 'Füge ein Tag hinzu, um Projekte, Standorte, Abteilungen und mehr zu verfolgen.',
subtitleHTML: `Fügen Sie Tags hinzu, um Projekte, Standorte, Abteilungen und mehr nachzuverfolgen. Erfahren Sie mehr über das Formatieren von Tag-Dateien für den Import. `,
- subtitleWithAccounting: ({accountingPageURL}: EmptyTagsSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Ihre Tags werden derzeit über eine Buchhaltungsverbindung importiert. Gehen Sie zu Buchhaltung , um Änderungen vorzunehmen. `,
},
deleteTag: 'Tag löschen',
@@ -5552,7 +5545,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
}
}
},
- errorODIntegration: ({oldDotPolicyConnectionsURL}: ErrorODIntegrationParams) =>
+ errorODIntegration: (oldDotPolicyConnectionsURL: string) =>
`Bei einer in Expensify Classic eingerichteten Verbindung ist ein Fehler aufgetreten. [Gehe zu Expensify Classic, um dieses Problem zu beheben.](${oldDotPolicyConnectionsURL})`,
goToODToSettings: 'Gehe zu Expensify Classic, um deine Einstellungen zu verwalten.',
setup: 'Verbinden',
@@ -5608,6 +5601,20 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
connectPrompt: ({connectionName}: ConnectionNameParams) =>
`Sind Sie sicher, dass Sie ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'diese Buchhaltungsintegration'} verbinden möchten? Dadurch werden alle bestehenden Buchhaltungsverbindungen entfernt.`,
enterCredentials: 'Geben Sie Ihre Anmeldedaten ein',
+ claimOffer: {
+ badgeText: 'Angebot verfügbar!',
+ xero: {
+ headline: '6 Monate kostenlos mit Xero!',
+ description: 'Neu bei Xero? Expensify-Kunden erhalten 6 Monate kostenlos. Fordern Sie Ihr Angebot unten an. ',
+ connectButton: 'Mit Xero verbinden',
+ },
+ uber: {
+ headerTitle: 'Uber for Business',
+ headline: '5% Rabatt auf Uber-Fahrten',
+ description: `Aktivieren Sie Uber for Business über Expensify und sparen Sie 5% bei allen Geschäftsreisen bis Juni. Bedingungen gelten. `,
+ connectButton: 'Mit Uber for Business verbinden',
+ },
+ },
connections: {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
@@ -5760,7 +5767,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
continuousReconciliation: 'Kontinuierliche Abstimmung',
saveHoursOnReconciliation:
'Sparen Sie bei jedem Abrechnungszeitraum Stunden bei der Abstimmung, indem Sie Expensify die Auszüge und Ausgleichszahlungen der Expensify Card fortlaufend automatisch für Sie abstimmen lassen.',
- enableContinuousReconciliation: ({accountingAdvancedSettingsLink, connectionName}: EnableContinuousReconciliationParams) =>
+ enableContinuousReconciliation: (accountingAdvancedSettingsLink: string, connectionName: string) =>
`Um die kontinuierliche Abstimmung zu aktivieren, aktiviere bitte die automatische Synchronisierung für ${connectionName}. `,
chooseReconciliationAccount: {
chooseBankAccount: 'Wählen Sie das Bankkonto aus, mit dem Ihre Expensify Card-Zahlungen abgeglichen werden.',
@@ -6215,8 +6222,8 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
autoPayApprovedReportsLockedSubtitle: 'Gehen Sie zu „Weitere Funktionen“ und aktivieren Sie „Workflows“, dann fügen Sie „Zahlungen“ hinzu, um diese Funktion freizuschalten.',
autoPayReportsUnderTitle: 'Berichte automatisch bezahlen unter',
autoPayReportsUnderDescription: 'Vollständig konforme Spesenabrechnungen unter diesem Betrag werden automatisch bezahlt.',
- unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `Fügen Sie ${featureName} hinzu, um diese Funktion freizuschalten.`,
- enableFeatureSubtitle: ({featureName, moreFeaturesLink}: FeatureNameParams) =>
+ unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `Fügen Sie ${featureName} hinzu, um diese Funktion freizuschalten.`,
+ enableFeatureSubtitle: (featureName: string, moreFeaturesLink?: string) =>
`Gehe zu [Weitere Funktionen](${moreFeaturesLink}) und aktiviere ${featureName}, um diese Funktion freizuschalten.`,
},
categoryRules: {
@@ -6341,6 +6348,8 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
billcom: 'BILLCOM',
},
workspaceActions: {
+ changedCompanyAddress: ({newAddress, previousAddress}: {newAddress: string; previousAddress?: string}) =>
+ previousAddress ? `Firmenadresse geändert zu „${newAddress}“ (zuvor „${previousAddress}“)` : `Unternehmensadresse auf „${newAddress}“ festlegen`,
addApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
`${approverName} (${approverEmail}) als Genehmiger für das Feld ${field} „${name}“ hinzugefügt`,
deleteApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
@@ -6488,7 +6497,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
other: `hat dich aus den Genehmigungsabläufen und Spesen-Chats von ${joinedNames} entfernt. Bereits eingereichte Reports bleiben in deinem Posteingang zur Genehmigung verfügbar.`,
};
},
- demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) =>
+ demotedFromWorkspace: (policyName: string, oldRole: string) =>
`hat deine Rolle in ${policyName} von ${oldRole} zu Nutzer aktualisiert. Du wurdest aus allen Ausgabenchats von Einreichenden entfernt, außer aus deinen eigenen.`,
updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `Standardwährung auf ${newCurrency} aktualisiert (zuvor ${oldCurrency})`,
updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) =>
@@ -6845,6 +6854,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
selectAllMatchingItems: 'Alle passenden Elemente auswählen',
allMatchingItemsSelected: 'Alle passenden Elemente ausgewählt',
},
+ topSpenders: 'Top-Ausgaben',
},
genericErrorPage: {
title: 'Oh je, etwas ist schiefgelaufen!',
@@ -6923,7 +6933,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
changeType: (oldType: string, newType: string) => `Typ von ${oldType} in ${newType} geändert`,
exportedToCSV: `in CSV exportiert`,
exportedToIntegration: {
- automatic: ({label}: ExportedToIntegrationParams) => {
+ automatic: (label: string) => {
const labelTranslations: Record = {
[CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT]: translations.export.expenseLevelExport,
[CONST.REPORT.EXPORT_OPTION_LABELS.REPORT_LEVEL_EXPORT]: translations.export.reportLevelExport,
@@ -6931,13 +6941,13 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
const translatedLabel = labelTranslations[label] || label;
return `exportiert nach ${translatedLabel}`;
},
- automaticActionOne: ({label}: ExportedToIntegrationParams) => `exportiert nach ${label} über`,
+ automaticActionOne: (label: string) => `exportiert nach ${label} über`,
automaticActionTwo: 'Buchhaltungseinstellungen',
- manual: ({label}: ExportedToIntegrationParams) => `hat diesen Bericht als manuell exportiert nach ${label} markiert.`,
+ manual: (label: string) => `hat diesen Bericht als manuell exportiert nach ${label} markiert.`,
automaticActionThree: 'und erfolgreich einen Datensatz erstellt für',
reimburseableLink: 'Auslagen',
nonReimbursableLink: 'Firmenkarten-Ausgaben',
- pending: ({label}: ExportedToIntegrationParams) => `Begann mit dem Exportieren dieses Berichts nach ${label}...`,
+ pending: (label: string) => `Begann mit dem Exportieren dieses Berichts nach ${label}...`,
},
integrationsMessage: ({errorMessage, label, linkText, linkURL}: IntegrationSyncFailedParams) =>
`Fehler beim Exportieren dieses Berichts nach ${label} („${errorMessage}${linkText ? `${linkText} ` : ''}“)`,
@@ -6981,6 +6991,8 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
removedConnection: ({connectionName}: ConnectionNameParams) => `Verbindung zu ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} entfernt`,
addedConnection: ({connectionName}: ConnectionNameParams) => `verbunden mit ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
leftTheChat: 'hat den Chat verlassen',
+ companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
+ `Die ${feedName}-Verbindung ist unterbrochen. Um Kartenimporte wiederherzustellen, melden Sie sich bei Ihrer Bank an `,
},
error: {
invalidCredentials: 'Ungültige Anmeldedaten. Bitte überprüfen Sie die Konfiguration Ihrer Verbindung.',
@@ -7140,6 +7152,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
error: {
selectSuggestedAddress: 'Bitte wählen Sie eine vorgeschlagene Adresse aus oder verwenden Sie den aktuellen Standort',
},
+ odometer: {startReading: 'Mit dem Lesen beginnen', endReading: 'Lesen beenden', saveForLater: 'Für später speichern', totalDistance: 'Gesamtdistanz'},
},
reportCardLostOrDamaged: {
screenTitle: 'Zeugnis verloren oder beschädigt',
@@ -7453,10 +7466,10 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
},
earlyDiscount: {
claimOffer: 'Angebot einlösen',
- subscriptionPageTitle: ({discountType}: EarlyDiscountTitleParams) =>
+ subscriptionPageTitle: (discountType: number) =>
`${discountType}% Rabatt im ersten Jahr! Fügen Sie einfach eine Zahlungsmethode hinzu und starten Sie ein Jahresabonnement.`,
- onboardingChatTitle: ({discountType}: EarlyDiscountTitleParams) => `Zeitlich begrenztes Angebot: ${discountType}% Rabatt auf dein erstes Jahr!`,
- subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) =>
+ onboardingChatTitle: (discountType: number) => `Zeitlich begrenztes Angebot: ${discountType}% Rabatt auf dein erstes Jahr!`,
+ subtitle: (days: number, hours: number, minutes: number, seconds: number) =>
`Einlösen innerhalb von ${days > 0 ? `${days}T :` : ''}${hours}Std : ${minutes}Min : ${seconds}Sek`,
},
},
@@ -7654,9 +7667,9 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
removeCopilotConfirmation: 'Möchten Sie diesen Copilot wirklich entfernen?',
changeAccessLevel: 'Zugriffsebene ändern',
makeSureItIsYou: 'Stellen wir sicher, dass du es bist',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
+ enterMagicCode: (contactMethod: string) =>
`Bitte gib den magischen Code ein, der an ${contactMethod} gesendet wurde, um einen Copilot hinzuzufügen. Er sollte innerhalb von ein bis zwei Minuten ankommen.`,
- enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `Bitte gib den magischen Code ein, der an ${contactMethod} gesendet wurde, um deinen Copilot zu aktualisieren.`,
+ enterMagicCodeUpdate: (contactMethod: string) => `Bitte gib den magischen Code ein, der an ${contactMethod} gesendet wurde, um deinen Copilot zu aktualisieren.`,
notAllowed: 'Nicht so schnell …',
noAccessMessage: dedent(`
Als Copilot hast du keinen Zugriff auf
@@ -7824,7 +7837,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
readyForTheRealThing: 'Bereit für das Richtige?',
getStarted: 'Loslegen',
},
- employeeInviteMessage: ({name}: EmployeeInviteMessageParams) => `# ${name} hat dich eingeladen, Expensify auszuprobieren
+ employeeInviteMessage: (name: string) => `# ${name} hat dich eingeladen, Expensify auszuprobieren
Hey! Ich habe uns gerade *3 kostenlose Monate* gesichert, um Expensify auszuprobieren, den schnellsten Weg, Spesen abzurechnen.
Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
@@ -7937,8 +7950,17 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
addAdminError: 'Dieser Benutzer kann nicht als Admin hinzugefügt werden. Bitte versuche es erneut.',
revokeAdminAccess: 'Administratorzugriff widerrufen',
cantRevokeAdminAccess: 'Adminzugriff kann dem technischen Ansprechpartner nicht entzogen werden',
- error: {removeAdmin: 'Dieser Benutzer kann nicht als Admin entfernt werden. Bitte versuchen Sie es erneut.'},
+ error: {
+ removeAdmin: 'Dieser Benutzer kann nicht als Administrator entfernt werden. Bitte versuche es erneut.',
+ removeDomain: 'Diese Domain kann nicht entfernt werden. Bitte versuche es erneut.',
+ removeDomainNameInvalid: 'Bitte gib deinen Domainnamen ein, um ihn zurückzusetzen.',
+ },
+ resetDomain: 'Domain zurücksetzen',
+ resetDomainExplanation: ({domainName}: {domainName?: string}) => `Bitte geben Sie ${domainName} ein, um das Zurücksetzen der Domain zu bestätigen.`,
+ enterDomainName: 'Geben Sie hier Ihren Domänennamen ein',
+ resetDomainInfo: `Diese Aktion ist dauerhaft und die folgenden Daten werden gelöscht: Firmenkarten-Verbindungen und alle nicht eingereichten Ausgaben von diesen Karten SAML- und Gruppeneinstellungen Alle Konten, Workspaces, Berichte, Ausgaben und anderen Daten bleiben erhalten. Hinweis: Sie können diese Domain aus Ihrer Domainliste entfernen, indem Sie die zugehörige E-Mail aus Ihren Kontaktmethoden löschen.`,
},
+ members: {title: 'Mitglieder', findMember: 'Mitglied suchen'},
},
gps: {
tooltip: 'GPS-Verfolgung läuft! Wenn du fertig bist, stoppe die Verfolgung unten.',
@@ -7961,6 +7983,19 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
confirm: 'Entfernungsverfolgung verwerfen',
},
zeroDistanceTripModal: {title: 'Ausgabe kann nicht erstellt werden', prompt: 'Sie können keine Ausgabe mit demselben Start- und Zielort erstellen.'},
+ locationRequiredModal: {
+ title: 'Standortzugriff erforderlich',
+ prompt: 'Bitte erlaube den Standortzugriff in den Einstellungen deines Geräts, um die GPS-Distanzverfolgung zu starten.',
+ allow: 'Erlauben',
+ },
+ androidBackgroundLocationRequiredModal: {
+ title: 'Zugriff auf den Standort im Hintergrund erforderlich',
+ prompt: 'Bitte erlaube den Zugriff auf den Standort im Hintergrund in den Geräteeinstellungen (Option „Immer zulassen“), um die GPS-Distanzverfolgung zu starten.',
+ },
+ preciseLocationRequiredModal: {
+ title: 'Genaue Position erforderlich',
+ prompt: 'Bitte aktiviere „genaue Standortbestimmung“ in den Einstellungen deines Geräts, um die GPS‑Streckenverfolgung zu starten.',
+ },
desktop: {
title: 'Entfernung auf deinem Handy verfolgen',
subtitle: 'Protokolliere Meilen oder Kilometer automatisch mit GPS und verwandle Fahrten sofort in Ausgaben.',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index e3be80bd4f39..9eadcf0bc10f 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -9,28 +9,13 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage';
import type {
ChangeFieldParams,
ConnectionNameParams,
- CustomersOrJobsLabelParams,
+ CreatedReportForUnapprovedTransactionsParams,
DelegateRoleParams,
DeleteActionParams,
DeleteConfirmationParams,
- DeleteTransactionParams,
- DemotedFromWorkspaceParams,
- DidSplitAmountMessageParams,
- EarlyDiscountSubtitleParams,
- EarlyDiscountTitleParams,
EditActionParams,
- EditDestinationSubtitleParams,
- ElectronicFundsParams,
- EmployeeInviteMessageParams,
- EmptyCategoriesSubtitleWithAccountingParams,
- EmptyTagsSubtitleWithAccountingParams,
- EnableContinuousReconciliationParams,
- EnterMagicCodeParams,
- ErrorODIntegrationParams,
ExportAgainModalDescriptionParams,
- ExportedToIntegrationParams,
ExportIntegrationSelectedParams,
- FeatureNameParams,
FileLimitParams,
FileTypeParams,
FiltersAmountBetweenParams,
@@ -85,6 +70,7 @@ import type {
OptionalParam,
OurEmailProviderParams,
OwnerOwesAmountParams,
+ PaidElsewhereParams,
ParentNavigationSummaryParams,
PayAndDowngradeDescriptionParams,
PayerOwesParams,
@@ -111,7 +97,6 @@ import type {
ReportFieldParams,
ReportPolicyNameParams,
RequestAmountParams,
- RequestedAmountMessageParams,
RequiredFieldParams,
ResolutionConstraintsParams,
ReviewParams,
@@ -261,6 +246,7 @@ const translations = {
dismiss: 'Dismiss',
// @context Used on a button to continue an action or workflow, not the formal or procedural sense of “to proceed.”
proceed: 'Proceed',
+ unshare: 'Unshare',
yes: 'Yes',
no: 'No',
// @context Universal confirmation button. Keep the UI-standard term “OK” unless the locale strongly prefers an alternative.
@@ -623,6 +609,7 @@ const translations = {
sharedIn: 'Shared in',
unreported: 'Unreported',
explore: 'Explore',
+ insights: 'Insights',
todo: 'To-do',
invoice: 'Invoice',
expense: 'Expense',
@@ -919,6 +906,8 @@ const translations = {
asCopilot: 'as copilot for',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`created this report to hold all expenses from ${reportName} that couldn't be submitted on your chosen frequency`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `created this report for any held expenses from ${reportName} `,
},
mentionSuggestions: {
hereAlternateText: 'Notify everyone in this conversation',
@@ -982,6 +971,7 @@ const translations = {
scan: 'Scan',
map: 'Map',
gps: 'GPS',
+ odometer: 'Odometer',
},
spreadsheet: {
upload: 'Upload a spreadsheet',
@@ -1131,11 +1121,10 @@ const translations = {
posted: 'Posted',
deleteReceipt: 'Delete receipt',
findExpense: 'Find expense',
- deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `deleted an expense (${amount} for ${merchant})`,
+ deletedTransaction: (amount: string, merchant: string) => `deleted an expense (${amount} for ${merchant})`,
movedFromReport: ({reportName}: MovedFromReportParams) => `moved an expense${reportName ? ` from ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `moved this expense${reportName ? ` to ${reportName} ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `moved this expense${reportName ? ` from ${reportName} ` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `moved this expense from your personal space `,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `moved this expense to your personal space `,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1222,13 +1211,13 @@ const translations = {
finished: 'Finished',
flip: 'Flip',
sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`,
- expenseAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`,
+ expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`,
submitted: ({memo}: SubmittedWithMemoParams) => `submitted${memo ? `, saying ${memo}` : ''}`,
automaticallySubmitted: `submitted via delay submissions `,
queuedToSubmitViaDEW: 'queued to submit via custom approval workflow',
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
+ trackedAmount: (formattedAmount: string, comment?: string) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`,
- didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
+ didSplitAmount: (formattedAmount: string, comment: string) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `Your split ${amount}`,
payerOwesAmount: (amount: number | string, payer: string, comment?: string) => `${payer} owes ${amount}${comment ? ` for ${comment}` : ''}`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} owes: `,
@@ -1253,7 +1242,7 @@ const translations = {
`canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
`${submitterDisplayName} added a bank account. The ${amount} payment has been made.`,
- paidElsewhere: (payer?: string) => `${payer ? `${payer} ` : ''}marked as paid`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marked as paid${comment ? `, saying "${comment}"` : ''}`,
paidWithExpensify: (payer?: string) => `${payer ? `${payer} ` : ''}paid with wallet`,
automaticallyPaidWithExpensify: (payer?: string) =>
`${payer ? `${payer} ` : ''}paid with Expensify via workspace rules `,
@@ -1279,6 +1268,8 @@ const translations = {
invalidTagLength: 'The tag name exceeds 255 characters. Please shorten it or choose a different tag.',
invalidAmount: 'Please enter a valid amount before continuing',
invalidDistance: 'Please enter a valid distance before continuing',
+ invalidReadings: 'Please enter both start and end readings',
+ negativeDistanceNotAllowed: 'End reading must be greater than start reading',
invalidIntegerAmount: 'Please enter a whole dollar amount before continuing',
invalidTaxAmount: ({amount}: RequestAmountParams) => `Maximum tax amount is ${amount}`,
invalidSplit: 'The sum of splits must equal the total amount',
@@ -1307,6 +1298,8 @@ const translations = {
invalidRate: 'Rate not valid for this workspace. Please select an available rate from the workspace.',
endDateBeforeStartDate: "The end date can't be before the start date",
endDateSameAsStartDate: "The end date can't be the same as the start date",
+ manySplitsProvided: `The maximum splits allowed is ${CONST.IOU.SPLITS_LIMIT}.`,
+ dateRangeExceedsMaxDays: `The date range can't exceed ${CONST.IOU.SPLITS_LIMIT} days.`,
},
dismissReceiptError: 'Dismiss error',
dismissReceiptErrorConfirmation: 'Heads up! Dismissing this error will remove your uploaded receipt entirely. Are you sure?',
@@ -1453,6 +1446,10 @@ const translations = {
},
chooseWorkspace: 'Choose a workspace',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `report routed to ${to} due to custom approval workflow`,
+ timeTracking: {
+ hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'hour' : 'hours'} @ ${rate} / hour`,
+ hrs: 'hrs',
+ },
},
transactionMerge: {
listPage: {
@@ -1708,7 +1705,7 @@ const translations = {
`Add more ways to log in and send receipts to Expensify. Add an email address to forward receipts to ${email} or add a phone number to text receipts to 47777 (US numbers only).`,
pleaseVerify: 'Please verify this contact method.',
getInTouch: "We'll use this method to contact you.",
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod}. It should arrive within a minute or two.`,
+ enterMagicCode: (contactMethod: string) => `Please enter the magic code sent to ${contactMethod}. It should arrive within a minute or two.`,
setAsDefault: 'Set as default',
yourDefaultContactMethod: "This is your current default contact method. Before you can delete it, you'll need to choose another contact method and click “Set as default”.",
removeContactMethod: 'Remove contact method',
@@ -1819,6 +1816,8 @@ const translations = {
invalidFile: 'Invalid file',
invalidFileDescription: 'The file you are trying to import is not valid. Please try again.',
invalidateWithDelay: 'Invalidate with delay',
+ leftHandNavCache: 'Left Hand Nav cache',
+ clearleftHandNavCache: 'Clear',
recordTroubleshootData: 'Record Troubleshoot Data',
softKillTheApp: 'Soft kill the app',
kill: 'Kill',
@@ -2113,6 +2112,11 @@ const translations = {
shareBankAccountEmptyTitle: 'No admins available',
shareBankAccountEmptyDescription: 'There are no workspace admins you can share this bank account with.',
shareBankAccountNoAdminsSelected: 'Please select an admin before continuing',
+ unshareBankAccount: 'Unshare bank account',
+ unshareBankAccountDescription: 'Everyone below has access to this bank account. You can remove access at any point. We’ll still complete any payments in process.',
+ unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} will lose access to this business bank account. We’ll still complete any payments in process.`,
+ reachOutForHelp: 'It’s being used with the Expensify Card. Reach out to Concierge if you need to unshare it.',
+ unshareErrorModalTitle: 'Can’t unshare bank account',
},
cardPage: {
expensifyCard: 'Expensify Card',
@@ -2155,7 +2159,7 @@ const translations = {
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `Added to ${platform} Wallet`,
cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.',
validateCardTitle: "Let's make sure it's you",
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to view your card details. It should arrive within a minute or two.`,
+ enterMagicCode: (contactMethod: string) => `Please enter the magic code sent to ${contactMethod} to view your card details. It should arrive within a minute or two.`,
missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Please add your personal details , then try again.`,
unexpectedError: 'There was an error trying to get your Expensify card details. Please try again.',
cardFraudAlert: {
@@ -3092,6 +3096,7 @@ const translations = {
currencyHeader: "What's your bank account's currency?",
confirmationStepHeader: 'Check your info.',
confirmationStepSubHeader: 'Double check the details below, and check the terms box to confirm.',
+ toGetStarted: 'Add a personal bank account to receive reimbursements, pay invoices, or enable the Expensify Wallet.',
},
addPersonalBankAccountPage: {
enterPassword: 'Enter Expensify password',
@@ -3205,7 +3210,7 @@ const translations = {
sendingFundsDetails: "There's no fee to send funds to another account holder using your balance, bank account, or debit card.",
electronicFundsStandardDetails:
"There's no fee to transfer funds from your Expensify Wallet to your bank account using the standard option. This transfer usually completes within 1-3 business days.",
- electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
+ electronicFundsInstantDetails: (percentage: string, amount: string) =>
"There's a fee to transfer funds from your Expensify Wallet to your linked debit card using the instant transfer option. This transfer usually completes within several minutes." +
` The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`,
fdicInsuranceBancorp: ({amount}: TermsParams) =>
@@ -3832,9 +3837,9 @@ const translations = {
lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Last synced ${formattedDate}`,
authenticationError: (connectionName: string) => `Can’t connect to ${connectionName} due to an authentication error.`,
learnMore: 'Learn more',
- memberAlternateText: 'Members can submit and approve reports.',
- adminAlternateText: 'Admins have full edit access to all reports and workspace settings.',
- auditorAlternateText: 'Auditors can view and comment on reports.',
+ memberAlternateText: 'Submit and approve reports.',
+ adminAlternateText: 'Manage reports and workspace settings.',
+ auditorAlternateText: 'View and comment on reports.',
roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
@@ -3917,8 +3922,8 @@ const translations = {
importPerDiemRates: 'Import per diem rates',
editPerDiemRate: 'Edit per diem rate',
editPerDiemRates: 'Edit per diem rates',
- editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this destination will change it for all ${destination} per diem subrates.`,
- editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this currency will change it for all ${destination} per diem subrates.`,
+ editDestinationSubtitle: (destination: string) => `Updating this destination will change it for all ${destination} per diem subrates.`,
+ editCurrencySubtitle: (destination: string) => `Updating this currency will change it for all ${destination} per diem subrates.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Desktop.',
@@ -4520,7 +4525,7 @@ const translations = {
importJobs: 'Import projects',
customers: 'customers',
jobs: 'projects',
- label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join(' and ')}, ${importType}`,
+ label: (importFields: string[], importType: string) => `${importFields.join(' and ')}, ${importType}`,
},
importTaxDescription: 'Import tax groups from NetSuite.',
importCustomFields: {
@@ -4871,7 +4876,7 @@ const translations = {
emptyCategories: {
title: "You haven't created any categories",
subtitle: 'Add a category to organize your spend.',
- subtitleWithAccounting: ({accountingPageURL}: EmptyCategoriesSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Your categories are currently importing from an accounting connection. Head over to accounting to make any changes. `,
},
updateFailureMessage: 'An error occurred while updating the category, please try again',
@@ -5182,7 +5187,7 @@ const translations = {
// We need to remove the subtitle and use the below one when we remove the canUseMultiLevelTags beta
subtitle: 'Add a tag to track projects, locations, departments, and more.',
subtitleHTML: `Add tags to track projects, locations, departments, and more. Learn more about formatting tag files for import. `,
- subtitleWithAccounting: ({accountingPageURL}: EmptyTagsSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Your tags are currently importing from an accounting connection. Head over to accounting to make any changes. `,
},
deleteTag: 'Delete tag',
@@ -5442,7 +5447,7 @@ const translations = {
}
}
},
- errorODIntegration: ({oldDotPolicyConnectionsURL}: ErrorODIntegrationParams) =>
+ errorODIntegration: (oldDotPolicyConnectionsURL: string) =>
`There's an error with a connection that's been set up in Expensify Classic. [Go to Expensify Classic to fix this issue.](${oldDotPolicyConnectionsURL})`,
goToODToSettings: 'Go to Expensify Classic to manage your settings.',
setup: 'Connect',
@@ -5501,6 +5506,20 @@ const translations = {
CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'this accounting integration'
}? This will remove any existing accounting connections.`,
enterCredentials: 'Enter your credentials',
+ claimOffer: {
+ badgeText: 'Offer available!',
+ xero: {
+ headline: 'Get Xero free for 6 months!',
+ description: 'New to Xero? Expensify customers get 6 months free. Claim your offer below. ',
+ connectButton: 'Connect to Xero',
+ },
+ uber: {
+ headerTitle: 'Uber for Business',
+ headline: 'Get 5% off Uber rides',
+ description: `Activate Uber for Business through Expensify and save 5% on all business rides through June. Terms apply. `,
+ connectButton: 'Connect to Uber for Business',
+ },
+ },
connections: {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
@@ -5653,7 +5672,7 @@ const translations = {
continuousReconciliation: 'Continuous Reconciliation',
saveHoursOnReconciliation:
'Save hours on reconciliation each accounting period by having Expensify continuously reconcile Expensify Card statements and settlements on your behalf.',
- enableContinuousReconciliation: ({accountingAdvancedSettingsLink, connectionName}: EnableContinuousReconciliationParams) =>
+ enableContinuousReconciliation: (accountingAdvancedSettingsLink: string, connectionName: string) =>
`In order to enable Continuous Reconciliation, please enable auto-sync for ${connectionName}. `,
chooseReconciliationAccount: {
chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.',
@@ -6084,8 +6103,8 @@ const translations = {
autoPayApprovedReportsLockedSubtitle: 'Go to more features and enable workflows, then add payments to unlock this feature.',
autoPayReportsUnderTitle: 'Auto-pay reports under',
autoPayReportsUnderDescription: 'Fully compliant expense reports under this amount will be automatically paid.',
- unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `Add ${featureName} to unlock this feature.`,
- enableFeatureSubtitle: ({featureName, moreFeaturesLink}: FeatureNameParams) => `Go to [more features](${moreFeaturesLink}) and enable ${featureName} to unlock this feature.`,
+ unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `Add ${featureName} to unlock this feature.`,
+ enableFeatureSubtitle: (featureName: string, moreFeaturesLink?: string) => `Go to [more features](${moreFeaturesLink}) and enable ${featureName} to unlock this feature.`,
},
categoryRules: {
title: 'Category rules',
@@ -6209,6 +6228,8 @@ const translations = {
billcom: 'BILLCOM',
},
workspaceActions: {
+ changedCompanyAddress: ({newAddress, previousAddress}: {newAddress: string; previousAddress?: string}) =>
+ previousAddress ? `changed the company address to "${newAddress}" (previously "${previousAddress}")` : `set the company address to "${newAddress}"`,
addApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) => `added ${approverName} (${approverEmail}) as an approver for the ${field} "${name}"`,
deleteApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
`removed ${approverName} (${approverEmail}) as an approver for the ${field} "${name}"`,
@@ -6360,7 +6381,7 @@ const translations = {
other: `removed you from ${joinedNames}'s approval workflows and expense chats. Previously submitted reports will remain available for approval in your Inbox.`,
};
},
- demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) =>
+ demotedFromWorkspace: (policyName: string, oldRole: string) =>
`updated your role in ${policyName} from ${oldRole} to user. You have been removed from all submitter expense chats except for you own.`,
updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `updated the default currency to ${newCurrency} (previously ${oldCurrency})`,
updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) =>
@@ -6613,6 +6634,7 @@ const translations = {
unapprovedCash: 'Unapproved cash',
unapprovedCard: 'Unapproved card',
reconciliation: 'Reconciliation',
+ topSpenders: 'Top spenders',
saveSearch: 'Save search',
deleteSavedSearch: 'Delete saved search',
deleteSavedSearchConfirm: 'Are you sure you want to delete this search?',
@@ -6792,7 +6814,7 @@ const translations = {
changeType: (oldType: string, newType: string) => `changed type from ${oldType} to ${newType}`,
exportedToCSV: `exported to CSV`,
exportedToIntegration: {
- automatic: ({label}: ExportedToIntegrationParams) => {
+ automatic: (label: string) => {
const labelTranslations: Record = {
[CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT]: translations.export.expenseLevelExport,
[CONST.REPORT.EXPORT_OPTION_LABELS.REPORT_LEVEL_EXPORT]: translations.export.reportLevelExport,
@@ -6800,13 +6822,13 @@ const translations = {
const translatedLabel = labelTranslations[label] || label;
return `exported to ${translatedLabel}`;
},
- automaticActionOne: ({label}: ExportedToIntegrationParams) => `exported to ${label} via`,
+ automaticActionOne: (label: string) => `exported to ${label} via`,
automaticActionTwo: 'accounting settings',
- manual: ({label}: ExportedToIntegrationParams) => `marked this report as manually exported to ${label}.`,
+ manual: (label: string) => `marked this report as manually exported to ${label}.`,
automaticActionThree: 'and successfully created a record for',
reimburseableLink: 'out-of-pocket expenses',
nonReimbursableLink: 'company card expenses',
- pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`,
+ pending: (label: string) => `started exporting this report to ${label}...`,
},
integrationsMessage: ({errorMessage, label, linkText, linkURL}: IntegrationSyncFailedParams) =>
`failed to export this report to ${label} ("${errorMessage}${linkText ? ` ${linkText} ` : ''}")`,
@@ -6827,6 +6849,8 @@ const translations = {
takeControl: `took control`,
integrationSyncFailed: ({label, errorMessage, workspaceAccountingLink}: IntegrationSyncFailedParams) =>
`there was a problem syncing with ${label}${errorMessage ? ` ("${errorMessage}")` : ''}. Please fix the issue in workspace settings .`,
+ companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
+ `The ${feedName} connection is broken. To restore card imports, log into your bank `,
addEmployee: (email: string, role: string) => `added ${email} as ${role === 'member' ? 'a' : 'an'} ${role}`,
updateRole: ({email, currentRole, newRole}: UpdateRoleParams) => `updated the role of ${email} to ${newRole} (previously ${currentRole})`,
updatedCustomField1: ({email, previousValue, newValue}: UpdatedCustomFieldParams) => {
@@ -7007,6 +7031,12 @@ const translations = {
error: {
selectSuggestedAddress: 'Please select a suggested address or use current location',
},
+ odometer: {
+ startReading: 'Start reading',
+ endReading: 'End reading',
+ saveForLater: 'Save for later',
+ totalDistance: 'Total distance',
+ },
},
gps: {
tooltip: "GPS tracking in progress! When you're done, stop tracking below.",
@@ -7035,6 +7065,19 @@ const translations = {
title: "Can't create expense",
prompt: "You can't create an expense with the same start and stop location.",
},
+ locationRequiredModal: {
+ title: 'Location access required',
+ prompt: 'Please allow location access in your device settings to start GPS distance tracking.',
+ allow: 'Allow',
+ },
+ androidBackgroundLocationRequiredModal: {
+ title: 'Background location access required',
+ prompt: 'Please allow background location access in your device settings ("Allow all the time" option) to start GPS distance tracking.',
+ },
+ preciseLocationRequiredModal: {
+ title: 'Precise location required',
+ prompt: 'Please enable "precise location" in your device settings to start GPS distance tracking.',
+ },
desktop: {
title: 'Track distance on your phone',
subtitle: 'Log miles or kilometers automatically with GPS and turn trips into expenses instantly.',
@@ -7353,10 +7396,9 @@ const translations = {
},
earlyDiscount: {
claimOffer: 'Claim offer',
- subscriptionPageTitle: ({discountType}: EarlyDiscountTitleParams) =>
- `${discountType}% off your first year! Just add a payment card and start an annual subscription.`,
- onboardingChatTitle: ({discountType}: EarlyDiscountTitleParams) => `Limited-time offer: ${discountType}% off your first year!`,
- subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Claim within ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
+ subscriptionPageTitle: (discountType: number) => `${discountType}% off your first year! Just add a payment card and start an annual subscription.`,
+ onboardingChatTitle: (discountType: number) => `Limited-time offer: ${discountType}% off your first year!`,
+ subtitle: (days: number, hours: number, minutes: number, seconds: number) => `Claim within ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
},
},
cardSection: {
@@ -7553,8 +7595,8 @@ const translations = {
removeCopilotConfirmation: 'Are you sure you want to remove this copilot?',
changeAccessLevel: 'Change access level',
makeSureItIsYou: "Let's make sure it's you",
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to add a copilot. It should arrive within a minute or two.`,
- enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to update your copilot.`,
+ enterMagicCode: (contactMethod: string) => `Please enter the magic code sent to ${contactMethod} to add a copilot. It should arrive within a minute or two.`,
+ enterMagicCodeUpdate: (contactMethod: string) => `Please enter the magic code sent to ${contactMethod} to update your copilot.`,
notAllowed: 'Not so fast...',
noAccessMessage: dedent(`
As a copilot, you don't have access to
@@ -7721,7 +7763,7 @@ const translations = {
readyForTheRealThing: 'Ready for the real thing?',
getStarted: 'Get started',
},
- employeeInviteMessage: ({name}: EmployeeInviteMessageParams) =>
+ employeeInviteMessage: (name: string) =>
`# ${name} invited you to test drive Expensify\nHey! I just got us *3 months free* to test drive Expensify, the fastest way to do expenses.\n\nHere’s a *test receipt* to show you how it works:`,
},
export: {
@@ -7832,7 +7874,17 @@ const translations = {
cantRevokeAdminAccess: "Can't revoke admin access from the technical contact",
error: {
removeAdmin: 'Unable to remove this user as an Admin. Please try again.',
+ removeDomain: 'Unable to remove this domain. Please try again.',
+ removeDomainNameInvalid: 'Please enter your domain name to reset it.',
},
+ resetDomain: 'Reset domain',
+ resetDomainExplanation: ({domainName}: {domainName?: string}) => `Please type ${domainName} to confirm the domain reset.`,
+ enterDomainName: 'Enter your domain name here',
+ resetDomainInfo: `This action is permanent and the following data will be deleted: Company card connections and any unreported expenses from those cards SAML and group settings All accounts, workspaces, reports, expenses, and other data will remain. Note: You can clear this domain from your domains list by removing the associated email from your contact methods .`,
+ },
+ members: {
+ title: 'Members',
+ findMember: 'Find member',
},
},
};
diff --git a/src/languages/es.ts b/src/languages/es.ts
index dccbf5528ca3..743afa41211c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2,7 +2,14 @@ import {CONST as COMMON_CONST} from 'expensify-common';
import dedent from '@libs/StringUtils/dedent';
import CONST from '@src/CONST';
import type en from './en';
-import type {HarvestCreatedExpenseReportParams, RoutedDueToDEWParams, SplitDateRangeParams, ViolationsRterParams} from './params';
+import type {
+ CreatedReportForUnapprovedTransactionsParams,
+ HarvestCreatedExpenseReportParams,
+ PaidElsewhereParams,
+ RoutedDueToDEWParams,
+ SplitDateRangeParams,
+ ViolationsRterParams,
+} from './params';
import type {TranslationDeepObject} from './types';
/* eslint-disable max-len */
@@ -12,6 +19,7 @@ const translations: TranslationDeepObject = {
cancel: 'Cancelar',
dismiss: 'Descartar',
proceed: 'Proceder',
+ unshare: 'Dejar de compartir',
yes: 'Sí',
no: 'No',
ok: 'OK',
@@ -348,6 +356,7 @@ const translations: TranslationDeepObject = {
sharedIn: 'Compartido en',
unreported: 'No reportado',
explore: 'Explorar',
+ insights: 'Información',
todo: 'Tereas',
invoice: 'Factura',
expense: 'Gasto',
@@ -631,6 +640,8 @@ const translations: TranslationDeepObject = {
asCopilot: 'como copiloto de',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`creó este informe para contener todos los gastos de ${reportName} que no se pudieron enviar con la frecuencia que elegiste`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `creó este informe para cualquier gasto retenido de ${reportName} `,
},
mentionSuggestions: {
hereAlternateText: 'Notificar a todos en esta conversación',
@@ -691,6 +702,7 @@ const translations: TranslationDeepObject = {
scan: 'Escanear',
map: 'Map',
gps: 'GPS',
+ odometer: 'Odómetro',
},
spreadsheet: {
upload: 'Importar',
@@ -845,11 +857,10 @@ const translations: TranslationDeepObject = {
markAsCash: 'Marcar como efectivo',
routePending: 'Ruta pendiente...',
findExpense: 'Buscar gasto',
- deletedTransaction: ({amount, merchant}) => `eliminó un gasto (${amount} para ${merchant})`,
+ deletedTransaction: (amount, merchant) => `eliminó un gasto (${amount} para ${merchant})`,
movedFromReport: ({reportName}) => `movió un gasto${reportName ? ` desde ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}) => `movió este gasto${reportName ? ` a ${reportName} ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}) => `movió este gasto${reportName ? ` desde ${reportName} ` : ''}`,
- movedUnreportedTransaction: ({reportUrl}) => `movió este gasto desde tu espacio personal `,
unreportedTransaction: ({reportUrl}) => `movió este gasto a tu espacio personal `,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}) => {
if (shouldHideMovedReportUrl) {
@@ -930,13 +941,13 @@ const translations: TranslationDeepObject = {
finished: 'Finalizado',
flip: 'Cambiar',
sendInvoice: ({amount}) => `Enviar factura de ${amount}`,
- expenseAmount: ({formattedAmount, comment}) => `${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ expenseAmount: (formattedAmount, comment) => `${formattedAmount}${comment ? ` para ${comment}` : ''}`,
submitted: ({memo}) => `enviado${memo ? `, dijo ${memo}` : ''}`,
automaticallySubmitted: `envió mediante retrasar envíos `,
queuedToSubmitViaDEW: 'en cola para enviar a través del flujo de aprobación personalizado',
- trackedAmount: ({formattedAmount, comment}) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ trackedAmount: (formattedAmount, comment) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
splitAmount: ({amount}) => `dividir ${amount}`,
- didSplitAmount: ({formattedAmount, comment}) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ didSplitAmount: (formattedAmount, comment) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
yourSplit: ({amount}) => `Tu parte ${amount}`,
payerOwesAmount: (amount, payer, comment) => `${payer} debe ${amount}${comment ? ` para ${comment}` : ''}`,
payerOwes: ({payer}) => `${payer} debe: `,
@@ -959,7 +970,7 @@ const translations: TranslationDeepObject = {
adminCanceledRequest: 'canceló el pago',
canceledRequest: (amount, submitterDisplayName) => `canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó tu Billetera Expensify en un plazo de 30 días.`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`,
- paidElsewhere: (payer) => `${payer ? `${payer} ` : ''}marcó como pagado`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marcó como pagado${comment ? `, diciendo "${comment}"` : ''}`,
paidWithExpensify: (payer) => `${payer ? `${payer} ` : ''}pagó con la billetera`,
automaticallyPaidWithExpensify: (payer) =>
`${payer ? `${payer} ` : ''}pagó con Expensify via reglas del espacio de trabajo `,
@@ -988,6 +999,8 @@ const translations: TranslationDeepObject = {
invalidTagLength: 'La longitud de la etiqueta escogida excede el máximo permitido (255). Por favor, escoge otra etiqueta o acorta la etiqueta primero.',
invalidAmount: 'Por favor, ingresa un importe válido antes de continuar',
invalidDistance: 'Por favor, ingresa una distancia válida antes de continuar',
+ invalidReadings: 'Por favor ingrese ambas lecturas de inicio y fin',
+ negativeDistanceNotAllowed: 'La lectura final debe ser mayor que la lectura inicial',
invalidIntegerAmount: 'Por favor, introduce una cantidad entera en dólares antes de continuar',
invalidTaxAmount: ({amount}) => `El importe máximo del impuesto es ${amount}`,
invalidSplit: 'La suma de las partes debe ser igual al importe total',
@@ -1016,6 +1029,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Tasa no válida para este espacio de trabajo. Por favor, selecciona una tasa disponible en el espacio de trabajo.',
endDateBeforeStartDate: 'La fecha de finalización no puede ser anterior a la fecha de inicio',
endDateSameAsStartDate: 'La fecha de finalización no puede ser la misma que la fecha de inicio',
+ manySplitsProvided: `La cantidad máxima de divisiones permitidas es ${CONST.IOU.SPLITS_LIMIT}.`,
+ dateRangeExceedsMaxDays: `El rango de fechas no puede exceder los ${CONST.IOU.SPLITS_LIMIT} días.`,
},
dismissReceiptError: 'Descartar error',
dismissReceiptErrorConfirmation: '¡Atención! Descartar este error eliminará completamente tu recibo cargado. ¿Estás seguro?',
@@ -1163,6 +1178,10 @@ const translations: TranslationDeepObject = {
},
chooseWorkspace: 'Elige un espacio de trabajo',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `informe enviado a ${to} debido a un flujo de aprobación personalizado`,
+ timeTracking: {
+ hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'hora' : 'horas'} a ${rate} / hora`,
+ hrs: 'h',
+ },
},
transactionMerge: {
listPage: {
@@ -1405,7 +1424,7 @@ const translations: TranslationDeepObject = {
`Agrega más formas de iniciar sesión y enviar recibos a Expensify. Agrega una dirección de correo electrónico para reenviar recibos a ${email} o agrega un número de teléfono para enviar recibos por mensaje de texto al 47777 (solo números de EE. UU.).`,
pleaseVerify: 'Por favor, verifica este método de contacto.',
getInTouch: 'Usaremos este método para comunicarnos contigo.',
- enterMagicCode: ({contactMethod}) => `Por favor, introduce el código mágico enviado a ${contactMethod}. Debería llegar en un par de minutos.`,
+ enterMagicCode: (contactMethod) => `Por favor, introduce el código mágico enviado a ${contactMethod}. Debería llegar en un par de minutos.`,
setAsDefault: 'Establecer como predeterminado',
yourDefaultContactMethod:
'Este es tu método de contacto predeterminado. Antes de poder eliminarlo, tendrás que elegir otro método de contacto y haz clic en "Establecer como predeterminado".',
@@ -1515,6 +1534,8 @@ const translations: TranslationDeepObject = {
invalidFile: 'Archivo inválido',
invalidFileDescription: 'El archivo que ests intentando importar no es válido. Por favor, inténtalo de nuevo.',
invalidateWithDelay: 'Invalidar con retraso',
+ leftHandNavCache: 'Caché del menú de navegación izquierdo',
+ clearleftHandNavCache: 'borrar',
recordTroubleshootData: 'Registrar datos de resolución de problemas',
softKillTheApp: 'Desactivar la aplicación',
kill: 'Matar',
@@ -1805,6 +1826,12 @@ const translations: TranslationDeepObject = {
shareBankAccountEmptyTitle: 'No hay administradores disponibles',
shareBankAccountEmptyDescription: 'No hay administradores del espacio de trabajo con los que puedas compartir esta cuenta bancaria',
shareBankAccountNoAdminsSelected: 'Seleccione un administrador antes de continuar',
+ unshareBankAccount: 'Dejar de compartir la cuenta bancaria',
+ unshareBankAccountDescription:
+ 'Todas las personas a continuación tienen acceso a esta cuenta bancaria. Puede retirar el acceso en cualquier momento. Seguiremos completando los pagos en proceso.',
+ unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} perderá el acceso a esta cuenta bancaria comercial. Seguiremos completando los pagos en proceso.`,
+ reachOutForHelp: 'Se está usando con la tarjeta Expensify. Contacte con Concierge si necesita dejar de compartirla.',
+ unshareErrorModalTitle: 'No se puede dejar de compartir la cuenta bancaria',
},
cardPage: {
expensifyCard: 'Tarjeta Expensify',
@@ -1846,7 +1873,7 @@ const translations: TranslationDeepObject = {
cardAddedToWallet: ({platform}) => `Añadida a ${platform} Wallet`,
cardDetailsLoadingFailure: 'Se ha producido un error al cargar los datos de la tarjeta. Comprueba tu conexión a Internet e inténtalo de nuevo.',
validateCardTitle: 'Asegurémonos de que eres tú',
- enterMagicCode: ({contactMethod}) => `Introduzca el código mágico enviado a ${contactMethod} para ver los datos de su tarjeta. Debería llegar en un par de minutos.`,
+ enterMagicCode: (contactMethod) => `Introduzca el código mágico enviado a ${contactMethod} para ver los datos de su tarjeta. Debería llegar en un par de minutos.`,
missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Por favor, agrega tus datos personales y vuelve a intentarlo.`,
unexpectedError: 'Se produjo un error al intentar obtener los detalles de tu tarjeta Expensify. Vuelve a intentarlo.',
cardFraudAlert: {
@@ -2790,6 +2817,7 @@ ${amount} para ${merchant} - ${date}`,
currencyHeader: '¿Cuál es la moneda de tu cuenta bancaria?',
confirmationStepHeader: 'Verifica tu información.',
confirmationStepSubHeader: 'Verifica dos veces los detalles a continuación y marca la casilla de términos para confirmar.',
+ toGetStarted: 'Agrega una cuenta bancaria personal para recibir reembolsos, pagar facturas o habilitar la Cartera de Expensify.',
},
addPersonalBankAccountPage: {
enterPassword: 'Escribe tu contraseña de Expensify',
@@ -2904,7 +2932,7 @@ ${amount} para ${merchant} - ${date}`,
sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando tu saldo cuenta bancaria o tarjeta de débito',
electronicFundsStandardDetails:
"'No hay cargo por transferir fondos desde tu Billetera Expensify a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en 1-3 días laborables.",
- electronicFundsInstantDetails: ({percentage, amount}) =>
+ electronicFundsInstantDetails: (percentage, amount) =>
dedent(`
Hay una tarifa para transferir fondos desde tu Billetera Expensify a la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la transferencia (con una tarifa mínima de ${amount}).
`),
@@ -3528,9 +3556,9 @@ ${amount} para ${merchant} - ${date}`,
topLevel: 'Nivel superior',
authenticationError: (connectionName) => `No se puede conectar a ${connectionName} debido a un error de autenticación.`,
learnMore: 'Más información',
- memberAlternateText: 'Los miembros pueden presentar y aprobar informes.',
- adminAlternateText: 'Los administradores tienen acceso total para editar todos los informes y la configuración del área de trabajo.',
- auditorAlternateText: 'Los auditores pueden ver y comentar los informes.',
+ memberAlternateText: 'Presentar y aprobar informes.',
+ adminAlternateText: 'Gestionar informes y configuración del área de trabajo.',
+ auditorAlternateText: 'Ver y comentar los informes.',
roleName: ({role} = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
@@ -3612,8 +3640,8 @@ ${amount} para ${merchant} - ${date}`,
importPerDiemRates: 'Importar tasas de per diem',
editPerDiemRate: 'Editar la tasa de per diem',
editPerDiemRates: 'Editar las tasas de per diem',
- editDestinationSubtitle: ({destination}) => `Actualizar este destino lo modificará para todas las subtasas per diem de ${destination}.`,
- editCurrencySubtitle: ({destination}) => `Actualizar esta moneda la modificará para todas las subtasas per diem de ${destination}.`,
+ editDestinationSubtitle: (destination) => `Actualizar este destino lo modificará para todas las subtasas per diem de ${destination}.`,
+ editCurrencySubtitle: (destination) => `Actualizar esta moneda la modificará para todas las subtasas per diem de ${destination}.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Desktop.',
@@ -4230,7 +4258,7 @@ ${amount} para ${merchant} - ${date}`,
importJobs: 'Importar proyectos',
customers: 'clientes',
jobs: 'proyectos',
- label: ({importFields, importType}) => `${importFields.join(' y ')}, ${importType}`,
+ label: (importFields, importType) => `${importFields.join(' y ')}, ${importType}`,
},
importTaxDescription: 'Importar grupos de impuestos desde NetSuite.',
importCustomFields: {
@@ -4585,7 +4613,7 @@ ${amount} para ${merchant} - ${date}`,
emptyCategories: {
title: 'No has creado ninguna categoría',
subtitle: 'Añade una categoría para organizar tu gasto.',
- subtitleWithAccounting: ({accountingPageURL}) =>
+ subtitleWithAccounting: (accountingPageURL) =>
`Tus categorías se están importando actualmente desde una conexión de contabilidad. Dirígete a contabilidad para hacer cualquier cambio. `,
},
updateFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.',
@@ -4896,7 +4924,7 @@ ${amount} para ${merchant} - ${date}`,
title: 'No has creado ninguna etiqueta',
subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.',
subtitleHTML: `Importa una hoja de cálculo para añadir etiquetas y organizar proyectos, ubicaciones, departamentos y más. Obtén más información sobre cómo dar formato a los archivos de etiquetas. `,
- subtitleWithAccounting: ({accountingPageURL}) =>
+ subtitleWithAccounting: (accountingPageURL) =>
`Tus etiquetas se están importando actualmente desde una conexión de contabilidad. Dirígete a contabilidad para hacer cualquier cambio. `,
},
deleteTag: 'Eliminar etiqueta',
@@ -5118,7 +5146,7 @@ ${amount} para ${merchant} - ${date}`,
}
}
},
- errorODIntegration: ({oldDotPolicyConnectionsURL}) =>
+ errorODIntegration: (oldDotPolicyConnectionsURL) =>
`Hay un error con una conexión que se ha configurado en Expensify Classic. [Ve a Expensify Classic para solucionar este problema.](${oldDotPolicyConnectionsURL})`,
goToODToSettings: 'Ve a Expensify Classic para gestionar tus configuraciones.',
setup: 'Configurar',
@@ -5176,6 +5204,21 @@ ${amount} para ${merchant} - ${date}`,
CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'esta integración contable'
}? Esto eliminará cualquier conexión contable existente.`,
enterCredentials: 'Ingresa tus credenciales',
+ claimOffer: {
+ badgeText: '¡Oferta disponible!',
+ xero: {
+ headline: '¡Obtén Xero gratis por 6 meses!',
+ description:
+ '¿Nuevo en Xero? Los clientes de Expensify obtienen 6 meses gratis. Reclama tu oferta a continuación. ',
+ connectButton: 'Conectar con Xero',
+ },
+ uber: {
+ headerTitle: 'Uber for Business',
+ headline: 'Obtén 5% de descuento en viajes de Uber',
+ description: `Activa Uber for Business a través de Expensify y ahorra 5% en todos los viajes de negocios hasta junio. Aplican términos. `,
+ connectButton: 'Conectar con Uber for Business',
+ },
+ },
connections: {
syncStageName: ({stage}) => {
switch (stage) {
@@ -5328,7 +5371,7 @@ ${amount} para ${merchant} - ${date}`,
continuousReconciliation: 'Conciliación continua',
saveHoursOnReconciliation:
'Ahorra horas de conciliación en cada período contable haciendo que Expensify concilie continuamente los extractos y liquidaciones de la Tarjeta Expensify en tu nombre.',
- enableContinuousReconciliation: ({accountingAdvancedSettingsLink, connectionName}) =>
+ enableContinuousReconciliation: (accountingAdvancedSettingsLink, connectionName) =>
`Para activar la Conciliación Continua, activa la auto-sync para ${connectionName}. `,
chooseReconciliationAccount: {
chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.',
@@ -5823,8 +5866,8 @@ ${amount} para ${merchant} - ${date}`,
autoPayApprovedReportsLockedSubtitle: 'Ve a más funciones y habilita flujos de trabajo, luego agrega pagos para desbloquear esta función.',
autoPayReportsUnderTitle: 'Pagar automáticamente informes por debajo de',
autoPayReportsUnderDescription: 'Los informes de gastos totalmente conformes por debajo de esta cantidad se pagarán automáticamente.',
- unlockFeatureEnableWorkflowsSubtitle: ({featureName}) => `Añade ${featureName} para desbloquear esta función.`,
- enableFeatureSubtitle: ({featureName, moreFeaturesLink}) => `Ir a [más características](${moreFeaturesLink}) y habilita ${featureName} para desbloquear esta función.`,
+ unlockFeatureEnableWorkflowsSubtitle: (featureName) => `Añade ${featureName} para desbloquear esta función.`,
+ enableFeatureSubtitle: (featureName, moreFeaturesLink) => `Ir a [más características](${moreFeaturesLink}) y habilita ${featureName} para desbloquear esta función.`,
},
categoryRules: {
title: 'Reglas de categoría',
@@ -5929,6 +5972,8 @@ ${amount} para ${merchant} - ${date}`,
billcom: 'BILLCOM',
},
workspaceActions: {
+ changedCompanyAddress: ({newAddress, previousAddress}: {newAddress: string; previousAddress?: string}) =>
+ previousAddress ? `cambió la dirección de la empresa a "${newAddress}" (anteriormente "${previousAddress}")` : `estableció la dirección de la empresa en "${newAddress}"`,
addApprovalRule: (approverEmail, approverName, field, name) => `añadió a ${approverName} (${approverEmail}) como aprobador para la ${field} "${name}"`,
deleteApprovalRule: (approverEmail, approverName, field, name) => `eliminó a ${approverName} (${approverEmail}) como aprobador para la ${field} "${name}"`,
updateApprovalRule: ({field, name, newApproverEmail, newApproverName, oldApproverEmail, oldApproverName}) => {
@@ -6074,7 +6119,7 @@ ${amount} para ${merchant} - ${date}`,
other: `te eliminó de los flujos de trabajo de aprobaciones y de los chats de gastos de ${joinedNames}. Los informes enviados anteriormente seguirán estando disponibles para su aprobación en tu bandeja de entrada.`,
};
},
- demotedFromWorkspace: ({policyName, oldRole}) => `cambió tu rol en ${policyName} de ${oldRole} a miembro. Te eliminamos de todos los chats de gastos, excepto el suyo.`,
+ demotedFromWorkspace: (policyName, oldRole) => `cambió tu rol en ${policyName} de ${oldRole} a miembro. Te eliminamos de todos los chats de gastos, excepto el suyo.`,
updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}) => `actualizó la moneda predeterminada a ${newCurrency} (previamente ${oldCurrency})`,
updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}) => `actualizó la frecuencia de generación automática de informes a "${newFrequency}" (previamente "${oldFrequency}")`,
updateApprovalMode: ({newValue, oldValue}) => `actualizó el modo de aprobación a "${newValue}" (previamente "${oldValue}")`,
@@ -6321,6 +6366,7 @@ ${amount} para ${merchant} - ${date}`,
unapprovedCash: 'Efectivo no aprobado',
unapprovedCard: 'Tarjeta no aprobada',
reconciliation: 'Conciliación',
+ topSpenders: 'Mayores gastadores',
saveSearch: 'Guardar búsqueda',
savedSearchesMenuItemTitle: 'Guardadas',
searchName: 'Nombre de la búsqueda',
@@ -6500,7 +6546,7 @@ ${amount} para ${merchant} - ${date}`,
changeType: (oldType, newType) => `cambió type de ${oldType} a ${newType}`,
exportedToCSV: `exportado a CSV`,
exportedToIntegration: {
- automatic: ({label}) => {
+ automatic: (label) => {
// The label will always be in English, so we need to translate it
const labelTranslations: Record = {
[CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT]: translations.export.expenseLevelExport,
@@ -6509,13 +6555,13 @@ ${amount} para ${merchant} - ${date}`,
const translatedLabel = labelTranslations[label] || label;
return `exportado a ${translatedLabel}`;
},
- automaticActionOne: ({label}) => `exportado a ${label} mediante`,
+ automaticActionOne: (label) => `exportado a ${label} mediante`,
automaticActionTwo: 'configuración contable',
- manual: ({label}) => `marcó este informe como exportado manualmente a ${label}.`,
+ manual: (label) => `marcó este informe como exportado manualmente a ${label}.`,
automaticActionThree: 'y creó un registro con éxito para',
reimburseableLink: 'Exportar gastos por cuenta propia como',
nonReimbursableLink: 'gastos de la tarjeta de empresa',
- pending: ({label}) => `comenzó a exportar este informe a ${label}...`,
+ pending: (label) => `comenzó a exportar este informe a ${label}...`,
},
integrationsMessage: ({label, errorMessage, linkText, linkURL}) =>
`no se pudo exportar este informe a ${label} ("${errorMessage}${linkText ? ` ${linkText} ` : ''}")`,
@@ -6536,6 +6582,8 @@ ${amount} para ${merchant} - ${date}`,
takeControl: `tomó el control`,
integrationSyncFailed: ({label, errorMessage, workspaceAccountingLink}) =>
`hubo un problema al sincronizar con ${label}${errorMessage ? ` ("${errorMessage}")` : ''}. Por favor, soluciona el problema en la configuración del espacio de trabajo .`,
+ companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
+ `La conexión ${feedName} está rota. Para restaurar las importaciones de tarjetas, inicia sesión en tu banco `,
addEmployee: (email, role) => `agregó a ${email} como ${role}`,
updateRole: ({email, currentRole, newRole}) => `actualizó el rol ${email} a ${newRole} (previamente ${currentRole})`,
updatedCustomField1: ({email, previousValue, newValue}) => {
@@ -7179,6 +7227,12 @@ ${amount} para ${merchant} - ${date}`,
error: {
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual',
},
+ odometer: {
+ startReading: 'Lectura inicial',
+ endReading: 'Lectura final',
+ saveForLater: 'Guardar para después',
+ totalDistance: 'Distancia total',
+ },
},
reportCardLostOrDamaged: {
screenTitle: 'Notificar la pérdida o deterioro de la tarjeta',
@@ -7494,10 +7548,10 @@ ${amount} para ${merchant} - ${date}`,
},
earlyDiscount: {
claimOffer: 'Solicitar oferta',
- subscriptionPageTitle: ({discountType}) =>
+ subscriptionPageTitle: (discountType) =>
`¡${discountType}% de descuento en tu primer año! ¡Solo añade una tarjeta de pago y comienza una suscripción anual!`,
- onboardingChatTitle: ({discountType}) => `Oferta por tiempo limitado: ¡${discountType}% de descuento en tu primer año!`,
- subtitle: ({days, hours, minutes, seconds}) => `Solicítala en ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
+ onboardingChatTitle: (discountType) => `Oferta por tiempo limitado: ¡${discountType}% de descuento en tu primer año!`,
+ subtitle: (days, hours, minutes, seconds) => `Solicítala en ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
},
},
cardSection: {
@@ -7694,8 +7748,8 @@ ${amount} para ${merchant} - ${date}`,
removeCopilotConfirmation: '¿Estás seguro de que quieres eliminar este copiloto?',
changeAccessLevel: 'Cambiar nivel de acceso',
makeSureItIsYou: 'Vamos a asegurarnos de que eres tú',
- enterMagicCode: ({contactMethod}) => `Por favor, introduce el código mágico enviado a ${contactMethod} para agregar un copiloto. Debería llegar en un par de minutos.`,
- enterMagicCodeUpdate: ({contactMethod}) => `Por favor, introduce el código mágico enviado a ${contactMethod} para actualizar el nivel de acceso de tu copiloto.`,
+ enterMagicCode: (contactMethod) => `Por favor, introduce el código mágico enviado a ${contactMethod} para agregar un copiloto. Debería llegar en un par de minutos.`,
+ enterMagicCodeUpdate: (contactMethod) => `Por favor, introduce el código mágico enviado a ${contactMethod} para actualizar el nivel de acceso de tu copiloto.`,
notAllowed: 'No tan rápido...',
noAccessMessage: 'Como copiloto, no tienes acceso a esta página. ¡Lo sentimos!',
notAllowedMessage: (accountOwnerEmail) =>
@@ -7860,7 +7914,7 @@ ${amount} para ${merchant} - ${date}`,
readyForTheRealThing: '¿Listo para la versión real?',
getStarted: 'Comenzar',
},
- employeeInviteMessage: ({name}) =>
+ employeeInviteMessage: (name) =>
`# ${name} te invitó a probar Expensify\n\n¡Hola! Acabo de conseguirnos *3 meses gratis* para probar Expensify, la forma más rápida de gestionar gastos.\n\nAquí tienes un *recibo de prueba* para mostrarte cómo funciona:`,
},
export: {
@@ -7976,7 +8030,19 @@ ${amount} para ${merchant} - ${date}`,
addAdminError: 'No se pudo añadir a este miembro como administrador. Por favor, inténtalo de nuevo.',
revokeAdminAccess: 'Revocar acceso de administrador',
cantRevokeAdminAccess: 'No se puede revocar el acceso de administrador del contacto técnico',
- error: {removeAdmin: 'No se pudo eliminar a este usuario como administrador. Por favor, inténtalo de nuevo.'},
+ error: {
+ removeAdmin: 'No se pudo eliminar a este usuario como administrador. Por favor, inténtalo de nuevo.',
+ removeDomain: 'No se pudo eliminar este dominio. Inténtalo de nuevo.',
+ removeDomainNameInvalid: 'Introduce el nombre de tu dominio para restablecerlo.',
+ },
+ resetDomain: 'Restablecer dominio',
+ resetDomainExplanation: ({domainName}: {domainName?: string}) => `Escribe ${domainName} para confirmar el restablecimiento del dominio.`,
+ enterDomainName: 'Introduce aquí tu nombre de dominio',
+ resetDomainInfo: `Esta acción es permanente y se eliminarán los siguientes datos: Conexiones de tarjeta corporativa y cualquier gasto no reportado de esas tarjetas Configuración de SAML y grupos Todas las cuentas, espacios de trabajo, informes, gastos y otros datos se conservarán. Nota: Puedes eliminar este dominio de tu lista de dominios eliminando el correo electrónico asociado de tus métodos de contacto .`,
+ },
+ members: {
+ title: 'Miembros',
+ findMember: 'Buscar miembro',
},
},
gps: {
@@ -8006,6 +8072,19 @@ ${amount} para ${merchant} - ${date}`,
title: 'No se puede crear el gasto',
prompt: 'No puedes crear un gasto con la misma ubicación de inicio y fin.',
},
+ locationRequiredModal: {
+ title: 'Se requiere acceso a la ubicación',
+ prompt: 'Por favor, permite el acceso a la ubicación en la configuración de tu dispositivo para iniciar el seguimiento de distancia por GPS.',
+ allow: 'Permitir',
+ },
+ androidBackgroundLocationRequiredModal: {
+ title: 'Se requiere acceso a la ubicación en segundo plano',
+ prompt: 'Por favor, permite el acceso a la ubicación en segundo plano en la configuración de tu dispositivo (opción "Permitir solo con la app en uso") para iniciar el seguimiento de distancia por GPS.',
+ },
+ preciseLocationRequiredModal: {
+ title: 'Se requiere ubicación precisa',
+ prompt: 'Por favor, habilita la "ubicación precisa" en la configuración de tu dispositivo para iniciar el seguimiento de distancia por GPS.',
+ },
desktop: {
title: 'Registra la distancia en tu teléfono',
subtitle: 'Registra millas o kilómetros automáticamente con GPS y convierte los viajes en gastos al instante.',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 9887f7c0068a..86e3c72e1465 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -21,28 +21,13 @@ import type en from './en';
import type {
ChangeFieldParams,
ConnectionNameParams,
- CustomersOrJobsLabelParams,
+ CreatedReportForUnapprovedTransactionsParams,
DelegateRoleParams,
DeleteActionParams,
DeleteConfirmationParams,
- DeleteTransactionParams,
- DemotedFromWorkspaceParams,
- DidSplitAmountMessageParams,
- EarlyDiscountSubtitleParams,
- EarlyDiscountTitleParams,
EditActionParams,
- EditDestinationSubtitleParams,
- ElectronicFundsParams,
- EmployeeInviteMessageParams,
- EmptyCategoriesSubtitleWithAccountingParams,
- EmptyTagsSubtitleWithAccountingParams,
- EnableContinuousReconciliationParams,
- EnterMagicCodeParams,
- ErrorODIntegrationParams,
ExportAgainModalDescriptionParams,
- ExportedToIntegrationParams,
ExportIntegrationSelectedParams,
- FeatureNameParams,
FileLimitParams,
FileTypeParams,
FiltersAmountBetweenParams,
@@ -97,6 +82,7 @@ import type {
OptionalParam,
OurEmailProviderParams,
OwnerOwesAmountParams,
+ PaidElsewhereParams,
ParentNavigationSummaryParams,
PayAndDowngradeDescriptionParams,
PayerOwesParams,
@@ -123,7 +109,6 @@ import type {
ReportFieldParams,
ReportPolicyNameParams,
RequestAmountParams,
- RequestedAmountMessageParams,
RequiredFieldParams,
ResolutionConstraintsParams,
ReviewParams,
@@ -270,6 +255,7 @@ const translations: TranslationDeepObject = {
dismiss: 'Fermer',
// @context Used on a button to continue an action or workflow, not the formal or procedural sense of “to proceed.”
proceed: 'Continuer',
+ unshare: 'Partager',
yes: 'Oui',
no: 'Non',
// @context Universal confirmation button. Keep the UI-standard term “OK” unless the locale strongly prefers an alternative.
@@ -671,6 +657,7 @@ const translations: TranslationDeepObject = {
reimbursableTotal: 'Total remboursable',
nonReimbursableTotal: 'Total non remboursable',
originalAmount: 'Montant d’origine',
+ insights: 'Analyses',
},
supportalNoAccess: {
title: 'Pas si vite',
@@ -938,6 +925,8 @@ const translations: TranslationDeepObject = {
asCopilot: 'en tant que copilote pour',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`a créé ce rapport pour regrouper toutes les dépenses de ${reportName} qui n'ont pas pu être soumises selon la fréquence que vous avez choisie`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `a créé ce rapport pour toutes les dépenses en attente depuis ${reportName} `,
},
mentionSuggestions: {
hereAlternateText: 'Notifier tout le monde dans cette conversation',
@@ -993,15 +982,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abonnement',
domains: 'Domaines',
},
- tabSelector: {
- chat: 'Discussion',
- room: 'Salle',
- distance: 'Distance',
- manual: 'Manuel',
- scan: 'Scanner',
- map: 'Carte',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Discussion', room: 'Salle', distance: 'Distance', manual: 'Manuel', scan: 'Scanner', map: 'Carte', gps: 'GPS', odometer: 'Compteur kilométrique'},
spreadsheet: {
upload: 'Téléverser une feuille de calcul',
import: 'Importer une feuille de calcul',
@@ -1148,11 +1129,10 @@ const translations: TranslationDeepObject = {
posted: 'Publié',
deleteReceipt: 'Supprimer le reçu',
findExpense: 'Trouver une dépense',
- deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `a supprimé une dépense (${amount} pour ${merchant})`,
+ deletedTransaction: (amount: string, merchant: string) => `a supprimé une dépense (${amount} pour ${merchant})`,
movedFromReport: ({reportName}: MovedFromReportParams) => `a déplacé une dépense${reportName ? `de ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `a déplacé cette dépense${reportName ? `à ${reportName} ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `a déplacé cette dépense${reportName ? `de ${reportName} ` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `a déplacé cette dépense depuis votre espace personnel `,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `a déplacé cette dépense dans votre espace personnel `,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1243,13 +1223,13 @@ const translations: TranslationDeepObject = {
finished: 'Terminé',
flip: 'Retourner',
sendInvoice: ({amount}: RequestAmountParams) => `Envoyer ${amount} facture`,
- expenseAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? `pour ${comment}` : ''}`,
+ expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `pour ${comment}` : ''}`,
submitted: ({memo}: SubmittedWithMemoParams) => `envoyé${memo ? `, indiquant ${memo}` : ''}`,
automaticallySubmitted: `soumis via retarder les soumissions `,
queuedToSubmitViaDEW: "en file d'attente pour être soumis via le workflow d'approbation personnalisé",
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `suivi de ${formattedAmount}${comment ? `pour ${comment}` : ''}`,
+ trackedAmount: (formattedAmount: string, comment?: string) => `suivi de ${formattedAmount}${comment ? `pour ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `diviser ${amount}`,
- didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `Diviser ${formattedAmount}${comment ? `pour ${comment}` : ''}`,
+ didSplitAmount: (formattedAmount: string, comment: string) => `Diviser ${formattedAmount}${comment ? `pour ${comment}` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `Votre part de ${amount}`,
payerOwesAmount: (amount: number | string, payer: string, comment?: string) => `${payer} doit ${amount}${comment ? `pour ${comment}` : ''}`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} doit :`,
@@ -1274,7 +1254,7 @@ const translations: TranslationDeepObject = {
`a annulé le paiement de ${amount}, car ${submitterDisplayName} n’a pas activé son Expensify Wallet dans les 30 jours`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
`${submitterDisplayName} a ajouté un compte bancaire. Le paiement de ${amount} a été effectué.`,
- paidElsewhere: (payer?: string) => `${payer ? `${payer} ` : ''}marqué comme payé`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}marqué comme payé${comment ? `, en disant "${comment}"` : ''}`,
paidWithExpensify: (payer?: string) => `${payer ? `${payer} ` : ''} a payé avec le portefeuille`,
automaticallyPaidWithExpensify: (payer?: string) =>
`${payer ? `${payer} ` : ''}payé avec Expensify via les règles de l’espace de travail `,
@@ -1331,6 +1311,10 @@ const translations: TranslationDeepObject = {
invalidRate: 'Taux non valide pour cet espace de travail. Veuillez sélectionner un taux disponible dans l’espace de travail.',
endDateBeforeStartDate: 'La date de fin ne peut pas être antérieure à la date de début',
endDateSameAsStartDate: 'La date de fin ne peut pas être identique à la date de début',
+ manySplitsProvided: `Le nombre maximum de partages autorisés est ${CONST.IOU.SPLITS_LIMIT}.`,
+ dateRangeExceedsMaxDays: `La plage de dates ne peut pas dépasser ${CONST.IOU.SPLITS_LIMIT} jours.`,
+ invalidReadings: 'Veuillez saisir les relevés de début et de fin',
+ negativeDistanceNotAllowed: 'La lecture de fin doit être supérieure à la lecture de début',
},
dismissReceiptError: 'Ignorer l’erreur',
dismissReceiptErrorConfirmation: 'Attention ! Ignorer cette erreur supprimera entièrement votre reçu téléchargé. Êtes-vous sûr ?',
@@ -1483,6 +1467,7 @@ const translations: TranslationDeepObject = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `Du ${startDate} au ${endDate} (${count} jours)`,
splitByDate: 'Scinder par date',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `rapport acheminé vers ${to} en raison d'un workflow d'approbation personnalisé`,
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'heure' : 'heures'} @ ${rate} / heure`, hrs: 'h'},
},
transactionMerge: {
listPage: {
@@ -1727,7 +1712,7 @@ const translations: TranslationDeepObject = {
`Ajoutez davantage de moyens de vous connecter et d’envoyer des reçus à Expensify. Ajoutez une adresse e-mail pour transférer des reçus à ${email} ou ajoutez un numéro de téléphone pour envoyer des reçus par SMS au 47777 (numéros américains uniquement).`,
pleaseVerify: 'Veuillez vérifier cette méthode de contact.',
getInTouch: 'Nous utiliserons cette méthode pour vous contacter.',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Veuillez saisir le code magique envoyé à ${contactMethod}. Il devrait arriver d’ici une à deux minutes.`,
+ enterMagicCode: (contactMethod: string) => `Veuillez saisir le code magique envoyé à ${contactMethod}. Il devrait arriver d’ici une à deux minutes.`,
setAsDefault: 'Définir par défaut',
yourDefaultContactMethod:
'C’est votre méthode de contact par défaut actuelle. Avant de pouvoir la supprimer, vous devez choisir une autre méthode de contact et cliquer sur « Définir par défaut ».',
@@ -1846,6 +1831,8 @@ const translations: TranslationDeepObject = {
sentryDebugDescription: 'Enregistrer les requêtes Sentry dans la console',
sentryHighlightedSpanOps: 'Noms de spans mis en valeur',
sentryHighlightedSpanOpsPlaceholder: 'ui.interaction.click, navigation, ui.load',
+ leftHandNavCache: 'Cache de navigation gauche',
+ clearleftHandNavCache: 'Effacer',
},
debugConsole: {
saveLog: 'Enregistrer le journal',
@@ -2139,6 +2126,11 @@ const translations: TranslationDeepObject = {
shareBankAccountEmptyTitle: 'Aucun administrateur disponible',
shareBankAccountEmptyDescription: "Aucun administrateur d'espace de travail n'est disponible pour partager ce compte bancaire.",
shareBankAccountNoAdminsSelected: 'Veuillez sélectionner un administrateur avant de continuer',
+ unshareBankAccount: 'Retirer le partage du compte bancaire',
+ unshareBankAccountDescription: 'Toutes les personnes ci-dessous ont accès à ce compte bancaire. Vous pouvez révoquer l’accès à tout moment. Les paiements en cours seront honorés.',
+ unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} perdra l’accès à ce compte bancaire professionnel. Les paiements en cours seront honorés.`,
+ reachOutForHelp: 'Ce compte est utilisé avec la carte Expensify. Contactez le service de conciergerie si vous souhaitez le retirer du partage.',
+ unshareErrorModalTitle: 'Impossible de retirer le partage du compte bancaire',
},
cardPage: {
expensifyCard: 'Carte Expensify',
@@ -2182,7 +2174,7 @@ const translations: TranslationDeepObject = {
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `Ajouté au portefeuille ${platform}`,
cardDetailsLoadingFailure: 'Une erreur s’est produite lors du chargement des détails de la carte. Veuillez vérifier votre connexion Internet et réessayer.',
validateCardTitle: 'Vérifions que c’est bien vous',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
+ enterMagicCode: (contactMethod: string) =>
`Veuillez saisir le code magique envoyé à ${contactMethod} pour afficher les détails de votre carte. Il devrait arriver d’ici une à deux minutes.`,
missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Veuillez ajouter vos informations personnelles , puis réessayer.`,
unexpectedError: 'Une erreur s’est produite lors de la récupération des détails de votre carte Expensify. Veuillez réessayer.',
@@ -3127,6 +3119,7 @@ ${
currencyHeader: 'Quelle est la devise de votre compte bancaire ?',
confirmationStepHeader: 'Vérifiez vos informations.',
confirmationStepSubHeader: 'Vérifiez les détails ci-dessous, puis cochez la case des conditions pour confirmer.',
+ toGetStarted: 'Ajoutez un compte bancaire personnel pour recevoir des remboursements, payer des factures ou activer le portefeuille Expensify.',
},
addPersonalBankAccountPage: {
enterPassword: 'Saisissez le mot de passe Expensify',
@@ -3242,7 +3235,7 @@ ${
sendingFundsDetails: 'Il n’y a aucuns frais pour envoyer des fonds à un autre titulaire de compte en utilisant votre solde, votre compte bancaire ou votre carte de débit.',
electronicFundsStandardDetails:
'Il n’y a aucun frais pour transférer des fonds de votre portefeuille Expensify vers votre compte bancaire en utilisant l’option standard. Ce virement est généralement effectué sous 1 à 3 jours ouvrables.',
- electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
+ electronicFundsInstantDetails: (percentage: string, amount: string) =>
'Des frais s’appliquent pour transférer des fonds depuis votre Portefeuille Expensify vers votre carte de débit liée en utilisant l’option de transfert instantané. Ce transfert est généralement effectué en quelques minutes.' +
`Les frais correspondent à ${percentage} % du montant du transfert (avec des frais minimum de ${amount}).`,
fdicInsuranceBancorp: ({amount}: TermsParams) =>
@@ -3872,9 +3865,9 @@ ${
lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Dernière synchronisation le ${formattedDate}`,
authenticationError: (connectionName: string) => `Impossible de se connecter à ${connectionName} en raison d’une erreur d’authentification.`,
learnMore: 'En savoir plus',
- memberAlternateText: 'Les membres peuvent soumettre et approuver des rapports.',
- adminAlternateText: 'Les administrateurs ont un accès complet en modification à tous les rapports et paramètres de l’espace de travail.',
- auditorAlternateText: 'Les auditeurs peuvent consulter et commenter les rapports.',
+ memberAlternateText: 'Soumettre et approuver des rapports.',
+ adminAlternateText: 'Gérez les rapports et les paramètres de l’espace de travail.',
+ auditorAlternateText: 'Afficher et commenter les rapports.',
roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
@@ -3958,8 +3951,8 @@ ${
importPerDiemRates: 'Importer des taux de per diem',
editPerDiemRate: 'Modifier le taux de per diem',
editPerDiemRates: 'Modifier les indemnités journalières',
- editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `La mise à jour de cette destination la modifiera pour tous les sous-taux de per diem ${destination}.`,
- editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `La mise à jour de cette devise la modifiera pour tous les sous-taux de per diem ${destination}.`,
+ editDestinationSubtitle: (destination: string) => `La mise à jour de cette destination la modifiera pour tous les sous-taux de per diem ${destination}.`,
+ editCurrencySubtitle: (destination: string) => `La mise à jour de cette devise la modifiera pour tous les sous-taux de per diem ${destination}.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Définissez la façon dont les dépenses remboursables sont exportées vers QuickBooks Desktop.',
@@ -4569,7 +4562,7 @@ ${
importJobs: 'Importer des projets',
customers: 'clients',
jobs: 'Projets',
- label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join('et')}, ${importType}`,
+ label: (importFields: string[], importType: string) => `${importFields.join('et')}, ${importType}`,
},
importTaxDescription: 'Importer des groupes de taxes depuis NetSuite.',
importCustomFields: {
@@ -4983,7 +4976,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
emptyCategories: {
title: 'Vous n’avez créé aucune catégorie',
subtitle: 'Ajoutez une catégorie pour organiser vos dépenses.',
- subtitleWithAccounting: ({accountingPageURL}: EmptyCategoriesSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Vos catégories sont actuellement importées depuis une connexion comptable. Rendez-vous dans la section Comptabilité pour effectuer des modifications. `,
},
updateFailureMessage: "Une erreur s'est produite lors de la mise à jour de la catégorie, veuillez réessayer",
@@ -5298,7 +5291,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
// We need to remove the subtitle and use the below one when we remove the canUseMultiLevelTags beta
subtitle: 'Ajoutez une étiquette pour suivre les projets, les lieux, les services et plus encore.',
subtitleHTML: `Ajoutez des tags pour suivre les projets, les sites, les services, et plus encore. En savoir plus sur le formatage des fichiers de tags pour l’importation. `,
- subtitleWithAccounting: ({accountingPageURL}: EmptyTagsSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Vos tags sont actuellement importés depuis une connexion comptable. Rendez-vous dans la section Comptabilité pour effectuer des modifications. `,
},
deleteTag: 'Supprimer le tag',
@@ -5560,7 +5553,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
}
}
},
- errorODIntegration: ({oldDotPolicyConnectionsURL}: ErrorODIntegrationParams) =>
+ errorODIntegration: (oldDotPolicyConnectionsURL: string) =>
`Une erreur s’est produite avec une connexion configurée dans Expensify Classic. [Allez sur Expensify Classic pour résoudre ce problème.](${oldDotPolicyConnectionsURL})`,
goToODToSettings: 'Accédez à Expensify Classic pour gérer vos paramètres.',
setup: 'Connecter',
@@ -5616,6 +5609,21 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
connectPrompt: ({connectionName}: ConnectionNameParams) =>
`Voulez-vous vraiment connecter ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'cette intégration comptable'} ? Cela supprimera toutes les connexions comptables existantes.`,
enterCredentials: 'Saisissez vos identifiants',
+ claimOffer: {
+ badgeText: 'Offre disponible !',
+ xero: {
+ headline: 'Obtenez Xero gratuitement pendant 6 mois !',
+ description:
+ 'Nouveau sur Xero ? Les clients Expensify bénéficient de 6 mois gratuits. Réclamez votre offre ci-dessous. ',
+ connectButton: 'Se connecter à Xero',
+ },
+ uber: {
+ headerTitle: 'Uber for Business',
+ headline: 'Obtenez 5% de réduction sur les trajets Uber',
+ description: `Activez Uber for Business via Expensify et économisez 5% sur tous les trajets professionnels jusqu\'en juin. Conditions applicables. `,
+ connectButton: 'Se connecter à Uber for Business',
+ },
+ },
connections: {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
@@ -5768,7 +5776,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
continuousReconciliation: 'Rapprochement continu',
saveHoursOnReconciliation:
'Gagnez des heures à chaque période comptable en laissant Expensify rapprocher en continu, pour vous, les relevés et règlements de la carte Expensify.',
- enableContinuousReconciliation: ({accountingAdvancedSettingsLink, connectionName}: EnableContinuousReconciliationParams) =>
+ enableContinuousReconciliation: (accountingAdvancedSettingsLink: string, connectionName: string) =>
`Pour activer la Réconciliation continue, veuillez activer la synchronisation automatique pour ${connectionName}. `,
chooseReconciliationAccount: {
chooseBankAccount: 'Choisissez le compte bancaire sur lequel les paiements de votre carte Expensify seront rapprochés.',
@@ -6223,8 +6231,8 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
autoPayApprovedReportsLockedSubtitle: 'Allez dans « Plus de fonctionnalités » et activez les workflows, puis ajoutez les paiements pour déverrouiller cette fonctionnalité.',
autoPayReportsUnderTitle: 'Rapports de paiement automatique sous',
autoPayReportsUnderDescription: 'Les notes de frais entièrement conformes en dessous de ce montant seront automatiquement réglées.',
- unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `Ajoutez ${featureName} pour débloquer cette fonctionnalité.`,
- enableFeatureSubtitle: ({featureName, moreFeaturesLink}: FeatureNameParams) =>
+ unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `Ajoutez ${featureName} pour débloquer cette fonctionnalité.`,
+ enableFeatureSubtitle: (featureName: string, moreFeaturesLink?: string) =>
`Allez dans [plus de fonctionnalités](${moreFeaturesLink}) et activez ${featureName} pour déverrouiller cette fonctionnalité.`,
},
categoryRules: {
@@ -6350,6 +6358,8 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
billcom: 'BILLCOM',
},
workspaceActions: {
+ changedCompanyAddress: ({newAddress, previousAddress}: {newAddress: string; previousAddress?: string}) =>
+ previousAddress ? `a modifié l’adresse de l’entreprise en « ${newAddress} » (auparavant « ${previousAddress} »)` : `définir l’adresse de l’entreprise sur « ${newAddress} »`,
addApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
`a ajouté ${approverName} (${approverEmail}) comme approbateur pour le ${field} « ${name} »`,
deleteApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
@@ -6499,7 +6509,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
other: `vous a retiré des workflows d’approbation et des discussions de dépenses de ${joinedNames}. Les rapports précédemment soumis resteront disponibles pour approbation dans votre boîte de réception.`,
};
},
- demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) =>
+ demotedFromWorkspace: (policyName: string, oldRole: string) =>
`a mis à jour votre rôle dans ${policyName} de ${oldRole} à utilisateur. Vous avez été retiré de toutes les discussions de dépenses des déclarants, à l’exception de la vôtre.`,
updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `a mis à jour la devise par défaut en ${newCurrency} (auparavant ${oldCurrency})`,
updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) =>
@@ -6855,6 +6865,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
selectAllMatchingItems: 'Sélectionner tous les éléments correspondants',
allMatchingItemsSelected: 'Tous les éléments correspondants sont sélectionnés',
},
+ topSpenders: 'Plus gros dépensiers',
},
genericErrorPage: {
title: 'Oh oh, quelque chose s’est mal passé !',
@@ -6933,7 +6944,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
changeType: (oldType: string, newType: string) => `type modifié de ${oldType} à ${newType}`,
exportedToCSV: `exporté en CSV`,
exportedToIntegration: {
- automatic: ({label}: ExportedToIntegrationParams) => {
+ automatic: (label: string) => {
const labelTranslations: Record = {
[CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT]: translations.export.expenseLevelExport,
[CONST.REPORT.EXPORT_OPTION_LABELS.REPORT_LEVEL_EXPORT]: translations.export.reportLevelExport,
@@ -6941,13 +6952,13 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
const translatedLabel = labelTranslations[label] || label;
return `exporté vers ${translatedLabel}`;
},
- automaticActionOne: ({label}: ExportedToIntegrationParams) => `exporté vers ${label} via`,
+ automaticActionOne: (label: string) => `exporté vers ${label} via`,
automaticActionTwo: 'paramètres de comptabilité',
- manual: ({label}: ExportedToIntegrationParams) => `a marqué ce rapport comme exporté manuellement vers ${label}.`,
+ manual: (label: string) => `a marqué ce rapport comme exporté manuellement vers ${label}.`,
automaticActionThree: 'et a créé avec succès un enregistrement pour',
reimburseableLink: 'dépenses personnelles',
nonReimbursableLink: 'dépenses de carte d’entreprise',
- pending: ({label}: ExportedToIntegrationParams) => `a commencé à exporter ce rapport vers ${label}...`,
+ pending: (label: string) => `a commencé à exporter ce rapport vers ${label}...`,
},
integrationsMessage: ({errorMessage, label, linkText, linkURL}: IntegrationSyncFailedParams) =>
`échec de l’exportation de ce rapport vers ${label} (« ${errorMessage}${linkText ? `${linkText} ` : ''} »)`,
@@ -6991,6 +7002,8 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
removedConnection: ({connectionName}: ConnectionNameParams) => `connexion à ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} supprimée`,
addedConnection: ({connectionName}: ConnectionNameParams) => `connecté à ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
leftTheChat: 'a quitté la discussion',
+ companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
+ `La connexion à ${feedName} est rompue. Pour rétablir l’importation des cartes, connectez-vous à votre banque `,
},
error: {
invalidCredentials: 'Identifiants invalides, veuillez vérifier la configuration de votre connexion.',
@@ -7150,6 +7163,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
error: {
selectSuggestedAddress: 'Veuillez sélectionner une adresse suggérée ou utiliser la position actuelle',
},
+ odometer: {startReading: 'Commencer la lecture', endReading: 'Fin de lecture', saveForLater: 'Enregistrer pour plus tard', totalDistance: 'Distance totale'},
},
reportCardLostOrDamaged: {
screenTitle: 'Bulletin perdu ou endommagé',
@@ -7462,10 +7476,10 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
},
earlyDiscount: {
claimOffer: 'Réclamer l’offre',
- subscriptionPageTitle: ({discountType}: EarlyDiscountTitleParams) =>
+ subscriptionPageTitle: (discountType: number) =>
`${discountType} % de réduction sur votre première année ! Ajoutez simplement une carte de paiement et démarrez un abonnement annuel.`,
- onboardingChatTitle: ({discountType}: EarlyDiscountTitleParams) => `Offre à durée limitée : ${discountType} % de réduction sur votre première année !`,
- subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Réclamer dans ${days > 0 ? `${days}j :` : ''}${hours}h : ${minutes}m : ${seconds}s`,
+ onboardingChatTitle: (discountType: number) => `Offre à durée limitée : ${discountType} % de réduction sur votre première année !`,
+ subtitle: (days: number, hours: number, minutes: number, seconds: number) => `Réclamer dans ${days > 0 ? `${days}j :` : ''}${hours}h : ${minutes}m : ${seconds}s`,
},
},
cardSection: {
@@ -7663,9 +7677,8 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
removeCopilotConfirmation: 'Êtes-vous sûr de vouloir supprimer ce copilote ?',
changeAccessLevel: 'Modifier le niveau d’accès',
makeSureItIsYou: 'Vérifions que c’est bien vous',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
- `Veuillez saisir le code magique envoyé à ${contactMethod} pour ajouter un copilote. Il devrait arriver d’ici une à deux minutes.`,
- enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `Veuillez saisir le code magique envoyé à ${contactMethod} pour mettre à jour votre copilote.`,
+ enterMagicCode: (contactMethod: string) => `Veuillez saisir le code magique envoyé à ${contactMethod} pour ajouter un copilote. Il devrait arriver d’ici une à deux minutes.`,
+ enterMagicCodeUpdate: (contactMethod: string) => `Veuillez saisir le code magique envoyé à ${contactMethod} pour mettre à jour votre copilote.`,
notAllowed: 'Pas si vite...',
noAccessMessage: dedent(`
En tant que copilote, vous n’avez pas accès à
@@ -7833,7 +7846,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
readyForTheRealThing: 'Prêt pour de vrai ?',
getStarted: 'Commencer',
},
- employeeInviteMessage: ({name}: EmployeeInviteMessageParams) => `# ${name} vous a invité à essayer Expensify
+ employeeInviteMessage: (name: string) => `# ${name} vous a invité à essayer Expensify
Salut ! Je viens de nous obtenir *3 mois gratuits* pour essayer Expensify, la façon la plus rapide de gérer les notes de frais.
Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
@@ -7942,8 +7955,17 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
addAdminError: 'Impossible d’ajouter ce membre en tant qu’administrateur. Veuillez réessayer.',
revokeAdminAccess: 'Révoquer l’accès administrateur',
cantRevokeAdminAccess: 'Impossible de révoquer l’accès administrateur au contact technique',
- error: {removeAdmin: 'Impossible de supprimer cet utilisateur en tant qu’administrateur. Veuillez réessayer.'},
+ error: {
+ removeAdmin: 'Impossible de supprimer cet utilisateur en tant qu’administrateur. Veuillez réessayer.',
+ removeDomain: 'Impossible de supprimer ce domaine. Veuillez réessayer.',
+ removeDomainNameInvalid: 'Veuillez saisir votre nom de domaine pour le réinitialiser.',
+ },
+ resetDomain: 'Réinitialiser le domaine',
+ resetDomainExplanation: ({domainName}: {domainName?: string}) => `Veuillez saisir ${domainName} pour confirmer la réinitialisation du domaine.`,
+ enterDomainName: 'Saisissez votre nom de domaine ici',
+ resetDomainInfo: `Cette action est définitive et les données suivantes seront supprimées : Connexions aux cartes d'entreprise et toutes les dépenses non déclarées de ces cartes Paramètres SAML et de groupe Tous les comptes, espaces de travail, rapports, dépenses et autres données seront conservés. Remarque : Vous pouvez supprimer ce domaine de votre liste de domaines en retirant l'adresse e-mail associée de vos méthodes de contact .`,
},
+ members: {title: 'Membres', findMember: 'Rechercher un membre'},
},
gps: {
tooltip: 'Suivi GPS en cours ! Quand vous avez terminé, arrêtez le suivi ci-dessous.',
@@ -7966,6 +7988,19 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
confirm: 'Ignorer le suivi de la distance',
},
zeroDistanceTripModal: {title: 'Impossible de créer la dépense', prompt: 'Vous ne pouvez pas créer une dépense avec le même lieu de départ et d’arrivée.'},
+ locationRequiredModal: {
+ title: 'Accès à la localisation requis',
+ prompt: 'Veuillez autoriser l’accès à la localisation dans les paramètres de votre appareil pour lancer le suivi de distance GPS.',
+ allow: 'Autoriser',
+ },
+ androidBackgroundLocationRequiredModal: {
+ title: 'Accès à la position en arrière-plan requis',
+ prompt: 'Veuillez autoriser l’accès à la localisation en arrière-plan dans les paramètres de votre appareil (option « Autoriser tout le temps ») pour démarrer le suivi de distance par GPS.',
+ },
+ preciseLocationRequiredModal: {
+ title: 'Emplacement précis requis',
+ prompt: 'Veuillez activer la « localisation précise » dans les paramètres de votre appareil pour commencer le suivi de distance GPS.',
+ },
desktop: {
title: 'Suivez la distance sur votre téléphone',
subtitle: 'Enregistrez automatiquement les miles ou kilomètres avec le GPS et transformez instantanément vos trajets en dépenses.',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 314d375882cc..e593f9c6306e 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -21,28 +21,13 @@ import type en from './en';
import type {
ChangeFieldParams,
ConnectionNameParams,
- CustomersOrJobsLabelParams,
+ CreatedReportForUnapprovedTransactionsParams,
DelegateRoleParams,
DeleteActionParams,
DeleteConfirmationParams,
- DeleteTransactionParams,
- DemotedFromWorkspaceParams,
- DidSplitAmountMessageParams,
- EarlyDiscountSubtitleParams,
- EarlyDiscountTitleParams,
EditActionParams,
- EditDestinationSubtitleParams,
- ElectronicFundsParams,
- EmployeeInviteMessageParams,
- EmptyCategoriesSubtitleWithAccountingParams,
- EmptyTagsSubtitleWithAccountingParams,
- EnableContinuousReconciliationParams,
- EnterMagicCodeParams,
- ErrorODIntegrationParams,
ExportAgainModalDescriptionParams,
- ExportedToIntegrationParams,
ExportIntegrationSelectedParams,
- FeatureNameParams,
FileLimitParams,
FileTypeParams,
FiltersAmountBetweenParams,
@@ -97,6 +82,7 @@ import type {
OptionalParam,
OurEmailProviderParams,
OwnerOwesAmountParams,
+ PaidElsewhereParams,
ParentNavigationSummaryParams,
PayAndDowngradeDescriptionParams,
PayerOwesParams,
@@ -123,7 +109,6 @@ import type {
ReportFieldParams,
ReportPolicyNameParams,
RequestAmountParams,
- RequestedAmountMessageParams,
RequiredFieldParams,
ResolutionConstraintsParams,
ReviewParams,
@@ -270,6 +255,7 @@ const translations: TranslationDeepObject = {
dismiss: 'Chiudi',
// @context Used on a button to continue an action or workflow, not the formal or procedural sense of “to proceed.”
proceed: 'Continua',
+ unshare: 'Non condividere',
yes: 'Sì',
no: 'No',
// @context Universal confirmation button. Keep the UI-standard term “OK” unless the locale strongly prefers an alternative.
@@ -670,6 +656,7 @@ const translations: TranslationDeepObject = {
reimbursableTotal: 'Totale rimborsabile',
nonReimbursableTotal: 'Totale non rimborsabile',
originalAmount: 'Importo originale',
+ insights: 'Analisi',
},
supportalNoAccess: {
title: 'Non così in fretta',
@@ -935,6 +922,8 @@ const translations: TranslationDeepObject = {
asCopilot: 'come copilota per',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`ha creato questo rapporto per raccogliere tutte le spese di ${reportName} che non sono state inviate con la frequenza scelta`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `ha creato questo report per eventuali spese in sospeso da ${reportName} `,
},
mentionSuggestions: {
hereAlternateText: 'Notifica tutti in questa conversazione',
@@ -990,15 +979,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abbonamento',
domains: 'Domini',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Stanza',
- distance: 'Distanza',
- manual: 'Manuale',
- scan: 'Scannerizza',
- map: 'Mappa',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Stanza', distance: 'Distanza', manual: 'Manuale', scan: 'Scannerizza', map: 'Mappa', gps: 'GPS', odometer: 'Contachilometri'},
spreadsheet: {
upload: 'Carica un foglio di calcolo',
import: 'Importa foglio di calcolo',
@@ -1144,11 +1125,10 @@ const translations: TranslationDeepObject = {
posted: 'Pubblicato',
deleteReceipt: 'Elimina ricevuta',
findExpense: 'Trova spesa',
- deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `ha eliminato una spesa (${amount} per ${merchant})`,
+ deletedTransaction: (amount: string, merchant: string) => `ha eliminato una spesa (${amount} per ${merchant})`,
movedFromReport: ({reportName}: MovedFromReportParams) => `ha spostato una spesa${reportName ? `da ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `spostato questa spesa${reportName ? `a ${reportName} ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `ha spostato questa spesa${reportName ? `da ${reportName} ` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `ha spostato questa spesa dal tuo spazio personale `,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `ha spostato questa spesa nel tuo spazio personale `,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1239,13 +1219,13 @@ const translations: TranslationDeepObject = {
finished: 'Completato',
flip: 'Capovolgi',
sendInvoice: ({amount}: RequestAmountParams) => `Invia fattura di ${amount}`,
- expenseAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? `per ${comment}` : ''}`,
+ expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `per ${comment}` : ''}`,
submitted: ({memo}: SubmittedWithMemoParams) => `inviato${memo ? `, dicendo ${memo}` : ''}`,
automaticallySubmitted: `inviato tramite invio ritardato `,
queuedToSubmitViaDEW: "in coda per l'invio tramite flusso di approvazione personalizzato",
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `monitoraggio ${formattedAmount}${comment ? `per ${comment}` : ''}`,
+ trackedAmount: (formattedAmount: string, comment?: string) => `monitoraggio ${formattedAmount}${comment ? `per ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `dividi ${amount}`,
- didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividi ${formattedAmount}${comment ? `per ${comment}` : ''}`,
+ didSplitAmount: (formattedAmount: string, comment: string) => `dividi ${formattedAmount}${comment ? `per ${comment}` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `La tua quota ${amount}`,
payerOwesAmount: (amount: number | string, payer: string, comment?: string) => `${payer} deve ${amount}${comment ? `per ${comment}` : ''}`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} deve:`,
@@ -1270,7 +1250,7 @@ const translations: TranslationDeepObject = {
`ha annullato il pagamento di ${amount}, perché ${submitterDisplayName} non ha abilitato il proprio Expensify Wallet entro 30 giorni`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
`${submitterDisplayName} ha aggiunto un conto bancario. Il pagamento di ${amount} è stato effettuato.`,
- paidElsewhere: (payer?: string) => `${payer ? `${payer} ` : ''}segnato come pagato`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}segnato come pagato${comment ? `, dicendo "${comment}"` : ''}`,
paidWithExpensify: (payer?: string) => `${payer ? `${payer} ` : ''}pagato con portafoglio`,
automaticallyPaidWithExpensify: (payer?: string) =>
`${payer ? `${payer} ` : ''}pagato con Expensify tramite le regole dello spazio di lavoro `,
@@ -1326,6 +1306,10 @@ const translations: TranslationDeepObject = {
invalidRate: 'Tariffa non valida per questo workspace. Seleziona una tariffa disponibile dal workspace.',
endDateBeforeStartDate: 'La data di fine non può essere precedente alla data di inizio',
endDateSameAsStartDate: 'La data di fine non può essere uguale alla data di inizio',
+ manySplitsProvided: `Il numero massimo di suddivisioni consentite è ${CONST.IOU.SPLITS_LIMIT}.`,
+ dateRangeExceedsMaxDays: `L'intervallo di date non può superare i ${CONST.IOU.SPLITS_LIMIT} giorni.`,
+ invalidReadings: 'Inserisci sia la lettura iniziale che quella finale',
+ negativeDistanceNotAllowed: 'La lettura finale deve essere maggiore della lettura iniziale',
},
dismissReceiptError: 'Ignora errore',
dismissReceiptErrorConfirmation: 'Attenzione! Se ignori questo errore, la ricevuta caricata verrà rimossa completamente. Sei sicuro?',
@@ -1477,6 +1461,7 @@ const translations: TranslationDeepObject = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} a ${endDate} (${count} giorni)`,
splitByDate: 'Dividi per data',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `rapporto inoltrato a ${to} a causa del flusso di lavoro di approvazione personalizzato`,
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'ora' : 'ore'} @ ${rate} / ora`, hrs: 'ore'},
},
transactionMerge: {
listPage: {
@@ -1721,7 +1706,7 @@ const translations: TranslationDeepObject = {
`Aggiungi altri modi per accedere e inviare ricevute a Expensify. Aggiungi un indirizzo email per inoltrare le ricevute a ${email} o aggiungi un numero di telefono per inviare le ricevute tramite SMS al 47777 (solo numeri degli Stati Uniti).`,
pleaseVerify: 'Verifica questo metodo di contatto.',
getInTouch: 'Useremo questo metodo per contattarti.',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Inserisci il codice magico inviato a ${contactMethod}. Dovrebbe arrivare entro uno o due minuti.`,
+ enterMagicCode: (contactMethod: string) => `Inserisci il codice magico inviato a ${contactMethod}. Dovrebbe arrivare entro uno o due minuti.`,
setAsDefault: 'Imposta come predefinito',
yourDefaultContactMethod:
'Questo è il tuo metodo di contatto predefinito. Prima di poterlo eliminare, devi scegliere un altro metodo di contatto e fare clic su “Imposta come predefinito”.',
@@ -1840,6 +1825,8 @@ const translations: TranslationDeepObject = {
sentryDebugDescription: 'Registra le richieste Sentry nella console',
sentryHighlightedSpanOps: 'Nomi degli span evidenziati',
sentryHighlightedSpanOpsPlaceholder: 'ui.interaction.click, navigation, ui.load',
+ leftHandNavCache: 'Cache della navigazione sinistra',
+ clearleftHandNavCache: 'Cancella',
},
debugConsole: {
saveLog: 'Salva registro',
@@ -2129,6 +2116,12 @@ const translations: TranslationDeepObject = {
shareBankAccountEmptyTitle: 'Nessun amministratore disponibile',
shareBankAccountEmptyDescription: "Non ci sono amministratori dell'area di lavoro con cui puoi condividere questo conto bancario.",
shareBankAccountNoAdminsSelected: 'Seleziona un amministratore prima di continuare',
+ unshareBankAccount: 'Annulla condivisione conto bancario',
+ unshareBankAccountDescription:
+ "Tutti i seguenti hanno accesso a questo conto bancario. Puoi revocare l'accesso in qualsiasi momento. Completeremo comunque tutti i pagamenti in corso.",
+ unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} perderà l'accesso a questo conto bancario aziendale. Completeremo comunque tutti i pagamenti in corso.`,
+ reachOutForHelp: 'È in uso con la carta Expensify. Contatta il Concierge se devi revocare la condivisione.',
+ unshareErrorModalTitle: 'Impossibile revocare la condivisione del conto bancario',
},
cardPage: {
expensifyCard: 'Carta Expensify',
@@ -2172,7 +2165,7 @@ const translations: TranslationDeepObject = {
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `Aggiunto al Wallet ${platform}`,
cardDetailsLoadingFailure: 'Si è verificato un errore durante il caricamento dei dettagli della carta. Controlla la tua connessione a Internet e riprova.',
validateCardTitle: 'Verifichiamo che tu sia davvero tu',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
+ enterMagicCode: (contactMethod: string) =>
`Inserisci il codice magico inviato a ${contactMethod} per visualizzare i dettagli della tua carta. Dovrebbe arrivare entro uno o due minuti.`,
missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `Per favore aggiungi i tuoi dati personali , poi riprova.`,
unexpectedError: 'Si è verificato un errore durante il tentativo di recuperare i dettagli della tua carta Expensify. Riprova.',
@@ -3109,6 +3102,7 @@ ${
currencyHeader: 'Qual è la valuta del tuo conto bancario?',
confirmationStepHeader: 'Controlla le tue informazioni.',
confirmationStepSubHeader: 'Controlla attentamente i dettagli qui sotto e seleziona la casella delle condizioni per confermare.',
+ toGetStarted: 'Aggiungi un conto bancario personale per ricevere rimborsi, pagare fatture o abilitare il portafoglio Expensify.',
},
addPersonalBankAccountPage: {
enterPassword: 'Inserisci la password di Expensify',
@@ -3223,7 +3217,7 @@ ${
sendingFundsDetails: 'Non è prevista alcuna commissione per inviare fondi a un altro titolare di conto utilizzando il tuo saldo, conto bancario o carta di debito.',
electronicFundsStandardDetails:
'Non ci sono commissioni per trasferire fondi dal tuo Expensify Wallet al tuo conto bancario utilizzando l’opzione standard. Questo trasferimento di solito viene completato entro 1-3 giorni lavorativi.',
- electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
+ electronicFundsInstantDetails: (percentage: string, amount: string) =>
'È prevista una commissione per trasferire fondi dal tuo Wallet Expensify alla carta di debito collegata utilizzando l’opzione di trasferimento istantaneo. Questo trasferimento di solito viene completato entro pochi minuti.' +
`La commissione è pari al ${percentage}% dell'importo del trasferimento (con una commissione minima di ${amount}).`,
fdicInsuranceBancorp: ({amount}: TermsParams) =>
@@ -3852,9 +3846,9 @@ ${
lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - Ultima sincronizzazione ${formattedDate}`,
authenticationError: (connectionName: string) => `Impossibile connettersi a ${connectionName} a causa di un errore di autenticazione.`,
learnMore: 'Scopri di più',
- memberAlternateText: 'I membri possono inviare e approvare i report.',
- adminAlternateText: 'Gli amministratori hanno pieno accesso di modifica a tutti i report e alle impostazioni dello spazio di lavoro.',
- auditorAlternateText: 'I revisori possono visualizzare e commentare i report.',
+ memberAlternateText: 'Invia e approva i report.',
+ adminAlternateText: 'Gestisci i report e le impostazioni dello spazio di lavoro.',
+ auditorAlternateText: 'Visualizza e commenta i report.',
roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
@@ -3938,9 +3932,8 @@ ${
importPerDiemRates: 'Importa tariffe di diaria',
editPerDiemRate: 'Modifica tariffa diaria',
editPerDiemRates: 'Modifica tariffe di diaria',
- editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) =>
- `L’aggiornamento di questa destinazione la modificherà per tutte le sottotariffe di diaria ${destination}.`,
- editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `L’aggiornamento di questa valuta la modificherà per tutte le sottotariffe di diaria ${destination}.`,
+ editDestinationSubtitle: (destination: string) => `L’aggiornamento di questa destinazione la modificherà per tutte le sottotariffe di diaria ${destination}.`,
+ editCurrencySubtitle: (destination: string) => `L’aggiornamento di questa valuta la modificherà per tutte le sottotariffe di diaria ${destination}.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Imposta come le spese anticipate vengono esportate in QuickBooks Desktop.',
@@ -4551,7 +4544,7 @@ ${
importJobs: 'Importa progetti',
customers: 'clienti',
jobs: 'progetti',
- label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join('e')}, ${importType}`,
+ label: (importFields: string[], importType: string) => `${importFields.join('e')}, ${importType}`,
},
importTaxDescription: 'Importa gruppi di imposte da NetSuite.',
importCustomFields: {
@@ -4963,7 +4956,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
emptyCategories: {
title: 'Non hai ancora creato nessuna categoria',
subtitle: 'Aggiungi una categoria per organizzare le tue spese.',
- subtitleWithAccounting: ({accountingPageURL}: EmptyCategoriesSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`Le tue categorie sono attualmente in fase di importazione da una connessione contabile. Vai alla pagina contabilità per apportare modifiche. `,
},
updateFailureMessage: 'Si è verificato un errore durante l’aggiornamento della categoria, riprova per favore',
@@ -5276,7 +5269,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
// We need to remove the subtitle and use the below one when we remove the canUseMultiLevelTags beta
subtitle: 'Aggiungi un tag per tenere traccia di progetti, sedi, reparti e altro.',
subtitleHTML: `Aggiungi tag per monitorare progetti, sedi, reparti e altro. Scopri di più sulla formattazione dei file di tag per l’importazione. `,
- subtitleWithAccounting: ({accountingPageURL}: EmptyTagsSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`I tuoi tag vengono attualmente importati da una connessione contabile. Vai su contabilità per apportare eventuali modifiche. `,
},
deleteTag: 'Elimina etichetta',
@@ -5537,7 +5530,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
}
}
},
- errorODIntegration: ({oldDotPolicyConnectionsURL}: ErrorODIntegrationParams) =>
+ errorODIntegration: (oldDotPolicyConnectionsURL: string) =>
`Si è verificato un errore con una connessione configurata in Expensify Classic. [Vai a Expensify Classic per risolvere questo problema.](${oldDotPolicyConnectionsURL})`,
goToODToSettings: 'Vai a Expensify Classic per gestire le tue impostazioni.',
setup: 'Connetti',
@@ -5593,6 +5586,20 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
connectPrompt: ({connectionName}: ConnectionNameParams) =>
`Sei sicuro di voler collegare ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'questa integrazione contabile'}? Questo rimuoverà tutte le connessioni contabili esistenti.`,
enterCredentials: 'Inserisci le tue credenziali',
+ claimOffer: {
+ badgeText: 'Offerta disponibile!',
+ xero: {
+ headline: 'Ottieni Xero gratis per 6 mesi!',
+ description: 'Nuovo su Xero? I clienti Expensify ottengono 6 mesi gratuiti. Richiedi la tua offerta qui sotto. ',
+ connectButton: 'Connetti a Xero',
+ },
+ uber: {
+ headerTitle: 'Uber for Business',
+ headline: 'Ottieni il 5% di sconto sui viaggi Uber',
+ description: `Attiva Uber for Business tramite Expensify e risparmia il 5% su tutti i viaggi di lavoro fino a giugno. Si applicano i termini. `,
+ connectButton: 'Connetti a Uber for Business',
+ },
+ },
connections: {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
@@ -5745,7 +5752,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
continuousReconciliation: 'Riconciliazione continua',
saveHoursOnReconciliation:
'Risparmia ore di riconciliazione a ogni periodo contabile facendo sì che Expensify riconcili in modo continuo, per tuo conto, gli estratti conto e i regolamenti della Expensify Card.',
- enableContinuousReconciliation: ({accountingAdvancedSettingsLink, connectionName}: EnableContinuousReconciliationParams) =>
+ enableContinuousReconciliation: (accountingAdvancedSettingsLink: string, connectionName: string) =>
`Per abilitare la Riconciliazione continua, abilita la sincronizzazione automatica per ${connectionName}. `,
chooseReconciliationAccount: {
chooseBankAccount: 'Scegli il conto bancario con cui verranno riconciliati i pagamenti della tua Expensify Card.',
@@ -6197,8 +6204,8 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
autoPayApprovedReportsLockedSubtitle: 'Vai su Altre funzionalità e abilita i workflow, quindi aggiungi i pagamenti per sbloccare questa funzionalità.',
autoPayReportsUnderTitle: 'Pagamento automatico dei report inferiori a',
autoPayReportsUnderDescription: 'Le note spese completamente conformi inferiori a questo importo verranno rimborsate automaticamente.',
- unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `Aggiungi ${featureName} per sbloccare questa funzionalità.`,
- enableFeatureSubtitle: ({featureName, moreFeaturesLink}: FeatureNameParams) =>
+ unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `Aggiungi ${featureName} per sbloccare questa funzionalità.`,
+ enableFeatureSubtitle: (featureName: string, moreFeaturesLink?: string) =>
`Vai a [altre funzionalità](${moreFeaturesLink}) e abilita ${featureName} per sbloccare questa funzione.`,
},
categoryRules: {
@@ -6325,6 +6332,8 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
billcom: 'BILLCOM',
},
workspaceActions: {
+ changedCompanyAddress: ({newAddress, previousAddress}: {newAddress: string; previousAddress?: string}) =>
+ previousAddress ? `ha modificato l’indirizzo dell’azienda in "${newAddress}" (precedentemente "${previousAddress}")` : `imposta l’indirizzo dell’azienda su "${newAddress}"`,
addApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
`ha aggiunto ${approverName} (${approverEmail}) come approvatore per il campo ${field} "${name}"`,
deleteApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
@@ -6473,7 +6482,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
other: `ti ha rimosso dai workflow di approvazione e dalle chat delle spese di ${joinedNames}. I report inviati in precedenza resteranno disponibili per l’approvazione nella tua Inbox.`,
};
},
- demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) =>
+ demotedFromWorkspace: (policyName: string, oldRole: string) =>
`ha aggiornato il tuo ruolo in ${policyName} da ${oldRole} a utente. Sei stato rimosso da tutte le chat di spesa dei presentatori tranne che dalla tua.`,
updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `ha aggiornato la valuta predefinita in ${newCurrency} (precedentemente ${oldCurrency})`,
updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) =>
@@ -6830,6 +6839,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
selectAllMatchingItems: 'Seleziona tutti gli elementi corrispondenti',
allMatchingItemsSelected: 'Tutti gli elementi corrispondenti selezionati',
},
+ topSpenders: 'Maggiori spenditori',
},
genericErrorPage: {
title: 'Uh-oh, qualcosa è andato storto!',
@@ -6908,7 +6918,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
changeType: (oldType: string, newType: string) => `ha cambiato il tipo da ${oldType} a ${newType}`,
exportedToCSV: `esportato in CSV`,
exportedToIntegration: {
- automatic: ({label}: ExportedToIntegrationParams) => {
+ automatic: (label: string) => {
const labelTranslations: Record = {
[CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT]: translations.export.expenseLevelExport,
[CONST.REPORT.EXPORT_OPTION_LABELS.REPORT_LEVEL_EXPORT]: translations.export.reportLevelExport,
@@ -6916,13 +6926,13 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
const translatedLabel = labelTranslations[label] || label;
return `esportato in ${translatedLabel}`;
},
- automaticActionOne: ({label}: ExportedToIntegrationParams) => `esportato in ${label} tramite`,
+ automaticActionOne: (label: string) => `esportato in ${label} tramite`,
automaticActionTwo: 'impostazioni contabili',
- manual: ({label}: ExportedToIntegrationParams) => `ha contrassegnato questo report come esportato manualmente in ${label}.`,
+ manual: (label: string) => `ha contrassegnato questo report come esportato manualmente in ${label}.`,
automaticActionThree: 'e ha creato correttamente un record per',
reimburseableLink: 'spese vive',
nonReimbursableLink: 'spese su carta aziendale',
- pending: ({label}: ExportedToIntegrationParams) => `ha iniziato l’esportazione di questo report in ${label}...`,
+ pending: (label: string) => `ha iniziato l’esportazione di questo report in ${label}...`,
},
integrationsMessage: ({errorMessage, label, linkText, linkURL}: IntegrationSyncFailedParams) =>
`impossibile esportare questo report su ${label} ("${errorMessage}${linkText ? `${linkText} ` : ''}")`,
@@ -6966,6 +6976,8 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
removedConnection: ({connectionName}: ConnectionNameParams) => `connessione a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} rimossa`,
addedConnection: ({connectionName}: ConnectionNameParams) => `connesso a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
leftTheChat: 'ha lasciato la chat',
+ companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
+ `La connessione ${feedName} non funziona. Per ripristinare le importazioni delle carte, accedi alla tua banca `,
},
error: {
invalidCredentials: 'Credenziali non valide, controlla la configurazione della connessione.',
@@ -7125,6 +7137,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
error: {
selectSuggestedAddress: 'Seleziona un indirizzo suggerito o usa la posizione attuale',
},
+ odometer: {startReading: 'Inizia a leggere', endReading: 'Termina lettura', saveForLater: 'Salva per dopo', totalDistance: 'Distanza totale'},
},
reportCardLostOrDamaged: {
screenTitle: 'Pagella smarrita o danneggiata',
@@ -7438,10 +7451,10 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
},
earlyDiscount: {
claimOffer: 'Riscatta offerta',
- subscriptionPageTitle: ({discountType}: EarlyDiscountTitleParams) =>
+ subscriptionPageTitle: (discountType: number) =>
`${discountType}% di sconto sul tuo primo anno! Ti basta aggiungere una carta di pagamento e attivare un abbonamento annuale.`,
- onboardingChatTitle: ({discountType}: EarlyDiscountTitleParams) => `Offerta a tempo limitato: ${discountType}% di sconto sul primo anno!`,
- subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Richiedi entro ${days > 0 ? `${days}g :` : ''}${hours}h : ${minutes}m : ${seconds}s`,
+ onboardingChatTitle: (discountType: number) => `Offerta a tempo limitato: ${discountType}% di sconto sul primo anno!`,
+ subtitle: (days: number, hours: number, minutes: number, seconds: number) => `Richiedi entro ${days > 0 ? `${days}g :` : ''}${hours}h : ${minutes}m : ${seconds}s`,
},
},
cardSection: {
@@ -7639,9 +7652,8 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
removeCopilotConfirmation: 'Sei sicuro di voler rimuovere questo copilot?',
changeAccessLevel: 'Modifica livello di accesso',
makeSureItIsYou: 'Verifichiamo che tu sia davvero tu',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) =>
- `Inserisci il codice magico inviato a ${contactMethod} per aggiungere un copilota. Dovrebbe arrivare entro uno o due minuti.`,
- enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `Inserisci il codice magico inviato a ${contactMethod} per aggiornare il tuo copilota.`,
+ enterMagicCode: (contactMethod: string) => `Inserisci il codice magico inviato a ${contactMethod} per aggiungere un copilota. Dovrebbe arrivare entro uno o due minuti.`,
+ enterMagicCodeUpdate: (contactMethod: string) => `Inserisci il codice magico inviato a ${contactMethod} per aggiornare il tuo copilota.`,
notAllowed: 'Non così in fretta...',
noAccessMessage: dedent(`
Come copilota, non hai accesso
@@ -7809,7 +7821,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
readyForTheRealThing: 'Pronto per la cosa reale?',
getStarted: 'Inizia',
},
- employeeInviteMessage: ({name}: EmployeeInviteMessageParams) => `# ${name} ti ha invitato a provare Expensify
+ employeeInviteMessage: (name: string) => `# ${name} ti ha invitato a provare Expensify
Ehi! Ho appena ottenuto per noi *3 mesi gratis* per provare Expensify, il modo più veloce per gestire le spese.
Ecco una *ricevuta di prova* per mostrarti come funziona:`,
@@ -7920,8 +7932,17 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
addAdminError: 'Impossibile aggiungere questo membro come amministratore. Riprova.',
revokeAdminAccess: 'Revoca accesso amministratore',
cantRevokeAdminAccess: 'Impossibile revocare i privilegi di amministratore dal referente tecnico',
- error: {removeAdmin: 'Impossibile rimuovere questo utente come amministratore. Riprova.'},
+ error: {
+ removeAdmin: 'Impossibile rimuovere questo utente come amministratore. Riprova.',
+ removeDomain: 'Impossibile rimuovere questo dominio. Riprova.',
+ removeDomainNameInvalid: 'Inserisci il tuo nome di dominio per reimpostarlo.',
+ },
+ resetDomain: 'Reimposta dominio',
+ resetDomainExplanation: ({domainName}: {domainName?: string}) => `Per favore digita ${domainName} per confermare il ripristino del dominio.`,
+ enterDomainName: 'Inserisci qui il tuo nome di dominio',
+ resetDomainInfo: `Questa azione è permanente e i seguenti dati verranno eliminati: Connessioni alle carte aziendali e tutte le spese non riportate da tali carte Impostazioni SAML e di gruppo Tutti gli account, gli spazi di lavoro, i report, le spese e gli altri dati rimarranno. Nota: puoi rimuovere questo dominio dall'elenco dei tuoi domini eliminando l'email associata dalle tue modalità di contatto .`,
},
+ members: {title: 'Membri', findMember: 'Trova membro'},
},
gps: {
tooltip: 'Monitoraggio GPS in corso! Quando hai finito, interrompi il monitoraggio qui sotto.',
@@ -7944,6 +7965,19 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
confirm: 'Scarta monitoraggio distanza',
},
zeroDistanceTripModal: {title: 'Impossibile creare la spesa', prompt: 'Non puoi creare una spesa con la stessa località di partenza e di arrivo.'},
+ locationRequiredModal: {
+ title: 'Accesso alla posizione richiesto',
+ prompt: 'Consenti l’accesso alla posizione nelle impostazioni del dispositivo per avviare il tracciamento della distanza GPS.',
+ allow: 'Consenti',
+ },
+ androidBackgroundLocationRequiredModal: {
+ title: 'Accesso alla posizione in background richiesto',
+ prompt: 'Consenti l’accesso alla posizione in background nelle impostazioni del dispositivo (opzione “Consenti sempre”) per avviare il tracciamento della distanza tramite GPS.',
+ },
+ preciseLocationRequiredModal: {
+ title: 'Posizione precisa richiesta',
+ prompt: 'Per favore, abilita la “posizione precisa” nelle impostazioni del dispositivo per avviare il tracciamento della distanza GPS.',
+ },
desktop: {
title: 'Tieni traccia della distanza sul tuo telefono',
subtitle: 'Registra automaticamente miglia o chilometri con il GPS e trasforma i viaggi in spese all’istante.',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 2ed752b2245e..bddb954ab7f7 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -21,28 +21,13 @@ import type en from './en';
import type {
ChangeFieldParams,
ConnectionNameParams,
- CustomersOrJobsLabelParams,
+ CreatedReportForUnapprovedTransactionsParams,
DelegateRoleParams,
DeleteActionParams,
DeleteConfirmationParams,
- DeleteTransactionParams,
- DemotedFromWorkspaceParams,
- DidSplitAmountMessageParams,
- EarlyDiscountSubtitleParams,
- EarlyDiscountTitleParams,
EditActionParams,
- EditDestinationSubtitleParams,
- ElectronicFundsParams,
- EmployeeInviteMessageParams,
- EmptyCategoriesSubtitleWithAccountingParams,
- EmptyTagsSubtitleWithAccountingParams,
- EnableContinuousReconciliationParams,
- EnterMagicCodeParams,
- ErrorODIntegrationParams,
ExportAgainModalDescriptionParams,
- ExportedToIntegrationParams,
ExportIntegrationSelectedParams,
- FeatureNameParams,
FileLimitParams,
FileTypeParams,
FiltersAmountBetweenParams,
@@ -97,6 +82,7 @@ import type {
OptionalParam,
OurEmailProviderParams,
OwnerOwesAmountParams,
+ PaidElsewhereParams,
ParentNavigationSummaryParams,
PayAndDowngradeDescriptionParams,
PayerOwesParams,
@@ -123,7 +109,6 @@ import type {
ReportFieldParams,
ReportPolicyNameParams,
RequestAmountParams,
- RequestedAmountMessageParams,
RequiredFieldParams,
ResolutionConstraintsParams,
ReviewParams,
@@ -270,6 +255,7 @@ const translations: TranslationDeepObject = {
dismiss: '閉じる',
// @context Used on a button to continue an action or workflow, not the formal or procedural sense of “to proceed.”
proceed: '続行',
+ unshare: '共有解除',
yes: 'はい',
no: 'いいえ',
// @context Universal confirmation button. Keep the UI-standard term “OK” unless the locale strongly prefers an alternative.
@@ -669,6 +655,7 @@ const translations: TranslationDeepObject = {
reimbursableTotal: '経費精算対象の合計',
nonReimbursableTotal: '非払い戻し合計',
originalAmount: '元の金額',
+ insights: 'インサイト',
},
supportalNoAccess: {
title: 'ちょっと待ってください',
@@ -933,6 +920,8 @@ const translations: TranslationDeepObject = {
asCopilot: 'のコパイロットとして',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`選択した頻度では提出できなかった ${reportName} のすべての経費をまとめるためにこのレポートを作成しました`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `${reportName} から保留中の経費のためにこのレポートを作成しました`,
},
mentionSuggestions: {
hereAlternateText: 'この会話の全員に通知',
@@ -989,15 +978,7 @@ const translations: TranslationDeepObject = {
subscription: 'サブスクリプション',
domains: 'ドメイン',
},
- tabSelector: {
- chat: 'チャット',
- room: '部屋',
- distance: '距離',
- manual: '手動',
- scan: 'スキャン',
- map: '地図',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'チャット', room: '部屋', distance: '距離', manual: '手動', scan: 'スキャン', map: '地図', gps: 'GPS', odometer: 'オドメーター'},
spreadsheet: {
upload: 'スプレッドシートをアップロード',
import: 'スプレッドシートをインポート',
@@ -1145,11 +1126,10 @@ const translations: TranslationDeepObject = {
posted: '投稿済み',
deleteReceipt: '領収書を削除',
findExpense: '経費を検索',
- deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `経費を削除しました(${merchant} への ${amount})`,
+ deletedTransaction: (amount: string, merchant: string) => `経費を削除しました(${merchant} への ${amount})`,
movedFromReport: ({reportName}: MovedFromReportParams) => `経費を移動しました${reportName ? `${reportName} から` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `この経費を移動しました${reportName ? `${reportName} へ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `この経費を移動しました${reportName ? `${reportName} から` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `この経費を個人スペース から移動しました`,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `この経費はあなたのパーソナルスペース に移動されました`,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1240,13 +1220,13 @@ const translations: TranslationDeepObject = {
finished: '完了',
flip: '反転',
sendInvoice: ({amount}: RequestAmountParams) => `${amount} の請求書を送信`,
- expenseAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? `${comment} 用` : ''}`,
+ expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `${comment} 用` : ''}`,
submitted: ({memo}: SubmittedWithMemoParams) => `送信済み${memo ? `、メモ「${memo}」と述べています` : ''}`,
automaticallySubmitted: `提出を遅らせる を通じて送信されました`,
queuedToSubmitViaDEW: 'カスタム承認ワークフローを介して送信待ちキューに入れられました',
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? `${comment} 用` : ''} を追跡中`,
+ trackedAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `${comment} 用` : ''} を追跡中`,
splitAmount: ({amount}: SplitAmountParams) => `${amount} を分割`,
- didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `分割 ${formattedAmount}${comment ? `${comment} 用` : ''}`,
+ didSplitAmount: (formattedAmount: string, comment: string) => `分割 ${formattedAmount}${comment ? `${comment} 用` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `あなたの分担額 ${amount}`,
payerOwesAmount: (amount: number | string, payer: string, comment?: string) => `${payer} の未払い金額は ${amount}${comment ? `${comment} 用` : ''}`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} の負担額:`,
@@ -1271,7 +1251,7 @@ const translations: TranslationDeepObject = {
`${submitterDisplayName} が30日以内に Expensify Wallet を有効化しなかったため、${amount} の支払いはキャンセルされました`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
`${submitterDisplayName} が銀行口座を追加しました。${amount} の支払いが行われました。`,
- paidElsewhere: (payer?: string) => `${payer ? `${payer} ` : ''}は支払済みにマークされました`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}は支払済みにマークされました${comment ? `、「${comment}」と言っています` : ''}`,
paidWithExpensify: (payer?: string) => `${payer ? `${payer} ` : ''}はウォレットで支払い済み`,
automaticallyPaidWithExpensify: (payer?: string) =>
`${payer ? `${payer} ` : ''}はワークスペースルール を通じてExpensifyで支払われました`,
@@ -1325,6 +1305,10 @@ const translations: TranslationDeepObject = {
invalidRate: 'このワークスペースでは無効なレートです。ワークスペースから利用可能なレートを選択してください。',
endDateBeforeStartDate: '終了日は開始日より前にはできません',
endDateSameAsStartDate: '終了日は開始日と同じにはできません',
+ manySplitsProvided: `許可される最大分割数は${CONST.IOU.SPLITS_LIMIT}です。`,
+ dateRangeExceedsMaxDays: `期間は${CONST.IOU.SPLITS_LIMIT}日を超えることはできません。`,
+ invalidReadings: '開始と終了の両方の読みを入力してください',
+ negativeDistanceNotAllowed: '終了値は開始値より大きくなければなりません',
},
dismissReceiptError: 'エラーを閉じる',
dismissReceiptErrorConfirmation: '注意!このエラーを無視すると、アップロードした領収書が完全に削除されます。本当に実行しますか?',
@@ -1476,6 +1460,7 @@ const translations: TranslationDeepObject = {
splitDateRange: ({startDate, endDate, count}: SplitDateRangeParams) => `${startDate} から ${endDate} まで(${count} 日間)`,
splitByDate: '日付で分割',
routedDueToDEW: ({to}: RoutedDueToDEWParams) => `カスタム承認ワークフローにより、${to} 宛にルーティングされたレポート`,
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? '時間' : '時間'} @ ${rate} / 時間`, hrs: '時間'},
},
transactionMerge: {
listPage: {
@@ -1722,7 +1707,7 @@ const translations: TranslationDeepObject = {
`Expensify にログインしたり、領収書を送信したりする方法をさらに追加しましょう。${email} に領収書を転送するメールアドレスを追加するか、電話番号を追加して領収書を 47777 にテキスト送信してください(米国の電話番号のみ)。`,
pleaseVerify: 'この連絡方法を確認してください。',
getInTouch: '今後のご連絡はこの方法で行います。',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届きます。`,
+ enterMagicCode: (contactMethod: string) => `${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届きます。`,
setAsDefault: 'デフォルトに設定',
yourDefaultContactMethod: 'これは現在の既定の連絡方法です。削除する前に、別の連絡方法を選択して「既定として設定」をクリックする必要があります。',
removeContactMethod: '連絡方法を削除',
@@ -1840,6 +1825,8 @@ const translations: TranslationDeepObject = {
sentryDebugDescription: 'Sentryリクエストをコンソールに記録',
sentryHighlightedSpanOps: 'ハイライト表示するspan名',
sentryHighlightedSpanOpsPlaceholder: 'ui.interaction.click, navigation, ui.load',
+ leftHandNavCache: '左側ナビキャッシュ',
+ clearleftHandNavCache: 'クリア',
},
debugConsole: {
saveLog: 'ログを保存',
@@ -2126,6 +2113,11 @@ const translations: TranslationDeepObject = {
shareBankAccountEmptyTitle: '管理者がいません',
shareBankAccountEmptyDescription: 'この銀行口座を共有できるワークスペース管理者がいません',
shareBankAccountNoAdminsSelected: '続行する前に管理者を選択してください',
+ unshareBankAccount: '銀行口座の共有を解除してください',
+ unshareBankAccountDescription: '以下の全員がこの銀行口座にアクセスできます。いつでもアクセスを削除できます。処理中のお支払いは引き続き完了します。',
+ unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} はこのビジネス銀行口座にアクセスできなくなります。処理中のお支払いは引き続き完了します。`,
+ reachOutForHelp: 'この口座は Expensify カードで使用されています。共有を解除する必要がある場合は、コンシェルジュまでお問い合わせください 。',
+ unshareErrorModalTitle: '銀行口座の共有を解除できません',
},
cardPage: {
expensifyCard: 'Expensify Card',
@@ -2167,7 +2159,7 @@ const translations: TranslationDeepObject = {
cardAddedToWallet: ({platform}: {platform: 'Google' | 'Apple'}) => `${platform}ウォレットに追加しました`,
cardDetailsLoadingFailure: 'カードの詳細を読み込む際にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。',
validateCardTitle: 'あなた本人であることを確認しましょう',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `カード情報を表示するには、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`,
+ enterMagicCode: (contactMethod: string) => `カード情報を表示するには、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`,
missingPrivateDetails: ({missingDetailsLink}: {missingDetailsLink: string}) => `個人情報を追加 してから、もう一度お試しください。`,
unexpectedError: 'Expensifyカードの詳細を取得中にエラーが発生しました。もう一度お試しください。',
cardFraudAlert: {
@@ -3101,6 +3093,7 @@ ${
currencyHeader: 'あなたの銀行口座の通貨は何ですか?',
confirmationStepHeader: '情報を確認してください。',
confirmationStepSubHeader: '以下の詳細を再確認し、確認するには利用規約のチェックボックスをオンにしてください。',
+ toGetStarted: '払い戻しを受け取ったり、請求書を支払ったり、Expensify Wallet を有効にしたりするには、個人の銀行口座を追加します。',
},
addPersonalBankAccountPage: {
enterPassword: 'Expensify のパスワードを入力',
@@ -3214,7 +3207,7 @@ ${
sendingFundsTitle: '別のアカウント保有者への送金',
sendingFundsDetails: '残高、銀行口座、またはデビットカードを使って他のアカウント保有者に送金しても、手数料はかかりません。',
electronicFundsStandardDetails: '標準オプションを利用してExpensifyウォレットから銀行口座へ資金を振り込む場合、手数料はかかりません。通常、この振込は1~3営業日以内に完了します。',
- electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
+ electronicFundsInstantDetails: (percentage: string, amount: string) =>
'即時振込オプションを使用して、Expensifyウォレットからリンク済みデビットカードへ資金を振り替える場合、手数料が発生します。通常、この振込は数分以内に完了します。' +
`手数料は送金額の${percentage}%(最低手数料${amount})です。`,
fdicInsuranceBancorp: ({amount}: TermsParams) =>
@@ -3842,9 +3835,9 @@ ${
lastSyncDate: ({connectionName, formattedDate}: LastSyncDateParams) => `${connectionName} - 最終同期日 ${formattedDate}`,
authenticationError: (connectionName: string) => `認証エラーのため、${connectionName} に接続できません。`,
learnMore: '詳細はこちら',
- memberAlternateText: 'メンバーはレポートを提出および承認できます。',
- adminAlternateText: '管理者は、すべてのレポートとワークスペース設定を完全に編集できます。',
- auditorAlternateText: '監査担当者はレポートを閲覧し、コメントすることができます。',
+ memberAlternateText: 'レポートを提出して承認する。',
+ adminAlternateText: 'レポートとワークスペース設定を管理します。',
+ auditorAlternateText: 'レポートを表示してコメントする。',
roleName: ({role}: OptionalParam = {}) => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
@@ -3927,8 +3920,8 @@ ${
importPerDiemRates: '日当レートをインポート',
editPerDiemRate: '日当レートを編集',
editPerDiemRates: '日当レートを編集',
- editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `この宛先を更新すると、すべての${destination}の日当サブレートに適用されます。`,
- editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `この通貨を更新すると、すべての${destination}の日当サブレートが変更されます。`,
+ editDestinationSubtitle: (destination: string) => `この宛先を更新すると、すべての${destination}の日当サブレートに適用されます。`,
+ editCurrencySubtitle: (destination: string) => `この通貨を更新すると、すべての${destination}の日当サブレートが変更されます。`,
},
qbd: {
exportOutOfPocketExpensesDescription: '自己負担経費を QuickBooks Desktop にどのようにエクスポートするかを設定します。',
@@ -4525,7 +4518,7 @@ ${
importJobs: 'プロジェクトをインポート',
customers: '顧客',
jobs: 'プロジェクト',
- label: ({importFields, importType}: CustomersOrJobsLabelParams) => `${importFields.join('と')}, ${importType}`,
+ label: (importFields: string[], importType: string) => `${importFields.join('と')}, ${importType}`,
},
importTaxDescription: 'NetSuite から税グループをインポートします。',
importCustomFields: {
@@ -4934,7 +4927,7 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
emptyCategories: {
title: 'まだカテゴリを作成していません',
subtitle: '支出を整理するためにカテゴリを追加してください。',
- subtitleWithAccounting: ({accountingPageURL}: EmptyCategoriesSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`現在、カテゴリーは会計接続からインポートされています。変更を加えるには、会計 に移動してください。 `,
},
updateFailureMessage: 'カテゴリーの更新中にエラーが発生しました。もう一度お試しください',
@@ -5243,7 +5236,7 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
// We need to remove the subtitle and use the below one when we remove the canUseMultiLevelTags beta
subtitle: 'タグを追加して、プロジェクト、勤務地、部門などを追跡しましょう。',
subtitleHTML: `タグを追加して、プロジェクト、所在地、部署などを追跡しましょう。インポート用のタグファイルの書式設定については、詳しくはこちら をご覧ください。 `,
- subtitleWithAccounting: ({accountingPageURL}: EmptyTagsSubtitleWithAccountingParams) =>
+ subtitleWithAccounting: (accountingPageURL: string) =>
`現在、タグは会計連携からインポートされています。変更を行うには、会計 に移動してください。 `,
},
deleteTag: 'タグを削除',
@@ -5503,7 +5496,7 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
}
}
},
- errorODIntegration: ({oldDotPolicyConnectionsURL}: ErrorODIntegrationParams) =>
+ errorODIntegration: (oldDotPolicyConnectionsURL: string) =>
`Expensify Classic で設定された接続にエラーがあります。[この問題を解決するには Expensify Classic に移動してください。](${oldDotPolicyConnectionsURL})`,
goToODToSettings: '設定を管理するには、Expensify Classic に移動してください。',
setup: '接続',
@@ -5558,6 +5551,21 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
connectPrompt: ({connectionName}: ConnectionNameParams) =>
`${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'この会計統合'} を接続してもよろしいですか?これにより、既存の会計連携はすべて削除されます。`,
enterCredentials: '認証情報を入力してください',
+ claimOffer: {
+ badgeText: 'オファー利用可能!',
+ xero: {
+ headline: 'Xero を6か月間無料で利用!',
+ description:
+ 'Xero を初めてご利用ですか?Expensify のお客様は6か月間無料でご利用いただけます。以下のオファーを獲得してください。 ',
+ connectButton: 'Xero に接続',
+ },
+ uber: {
+ headerTitle: 'Uber for Business',
+ headline: 'Uber の乗車で5%割引',
+ description: `Expensify を通じて Uber for Business を有効化すると、6月までのすべてのビジネス乗車で5%割引になります。条件が適用されます。 `,
+ connectButton: 'Uber for Business に接続',
+ },
+ },
connections: {
syncStageName: ({stage}: SyncStageNameConnectionsParams) => {
switch (stage) {
@@ -5709,7 +5717,7 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
reconciliationAccount: '照合勘定',
continuousReconciliation: '継続的な照合',
saveHoursOnReconciliation: '各会計期間ごとの照合作業にかかる時間を大幅に削減できます。Expensify が、あなたに代わって Expensify Card の明細と精算を継続的に照合します。',
- enableContinuousReconciliation: ({accountingAdvancedSettingsLink, connectionName}: EnableContinuousReconciliationParams) =>
+ enableContinuousReconciliation: (accountingAdvancedSettingsLink: string, connectionName: string) =>
`継続的な照合を有効にするには、${connectionName} の自動同期 を有効にしてください。 `,
chooseReconciliationAccount: {
chooseBankAccount: 'Expensify Card の支払いを照合する銀行口座を選択してください。',
@@ -6155,8 +6163,8 @@ ${reportName}
autoPayApprovedReportsLockedSubtitle: '「その他の機能」に移動してワークフローを有効にし、その後「支払い」を追加してこの機能を有効化してください。',
autoPayReportsUnderTitle: '以下のレポートを自動支払い',
autoPayReportsUnderDescription: 'この金額以下の、要件を完全に満たした経費精算書は自動的に支払われます。',
- unlockFeatureEnableWorkflowsSubtitle: ({featureName}: FeatureNameParams) => `${featureName} を追加して、この機能を有効にしてください。`,
- enableFeatureSubtitle: ({featureName, moreFeaturesLink}: FeatureNameParams) =>
+ unlockFeatureEnableWorkflowsSubtitle: (featureName: string) => `${featureName} を追加して、この機能を有効にしてください。`,
+ enableFeatureSubtitle: (featureName: string, moreFeaturesLink?: string) =>
`[その他の機能](${moreFeaturesLink})に移動し、${featureName} を有効にしてこの機能をアンロックしてください。`,
},
categoryRules: {
@@ -6280,6 +6288,8 @@ ${reportName}
billcom: 'Bill.com',
},
workspaceActions: {
+ changedCompanyAddress: ({newAddress, previousAddress}: {newAddress: string; previousAddress?: string}) =>
+ previousAddress ? `会社住所を「${newAddress}」(以前は「${previousAddress}」)に変更しました` : `会社の住所を「${newAddress}」に設定`,
addApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) => `${field}「${name}」の承認者として${approverName}(${approverEmail})を追加しました`,
deleteApprovalRule: (approverEmail: string, approverName: string, field: string, name: string) =>
`${field}「${name}」の承認者として${approverName}(${approverEmail})を削除しました`,
@@ -6421,7 +6431,7 @@ ${reportName}
other: `${joinedNames} の承認ワークフローと経費チャットからあなたを削除しました。以前に提出されたレポートは、引き続き受信トレイで承認可能な状態で残ります。`,
};
},
- demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) =>
+ demotedFromWorkspace: (policyName: string, oldRole: string) =>
`${policyName} 内でのあなたのロールが、${oldRole} からユーザーに更新されました。あなた自身のものを除き、すべての精算者の経費チャットから削除されています。`,
updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `デフォルト通貨を${newCurrency}(以前は${oldCurrency})に更新しました`,
updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) => `自動レポート頻度を「${newFrequency}」(以前は「${oldFrequency}」)に更新しました`,
@@ -6775,6 +6785,7 @@ ${reportName}
selectAllMatchingItems: '一致する項目をすべて選択',
allMatchingItemsSelected: '一致する項目をすべて選択済み',
},
+ topSpenders: 'トップ支出者',
},
genericErrorPage: {
title: 'おっと、問題が発生しました!',
@@ -6852,7 +6863,7 @@ ${reportName}
changeType: (oldType: string, newType: string) => `${oldType} から ${newType} に変更しました`,
exportedToCSV: `CSV にエクスポート済み`,
exportedToIntegration: {
- automatic: ({label}: ExportedToIntegrationParams) => {
+ automatic: (label: string) => {
const labelTranslations: Record = {
[CONST.REPORT.EXPORT_OPTION_LABELS.EXPENSE_LEVEL_EXPORT]: translations.export.expenseLevelExport,
[CONST.REPORT.EXPORT_OPTION_LABELS.REPORT_LEVEL_EXPORT]: translations.export.reportLevelExport,
@@ -6860,13 +6871,13 @@ ${reportName}
const translatedLabel = labelTranslations[label] || label;
return `${translatedLabel} にエクスポートしました`;
},
- automaticActionOne: ({label}: ExportedToIntegrationParams) => `${label} にエクスポート済み(経由)`,
+ automaticActionOne: (label: string) => `${label} にエクスポート済み(経由)`,
automaticActionTwo: '会計設定',
- manual: ({label}: ExportedToIntegrationParams) => `このレポートを、${label} へ手動エクスポート済みとしてマークしました。`,
+ manual: (label: string) => `このレポートを、${label} へ手動エクスポート済みとしてマークしました。`,
automaticActionThree: 'のレコードを正常に作成しました',
reimburseableLink: '立替経費',
nonReimbursableLink: '会社カード経費',
- pending: ({label}: ExportedToIntegrationParams) => `このレポートの${label}へのエクスポートを開始しました…`,
+ pending: (label: string) => `このレポートの${label}へのエクスポートを開始しました…`,
},
integrationsMessage: ({errorMessage, label, linkText, linkURL}: IntegrationSyncFailedParams) =>
`このレポートを${label}にエクスポートできませんでした("${errorMessage}${linkText ? `${linkText} ` : ''}")`,
@@ -6910,6 +6921,8 @@ ${reportName}
removedConnection: ({connectionName}: ConnectionNameParams) => `${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} への接続を削除しました`,
addedConnection: ({connectionName}: ConnectionNameParams) => `${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} に接続済み`,
leftTheChat: 'チャットを退出しました',
+ companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
+ `${feedName} との接続が切断されています。カードの取引明細の取り込みを再開するには、銀行にログイン してください`,
},
error: {
invalidCredentials: '認証情報が無効です。接続の設定を確認してください。',
@@ -7069,6 +7082,7 @@ ${reportName}
error: {
selectSuggestedAddress: '候補の住所を選択するか、現在地を使用してください',
},
+ odometer: {startReading: '読み始める', endReading: '読み取り終了', saveForLater: '後で保存', totalDistance: '合計距離'},
},
reportCardLostOrDamaged: {
screenTitle: '成績証明書の紛失または損傷',
@@ -7379,10 +7393,9 @@ ${reportName}
},
earlyDiscount: {
claimOffer: 'オファーを獲得',
- subscriptionPageTitle: ({discountType}: EarlyDiscountTitleParams) =>
- `初年度が${discountType}%オフ! 支払いカードを追加して、年額サブスクリプションを開始しましょう。`,
- onboardingChatTitle: ({discountType}: EarlyDiscountTitleParams) => `期間限定オファー:初年度が${discountType}%オフ!`,
- subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `${days > 0 ? `${days}日 :` : ''}${hours}時間 : ${minutes}分 : ${seconds}秒以内に申請`,
+ subscriptionPageTitle: (discountType: number) => `初年度が${discountType}%オフ! 支払いカードを追加して、年額サブスクリプションを開始しましょう。`,
+ onboardingChatTitle: (discountType: number) => `期間限定オファー:初年度が${discountType}%オフ!`,
+ subtitle: (days: number, hours: number, minutes: number, seconds: number) => `${days > 0 ? `${days}日 :` : ''}${hours}時間 : ${minutes}分 : ${seconds}秒以内に申請`,
},
},
cardSection: {
@@ -7583,8 +7596,8 @@ ${reportName}
removeCopilotConfirmation: 'このコパイロットを削除してもよろしいですか?',
changeAccessLevel: 'アクセス権限レベルを変更',
makeSureItIsYou: 'あなた本人であることを確認しましょう',
- enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `コパイロットを追加するために、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`,
- enterMagicCodeUpdate: ({contactMethod}: EnterMagicCodeParams) => `${contactMethod} に送信されたマジックコードを入力して、あなたのコパイロットを更新してください。`,
+ enterMagicCode: (contactMethod: string) => `コパイロットを追加するために、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`,
+ enterMagicCodeUpdate: (contactMethod: string) => `${contactMethod} に送信されたマジックコードを入力して、あなたのコパイロットを更新してください。`,
notAllowed: 'ちょっと待ってください…',
noAccessMessage: dedent(`
このページには、コパイロットとしてアクセスできません。申し訳ありません。
@@ -7751,7 +7764,7 @@ ${reportName}
readyForTheRealThing: '本番の準備はできましたか?',
getStarted: 'はじめる',
},
- employeeInviteMessage: ({name}: EmployeeInviteMessageParams) => `# ${name} があなたを Expensify のお試しに招待しました
+ employeeInviteMessage: (name: string) => `# ${name} があなたを Expensify のお試しに招待しました
やあ!経費処理を最速で行える Expensify を、*3 か月無料* でお試しできるようにしておいたよ。
Expensify の使い方をお見せするための*テストレシート*がこちらです。`,
@@ -7858,8 +7871,17 @@ Expensify の使い方をお見せするための*テストレシート*がこ
addAdminError: 'このメンバーを管理者として追加できません。もう一度お試しください。',
revokeAdminAccess: '管理者アクセスを取り消す',
cantRevokeAdminAccess: '技術連絡先から管理者アクセス権を取り消すことはできません',
- error: {removeAdmin: 'このユーザーを管理者として削除できません。もう一度お試しください。'},
+ error: {
+ removeAdmin: 'このユーザーを管理者として削除できません。もう一度お試しください。',
+ removeDomain: 'このドメインを削除できません。もう一度お試しください。',
+ removeDomainNameInvalid: 'リセットするドメイン名を入力してください。',
+ },
+ resetDomain: 'ドメインをリセット',
+ resetDomainExplanation: ({domainName}: {domainName?: string}) => `ドメインのリセットを確認するため、${domainName} と入力してください。`,
+ enterDomainName: 'ここにドメイン名を入力してください',
+ resetDomainInfo: `この操作は永久的 であり、次のデータが削除されます: 会社カードの接続およびそれらのカードからの未報告の経費 SAML とグループ設定 すべてのアカウント、ワークスペース、レポート、経費、およびその他のデータは保持されます。 注:関連付けられているメールアドレスを連絡先方法 から削除することで、このドメインをドメイン一覧から消去できます。`,
},
+ members: {title: 'メンバー', findMember: 'メンバーを検索'},
},
gps: {
tooltip: 'GPS 追跡を進行中です!完了したら、下で追跡を停止してください。',
@@ -7873,6 +7895,12 @@ Expensify の使い方をお見せするための*テストレシート*がこ
stopGpsTrackingModal: {title: 'GPS追跡を停止', prompt: '本当に終了しますか?現在のジャーニーが終了します。', cancel: '追跡を再開', confirm: 'GPS追跡を停止'},
discardDistanceTrackingModal: {title: '距離の追跡を破棄', prompt: '本当に実行しますか?現在の行程が破棄され、元に戻すことはできません。', confirm: '距離の追跡を破棄'},
zeroDistanceTripModal: {title: '経費を作成できません', prompt: '開始地点と終了地点が同じ経路では経費を作成できません。'},
+ locationRequiredModal: {title: '位置情報へのアクセスが必要です', prompt: 'GPS で距離を追跡するには、デバイスの設定で位置情報へのアクセスを許可してください。', allow: '許可'},
+ androidBackgroundLocationRequiredModal: {
+ title: 'バックグラウンド位置情報へのアクセスが必要です',
+ prompt: 'GPS距離の追跡を開始するには、デバイスの設定でバックグラウンドの位置情報アクセスを許可し(「常に許可」オプション)、有効にしてください。',
+ },
+ preciseLocationRequiredModal: {title: '正確な位置情報が必要です', prompt: 'GPS距離の追跡を開始するには、デバイスの設定で「正確な位置情報」を有効にしてください。'},
desktop: {title: 'スマートフォンで距離を記録する', subtitle: 'GPS で自動的にマイルまたはキロメートルを記録し、移動をすぐに経費に変換します。', button: 'アプリをダウンロード'},
},
};
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index c8608d4d4453..a0d62b075fdc 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -21,28 +21,13 @@ import type en from './en';
import type {
ChangeFieldParams,
ConnectionNameParams,
- CustomersOrJobsLabelParams,
+ CreatedReportForUnapprovedTransactionsParams,
DelegateRoleParams,
DeleteActionParams,
DeleteConfirmationParams,
- DeleteTransactionParams,
- DemotedFromWorkspaceParams,
- DidSplitAmountMessageParams,
- EarlyDiscountSubtitleParams,
- EarlyDiscountTitleParams,
EditActionParams,
- EditDestinationSubtitleParams,
- ElectronicFundsParams,
- EmployeeInviteMessageParams,
- EmptyCategoriesSubtitleWithAccountingParams,
- EmptyTagsSubtitleWithAccountingParams,
- EnableContinuousReconciliationParams,
- EnterMagicCodeParams,
- ErrorODIntegrationParams,
ExportAgainModalDescriptionParams,
- ExportedToIntegrationParams,
ExportIntegrationSelectedParams,
- FeatureNameParams,
FileLimitParams,
FileTypeParams,
FiltersAmountBetweenParams,
@@ -97,6 +82,7 @@ import type {
OptionalParam,
OurEmailProviderParams,
OwnerOwesAmountParams,
+ PaidElsewhereParams,
ParentNavigationSummaryParams,
PayAndDowngradeDescriptionParams,
PayerOwesParams,
@@ -123,7 +109,6 @@ import type {
ReportFieldParams,
ReportPolicyNameParams,
RequestAmountParams,
- RequestedAmountMessageParams,
RequiredFieldParams,
ResolutionConstraintsParams,
ReviewParams,
@@ -270,6 +255,7 @@ const translations: TranslationDeepObject = {
dismiss: 'Sluiten',
// @context Used on a button to continue an action or workflow, not the formal or procedural sense of “to proceed.”
proceed: 'Doorgaan',
+ unshare: 'Niet meer delen',
yes: 'Ja',
no: 'Nee',
// @context Universal confirmation button. Keep the UI-standard term “OK” unless the locale strongly prefers an alternative.
@@ -670,6 +656,7 @@ const translations: TranslationDeepObject = {
reimbursableTotal: 'Totaal te vergoeden',
nonReimbursableTotal: 'Niet-vergoedbaar totaal',
originalAmount: 'Oorspronkelijk bedrag',
+ insights: 'Inzichten',
},
supportalNoAccess: {
title: 'Niet zo snel',
@@ -935,6 +922,8 @@ const translations: TranslationDeepObject = {
asCopilot: 'als copiloot voor',
harvestCreatedExpenseReport: ({reportUrl, reportName}: HarvestCreatedExpenseReportParams) =>
`heeft dit rapport aangemaakt om alle uitgaven van ${reportName} op te nemen die niet konden worden ingediend met de door jou gekozen frequentie`,
+ createdReportForUnapprovedTransactions: ({reportUrl, reportName}: CreatedReportForUnapprovedTransactionsParams) =>
+ `heeft dit rapport gemaakt voor uitgestelde uitgaven van ${reportName} `,
},
mentionSuggestions: {
hereAlternateText: 'Iedereen in dit gesprek op de hoogte stellen',
@@ -990,15 +979,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abonnement',
domains: 'Domeinen',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Kamer',
- distance: 'Afstand',
- manual: 'Handmatig',
- scan: 'Scannen',
- map: 'Kaart',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Kamer', distance: 'Afstand', manual: 'Handmatig', scan: 'Scannen', map: 'Kaart', gps: 'GPS', odometer: 'Kilometerstand'},
spreadsheet: {
upload: 'Een spreadsheet uploaden',
import: 'Spreadsheet importeren',
@@ -1144,11 +1125,10 @@ const translations: TranslationDeepObject = {
posted: 'Geplaatst',
deleteReceipt: 'Bon verwijderen',
findExpense: 'Uitgave zoeken',
- deletedTransaction: ({amount, merchant}: DeleteTransactionParams) => `heeft een uitgave verwijderd (${amount} voor ${merchant})`,
+ deletedTransaction: (amount: string, merchant: string) => `heeft een uitgave verwijderd (${amount} voor ${merchant})`,
movedFromReport: ({reportName}: MovedFromReportParams) => `heeft een uitgave verplaatst${reportName ? `van ${reportName}` : ''}`,
movedTransactionTo: ({reportUrl, reportName}: MovedTransactionParams) => `heeft deze uitgave verplaatst${reportName ? `naar ${reportName} ` : ''}`,
movedTransactionFrom: ({reportUrl, reportName}: MovedTransactionParams) => `heeft deze uitgave verplaatst${reportName ? `van ${reportName} ` : ''}`,
- movedUnreportedTransaction: ({reportUrl}: MovedTransactionParams) => `heeft deze uitgave verplaatst uit je persoonlijke ruimte `,
unreportedTransaction: ({reportUrl}: MovedTransactionParams) => `heeft deze uitgave verplaatst naar je persoonlijke ruimte `,
movedAction: ({shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName}: MovedActionParams) => {
if (shouldHideMovedReportUrl) {
@@ -1239,13 +1219,13 @@ const translations: TranslationDeepObject = {
finished: 'Voltooid',
flip: 'Omdraaien',
sendInvoice: ({amount}: RequestAmountParams) => `${amount} factuur verzenden`,
- expenseAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `${formattedAmount}${comment ? `voor ${comment}` : ''}`,
+ expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? `voor ${comment}` : ''}`,
submitted: ({memo}: SubmittedWithMemoParams) => `ingediend${memo ? `, met de melding ${memo}` : ''}`,
automaticallySubmitted: `ingediend via indiening uitstellen `,
queuedToSubmitViaDEW: 'in wachtrij geplaatst om in te dienen via aangepaste goedkeuringswerkstroom',
- trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `bijhouden ${formattedAmount}${comment ? `voor ${comment}` : ''}`,
+ trackedAmount: (formattedAmount: string, comment?: string) => `bijhouden ${formattedAmount}${comment ? `voor ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `${amount} splits`,
- didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `splitsen ${formattedAmount}${comment ? `voor ${comment}` : ''}`,
+ didSplitAmount: (formattedAmount: string, comment: string) => `splitsen ${formattedAmount}${comment ? `voor ${comment}` : ''}`,
yourSplit: ({amount}: UserSplitParams) => `Jouw deel ${amount}`,
payerOwesAmount: (amount: number | string, payer: string, comment?: string) => `${payer} is ${amount}${comment ? `voor ${comment}` : ''} verschuldigd`,
payerOwes: ({payer}: PayerOwesParams) => `${payer} is verschuldigd:`,
@@ -1270,7 +1250,7 @@ const translations: TranslationDeepObject = {
`heeft de betaling van ${amount} geannuleerd, omdat ${submitterDisplayName} hun Expensify Wallet niet binnen 30 dagen heeft ingeschakeld`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
`${submitterDisplayName} heeft een bankrekening toegevoegd. De betaling van ${amount} is gedaan.`,
- paidElsewhere: (payer?: string) => `${payer ? `${payer} ` : ''}gemarkeerd als betaald`,
+ paidElsewhere: ({payer, comment}: PaidElsewhereParams = {}) => `${payer ? `${payer} ` : ''}gemarkeerd als betaald${comment ? `, met de opmerking "${comment}"` : ''}`,
paidWithExpensify: (payer?: string) => `${payer ? `${payer} ` : ''}betaald met wallet`,
automaticallyPaidWithExpensify: (payer?: string) =>
`${payer ? `${payer} ` : ''}betaald met Expensify via