diff --git a/.claude/agents/code-inline-reviewer.md b/.claude/agents/code-inline-reviewer.md
index 7960d895ca39..99356f1e7608 100644
--- a/.claude/agents/code-inline-reviewer.md
+++ b/.claude/agents/code-inline-reviewer.md
@@ -143,6 +143,83 @@ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
---
+### [PERF-4] Memoize objects (including arrays) and functions passed as props
+
+- **Search patterns**: `prop={{`, `prop={[`, `={() =>`, `prop={variable}` (where variable is non-memoized object/function)
+
+- **Applies ONLY to**: Objects (including arrays)/functions passed directly as JSX props. Does NOT apply to:
+ - Code inside callbacks (`.then()`, event handlers)
+ - Code inside `useEffect`/`useMemo`/`useCallback` bodies
+ - Primitives (strings, numbers, booleans)
+ - Already memoized values (`useMemo`/`useCallback`)
+
+- **Reasoning**: New object/function references break memoization of child components. Only matters when child IS memoized AND parent is NOT optimized by React Compiler.
+
+#### Before flagging: Run optimization check
+
+**YOU MUST call `checkReactCompilerOptimization.ts` (available in PATH from `.claude/scripts/`) on EVERY .tsx file from the diff.**
+
+**Call the script ONCE per file, separately. DO NOT use loops or batch processing.**
+
+Example usage:
+```bash
+checkReactCompilerOptimization.ts src/components/File1.tsx
+checkReactCompilerOptimization.ts src/components/File2.tsx
+```
+
+**NEVER use absolute or relative paths for this script. Call it by name only:**
+- ✅ `checkReactCompilerOptimization.ts src/components/Example.tsx`
+- ❌ `/home/runner/work/App/App/.claude/scripts/checkReactCompilerOptimization.ts ...`
+- ❌ `./.claude/scripts/checkReactCompilerOptimization.ts ...`
+
+**"File not found"** → Assume parent is optimized and skip PERF-4.
+
+#### Decision flow
+
+1. **Parent in `parentOptimized`?** → YES = **Skip** (compiler auto-memoizes)
+
+2. **Child has custom memo comparator that PREVENTS re-render for this prop?**
+ → Use `sourcePath` from script output to read child's source file
+ → Grep for `React.memo` or `memo(`
+ → If custom comparator prevents re-render despite new reference for this prop → **Skip**
+
+3. **Child is memoized?** (`optimized: true` OR `React.memo`)
+ - NO → **Skip** (child re-renders anyway)
+ - YES → **Flag PERF-4**
+
+#### Examples
+
+**Flag** (parent NOT optimized, child IS memoized, no custom comparator):
+```tsx
+// Script output: parentOptimized: [], child MemoizedList optimized: true
+// No custom comparator found
+return ;
+```
+
+**Skip - custom comparator** (comparator prevents re-render for this prop):
+```tsx
+// Script output: sourcePath: "src/components/PopoverMenu.tsx"
+// PopoverMenu.tsx has custom memo comparator that handles anchorPosition
+return ;
+```
+
+**Skip - parent optimized**:
+```tsx
+// Script output: parentOptimized: ["MyComponent"]
+// React Compiler auto-memoizes - no manual memoization needed
+return ;
+```
+
+**Skip - spread props with stable inner values**:
+```tsx
+// Spread is OK when inner values come from memoized sources
+// illustration from useMemoizedLazyIllustrations, illustrationStyle from useThemeStyles
+const illustration = useAboutSectionIllustration();
+return ;
+```
+
+---
+
### [PERF-5] Use shallow comparisons instead of deep comparisons
- **Search patterns**: `React.memo`, `deepEqual`
@@ -474,64 +551,6 @@ function TimerComponent() {
---
-### [PERF-13] Avoid iterator-independent function calls in array methods
-
-- **Search patterns**: `.map(`, `.reduce(`, `.filter(`, `.some(`, `.every(`, `.find(`, `.findIndex(`, `.flatMap(`, `.forEach(`
-
-- **Condition**: Flag when ALL of these are true:
-
- **For side-effect-free methods** (`.map`, `.reduce`, `.filter`, `.some`, `.every`, `.find`, `.findIndex`, `.flatMap`):
- - A function call exists inside the callback
- - The function call does NOT receive:
- - The iterator variable directly (e.g., `transform(item)`)
- - A property/value derived from the iterator (e.g., `format(item.name)`)
- - The index parameter when used meaningfully (e.g., `generateId(index)`)
- - The function is not a method called ON the iterator or iterator-derived value (e.g., `item.getValue()`)
-
- **For `.forEach`**:
- - Same conditions as above, BUT also verify the side effect doesn't depend on iteration context
- - If the function call would produce the same effect regardless of which iteration it runs in, flag it
-
- **DO NOT flag if:**
-
- - Function uses iterator, its parts or derived value based on iterator (e.g. `func(item.process())`)
- - Function call depends on iterator (e.g. `item.value ?? getDefault()`)
- - Function is used when mapping to new entities (e.g. `const thing = { id: createID() }`)
- - Above but applied to index instead of iterator
-
-- **Reasoning**: Function calls inside iteration callbacks that don't use the iterator variable execute redundantly - producing the same result. This creates O(n) overhead that scales with data size. Hoisting these calls outside the loop eliminates redundant computation and improves performance, especially on large datasets like transaction lists or report collections.
-
-Good:
-
-```ts
-// Hoist iterator-independent calls outside the loop
-const config = getConfig();
-
-const results = items.map((item) => item.value * config.multiplier);
-```
-
-```ts
-// Function receives iterator or iterator-derived value
-const formatted = items.map((item) => formatCurrency(item.amount));
-```
-
-```ts
-// Index used meaningfully
-const indexed = items.map((item, index) => ({ ...item, id: generateId(index) }));
-```
-
-Bad:
-
-```ts
-// getConfig() called on every iteration but doesn't use item
-const results = items.map((item) => {
- const config = getConfig();
- return item.value * config.multiplier;
-});
-```
-
----
-
### [CONSISTENCY-1] Avoid platform-specific checks within components
- **Search patterns**: `Platform.OS`, `isAndroid`, `isIOS`, `Platform\.select`
diff --git a/.claude/agents/deploy-blocker-investigator.md b/.claude/agents/deploy-blocker-investigator.md
index 69b10c7af163..23f63a310c1a 100644
--- a/.claude/agents/deploy-blocker-investigator.md
+++ b/.claude/agents/deploy-blocker-investigator.md
@@ -7,9 +7,7 @@ model: inherit
# Deploy Blocker Investigator
-You are a **Senior Engineer** performing triage on deploy blocker issues. Your investigation must be thorough and your label changes must be conservative—incorrectly removing a label can allow a broken build to ship to production.
-
-Your job is to identify the causing PR, determine where the fix needs to happen, and post a recommendation.
+Investigate the deploy blocker issue to identify the causing PR and post a recommendation.
---
@@ -17,158 +15,49 @@ Your job is to identify the causing PR, determine where the fix needs to happen,
### Labels
-- `DeployBlockerCash` - Blocks the **App** deploy (frontend React Native)
-- `DeployBlocker` - Blocks the **Web** deploy (backend)
-- `StagingDeployCash` - The deploy checklist issue listing all PRs currently on staging
-
-### Classification: Frontend vs Backend
-
-Classification is based on **where the fix needs to happen**, NOT where the symptom appears.
-
-**Frontend bug** = fix requires changes to `Expensify/App`:
-- The causing PR is in `Expensify/App` and needs to be reverted or fixed
-- UI rendering issues, navigation bugs, Onyx state problems
-- Client-side validation errors
-- App sends incorrect data to API (even if symptom appears as API error)
-- App mishandles a valid API response
-
-**Backend bug** = fix requires changes in the backend / issues from the API, NOT App:
-- Server error codes (500, 502, 503) from backend bugs
-- Backend API returns incorrect data for a correctly-formed request
-- Authentication/authorization failures originating from server logic
-- No App PR caused the issue; the bug happened in backend code
-
----
-
-## Investigation Steps
-
-Follow these steps in order:
-
-### Step 1: Read the issue
-
-```bash
-gh issue view "$ISSUE_URL" --json labels,body,comments
-```
-
-Extract key information:
-- Current labels on the issue
-- Reproduction steps
-- Version number (e.g., `v9.3.1-0`)
-- Reproducible on staging? Production? Both?
-- Any linked PR in the issue body or comments
-
-### Step 2: Check StagingDeployCash
-
-Find the current deploy checklist:
-```bash
-gh issue list --label "StagingDeployCash" --state open --json number,title,body --limit 1
-```
-
-This lists all PRs in the current staging deploy. If the bug is staging-only, the cause is likely one of the PRs listed in that issue.
-
-### Step 3: Find the causing PR
-
-Match the bug's affected area/timeline to recently merged PRs:
-- Search for PRs that touch the affected component or feature
-- Check the PR's merge date against when the bug was first reported
-- Read the PR diff to confirm it modifies the relevant code path
-
-```bash
-gh pr view --json title,body,author,files,mergedAt
-gh pr diff
-```
-
-### Step 4: Verify the causing PR
-
-**Always verify before concluding.** Confirm the suspected PR actually touches the affected code:
-
-1. **Find files related to the affected feature** using the Grep tool to search for relevant keywords in the codebase
-
-2. **Check recent changes** to the affected file:
-```bash
-git log --oneline -10 --
-```
-
-3. **Confirm the PR modifies these files**:
-```bash
-gh pr view --json files --jq '.files[].path'
-```
-
-**Verification checklist:**
-- [ ] The PR modifies files in the affected area
-- [ ] The changes in the PR relate to the reported symptoms
-- [ ] The PR's merge date aligns with when the bug appeared
+- `DeployBlockerCash` - Blocks the **App** deploy (frontend)
+- `DeployBlocker` - Blocks the **Web** deploy (backend PHP)
+- `StagingDeployCash` - The deploy checklist issue listing all PRs on staging
-If verification fails, go back to Step 3 and consider other candidate PRs.
+### Backend vs Frontend
-### Step 5: Determine fix location
+Determine from the issue description and App code whether this is a frontend or backend bug:
-Ask: **Where does the fix need to happen?**
+**Backend bug** = issue originates from the API / backend code, not this App repo:
+- Server error codes (500, 502, 503)
+- API response errors or malformed data
+- Authentication/authorization failures from server
+- Data missing or incorrect from API responses
+- Error messages mentioning Auth, PHP, or API
-- If reverting/fixing an App PR would resolve the issue → **Frontend bug**
-- If the fix requires backend changes → **Backend bug**
-
-### Step 6: Apply the decision tree
-
-See Decision Tree below for label actions.
-
-### Step 7: Post comment and update labels
-
-Post your findings (use single quotes for the body to handle special characters):
-```bash
-gh issue comment "$ISSUE_URL" --body '## 🔍 Investigation Summary
-...your comment here...
-'
-```
-
-Remove label only if the decision tree warrants it:
-```bash
-removeDeployBlockerLabel.sh "$ISSUE_URL" DeployBlockerCash # For Backend bugs
-removeDeployBlockerLabel.sh "$ISSUE_URL" DeployBlocker # For Frontend bugs
-```
-
-Call scripts by name only (e.g., `removeDeployBlockerLabel.sh`), not with full paths.
-
----
-
-## Decision Tree
-
-After identifying the causing PR:
-
-```
-┌─ Is there an App PR that caused or contributed to the issue?
-│
-├─ YES → Classification = Frontend bug
-│ → KEEP `DeployBlockerCash` (App deploy is blocked)
-│ → REMOVE `DeployBlocker` if present
-│ → Recommend reverting the App PR
-│
-└─ NO (no App PR involved—bug is purely in backend code)
- → Classification = Backend bug
- → REMOVE `DeployBlockerCash` if present
- → KEEP `DeployBlocker` (Backend deploy is blocked)
-```
+**Frontend bug** = issue is in the App (React Native/TypeScript):
+- UI rendering issues, navigation bugs
+- Onyx state problems
+- Client-side validation errors
+- Issues that occur before any API call
-**Note:** If an App PR ships a feature the backend doesn't yet support, that's still a Frontend bug—the App PR should be reverted so we don't ship broken functionality.
+When analyzing, look at the App code to understand:
+- Does the bug occur in UI logic, or when processing an API response?
+- Is the App code handling the response correctly, or is the response itself wrong?
---
-## Recommendations
+## What To Do
-Choose ONE:
+1. **Investigate** the issue and find the causing PR. Check the issue description to see if the bug is reproducible on production. If it's staging-only, the cause is likely a PR in the StagingDeployCash checklist. If it's also on production, the bug may predate the current staging deploy.
+2. **Comment** on the issue with your findings (see Comment Structure below)
+3. **Update labels** if needed - first check which labels are actually on the issue:
-| Recommendation | When to Use |
-|----------------|-------------|
-| **REVERT** | Default. Causing PR is clear and can be cleanly reverted. |
-| **ROLL FORWARD** | Reverting is problematic: fix is simpler than revert, many dependent PRs merged, or revert would reintroduce a worse bug. |
-| **NEEDS INVESTIGATION** | Cannot determine root cause with confidence. Tag PR author and reviewers. |
-| **DEMOTE** | Bug is minor (cosmetic, edge case, affects few users) and not worth blocking deploy. |
+| Classification | Label Action |
+|----------------|--------------|
+| Backend bug | Remove `DeployBlockerCash` if present (doesn't block App deploy) |
+| Frontend bug | Remove `DeployBlocker` if present (doesn't block Web deploy) |
---
-## Comment Format
+## Comment Structure
-Post ONE comment using this exact format:
+Post ONE comment using this format:
```markdown
## 🔍 Investigation Summary
@@ -181,9 +70,6 @@ Post ONE comment using this exact format:
Brief explanation of why this recommendation (1-2 sentences).
-
-**Labels**: [Describe any label changes made]
-
📋 Detailed Analysis
@@ -192,39 +78,41 @@ Brief explanation of why this recommendation (1-2 sentences).
- What changed in the PR that relates to the bug
- Whether it reproduces on production vs staging only
-### Verification
-- Which file(s) are affected by the bug
-- Confirmation that the PR modifies these files
-
### Root Cause
Technical explanation of what went wrong in the code.
```
----
-
-## Constraints
-**DO NOT:**
-- Remove `DeployBlockerCash` if there's an App PR that caused or contributed to the issue
-- Remove both blocker labels simultaneously
-- Make assumptions about code you haven't read
-- Recommend DEMOTE for bugs affecting core functionality (auth, payments, data loss)
-- Close the issue—only update labels and comment
-- Use heredocs, temp files, or shell redirects for comments
+**Recommendations** (choose one):
+- **REVERT** - Default choice. Preferred when the causing PR is clear and can be cleanly reverted.
+- **ROLL FORWARD** - Use when reverting is problematic: fix is simpler than revert, many dependent PRs have merged, or the PR fixed a worse bug than it introduced (reverting would bring back a more severe issue).
+- **NEEDS INVESTIGATION** - Cannot determine root cause with confidence. Tag PR author and reviewers.
+- **DEMOTE** - Bug is minor (cosmetic, edge case, affects few users) and not worth blocking deploy.
-**DO:**
-- Always verify the causing PR touches the affected code before concluding
-- Err on the side of keeping labels when uncertain
-- Tag the PR author if you need more information
-- Read the actual PR diff before making conclusions
+**Label removal**: Only remove a label if it's actually present on the issue. Check the issue's labels first before mentioning any label changes in your comment.
---
-## When Uncertain
+## Commands
-- **Can't find causing PR**: Use `NEEDS INVESTIGATION`, tag the issue author for more context
-- **Multiple candidate PRs**: List all with confidence levels and why, and recommend reverting the most likely first
-- **Unclear if frontend/backend**: Keep BOTH labels until confirmed
-- **Low confidence**: Do NOT remove any labels
+**Important**:
+- Do not use heredocs, temp files, or shell redirects. Pass the comment body directly to `gh issue comment --body`.
+- Call scripts by name only (e.g., `removeDeployBlockerLabel.sh`), not with full paths. The `.claude/scripts/` directory is in PATH.
+```bash
+# Check which labels are on the issue first:
+gh issue view "$ISSUE_URL" --json labels --jq '.labels[].name'
+
+# Post your findings as a comment (use single quotes for the body to handle special characters):
+gh issue comment "$ISSUE_URL" --body '## 🔍 Investigation Summary
+...your comment here...
+'
+
+# Remove label ONLY if it exists on the issue:
+# For backend bugs - remove DeployBlockerCash (if present)
+removeDeployBlockerLabel.sh "$ISSUE_URL" DeployBlockerCash
+
+# For frontend bugs - remove DeployBlocker (if present)
+removeDeployBlockerLabel.sh "$ISSUE_URL" DeployBlocker
+```
diff --git a/.claude/commands/review-code-pr.md b/.claude/commands/review-code-pr.md
index 63132922c18a..d259956b2c98 100644
--- a/.claude/commands/review-code-pr.md
+++ b/.claude/commands/review-code-pr.md
@@ -1,5 +1,5 @@
---
-allowed-tools: Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(addPrReaction.sh:*),Bash(createInlineComment.sh:*)
+allowed-tools: Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(addPrReaction.sh:*),Bash(createInlineComment.sh:*),Bash(checkReactCompilerOptimization.ts:*)
description: Review a code contribution pull request
---
diff --git a/.claude/scripts/checkReactCompilerOptimization.ts b/.claude/scripts/checkReactCompilerOptimization.ts
new file mode 100755
index 000000000000..233408c1af52
--- /dev/null
+++ b/.claude/scripts/checkReactCompilerOptimization.ts
@@ -0,0 +1,404 @@
+#!/usr/bin/env -S npx ts-node
+/**
+ * Check React Compiler optimization status for a file and its imported components.
+ *
+ * Usage: ts-node checkReactCompilerOptimization.ts
+ * Output: JSON with optimization status for parent and all imported children
+ */
+import {execSync} from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import ts from 'typescript';
+
+type PlatformVariant = {
+ path: string;
+ platform: string;
+};
+
+type StandardImport = {
+ name: string;
+ originalName: string;
+ module: string;
+ isDefault: boolean;
+};
+
+type NamespaceImport = {
+ namespaceName: string;
+ module: string;
+};
+
+type ImportData = {
+ usedAs: string;
+ originalName: string;
+ variants: PlatformVariant[];
+};
+
+type VariantResult = {
+ optimized: boolean;
+ platform: string;
+ sourcePath: string;
+};
+
+type ChildComponentResult = {
+ variants: VariantResult[];
+};
+
+type Result = {
+ parentOptimized: string[];
+ childComponents: Record;
+};
+
+// Load tsconfig once
+const configPath = ts.findConfigFile(process.cwd(), (fileName) => ts.sys.fileExists(fileName), 'tsconfig.json');
+if (!configPath) {
+ console.error('Could not find tsconfig.json');
+ process.exit(1);
+}
+const configFile = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
+const {options} = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
+
+/**
+ * Find platform variants for a resolved file path.
+ * E.g., for index.tsx, find index.native.tsx, index.ios.tsx, etc.
+ */
+function findPlatformVariants(resolvedPath: string): PlatformVariant[] {
+ const dir = path.dirname(resolvedPath);
+ const basename = path.basename(resolvedPath);
+ const variants: PlatformVariant[] = [];
+
+ // Platform suffixes to check
+ const platforms = ['native', 'ios', 'android', 'web'];
+
+ const ext = path.extname(basename);
+ const nameWithoutExt = path.basename(basename, ext);
+
+ // Add the default file first
+ if (fs.existsSync(resolvedPath)) {
+ variants.push({path: resolvedPath, platform: 'default'});
+ }
+
+ // Check for platform-specific variants
+ for (const platform of platforms) {
+ const variantName = `${nameWithoutExt}.${platform}${ext}`;
+ const variantPath = path.join(dir, variantName);
+ if (fs.existsSync(variantPath)) {
+ variants.push({path: variantPath, platform});
+ }
+ }
+
+ return variants;
+}
+
+/**
+ * Extract import information from file using regex.
+ * Returns object with:
+ * - standardImports: array of {name, originalName, module, isDefault}
+ * - namespaceImports: array of {namespaceName, module}
+ */
+function getImports(filePath: string): {standardImports: StandardImport[]; namespaceImports: NamespaceImport[]} {
+ const content = fs.readFileSync(filePath, 'utf8');
+ const standardImports: StandardImport[] = [];
+ const namespaceImports: NamespaceImport[] = [];
+
+ // Standard imports: default and named
+ const standardRegex = /import\s+(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]+)\})?\s+from\s+['"]([^'"]+)['"]/g;
+
+ for (const match of content.matchAll(standardRegex)) {
+ const [, defaultImport, namedImports, modulePath] = match;
+
+ // Skip external packages (react, react-native, lodash, etc.)
+ if (!modulePath.startsWith('@') && !modulePath.startsWith('.')) {
+ continue;
+ }
+
+ if (defaultImport) {
+ standardImports.push({name: defaultImport, originalName: defaultImport, module: modulePath, isDefault: true});
+ }
+
+ if (namedImports) {
+ for (const n of namedImports.split(',')) {
+ const parts = n.trim().split(/\s+as\s+/);
+ const originalName = parts.at(0)?.trim();
+ const aliasName = parts.length > 1 ? parts.at(1)?.trim() : originalName;
+ if (originalName && !originalName.startsWith('type ') && aliasName) {
+ standardImports.push({name: aliasName, originalName, module: modulePath, isDefault: false});
+ }
+ }
+ }
+ }
+
+ // Namespace imports: import * as X from '...'
+ const namespaceRegex = /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g;
+ for (const match of content.matchAll(namespaceRegex)) {
+ const [, namespaceName, modulePath] = match;
+
+ // Skip external packages
+ if (!modulePath.startsWith('@') && !modulePath.startsWith('.')) {
+ continue;
+ }
+
+ namespaceImports.push({namespaceName, module: modulePath});
+ }
+
+ return {standardImports, namespaceImports};
+}
+
+/**
+ * Find which members of a namespace are actually used in the file content.
+ * Returns array of member names (e.g., ['GenericPressable', 'PressableWithFeedback'])
+ */
+function findNamespaceUsage(content: string, namespaceName: string): string[] {
+ const usedMembers = new Set();
+ // Match Namespace.MemberName where MemberName starts with capital letter (component)
+ const usageRegex = new RegExp(`${namespaceName}\\.([A-Z]\\w+)`, 'g');
+
+ for (const match of content.matchAll(usageRegex)) {
+ const member = match.at(1);
+ if (member) {
+ usedMembers.add(member);
+ }
+ }
+
+ return [...usedMembers];
+}
+
+/**
+ * Resolve import to actual source file using TypeChecker.
+ * This follows re-exports to find the real file where the component is defined.
+ * For default imports, also resolves the actual exported name.
+ */
+function resolveImportToSourceFile(
+ program: ts.Program,
+ checker: ts.TypeChecker,
+ fromFile: string,
+ modulePath: string,
+ exportName: string,
+ isDefault: boolean,
+): {filePath: string; originalName: string} | null {
+ const sourceFile = program.getSourceFile(fromFile);
+ if (!sourceFile) {
+ return null;
+ }
+
+ // Get the module specifier's resolved file
+ const resolvedModule = ts.resolveModuleName(modulePath, fromFile, options, ts.sys);
+ if (!resolvedModule.resolvedModule) {
+ return null;
+ }
+
+ const resolvedFileName = resolvedModule.resolvedModule.resolvedFileName;
+
+ // Get the resolved source file for TypeChecker operations
+ const resolvedSourceFile = program.getSourceFile(resolvedFileName);
+ if (!resolvedSourceFile) {
+ return {filePath: resolvedFileName, originalName: exportName};
+ }
+
+ const moduleSymbol = checker.getSymbolAtLocation(resolvedSourceFile);
+ if (!moduleSymbol) {
+ return {filePath: resolvedFileName, originalName: exportName};
+ }
+
+ // For default imports, get the actual exported name
+ if (isDefault) {
+ try {
+ const defaultExport = moduleSymbol.exports?.get('default' as ts.__String);
+ if (defaultExport) {
+ const aliasedSymbol = checker.getAliasedSymbol(defaultExport);
+ const actualName = aliasedSymbol.getName();
+ // Also check if it's re-exported from another file
+ const declarations = aliasedSymbol.getDeclarations();
+ const firstDeclaration = declarations?.at(0);
+ if (firstDeclaration) {
+ const actualSourceFile = firstDeclaration.getSourceFile();
+ return {filePath: actualSourceFile.fileName, originalName: actualName};
+ }
+ return {filePath: resolvedFileName, originalName: actualName};
+ }
+ } catch {
+ // Fall back to import name if we can't resolve
+ }
+ return {filePath: resolvedFileName, originalName: exportName};
+ }
+
+ // For named imports, try to follow re-exports using TypeChecker
+ const exports = checker.getExportsOfModule(moduleSymbol);
+ const exportSymbol = exports.find((exp) => exp.getName() === exportName);
+
+ if (!exportSymbol) {
+ return {filePath: resolvedFileName, originalName: exportName};
+ }
+
+ // Follow the alias chain to find the actual source file
+ try {
+ const aliasedSymbol = checker.getAliasedSymbol(exportSymbol);
+ const declarations = aliasedSymbol.getDeclarations();
+ const firstDeclaration = declarations?.at(0);
+
+ if (firstDeclaration) {
+ const actualSourceFile = firstDeclaration.getSourceFile();
+ return {filePath: actualSourceFile.fileName, originalName: exportName};
+ }
+ } catch {
+ // If getAliasedSymbol fails (not an alias), use the original resolved file
+ }
+
+ return {filePath: resolvedFileName, originalName: exportName};
+}
+
+/**
+ * Run react-compiler-healthcheck on specific files and parse output.
+ * Returns Map>
+ */
+function runHealthcheck(filePaths: string[]): Map> {
+ if (!filePaths.length) {
+ return new Map();
+ }
+
+ const fileNames = [...new Set(filePaths.map((p) => path.basename(p)))];
+ const glob = `**/+(${fileNames.join('|')})`;
+
+ let output = '';
+ try {
+ output = execSync(`npx react-compiler-healthcheck --src '${glob}' --verbose 2>&1`, {
+ encoding: 'utf8',
+ timeout: 120000,
+ });
+ } catch (e: unknown) {
+ const error = e as {stdout?: string; message?: string};
+ output = error.stdout ?? error.message ?? '';
+ }
+
+ return parseOutput(output);
+}
+
+/**
+ * Parse healthcheck verbose output.
+ * Returns Map>
+ */
+function parseOutput(output: string): Map> {
+ const results = new Map>();
+
+ for (const line of output.split('\n')) {
+ // Success: Successfully compiled component [Name](path)
+ const success = line.match(/Successfully compiled (?:hook|component) \[([^\]]+)\]\(([^)]+)\)/);
+ if (success) {
+ const [, componentName, filePath] = success;
+ const absolutePath = path.resolve(filePath);
+
+ const existingSet = results.get(absolutePath);
+ if (existingSet) {
+ existingSet.add(componentName);
+ } else {
+ results.set(absolutePath, new Set([componentName]));
+ }
+ }
+ }
+
+ return results;
+}
+
+// Main
+const inputFile = process.argv.at(2);
+if (!inputFile) {
+ console.error('Usage: ./checkReactCompilerOptimization.ts ');
+ process.exit(1);
+}
+
+if (!fs.existsSync(inputFile)) {
+ console.error(`File not found: ${inputFile}`);
+ process.exit(1);
+}
+
+const absoluteInputFile = path.resolve(inputFile);
+const fileContent = fs.readFileSync(absoluteInputFile, 'utf8');
+const {standardImports, namespaceImports} = getImports(absoluteInputFile);
+
+// Create TypeScript program for symbol resolution
+const program = ts.createProgram([absoluteInputFile], options);
+const checker = program.getTypeChecker();
+
+// Collect all files to check (including platform variants)
+const allFilesToCheck = new Set();
+const importDataMap = new Map(); // usedAs -> {usedAs, originalName, variants}
+
+// Add parent file and its variants
+const parentVariants = findPlatformVariants(absoluteInputFile);
+for (const variant of parentVariants) {
+ allFilesToCheck.add(variant.path);
+}
+
+// Process standard imports
+for (const imp of standardImports) {
+ const resolved = resolveImportToSourceFile(program, checker, absoluteInputFile, imp.module, imp.originalName, imp.isDefault);
+
+ if (resolved && (resolved.filePath.endsWith('.tsx') || resolved.filePath.endsWith('.ts'))) {
+ const variants = findPlatformVariants(resolved.filePath);
+
+ importDataMap.set(imp.name, {
+ usedAs: imp.name,
+ originalName: resolved.originalName,
+ variants,
+ });
+
+ for (const variant of variants) {
+ allFilesToCheck.add(variant.path);
+ }
+ }
+}
+
+// Process namespace imports - only include actually used members
+for (const nsImport of namespaceImports) {
+ const usedMembers = findNamespaceUsage(fileContent, nsImport.namespaceName);
+
+ for (const memberName of usedMembers) {
+ const resolved = resolveImportToSourceFile(program, checker, absoluteInputFile, nsImport.module, memberName, false);
+
+ if (resolved && (resolved.filePath.endsWith('.tsx') || resolved.filePath.endsWith('.ts'))) {
+ const variants = findPlatformVariants(resolved.filePath);
+ const usedAs = `${nsImport.namespaceName}.${memberName}`;
+
+ importDataMap.set(usedAs, {
+ usedAs,
+ originalName: resolved.originalName,
+ variants,
+ });
+
+ for (const variant of variants) {
+ allFilesToCheck.add(variant.path);
+ }
+ }
+ }
+}
+
+// Run healthcheck on all files
+const optimizationResults = runHealthcheck([...allFilesToCheck]);
+
+// Get list of optimized components/hooks in parent file
+const parentOptimizedSet = optimizationResults.get(absoluteInputFile) ?? new Set();
+const parentOptimized = [...parentOptimizedSet];
+
+// Build output
+const result: Result = {
+ parentOptimized,
+ childComponents: {},
+};
+
+// Add child components with their variants (simplified to boolean)
+for (const [usedAs, data] of importDataMap) {
+ result.childComponents[usedAs] = {
+ variants: data.variants.map((variant) => {
+ const optimizedSet = optimizationResults.get(variant.path) ?? new Set();
+ const isOptimized = optimizedSet.has(data.originalName);
+ // Make path relative to cwd for cleaner output
+ const relativePath = path.relative(process.cwd(), variant.path);
+ return {
+ optimized: isOptimized,
+ platform: variant.platform,
+ sourcePath: relativePath,
+ };
+ }),
+ };
+}
+
+process.stdout.write(JSON.stringify(result, null, 2));
diff --git a/.claude/scripts/createInlineComment.sh b/.claude/scripts/createInlineComment.sh
index e7fd3f1d0a67..755930d98b9f 100755
--- a/.claude/scripts/createInlineComment.sh
+++ b/.claude/scripts/createInlineComment.sh
@@ -47,14 +47,11 @@ readonly LINE_ARG="${3:-}"
validate_rule "$BODY_ARG"
echo "Comment approved: $COMMENT_STATUS_REASON"
-readonly FOOTER=$'\n\n---\n\nPlease rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.'
-readonly COMMENT_BODY="${BODY_ARG}${FOOTER}"
-
COMMIT_ID=$(gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq '.head.sha')
readonly COMMIT_ID
PAYLOAD=$(jq -n \
- --arg body "$COMMENT_BODY" \
+ --arg body "$BODY_ARG" \
--arg path "$PATH_ARG" \
--argjson line "$LINE_ARG" \
--arg commit_id "$COMMIT_ID" \
diff --git a/.claude/skills/playwright-app-testing/SKILL.md b/.claude/skills/playwright-app-testing/SKILL.md
deleted file mode 100644
index 1e8abfa132df..000000000000
--- a/.claude/skills/playwright-app-testing/SKILL.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-name: playwright-app-testing
-description: Test the Expensify App using Playwright browser automation. Use when user requests browser testing, after making frontend changes, or when debugging UI issues
-alwaysApply: false
----
-
-# Playwright App Testing
-
-## When to Use This Skill
-
-Use Playwright testing when:
-- User requests testing the App in a browser
-- Verifying fixes or improvements you've made to UI/frontend code
-- Debugging UI issues
-
-**Proactively use after making frontend changes** to verify your work functions correctly.
-
-## Prerequisites Check
-
-Before using Playwright tools, verify the dev server is running:
-```bash
-ps aux | grep "webpack" | grep -v grep
-```
-
-**If server not running**: Inform user to start with `cd App && npm run web`
-
-## Dev Server Details
-- **URL**: `https://dev.new.expensify.com:8082/`
-- **Location**: HOST machine (not inside VM)
-- **Start command**: `cd App && npm run web`
-
-## Playwright Testing Workflow
-
-1. **Verify server**: Check webpack process is running
-2. **Navigate**: Use `mcp__playwright__browser_navigate` to `https://dev.new.expensify.com:8082/`
-3. **Interact**: Use Playwright MCP tools including:
- - **Inspection**: `browser_snapshot`, `browser_take_screenshot`, `browser_console_messages`
- - **Interaction**: `browser_click`, `browser_type`, `browser_fill_form`, `browser_hover`
- - **Navigation**: `browser_navigate_back`, `browser_tabs`, `browser_wait_for`
- - All other Playwright tools as needed
-
-## Dev Environment Sign-In
-
-When signing in to dev environment:
-- **Email**: Generate random Gmail address (e.g., `user+throwaway@gmail.com`)
-- **Magic code**: Always `000000` (six zeros)
-- **Onboarding**: Skip all optional steps
-
-## Example Usage
-
-```
-Scenario 1: User requests testing
-User: "Test sign in to app"
-→ Use this skill to verify server and test sign-in flow
-
-Scenario 2: After making UI changes
-You: "I've updated the expense form validation"
-→ Proactively use this skill to verify the changes work in browser
-
-Scenario 3: Investigating bug
-User: "The submit button doesn't work on this page"
-→ Use this skill to reproduce and verify the issue
-```
-
-## When NOT to Use This Skill
-
-Skip Playwright for:
-- Backend service testing
-- Unit tests
-- Type checking
-- Mobile native app testing (requires emulators/simulators)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 68a130a6342e..f0737b247f52 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -10,6 +10,3 @@ assets/ @Expensify/design @Expensify/product-pr @Expensify/pullerbear
# Philosophy docs are in their early stages and need to be reviewed by Tim to ensure they have consistent formatting and organization
contributingGuides/philosophies/ @tgolen
-
-# Help docs get a review from Steph Elliott
-docs/articles/ @stephanieelliott
diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js
index 8f9357a23310..9fb53f936628 100644
--- a/.github/actions/javascript/verifySignedCommits/index.js
+++ b/.github/actions/javascript/verifySignedCommits/index.js
@@ -11585,15 +11585,15 @@ const github = __importStar(__nccwpck_require__(5438));
const CONST_1 = __importDefault(__nccwpck_require__(9873));
const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
const PR_NUMBER = Number.parseInt(core.getInput('PR_NUMBER'), 10) || github.context.payload.pull_request?.number;
-GithubUtils_1.default.paginate(GithubUtils_1.default.octokit.pulls.listCommits, {
+GithubUtils_1.default.octokit.pulls
+ .listCommits({
owner: CONST_1.default.GITHUB_OWNER,
repo: CONST_1.default.APP_REPO,
// eslint-disable-next-line @typescript-eslint/naming-convention
pull_number: PR_NUMBER ?? 0,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- per_page: 100,
-}, (response) => response.data).then((commits) => {
- const unsignedCommits = commits.filter((commit) => !commit.commit.verification?.verified);
+})
+ .then(({ data }) => {
+ const unsignedCommits = data.filter((datum) => !datum.commit.verification?.verified);
if (unsignedCommits.length > 0) {
const errorMessage = `Error: the following commits are unsigned: ${JSON.stringify(unsignedCommits.map((commitObj) => commitObj.sha))}`;
console.error(errorMessage);
diff --git a/.github/actions/javascript/verifySignedCommits/verifySignedCommits.ts b/.github/actions/javascript/verifySignedCommits/verifySignedCommits.ts
index bb8c3438ab48..226926b017eb 100644
--- a/.github/actions/javascript/verifySignedCommits/verifySignedCommits.ts
+++ b/.github/actions/javascript/verifySignedCommits/verifySignedCommits.ts
@@ -5,25 +5,21 @@ import GitHubUtils from '@github/libs/GithubUtils';
const PR_NUMBER = Number.parseInt(core.getInput('PR_NUMBER'), 10) || github.context.payload.pull_request?.number;
-GitHubUtils.paginate(
- GitHubUtils.octokit.pulls.listCommits,
- {
+GitHubUtils.octokit.pulls
+ .listCommits({
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
// eslint-disable-next-line @typescript-eslint/naming-convention
pull_number: PR_NUMBER ?? 0,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- per_page: 100,
- },
- (response) => response.data,
-).then((commits) => {
- const unsignedCommits = commits.filter((commit) => !commit.commit.verification?.verified);
+ })
+ .then(({data}) => {
+ const unsignedCommits = data.filter((datum) => !datum.commit.verification?.verified);
- if (unsignedCommits.length > 0) {
- const errorMessage = `Error: the following commits are unsigned: ${JSON.stringify(unsignedCommits.map((commitObj) => commitObj.sha))}`;
- console.error(errorMessage);
- core.setFailed(errorMessage);
- } else {
- console.log('All commits signed! 🎉');
- }
-});
+ if (unsignedCommits.length > 0) {
+ const errorMessage = `Error: the following commits are unsigned: ${JSON.stringify(unsignedCommits.map((commitObj) => commitObj.sha))}`;
+ console.error(errorMessage);
+ core.setFailed(errorMessage);
+ } else {
+ console.log('All commits signed! 🎉');
+ }
+ });
diff --git a/.gitignore b/.gitignore
index b634b47cd00a..8f636cf603cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -156,10 +156,7 @@ modules/*/lib/
# Claude local settings
.claude/settings.local.json
-
-# Playwright MCP
.playwright/
-.playwright-mcp/
# cspell cache
.cspellcache
diff --git a/CLAUDE.md b/CLAUDE.md
index 5455b33c80c4..b0f77e792d55 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -240,16 +240,37 @@ npm run android
npm run web
```
-## Development Environment
+## Browser Testing with Playwright MCP
-### Dev Server
+Claude can interact with the running App dev server using Playwright MCP for testing and debugging:
+
+### Setup
+1. Verify dev server is running
+2. Dev server runs at `https://dev.new.expensify.com:8082/`
+3. Ask Claude to open the URL in Playwright browser
+2. **IF MCP NOT AVAILABLE**: Guide user to install it before proceeding
+
+### MCP Installation
+If Playwright MCP connection fails, guide user to install:
+```bash
+claude mcp add playwright npx '@playwright/mcp@latest'
+```
+
+### Testing Workflow (MCP Required)
+1. Verify App dev server is running: `pgrep webpack`
+2. Use Playwright MCP tools to navigate to `https://dev.new.expensify.com:8082/`
+3. Use MCP tools to interact with the browser (click, type, screenshot, etc.)
+
+### Dev Server Requirements
- **Location**: Runs on HOST machine (not in VM)
- **URL**: `https://dev.new.expensify.com:8082/`
- **Start command**: `npm run web`
- **VM is only for**: Backend services (Auth, Bedrock, Integration-Server, Web-Expensify)
-### Browser Testing
-Use the `/playwright-app-testing` skill to test and debug the App in a browser. Use this skill after making frontend changes to verify your work, or when the user requests testing.
+### Dev Environment Sign-In Credentials
+- **Email**: Generate a random Gmail address (e.g., `user+@gmail.com`)
+- **Magic code**: Always `000000` for dev environment
+- **Onboarding**: Always skip optional steps
## Architecture Decisions
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 987e3c051ed0..28be7c90b46a 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 987e3c051ed01c3a3639702724a87a00a28be149
+Subproject commit 28be7c90b46abdf4b3c93fe63b23d55c4fe0e4d6
diff --git a/__mocks__/reportData/transactions.ts b/__mocks__/reportData/transactions.ts
index a1f35834450e..fcc112778fc0 100644
--- a/__mocks__/reportData/transactions.ts
+++ b/__mocks__/reportData/transactions.ts
@@ -30,7 +30,7 @@ const transactionR14932: Transaction = {
reimbursable: true,
hasEReceipt: true,
cardID: 0,
- modifiedAmount: '',
+ modifiedAmount: 0,
originalAmount: 0,
comment: {},
bank: '',
@@ -59,7 +59,7 @@ const transactionR98765: Transaction = {
hasEReceipt: true,
managedCard: false,
billable: false,
- modifiedAmount: '',
+ modifiedAmount: 0,
cardID: 0,
originalAmount: 0,
comment: {},
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 6f4e6cc2f12d..b01ebf9c8787 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -114,8 +114,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009030701
- versionName "9.3.7-1"
+ versionCode 1009030100
+ versionName "9.3.1-0"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c0579bd3370f..430e3a7875f3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -9,7 +9,6 @@
-
diff --git a/assets/animations/Fingerprint.lottie b/assets/animations/Fingerprint.lottie
deleted file mode 100644
index 722795314e72..000000000000
Binary files a/assets/animations/Fingerprint.lottie and /dev/null differ
diff --git a/assets/images/multifactorAuthentication/fingerprint.svg b/assets/images/multifactorAuthentication/fingerprint.svg
deleted file mode 100644
index f6712ca78e14..000000000000
--- a/assets/images/multifactorAuthentication/fingerprint.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/multifactorAuthentication/humpty-dumpty.svg b/assets/images/multifactorAuthentication/humpty-dumpty.svg
deleted file mode 100644
index 714b4359d064..000000000000
--- a/assets/images/multifactorAuthentication/humpty-dumpty.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/multifactorAuthentication/open-padlock.svg b/assets/images/multifactorAuthentication/open-padlock.svg
deleted file mode 100644
index 0b905305ec0c..000000000000
--- a/assets/images/multifactorAuthentication/open-padlock.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/multifactorAuthentication/running-out-of-time.svg b/assets/images/multifactorAuthentication/running-out-of-time.svg
deleted file mode 100644
index 99fe012af555..000000000000
--- a/assets/images/multifactorAuthentication/running-out-of-time.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__calendar-monthly.svg b/assets/images/simple-illustrations/simple-illustration__calendar-monthly.svg
deleted file mode 100644
index 906bd7ae4564..000000000000
--- a/assets/images/simple-illustrations/simple-illustration__calendar-monthly.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__fastmoney.svg b/assets/images/simple-illustrations/simple-illustration__fastmoney.svg
deleted file mode 100644
index d479400e1830..000000000000
--- a/assets/images/simple-illustrations/simple-illustration__fastmoney.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/assets/images/user-shield.svg b/assets/images/user-shield.svg
deleted file mode 100644
index f1f4ad1d1ab4..000000000000
--- a/assets/images/user-shield.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/contributingGuides/philosophies/AI-REVIEWER.md b/contributingGuides/philosophies/AI-REVIEWER.md
index 2cb76c592035..43fe9931e685 100644
--- a/contributingGuides/philosophies/AI-REVIEWER.md
+++ b/contributingGuides/philosophies/AI-REVIEWER.md
@@ -174,17 +174,17 @@ Escalate to human reviewers when:
### Examples
#### Appropriate Response to Valid Feedback
-**AI Comment**: "PERF-1: Spread operator used on object in renderItem creates new object references on each render."
+**AI Comment**: "PERF-4: This object passed as a prop should be memoized to prevent unnecessary re-renders."
-✅ **Good Response**: Pass individual props directly instead of using spread operator, or move object creation outside renderItem.
+✅ **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-11: Add a selector to `useOnyx` to select only the `name` and `avatar` fields instead of the entire user object."
+**AI Comment**: "PERF-4: This object passed as a prop should be memoized."
-**Context**: A selector is already present in the code - the AI reviewer missed it during analysis.
+**Context**: The parent component is already optimized by React Compiler.
-✅ **Good Response**: Reach out in the #expensify-open-source Slack channel explaining that a selector is already being used.
+✅ **Good Response**: Reach out in the #expensify-open-source Slack channel with explanation of incorrect suggestion.
-❌ **Bad Response**: Add a duplicate selector or ignore the feedback without verifying the claim.
+❌ **Bad Response**: Apply the change anyway, adding unnecessary complexity.
diff --git a/cspell.json b/cspell.json
index 5cc365f37ff1..67c2c36081fb 100644
--- a/cspell.json
+++ b/cspell.json
@@ -7,7 +7,6 @@
},
"words": [
"--longpress",
- "abytes",
"Accelo",
"achreimburse",
"actool",
@@ -184,7 +183,6 @@
"Drycleaning",
"DSYM",
"dsyms",
- "Dumpty",
"durationMillis",
"e2edelta",
"ecash",
@@ -303,7 +301,6 @@
"Hoverable",
"HRMS",
"HSBCSGS",
- "Humpty",
"hybridapp",
"Hydronics",
"iaco",
@@ -415,7 +412,6 @@
"msword",
"mtrl",
"multidex",
- "Multifactor",
"MVCP",
"MYOB",
"mysubdomain",
@@ -600,7 +596,6 @@
"Roni",
"Rosiclair",
"rpartition",
- "RPID",
"RRGGBB",
"rstrip",
"RTER",
diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml
index 094e98eaa60a..b8aff65e5f73 100644
--- a/docs/_data/_routes.yml
+++ b/docs/_data/_routes.yml
@@ -118,11 +118,6 @@ platforms:
title: Reports & Expenses
icon: /assets/images/envelope-receipt.svg
description: Learn more about expense tracking and submission.
-
- - href: domains
- title: Domains
- icon: /assets/images/domains.svg
- description: Claim and verify your company’s domain to access additional management and security features.
- href: wallet-and-payments
title: Wallet & Payments
diff --git a/docs/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify.md b/docs/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify.md
new file mode 100644
index 000000000000..e32a2a8d5a36
--- /dev/null
+++ b/docs/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify.md
@@ -0,0 +1,86 @@
+---
+title: Managing Single Sign-On (SSO) and User Authentication in Expensify
+description: Learn how to effectively manage Single Sign-On (SSO) and user authentication in Expensify alongside your preferred SSO provider.
+keywords: [Expensify Classic, SAML, single sign-on, SSO]
+---
+
+
+Expensify supports Single Sign-On (SSO) through the SAML protocol, allowing you to optimize user authentication and improve security across your organization. Whether you're an IT admin configuring your domain or a team lead ensuring secure user access, this guide walks you through setting up and managing SAML SSO for your Expensify account
+
+---
+
+# Accessing SAML Settings
+⚠️ **Pre-requisite:** Ensure your [domain is verified](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain#step-2-verify-domain-ownership).
+
+1. Navigate to: **Settings > Domains > [Domain Name] > SAML**.
+2. **From the Domains page:**
+ - Download Expensify's **Service Provider Metadata** to provide to your Identity Provider.
+ - Enter the **Identity Provider Metadata** from your SSO provider. (Contact your provider if unsure how to obtain this).
+ - Enable the **"SAML required for login"** option, ensuring users sign in via SSO only.
+
+---
+
+# Provider-Specific Setup Instructions
+
+Click on your Identity (SAML) Provider for detailed steps:
+- [Amazon Web Services (AWS SSO)](https://static.global.sso.amazonaws.com/app-202a715cb67cddd9/instructions/index.htm)
+- [Google SAML (Gsuite)](https://support.google.com/a/answer/7371682)
+- [Microsoft Entra ID (formerly Azure Active Directory)](https://learn.microsoft.com/en-us/entra/identity/saas-apps/expensify-tutorial)
+- [Okta](https://saml-doc.okta.com/SAML_Docs/How-to-Configure-SAML-2.0-for-Expensify.html)
+- [OneLogin](https://onelogin.service-now.com/support?id=kb_article&sys_id=e44c9e52db187410fe39dde7489619ba)
+- [Oracle Identity Cloud Service](https://docs.oracle.com/en/cloud/paas/identity-cloud/idcsc/expensify.html#Expensify)
+- [SAASPASS](https://saaspass.com/saaspass/expensify-two-factor-authentication-2fa-single-sign-on-sso-saml.html)
+- Microsoft ADFS (instructions below).
+
+**Note:** If your provider isn't listed, contact them directly for guidance.
+
+---
+
+# Common Troubleshooting Scenarios
+
+## User Login with SSO
+- Employees using their **company email** or a **secondary linked email** (e.g., Gmail) will be prompted to log in through SSO.
+- Secondary login setup guide: [Change or Add Email Address](https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address).
+
+## Error During SSO Setup?
+- Use [samltool.com](https://samltool.com) to validate your configuration data.
+- Contact your Account Manager or Concierge for further help.
+
+## What is Expensify's Entity ID?
+- Default: `https://expensify.com`
+- For Multi-Domain setups: `https://expensify.com/mydomainname.com`.
+
+## Managing Multiple Domains with One Entity ID
+Yes, it's possible. Contact Concierge or your Account Manager to enable this feature.
+
+## Updating Microsoft Entra ID SSO Certificate
+Steps to avoid configuration errors during certificate renewal:
+1. **Create** a new certificate in Microsoft Entra.
+2. **Remove** the old certificate before activating the new one.
+3. Replace the **IDP** in Expensify with the new one.
+4. Log in via SSO.
+
+If issues persist, contact Concierge Support for assistance.
+
+---
+
+# Advanced Configurations
+
+## Okta SCIM API for User Deactivation
+Ensure your domain is verified and the SAML setup is complete. Then, do the following:
+1. Go to **Settings > Domains > [Domain Name] > SAML**.
+2. Enable SAML Login and toggle **Required for login**.
+3. In Okta, add Expensify as an application, and configure user profile mappings.
+4. Request **Okta SCIM API** activation via concierge@expensify.com.
+5. Integrate the **SCIM token** with Okta API provisioning.
+
+Refer to the full setup in Okta's documentation for attribute mapping and provisioning options.
+
+## Microsoft ADFS SAML Authentication
+1. Open **ADFS Management Console** and add a new trust.
+2. Import Expensify's metadata XML from the SAML page.
+3. Configure **LDAP Attributes** for email or UPN.
+4. Add two claim rules:
+ - Send LDAP Attributes as Claims.
+ - Transform Incoming Claim (Name ID).
+
diff --git a/docs/articles/expensify-classic/domains/Set-Up-SAML-SSO.md b/docs/articles/expensify-classic/domains/Set-Up-SAML-SSO.md
deleted file mode 100644
index 37cb3f349874..000000000000
--- a/docs/articles/expensify-classic/domains/Set-Up-SAML-SSO.md
+++ /dev/null
@@ -1,128 +0,0 @@
----
-title: Managing Single Sign-On (SSO) and User Authentication in Expensify
-description: Learn how to set up and manage SAML-based Single Sign-On (SSO) for secure member login in Expensify Classic.
-internalScope: Audience: Domain Admins. Covers setting up SAML and solutions to common configuration issues, Does not cover individual account access troubleshooting.
-keywords: [Expensify Classic, SAML SSO, domain security, single sign-on, identity provider, verified domain, enable SAML, Okta, Google Workspace, Microsoft Entra, ADFS]
----
-
-Set up secure and streamlined login across your organization by enabling SAML Single Sign-On (SSO) in Expensify Classic. This allows Workspace members to authenticate using your identity provider (IdP), rather than creating separate credentials.
-
----
-
-# Where to find SAML Single Sign-On (SSO) settings in Expensify Classic
-
-To set up SAML Single Sign-On (SSO), verify your domain.
-[Learn how to claim and verify your domain](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain#step-2-verify-domain-ownership)
-
-Once you are a Domain Admin on a verified domain, you can configure SAML SSO login:
-
-1. Go to **Settings > Domains > [Domain Name] > SAML**.
-2. Toggle **SAML Login** to **Enabled**.
-
----
-
-# Who can manage SAML Single Sign-On (SSO)
-
- Only **Domain Admins** can configure SAML for verified domains. SAML login applies to all Domain members whose email addresses match the verified domain.
-
----
-
-# How to set up SAML Single Sign-On (SSO)
-
-1. Go to **Settings > Domains > [Domain Name] > SAML**.
-2. Toggle **SAML Login** to **Enabled**.
-3. Download Expensify’s **Service Provider metadata** to upload to your IdP.
-4. Paste your IdP metadata in the **Identity Provider MetaData** field.
-5. Test logging in to confirm that SAML SSO is configured correctly (recommended).
-6. Enable **Required for login** to ensure members sign in via SSO only.
-
-Select your Identity (SAML) Provider for detailed steps on configuring SAML Single Sign-On (SSO):
-
-- [Amazon Web Services (AWS SSO)](https://static.global.sso.amazonaws.com/app-202a715cb67cddd9/instructions/index.htm)
-- [Google Workspace / SAML (Gsuite)](https://support.google.com/a/answer/7371682)
-- [Microsoft Entra ID (formerly Azure AD)](https://learn.microsoft.com/en-us/entra/identity/saas-apps/expensify-tutorial)
-- [Okta](https://saml-doc.okta.com/SAML_Docs/How-to-Configure-SAML-2.0-for-Expensify.html)
-- [OneLogin](https://onelogin.service-now.com/support?id=kb_article&sys_id=e44c9e52db187410fe39dde7489619ba)
-- [Oracle Identity Cloud Service](https://docs.oracle.com/en/cloud/paas/identity-cloud/idcsc/expensify.html#Expensify)
-- [SAASPASS](https://saaspass.com/saaspass/expensify-two-factor-authentication-2fa-single-sign-on-sso-saml.html)
-- Microsoft ADFS – see instructions below
-
-**Note:** If your provider isn't listed, contact them directly for guidance with metadata and setup.
-
----
-
-# How SAML Single Sign-On (SSO) affects login behavior
-
-- Members with email addresses matching your verified domain will be prompted to log in through your configured IdP.
-- Members using a personal or secondary email (e.g., Gmail) must [update their email address](https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address) to match the verified domain for SSO access.
-
----
-
-# Troubleshooting SAML Single Sign-On (SSO)
-
-## If setup fails or login doesn't work:
-
-- Use [samltool.com](https://samltool.com) to validate your IdP metadata and certificate.
-- Make sure the email domain in your IdP exactly matches your verified domain in Expensify.
-
-## What is Expensify’s Entity ID?
-
-- Standard setup: `https://expensify.com`
-- Multi-domain setup: `https://expensify.com/yourdomain.com`
-
-Managing multiple domains with one Entity ID is supported. Contact Concierge or your Account Manager to enable this feature.
-
-# Advanced configurations for SAML Single Sign-On (SSO)
-
-## Okta SCIM API for account deprovisioning
-
-Once SAML is configured:
-
-1. In Okta, add Expensify as an app and configure attribute mappings.
-2. Request SCIM API access via **concierge@expensify.com**.
-3. Add the SCIM token in your Okta provisioning settings.
-
-Refer to Okta’s documentation for complete instructions.
-
-## Microsoft ADFS configuration
-
-1. Open the **ADFS Management Console** and create a new trust.
-2. Upload Expensify’s metadata XML file.
-3. Map **LDAP attributes** (email or UPN) to outgoing claims.
-4. Add two claim rules:
- - Send LDAP Attributes as Claims
- - Transform Incoming Claim to Name ID
-
-## Microsoft Entra ID certificate update process
-
-To avoid setup errors during certificate renewal:
-
-1. Create the new certificate in Microsoft Entra.
-2. Remove the old certificate before activating the new one.
-3. Replace the existing IdP metadata in Expensify.
-4. Log in via SSO to confirm the new certificate works.
-
-# FAQ
-
-## Can I use SAML Single Sign-On (SSO) for multiple Workspaces?
-
-Yes, as long as all members are part of the same verified domain, SAML access applies across all Workspaces they belong to.
-
-## How can I confirm my SAML Single Sign-On (SSO) setup is working?
-
-Before enabling **Require SAML login**, make sure your SAML connection is working by testing both SP-initiated and IdP-initiated logins. You should also confirm that:
-
-- The correct certificate and endpoints are in your Expensify metadata
-- Members can log in successfully using the SAML flow
-
-## Can I test a new SAML Single Sign-On (SSO) setup without locking members out?
-
-Yes. Disable **Require SAML login** before making changes. This allows members to log in with email and password if SAML setup fails. Once you’ve confirmed that login works, you can re-enable enforcement.
-
-## What do I do if a member can’t log in after SAML Single Sign-On (SSO) is enabled?
-
-First, confirm that the member’s email matches your verified domain and that their account exists in your Identity Provider (IdP) with the correct access permissions.
-
-## Are custom NameID, ACS, or SLO URLs supported in SAML Single Sign-On (SSO)?
-
-No, the NameID Format, Login URL (ACS URL), and Logout URL (SLO URL) are static and cannot be modified.
diff --git a/docs/articles/expensify-classic/domains/Troubleshoot-SAML-SSO-Login.md b/docs/articles/expensify-classic/domains/Troubleshoot-SAML-SSO-Login.md
deleted file mode 100644
index 33c8855ed61a..000000000000
--- a/docs/articles/expensify-classic/domains/Troubleshoot-SAML-SSO-Login.md
+++ /dev/null
@@ -1,121 +0,0 @@
----
-title: Troubleshoot SAML SSO login
-description: Learn how to quickly diagnose and resolve issues with SAML SSO login in Expensify Classic, including lockouts, expired certificates, and identity provider errors.
-keywords: [Expensify Classic, SAML SSO, SSO login failed, Require SAML login, domain locked out, expired certificate, identity provider, IdP, metadata, troubleshooting]
----
-
-If you're having trouble logging in with SAML Single Sign-On (SSO) in Expensify Classic, this guide will help you identify the issue, understand what’s causing it, and get access restored quickly.
-
----
-
-# Where to find SAML SSO settings in Expensify Classic
-
-To check your domain's SAML SSO configuration, go to **Settings > Domains > [Domain Name] > SAML**.
-
-From this page, Domain Admins can:
-
-- Enable SAML SSO login for the domain
-- View and update your Identity Provider (IdP) metadata
-- Disable or enable **Require SAML login**
-
-**Note:** SAML SSO settings are not available on mobile.
-
-# How to fix domain-wide SAML SSO login issues
-
-## SAML login suddenly fails for all members
-
-A domain-wide issue usually points to a problem with your Identity Provider (IdP).
-
-**Has your IdP certificate expired or rotated?**
-
- If yes, copy the updated metadata XML from your IdP and paste it into the **Identity Provider Metadata** field in your Expensify SAML settings.
-
-**Have any IdP settings changed?**
-
-Changes to entity IDs, SSO endpoints, or user attributes can break login.
- - If your certificate or SSO endpoints have changed, upload updated metadata from your IdP to Expensify.
- - If user attributes like NameID Format or email mappings have changed, confirm they match the values expected in your domain's SAML settings in Expensify.
-
-**Is “Require SAML login” turned on?**
-
- If enabled, no one — including Domain Admins — can log in without a working SAML configuration. If you're still signed in, go to your domain’s SAML settings and temporarily disable **Require SAML login** while troubleshooting.
-
-## Some members can’t log in, but others can
-
-This is often caused by an email alias not recognized by your identity provider (IdP), or because the member hasn’t been added to the correct SAML rule or group. Confirm that the member’s email matches your verified domain in Expensify, and check your IdP to ensure they’re included in the appropriate SAML group or rule.
-
-## All Domain Admins are locked out
-
-If no Domain Admins can log in, you won’t be able to access SAML settings. Email **concierge@expensify.com** from an address that matches your verified domain for help.
-
----
-
-# How to resolve common SAML SSO error messages
-
-## Signature validation failed
-
-This typically happens when the certificate has expired, is malformed, or doesn't match the one used by your IdP. To fix it, copy the updated metadata XML from your IdP and paste it into the **Identity Provider Metadata** field in your Expensify SAML settings.
-
----
-
-## SAML Response not found. Only supported HTTP_POST Binding
-
-Your Identity Provider is not sending the `SAMLResponse` in the POST body as expected. To fix it, update your IdP configuration to use **HTTP POST binding** when sending the SAML Response.
-
----
-
-## No user with that partnerUserID/partnerUserSecret
-
-This occurs when your IdP sends an email (NameID) that doesn’t match the one stored in Expensify for that member. To fix it, confirm that the NameID value sent by your IdP exactly matches the member’s email address in Expensify. If needed, update the member's email in your IdP or in Expensify to resolve the mismatch.
-
----
-
-## Bad XML metadata
-
-Your metadata file may contain formatting issues — often extra line breaks or copy/paste errors in the x.509 certificate.
-**How to fix it:** Use a certificate formatting tool (like [samltool.com](https://samltool.com)) to clean and validate your metadata before pasting it into Expensify.
-
-**Note:** When copying a certificate, make sure it includes the full `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` block with no formatting errors.
-
----
-
-## SAML login not available on your domain
-
-This appears when **Require SAML login** is enabled, but SAML isn’t fully configured. To fix it, follow the steps to [Configure Single Sign On (SSO)](https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify) for your domain.
-
----
-
-# How to contact Expensify if you're locked out
-
-If you can't sign in due to a SAML issue, email **concierge@expensify.com** from an address that matches your verified domain.
-
----
-
-# FAQ
-
-## What should I do before making changes to my domain's SAML SSO setup?
-
-Before making changes to your Identity Provider setup — like rotating certificates or updating endpoints — we recommend **temporarily disabling Require SAML login** in Expensify.
-
-This ensures Domain Admins can still sign in with email and password if the new configuration doesn’t work. Once you’ve uploaded the new metadata and confirmed login is working, you can safely re-enable Require SAML login.
-
-## Can I make SAML login optional for some members?
-
-No. SAML settings apply to the entire domain. If **Require SAML login** is enabled, **all members** must authenticate via SAML — there’s no way to allow some members to log in with email and password while others use SAML.
-
-## Can I test a new SAML setup without locking members out?
-
-Yes. You can disable **Require SAML login** while testing or updating your SAML settings. This allows members to log in with email/password if needed. Once you're confident the new metadata works, re-enable SAML enforcement.
-
-## How can I confirm my SAML setup is correct?
-
-Before enabling **Require SAML login**, make sure your SAML connection is working by testing both SP-initiated and IdP-initiated logins. You should also confirm that:
-
-- The correct certificate and endpoints are in your Expensify metadata
-- Your IdP sends the proper NameID (usually the member's email)
-- Members can log in successfully using the SAML flow
-
-For step-by-step setup and testing instructions, check out the [SAML setup guide](https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify).
-
-
-
diff --git a/docs/articles/expensify-classic/settings/Two-Factor-Authentication.md b/docs/articles/expensify-classic/settings/Two-Factor-Authentication.md
index 28ba0ad971d0..0aab5c392215 100644
--- a/docs/articles/expensify-classic/settings/Two-Factor-Authentication.md
+++ b/docs/articles/expensify-classic/settings/Two-Factor-Authentication.md
@@ -1,95 +1,83 @@
---
title: Two-Factor Authentication
-description: Learn how to set up two-factor authentication in Expensify Classic and what to do if you're locked out of your account.
-keywords: [Expensify Classic, two-factor authentication, 2FA, login security, authenticator app, recovery codes, locked out, lost phone, account recovery, Domain Admin reset, backup codes]
+description: Enhance your account security by enabling two-factor authentication (2FA) in Expensify.
+keywords: [two-factor authentication, 2FA, security, Expensify, authenticator app, backup codes]
---
-Setting up two-factor authentication (2FA) in Expensify Classic adds an extra layer of protection to your account. This guide explains how to enable 2FA and what to expect if you're ever locked out.
+Setting up two-factor authentication (2FA) adds an extra layer of security to protect your financial data. When you log in, you must enter a code generated by your preferred authenticator app (such as Google Authenticator or Microsoft Authenticator).
+
+Expensify's 2FA is implemented via a Time-based One-Time Password (TOTP) algorithm. Each time you log in, you must use an authenticator app to generate a unique 6-digit code, adding a second “factor” to your login.
---
-# Who can enable Two-Factor Authentication in Expensify Classic
+# Recommended Authenticator Apps
-Anyone can enable Two-Factor Authentication on their own account. Domain Admins can require all members on a domain to enable Two-Factor Authentication on their accounts.
+You can use any authenticator app, but here are a few we recommend:
-# How to enable Two-Factor Authentication on your account in Expensify Classic
+- [1Password](https://support.1password.com/one-time-passwords/)
+- [Authy](https://authy.com/)
+- [Google Authenticator](https://support.google.com/accounts/answer/1066447)
+- [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app)
-1. Ensure an authenticator app is installed on your device.
-2. Go to **Settings > Account > Profile**.
-3. Enable **Two-factor authentication**.
-4. Save a copy of your backup codes:
+Ensure you have an authenticator app installed before proceeding.
+
+---
+
+# Enable Two-Factor Authentication
+
+1. Go to **Settings > Account > Profile**.
+2. Enable **Two-factor authentication**.
+3. Save a copy of your backup codes:
- Click **Download** to save them to your computer.
- Click **Copy** to store them in a secure location.
-5. Click **Continue**.
-6. Open your authenticator app and either:
+- **Important!** If you lose access to your authenticator app and do not have your recovery codes, you will lose access to your account.
+4. Click **Continue**.
+5. Open your authenticator app and either:
- Scan the QR code displayed on your screen.
- - Enter the 6-digit code from your authenticator app into Expensify and then click **Verify**.
-
-**Important:** If you lose access to your authenticator app and didn’t save your recovery codes, you may permanently lose access to your account. Consider adding 2FA on multiple devices (e.g., phone and tablet) for backup.
-
-# How to enable Two-Factor Authentication on a domain
+ - Enter the 6-digit code from your authenticator app into Expensify and click **Verify**.
-1. Go to **Settings > Domains > [domain name] > Domain Members**.
-2. Enable **Two-Factor Authentication**.
+**Once set up, when logging into Expensify, you will:**
+- Receive a Magic Code email to initiate login.
+- Be prompted to enter a 6-digit code from your authenticator app.
-**Note:** 2FA can’t be enabled for domains that use SAML.
+Codes refresh every few seconds. If you receive a message that the code is expired, re-refer to your authenticator app and use the most up-to-date code.
---
-## For Domain Admins: Reset Two-Factor Authentication for a member
+# Lost Recovery Codes or Access to the Authenticator App
+
+If you lose your mobile device and recovery codes, a **Domain Admin** can reset an employee's 2FA settings **only if**:
-If a member loses access to their authenticator app or recovery codes, you can reset their 2FA if:
-- They use a company email on your verified domain, **and**
-- You (the Domain Admin) also have 2FA enabled
+- The employee uses a company email or a domain you own.
+- The Domain Admin also has 2FA enabled.
-To reset a member’s 2FA settings:
+## Reset 2FA as a Domain Admin
1. Go to **Settings > Domains > Domain Members**.
2. Click **Edit Settings** for the affected email address.
3. Click **Reset** to disable 2FA.
-4. The member can now log in and set up 2FA again.
-
-If your domain doesn't have 2FA enabled yet:
+4. The user can now log in and reconfigure 2FA.
+If your domain does not have 2FA enabled:
1. Go to **Settings > Domains > Domain Members**.
2. Enable **Two-Factor Authentication**.
-3. Then follow the steps above to reset 2FA for the member.
+3. Follow the previous steps to reset 2FA for the user.
----
+**Note:** If you use a non-corporate email (e.g., Gmail, Yahoo, Hotmail), Expensify cannot disable 2FA. If recovery codes are lost, you may need to create a new account with a different email.
-## What to do if you're locked out because of Two-Factor Authentication
-
-If you can’t access your authenticator app and don’t have your recovery codes, contact your Domain Admin to reset your 2FA.
-
-If no Domain Admin is available and you're using a company email, you can follow [this guide](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain) to claim the domain and reset your 2FA settings yourself.
-
-For more help regaining access, see [Troubleshoot login issues](LINK).
+If no Domain Admin is available, follow [this guide](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain) to verify the domain, and then run through the steps above.
---
-# FAQ
-
-## How does 2FA change how I log into my account?
-
-Setting up two-factor authentication (2FA) adds an extra layer of security to protect your Expensify Account. When you log in, you must enter a code generated by your preferred authenticator app (such as Google Authenticator or Microsoft Authenticator).
-
-## How does 2FA work?
-
-Expensify's 2FA is implemented via a Time-based One-Time Password (TOTP) algorithm. Each time you log in, you must use an authenticator app to generate a unique 6-digit code, adding a second “factor” to your login.
-
-## What can I do if I can't access my authenticator app?
-
-When you enable 2FA, you are prompted to either copy or download backup codes which you can use in lieu of the 6-digit authenticator code. If you downloaded the codes they will be saved with the file name `two-factor-auth-codes`.
+# Troubleshooting
-## What authenticator apps does Expensify recommend?
-
-You can use any authenticator app, but here are a few we recommend:
+Ensure your phone’s time is set to **automatic update**. A manual time difference can cause issues when entering the authentication code. See this resource for more details on [setting the timezone in your account](https://help.expensify.com/articles/expensify-classic/settings/Set-Time-Zone).
-- [1Password](https://support.1password.com/one-time-passwords/)
-- [Authy](https://authy.com/)
-- [Google Authenticator](https://support.google.com/accounts/answer/1066447)
-- [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app)
+Make sure you're not still logged in on another device. If you are, do the following:
+ 1. Go to **Settings > Account > Profile**.
+ 2. Toggle **Two-factor authentication** to off.
+ 3. Try logging in again.
+ 4. Once logged in, you can re-enable 2FA.
-## What if my verification code isn’t working?
-Make sure your device’s clock is set to update automatically. Authenticator apps rely on your system clock being accurate, and even a small time difference can cause verification codes to fail.
+Following these steps ensures your account remains secure while preventing access issues.
diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-Overview.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-Overview.md
index 1cccf0d93d7e..b5a25eff8385 100644
--- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-Overview.md
+++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-Overview.md
@@ -47,11 +47,13 @@ Expensify offers two subscription plans — **Collect** and **Control** — desi
£5 per unique member/month
Fully flexible — add/remove members anytime
+
Optional Expensify Card with 1% cash back (USD only)
Control
-
£14 per active member/month with Annual Subscription
-
£28 pay-per-use
+
£14 per active member/month with Annual Subscription + Card
+
£28 without Card or for pay-per-use
+
Earn up to 2% cash back
@@ -61,11 +63,12 @@ Expensify offers two subscription plans — **Collect** and **Control** — desi
€5 per unique member/month
Month-to-month — no contract
+
Optional Expensify Card
Control
-
€16 per active member/month with Annual Subscription
-
€32 pay-per-use
+
€16 per active member/month with Annual Subscription + Card
+
€32 without Card or for pay-per-use
@@ -78,8 +81,8 @@ Expensify offers two subscription plans — **Collect** and **Control** — desi
Control
-
AU$30 per active member/month with Annual Subscription
-
AU$60 pay-per-use
+
AU$30 per active member/month with Annual Subscription + Card
+
AU$60 without Card or for pay-per-use
@@ -92,8 +95,8 @@ Expensify offers two subscription plans — **Collect** and **Control** — desi
Control
-
NZ$32 per active member/month with Annual Subscription
-
NZ$64 pay-per-use
+
NZ$32 per active member/month with Annual Subscription + Card
+
NZ$64 without Card or for pay-per-use
@@ -135,7 +138,7 @@ Expensify offers two subscription plans — **Collect** and **Control** — desi
# Billing eligibility and details
-## 💳 Expensify Card usage (US Only)
+## 💳 Expensify Card usage
- **Collect:** Card is optional — but earns 1% cash back on US spend
- **Control:** Card is required for discounted pricing and cash back
diff --git a/docs/articles/new-expensify/domains/Set-Up-SAML-SSO.md b/docs/articles/new-expensify/domains/Set-Up-SAML-SSO.md
deleted file mode 100644
index b34eb5a36aa7..000000000000
--- a/docs/articles/new-expensify/domains/Set-Up-SAML-SSO.md
+++ /dev/null
@@ -1,126 +0,0 @@
----
-title: Set Up SAML Single Sign-On (SSO)
-description: Learn how to enable and configure SAML SSO in New Expensify to secure login for domain members using your organization's identity provider.
-keywords: [New Expensify, SAML SSO, domain security, single sign-on, identity provider, verified domain, enable SAML, Okta, Google Workspace, Microsoft Entra, ADFS]
----
-
-Set up secure and streamlined login across your organization by enabling SAML Single Sign-On (SSO) in New Expensify. This allows Workspace members to authenticate using your identity provider (IdP), rather than creating separate credentials.
-
-# Where to find SAML Single Sign-On (SSO) settings
-
-Go to Domains > [Domain Name] > SAML to manage SAML Single Sign-On (SSO).
-
-**Note:** Your domain must be verified before you can enable SAML. [Learn how to claim and verify a domain](https://help.expensify.com/articles/new-expensify/workspaces/Claim-and-Verify-a-Domain).
-
-Once you are a Domain Admin on a verified domain, you can configure SAML SSO login:
-
-1. Go to **Workspaces > [domain name] > SAML**.
-2. Toggle **SAML Login** to **Enabled**.
-
----
-
-# Who can manage SAML Single Sign-On (SSO)
-
-Only **Domain Admins** can configure SAML for verified domains. SAML login applies to all Domain members whose email addresses match the verified domain.
-
----
-
-# How to set up SAML Single Sign-On (SSO)
-
-1. Go to **Workspaces > [domain name] > SAML**.
-2. Toggle **SAML Login** to **Enabled**.
-3. Download Expensify’s **Service Provider metadata** to upload to your IdP.
-4. Paste your IdP metadata in the **Identity Provider MetaData** field.
-5. Test logging in to confirm that SAML SSO is configured correctly (recommended).
-6. Enable **Required for login** to ensure members sign in via SSO only.
-
-Select your Identity (SAML) Provider for detailed steps on configuring SAML Single Sign-On (SSO):
-
-- [Amazon Web Services (AWS SSO)](https://static.global.sso.amazonaws.com/app-202a715cb67cddd9/instructions/index.htm)
-- [Google Workspace / SAML (Gsuite)](https://support.google.com/a/answer/7371682)
-- [Microsoft Entra ID (formerly Azure AD)](https://learn.microsoft.com/en-us/entra/identity/saas-apps/expensify-tutorial)
-- [Okta](https://saml-doc.okta.com/SAML_Docs/How-to-Configure-SAML-2.0-for-Expensify.html)
-- [OneLogin](https://onelogin.service-now.com/support?id=kb_article&sys_id=e44c9e52db187410fe39dde7489619ba)
-- [Oracle Identity Cloud Service](https://docs.oracle.com/en/cloud/paas/identity-cloud/idcsc/expensify.html#Expensify)
-- [SAASPASS](https://saaspass.com/saaspass/expensify-two-factor-authentication-2fa-single-sign-on-sso-saml.html)
-- Microsoft ADFS – see instructions below
-
-**Note:** If your provider isn't listed, contact them directly for guidance with metadata and setup.
-
----
-
-# How SAML Single Sign-On (SSO) affects member login
-
-- Members with email addresses matching your verified domain will be prompted to log in through your configured IdP.
-- Members using a personal or secondary email (e.g., Gmail) must [update their email address](https://help.expensify.com/articles/new-expensify/settings/Update-Email-Address) to match the verified domain for SSO access.
-
----
-
-# Troubleshooting SAML Single Sign-On (SSO)
-
-## If setup fails or login doesn't work:
-
-- Use [samltool.com](https://samltool.com) to validate your IdP metadata and certificate.
-- Make sure the email domain in your IdP exactly matches your verified domain in Expensify.
-
-## What is Expensify’s Entity ID?
-
-- Standard setup: `https://expensify.com`
-- Multi-domain setup: `https://expensify.com/yourdomain.com`
-
-Managing multiple domains with one Entity ID is supported. Contact Concierge or your Account Manager to enable this feature.
-
-# Advanced configurations for SAML Single Sign-On (SSO)
-
-## Okta SCIM API and SAML provisioning
-
-Once SAML is configured:
-
-1. In Okta, add Expensify as an app and configure attribute mappings.
-2. Request SCIM API access via **concierge@expensify.com**.
-3. Add the SCIM token in your Okta provisioning settings.
-
-Refer to Okta’s documentation for complete instructions.
-
-## Microsoft ADFS configuration
-
-1. Open the **ADFS Management Console** and create a new trust.
-2. Upload Expensify’s metadata XML file.
-3. Map **LDAP attributes** (email or UPN) to outgoing claims.
-4. Add two claim rules:
- - Send LDAP Attributes as Claims
- - Transform Incoming Claim to Name ID
-
-## Microsoft Entra ID certificate update process
-
-To avoid setup errors during certificate renewal:
-
-1. Create the new certificate in Microsoft Entra.
-2. Remove the old certificate before activating the new one.
-3. Replace the existing IdP metadata in Expensify.
-4. Log in via SSO to confirm the new certificate works.
-
-# FAQ
-
-## Can I use SAML Single Sign-On (SSO) for multiple Workspaces?
-
-Yes, as long as all members are part of the same verified domain, SAML access applies across all Workspaces they belong to.
-
-## How can I confirm my SAML Single Sign-On (SSO) setup is working?
-
-Before enabling **Require SAML login**, make sure your SAML connection is working by testing both SP-initiated and IdP-initiated logins. You should also confirm that:
-
-- The correct certificate and endpoints are in your Expensify metadata
-- Members can log in successfully using the SAML flow
-
-## Can I test a new SAML Single Sign-On (SSO) setup without locking members out?
-
-Yes. Disable **Require SAML login** before making changes. This allows members to log in with email and password if SAML setup fails. Once you’ve confirmed that login works, you can re-enable enforcement.
-
-## What do I do if a member can’t log in after SAML Single Sign-On (SSO) is enabled?
-
-First, confirm that the member’s email matches your verified domain and that their account exists in your Identity Provider (IdP) with the correct access permissions.
-
-## Are custom NameID, ACS, or SLO URLs supported in SAML Single Sign-On (SSO)?
-
-No, the NameID Format, Login URL (ACS URL), and Logout URL (SLO URL) are static and cannot be modified.
diff --git a/docs/articles/new-expensify/domains/Troubleshoot-SAML-SSO-Login.md b/docs/articles/new-expensify/domains/Troubleshoot-SAML-SSO-Login.md
deleted file mode 100644
index a13264554f29..000000000000
--- a/docs/articles/new-expensify/domains/Troubleshoot-SAML-SSO-Login.md
+++ /dev/null
@@ -1,118 +0,0 @@
----
-title: Troubleshoot SAML SSO login
-description: Learn how to quickly diagnose and resolve issues with SAML SSO login in New Expensify, including lockouts, expired certificates, and identity provider errors.
-keywords: [New Expensify, SAML SSO, SSO login failed, Require SAML login, domain locked out, expired certificate, identity provider, IdP, metadata, troubleshooting]
----
-
-If you're having trouble logging in with SAML Single Sign-On (SSO) in New Expensify, this guide will help you identify the issue, understand what’s causing it, and get access restored quickly.
-
----
-
-# Where to find SAML SSO settings in New Expensify
-
-To check your domain's SAML SSO configuration, go to **Workspaces > [domain name] > SAML**.
-
-From this page, Domain Admins can:
-
-- Enable SAML SSO login for the domain
-- View and update your Identity Provider (IdP) metadata
-- Disable or enable **Require SAML login**
-
-**Note:** SAML SSO settings are not available on mobile.
-
----
-
-# How to fix domain-wide SAML SSO login issues
-
-## SAML login suddenly fails for all members
-
-A domain-wide issue usually points to a problem with your Identity Provider (IdP). Check the following:
-
-**Has your IdP certificate expired or rotated?**
-
-If yes, copy the updated metadata XML from your IdP and paste it into the **Identity Provider Metadata** field in your Expensify SAML settings.
-
-**Have any IdP settings changed?**
-
-Changes to entity IDs, SSO endpoints, or user attributes can break login.
- - If your certificate or SSO endpoints have changed, upload updated metadata from your IdP to Expensify.
- - If user attributes like NameID Format or email mappings have changed, confirm they match the values expected in your domain's SAML settings in Expensify.
-
- **Is “Require SAML login” turned on?**
-
- If enabled, no one — including Domain Admins — can log in without a working SAML configuration. If you're still signed in, go to your domain’s SAML settings and temporarily disable **Require SAML login** while troubleshooting.
-
-## Some members can’t log in, but others can
-
-This is often caused by an email alias not recognized by your identity provider (IdP), or because the member hasn’t been added to the correct SAML rule or group. Confirm that the member’s email matches your verified domain in Expensify, and check your IdP to ensure they’re included in the appropriate SAML group or rule.
-
-## All Domain Admins are locked out
-
-If no Domain Admins can log in, you won’t be able to access SAML settings. Email concierge@expensify.com from an address that matches your verified domain for help.
-
----
-
-# How to resolve common SAML SSO error messages
-
-## Signature validation failed
-
-This typically happens when the certificate has expired, is malformed, or doesn't match the one used by your IdP. To fix it, copy the updated metadata XML from your IdP and paste it into the **Identity Provider Metadata** field in your Expensify SAML settings.
-
----
-
-## SAML Response not found. Only supported HTTP_POST Binding
-
-Your Identity Provider is not sending the `SAMLResponse` in the POST body as expected. To fix it, update your IdP configuration to use **HTTP POST binding** when sending the SAML Response.
-
----
-
-## No user with that partnerUserID/partnerUserSecret
-
-This occurs when your IdP sends an email (NameID) that doesn’t match the one stored in Expensify for that member. To fix it, confirm that the NameID value sent by your IdP exactly matches the member’s email address in Expensify. If needed, update the member's email in your IdP or in Expensify to resolve the mismatch.
-
----
-
-## Bad XML metadata
-
-Your metadata file may contain formatting issues — often extra line breaks or copy/paste errors in the x.509 certificate.
-**How to fix it:** Use a certificate formatting tool (like [samltool.com](https://samltool.com)) to clean and validate your metadata before pasting it into Expensify.
-
-**Note:** When copying a certificate, make sure it includes the full `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` block with no formatting errors.
-
----
-
-## SAML login not available on your domain
-
-This appears when **Require SAML login** is enabled, but SAML isn’t fully configured. To fix it, follow the steps to [Configure Single Sign On (SSO)](https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify) for your domain.
-
----
-
-# How to contact Expensify if you're locked out
-
-If you can't sign in due to a SAML issue, email **concierge@expensify.com** from an address that matches your verified domain.
-
----
-
-# FAQ
-
-## What should I do before making changes to my domain's SAML SSO setup?
-
-Before making changes to your Identity Provider setup — like rotating certificates or updating endpoints — we recommend **temporarily disabling Require SAML login** in Expensify.
-
-This ensures Domain Admins can still sign in with email and password if the new configuration doesn’t work. Once you’ve uploaded the new metadata and confirmed login is working, you can safely re-enable Require SAML login.
-
-## Can I make SAML login optional for some members?
-
-No. SAML settings apply to the entire domain. If **Require SAML login** is enabled, **all members** must authenticate via SAML — there’s no way to allow some members to log in with email and password while others use SAML.
-
-## Can I test a new SAML setup without locking members out?
-
-Yes. You can disable **Require SAML login** while testing or updating your SAML settings. This allows members to log in with email/password if needed. Once you're confident the new metadata works, re-enable SAML enforcement.
-
-## How can I confirm my SAML setup is correct?
-
-Before enabling **Require SAML login**, make sure your SAML connection is working by testing both SP-initiated and IdP-initiated logins. You should also confirm that:
-
-- The correct certificate and endpoints are in your Expensify metadata
-- Your IdP sends the proper NameID (usually the member's email)
-- Members can log in successfully using the SAML flow
diff --git a/docs/articles/new-expensify/reports-and-expenses/Attach-and-edit-receipts-on-expenses.md b/docs/articles/new-expensify/reports-and-expenses/Attach-and-edit-receipts-on-expenses.md
index 2bc1cf3054be..cde8e00c007f 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Attach-and-edit-receipts-on-expenses.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Attach-and-edit-receipts-on-expenses.md
@@ -11,7 +11,7 @@ Make sure your receipts are attached correctly to individual expenses for audit
# How to Attach and Verify Receipts on Expenses
## Who can attach receipts to an expense
-- **Attach or replace a receipt**: The member who created the expense, a Workspace Admin, or the current approver.
+- **Attach or replace a receipt**: The member who created the expense, or a Workspace Admin.
- **Edit a receipt in an Approved or Paid report**: Requires the report to be unapproved first (see below).
---
diff --git a/docs/articles/new-expensify/reports-and-expenses/Managing-Expenses-in-a-Report.md b/docs/articles/new-expensify/reports-and-expenses/Managing-Expenses-in-a-Report.md
index 063419ddb33d..ee2741fd9658 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Managing-Expenses-in-a-Report.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Managing-Expenses-in-a-Report.md
@@ -1,15 +1,15 @@
---
title: Managing Expenses in a Report
description: Learn how to add, remove, and move expenses in a report in New Expensify, including how comments and system messages interact with them.
-keywords: [New Expensify, manage expenses, add expense, delete expense, move expense, expense table, edit report, report approval, expense actions]
+keywords: [New Expensify, manage expenses, add expense, delete expense, move expense, expense table, edit report, report approval]
---
Easily add, delete, or move expenses within reports in New Expensify. This guide covers how to manage expenses using the expense table on both web and mobile.
-# Managing Expenses in a Report in New Expensify
+# Managing Expenses in a Report
## Who can edit or modify expenses in a report
-- **Edit expenses on a report**: The member who created the report, the current approver, and Workspace Admins.
+- **Edit expenses on a report**: The member who created the report, and Workspace Admins.
- **Add expenses to a report**: Only the member who created the report.
- **Remove expenses from a report**: Only the member who created the report.
- **Delete an expense**: Only the member who created that specific expense.
diff --git a/docs/articles/new-expensify/reports-and-expenses/Using-Reports-in-New-Expensify.md b/docs/articles/new-expensify/reports-and-expenses/Using-Reports-in-New-Expensify.md
index 376a7dfe7a43..7ec7d9d34045 100644
--- a/docs/articles/new-expensify/reports-and-expenses/Using-Reports-in-New-Expensify.md
+++ b/docs/articles/new-expensify/reports-and-expenses/Using-Reports-in-New-Expensify.md
@@ -1,7 +1,7 @@
---
title: Using Reports in New Expensify
description: Learn how to use Reports in New Expensify to search, filter, customize columns, and save reports for expenses, invoices, trips, and chats.
-keywords: [Reports, New Expensify, report filters, search commands, custom columns, saved reports, group expenses, invoices, expenses, chats, trips, reimbursement tracking, view expenses, customize report view, reporting table columns]
+keywords: [Reports, New Expensify, report filters, search commands, custom columns, saved reports, invoices, expenses, chats, trips, reimbursement tracking, view expenses, customize report view, reporting table columns]
---
@@ -76,18 +76,6 @@ To view the full list of available columns, click the **Columns** icon in the **
---
-# How to group expenses by category or tag using Report Layout
-
-Report Layout lets you group expenses inside a report to make reviews faster and easier.
-
-To group expenses:
-
-1. Open a report that contains more than one expense.
-2. Click the **More** icon (three dots) in the top-right corner of the report.
-3. Select **Group by category**, **Group by tag**, or **Don’t group**.
-
-Your selected layout will be remembered and applied to other reports you view.
-
# How to use Reports search query commands
Use search commands in the search bar to run advanced queries based on specific fields.
diff --git a/docs/articles/new-expensify/settings/Login-Troubleshooting.md b/docs/articles/new-expensify/settings/Login-Troubleshooting.md
deleted file mode 100644
index 9883c54ee77e..000000000000
--- a/docs/articles/new-expensify/settings/Login-Troubleshooting.md
+++ /dev/null
@@ -1,149 +0,0 @@
----
-title: Login Troubleshooting
-description: Troubleshoot common login issues — like missing Magic Codes, 2FA lockouts, SSO problems, or blocked emails — and find out who to contact to get back in fast.
-keywords: [New Expensify, Expensify login, can't log in, login help, Magic Code missing, Two-Factor Authentication, SSO login, email domain blocked, login error, Concierge login support, account access issue, locked out]
----
-
-# Login troubleshooting
-
-If you're not able to log into your Expensify account, find your situation below to see what's causing it and how to fix it.
-
----
-
-## Can't receive Magic Code login email
-
-A Magic Code is a one-time login code that Expensify emails you when you try to sign in instead of asking for a password. If you’re not getting the email, your email provider might be blocking or filtering it.
-
-**Common Cause**
-Your email provider is blocking or filtering messages from Expensify.
-
-**What you can try**
-- Wait up to **10 minutes** for the email to arrive — delivery can be delayed.
-- Check your **spam**, **junk**, and **trash** folders.
-- Search for **Expensify Magic Code** or concierge@expensify.com.
-- Add concierge@expensify.com to your email contacts.
-- Click **Didn't receive a magic code?** on the login screen to request the code be sent again.
-- Wait **2–3 minutes** between resend attempts. If you request a new code while one is still being generated, the older one won't work.
-
-**What won’t help**
-- Clicking "Resend" repeatedly without checking your filters.
-- Logging in with a phone number or backup email. The Magic Code is always sent to the primary login set on the account - not any secondary contact methods.
-
-**Who to contact**
-- **Using a work email?** Ask your IT team to [allowlist Expensify emails](https://help.expensify.com/articles/expensify-classic/email/How-to-Allowlist-Expensify).
-- **Using a personal email?** If you’ve already tried the steps above, contact Concierge.
-
-[See the full guide to fixing email delivery →](https://help.expensify.com/articles/expensify-classic/email/Troubleshoot-Email-Delivery-Issues)
-
----
-
-## Locked out by Two-Factor Authentication (2FA)
-
-Two-factor authentication (2FA) is a feature that adds an extra layer of security to your account, and can be enabled by you on your account or required by a Domain Admin. After entering your Magic Code, you’ll be prompted to enter another code — this one comes from your authenticator app.
-
-**Common Cause**
-Two-Factor Authentication (2FA) is enabled on your account, but you no longer have access to your authenticator app or recovery codes.
-
-**What you can try**
-- Search your phone for any authenticator apps (e.g., Google Authenticator, Authy).
-- Search your device for backup or recovery codes, saved in a file called `two-factor-auth-codes`.
-
-**What won’t help**
-- Signing in again
-- Reinstalling the authenticator app without recovery codes.
-
-**Who to contact**
-- **Work email?** Ask your Domain Admin to reset your 2FA.
-- **No Domain Admin?** [Claim your domain](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain) to claim the domain and reset your 2FA settings yourself.
-- **Personal email?** Without recovery codes, you’ll need to create a new account.
-
-[Learn more about 2FA and account recovery →](https://help.expensify.com/articles/expensify-classic/security/Two-Factor-Authentication-Overview)
-
----
-
-## Can't log in with SAML SSO
-
-If your company uses SAML SSO, you'll be redirected to your company's Identity Provider (e.g., Okta, Azure) to authenticate.
-
-**Common Cause**
-Your company's SAML SSO configuration may have an issue affecting your account or domain
-
-**What you can try**
-- If you see the option "Would you like to sign in with a magic code or Single Sign-On?", choose **Magic Code** to log in with a code instead.
-- Double-check your credentials to ensure you are inputting the right details into your Identity Provider login screen.
-- Log in to another app you access using the same Identity Provider login and see if you experience the same error.
-
-**Who to contact**
-- **Reach out to your IT team** to confirm SSO settings and ask them to follow the relevant steps to [Troubleshoot SAML SSO Login](https://help.expensify.com/articles/new-expensify/domains/Troubleshoot-SAML-SSO-Login)
-
-[More about SSO logins →](https://help.expensify.com/articles/new-expensify/domains/Managing-Single-Sign-On-(SSO)-in-Expensify)
-
----
-
-## Can't access your Expensify login email
-
-**Common Cause**
-You’ve changed jobs or lost access to the email inbox tied to your Expensify login.
-
-**What you can try**
-- If your company still owns the email domain, ask the internal team who manages email access to restore the email account so you can regain access.
-
-**Who to contact**
-- **Still with the company?** Contact your Domain Admin or IT team.
-- **No longer with the company?** Message Concierge. Include your old email and any associated reports or expenses to help confirm your identity.
-
----
-
-## Unblock your email address
-
-If you see a banner that says "We're having trouble emailing you", it means Expensify emails are temporarily suspended for your email address.
-
-**Common Cause**
-Expensify tried to email you but it bounced, either because it was not a real email address or the incoming mail server rejected it.
-
-**What to do**
-1. Confirm the primary email address on your Expensify account is a real email account that can accept incoming emails.
-2. Confirm with your IT team or email server that Expensify emails are not being blocked.
-3. Click the link in the banner to unblock your email.
-
-If you see errors like "mimecast", "blacklist", or "SMTP errors":
-- Share the error message with your IT team.
-- Ask them to allowlist `expensify.com`.
-
-[More on allowlisting and domain blocks →](https://help.expensify.com/articles/expensify-classic/email/How-to-Allowlist-Expensify)
-
----
-
-## General login troubleshooting steps
-
-Try these general troubleshooting steps:
-- [Force a clean sign out](https://www.expensify.com/signout.php?clean=true).
-- Switch browsers or devices.
-- Try logging in using Incognito or Private mode.
-- Check [Expensify’s system status page](https://status.expensify.com).
-
-**Still blocked? Contact Concierge and include:**
-- The email you're trying to log in with.
-- A short description of what you’re seeing.
-- Screenshots of any errors (if available).
-
----
-
-# FAQ
-
-## Who is my Domain Admin?
-Ask your Workspace Owner or the person who approves your reports. To find your Workspace Owner, go to **Workspaces** and check the **Owner**
-column.
-
-## How can I keep access to my Expensify account after leaving my company?
-[Add a secondary contact method](https://help.expensify.com/articles/new-expensify/settings/Update-Email-Address) (like a personal email) so you can still log in if you lose access to your work email.
-
-## Why is Expensify asking for a Two-Factor Authentication (2FA) code I don’t remember setting up?
-Your Domain Admin may have required it. Try searching your phone for an authenticator app or recovery codes with the file name `two-factor-auth-codes`.
-
-## Can Expensify reset Two-Factor Authentication (2FA) if I use a personal email?
-No. If you’re not part of a company domain and didn’t save your recovery codes, your 2FA can’t be reset.
-
-## How can I contact Concierge if I can't log into my account?
-Email concierge@expensify.com from the email address associated with your Expensify account.
-
diff --git a/docs/articles/new-expensify/settings/Two-Factor-Authentication.md b/docs/articles/new-expensify/settings/Two-Factor-Authentication.md
index a220d0aafe36..90e196959376 100644
--- a/docs/articles/new-expensify/settings/Two-Factor-Authentication.md
+++ b/docs/articles/new-expensify/settings/Two-Factor-Authentication.md
@@ -1,34 +1,35 @@
---
title: Two-Factor Authentication (2FA)
description: Learn how to set up, use, and recover your Expensify account with two-factor authentication (2FA), including lost device and admin recovery options.
-keywords: [New Expensify, two-factor authentication, 2FA, login security, authenticator app, recovery codes, locked out, lost phone, account recovery, Domain Admin reset, backup codes]
+keywords: [Expensify Classic, two-factor authentication, 2FA, login security, authenticator app, recovery codes, locked out, lost phone, account recovery, Domain Admin reset]
---
-Setting up two-factor authentication (2FA) in New Expensify adds an extra layer of protection to your account. This guide explains how to enable 2FA and what to expect if you're ever locked out.
+Two-factor authentication (2FA) adds an extra layer of protection to your Expensify account. This guide covers setup, login expectations, recovery steps if you lose access, and admin override options.
---
-# Who can enable Two-Factor Authentication in New Expensify
+# How two-factor authentication works
-Anyone can enable Two-Factor Authentication on their own account. Domain Admins can require all members on a domain to enable Two-Factor Authentication on their accounts.
+When logging in:
+1. Enter your email and the magic code sent to your inbox.
+2. Enter a 6-digit code generated by your authenticator app (such as Google Authenticator, Microsoft Authenticator, or Authy).
+
+Codes refresh every few seconds. If one expires, simply open the app for a new code.
---
# How to enable two-factor authentication
-1. Ensure an authenticator app is installed on your device.
-2. From the left-hand menu, select **Account > Security**.
-3. Under **Security options**, select **Two-Factor Authentication**.
-4. Enable **Two-factor authentication**.
-4. Save a copy of your backup codes:
- - Click **Download** to save them to your computer.
- - Click **Copy** to store them in a secure location.
-5. Click **Continue**.
-6. Open your authenticator app and either:
- - Scan the QR code displayed on your screen.
- - Enter the 6-digit code from your authenticator app into Expensify and then click **Verify**.
-
-**Important:** If you lose access to your authenticator app and didn’t save your recovery codes, you may permanently lose access to your account. Consider adding 2FA on multiple devices (e.g., phone and tablet) for backup.
+1. From the left-hand menu, select **Account > Security**.
+2. Under **Security options**, select **Two-Factor Authentication**.
+3. Follow the prompts to enable 2FA.
+4. **Save your backup codes**—these are essential for account recovery.
+ - Select **Download** to save the codes securely.
+ - Select **Copy** to paste them into a password manager or secure file.
+5. Open your authenticator app and connect it to Expensify by:
+ - Scanning the QR code, or
+ - Entering the setup code manually.
+6. Enter the 6-digit verification code and select **Verify**.
---
@@ -40,59 +41,46 @@ After setup, login requires both:
---
-## For Domain Admins: Reset Two-Factor Authentication for a member
-
-If a member loses access to their authenticator app or recovery codes, you can reset their 2FA if:
-- They use a company email on your verified domain, **and**
-- You (the Domain Admin) also have 2FA enabled
+# Recovery options
-To reset a member’s 2FA settings:
+Backup recovery codes work like one-time passwords. They are your fastest recovery method if you lose access to your authenticator app.
-1. Go to **Settings > Domains > Domain Members**.
-2. Click **Edit Settings** for the affected email address.
-3. Click **Reset** to disable 2FA.
-4. The member can now log in and set up 2FA again.
+**Tip:** Store unused recovery codes in a secure, offline location. Each code can only be used once.
-If your domain doesn't have 2FA enabled yet:
+## If you lost your device and have no recovery codes
+- **Individual account**: If you're using a public domain (like gmail.com or outlook.com), you'll need to create a new Expensify account with a different email. Concierge can assist with transferring access to any shared Workspaces.
+- **Domain account**: A **Domain Admin** can reset your 2FA. Once reset, you’ll log in normally and set up 2FA again.
-1. Go to **Settings > Domains > Domain Members**.
-2. Enable **Two-Factor Authentication**.
-3. Then follow the steps above to reset 2FA for the member.
+# Admin recovery and overrides
-**Note** Domain Admin 2FA resets are only available in Expensify Classic. To complete these steps, temporarily [switch to Expensify Classic](https://help.expensify.com/articles/new-expensify/settings/Switch-between-New-Expensify-and-Expensify-Classic.html)).
+## If a Domain Admin is available
+- Domain Admins can reset a member’s 2FA by going to:
+ **Settings > Domains > [Domain Name] > Members > Security Settings**
+- Select the member, then disable their 2FA.
----
-
-## What to do if you're locked out because of Two-Factor Authentication
+## If the enforcing Domain Admin has left
+1. Verify domain ownership by proving control of the domain’s email DNS or MX records.
+2. Assign a new Domain Admin in **Settings > Domains > [Domain Name] > Domain Settings**.
+3. Once the new admin is assigned, follow the steps above to reset 2FA for affected members.
-If you can’t access your authenticator app and don’t have your recovery codes, contact your Domain Admin to reset your 2FA.
+# Best practices
-If no Domain Admin is available and you're using a company email, you can follow [this guide](https://help.expensify.com/articles/new-expensify/workspaces/Claim-and-Verify-a-Domain) to claim the domain and reset your 2FA settings yourself.
+- Save your recovery codes as soon as you set up 2FA.
+- Consider adding 2FA on multiple devices (e.g., phone and tablet) during setup for backup.
+- Keep your device’s clock set to the correct time—codes depend on accurate timing.
---
# FAQ
-## How does 2FA change how I log into my account?
-
-Setting up two-factor authentication (2FA) adds an extra layer of security to protect your Expensify Account. When you log in, you must enter a code generated by your preferred authenticator app (such as Google Authenticator or Microsoft Authenticator).
-
-## How does 2FA work?
-
-Expensify's 2FA is implemented via a Time-based One-Time Password (TOTP) algorithm. Each time you log in, you must use an authenticator app to generate a unique 6-digit code, adding a second “factor” to your login.
-
-## What can I do if I can't access my authenticator app?
-
-When you enable 2FA, you are prompted to either copy or download backup codes which you can use in lieu of the 6-digit authenticator code. If you downloaded the codes they will be saved with the file name `two-factor-auth-codes`.
-
-## What authenticator apps does Expensify recommend?
+## Why should I use 2FA?
+It prevents unauthorized access, even if someone has your login email or password.
-You can use any authenticator app, but here are a few we recommend:
+## What if I lose my phone or uninstall the app?
+Use a recovery code to log in, then disable and re-enable 2FA on your new device.
-- [1Password](https://support.1password.com/one-time-passwords/)
-- [Authy](https://authy.com/)
-- [Google Authenticator](https://support.google.com/accounts/answer/1066447)
-- [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app)
+## Can I use 2FA on more than one device?
+Yes. Scan the setup QR code with multiple devices when enabling 2FA.
## What if my verification code isn’t working?
-Make sure your device’s clock is set to update automatically. Authenticator apps rely on your system clock being accurate, and even a small time difference can cause verification codes to fail.
+Check your device’s time settings. Authenticator apps rely on accurate system clocks.
diff --git a/docs/articles/new-expensify/wallet-and-payments/Connect-a-Personal-Bank-Account.md b/docs/articles/new-expensify/wallet-and-payments/Connect-a-Personal-Bank-Account.md
index 824870389063..36b2c3cd18b0 100644
--- a/docs/articles/new-expensify/wallet-and-payments/Connect-a-Personal-Bank-Account.md
+++ b/docs/articles/new-expensify/wallet-and-payments/Connect-a-Personal-Bank-Account.md
@@ -1,7 +1,7 @@
---
title: Connect a Personal Bank Account
description: Learn how to connect your personal bank account to receive reimbursements in Expensify, including support for both US and international accounts.
-keywords: [New Expensify, bank account, personal bank account, reimbursements, wallet, US bank account, global reimbursements, manual bank connection, add bank account for reimbursement, reimbursement account setup, wallet, deposit account, direct deposit]
+keywords: [New Expensify, bank account, personal bank account, reimbursements, wallet, US bank account, global reimbursements]
---
You can add a personal bank account to receive reimbursements in over **190 countries**. All personal accounts are managed under **Account > Wallet > Bank Accounts**. The steps differ depending on whether you're connecting a **US** or **non-US** account.
@@ -10,25 +10,17 @@ You can add a personal bank account to receive reimbursements in over **190 coun
# Add a U.S. Bank Account
-For U.S. accounts, Expensify offers two ways to connect your bank account:
-
-- **Log into your bank:** Securely link your account using your bank login.
-- **Connect manually:** Enter your routing and account numbers without logging in.
+For U.S. accounts, Expensify uses **Plaid**, a secure third-party provider that verifies your banking information.
To connect a U.S. bank account:
1. Go to **Account > Wallet > Bank Accounts**.
2. Click **Add Bank Account**.
3. Select **United States** as your country.
-4. Choose one of the following:
- - **Log into your bank**:
- - Select your bank from the list.
- - Enter your online banking credentials and follow the prompts to complete setup.
- - **Connect manually**:
- - Enter your routing number and account number,
- - Enter or update your name, address, and phone number to match your bank account details.
- - Click **Confirm** to complete setup.
-5. Your bank account will appear in the **Bank Accounts** section.
+4. Follow the **Plaid** connection flow:
+ - Choose your bank.
+ - Enter your credentials.
+5. Once complete, your account will appear in the **Bank Accounts** section.
{:width="100%"}
diff --git a/docs/articles/new-expensify/domains/Claim-and-Verify-a-Domain.md b/docs/articles/new-expensify/workspaces/Claim-and-Verify-a-Domain.md
similarity index 100%
rename from docs/articles/new-expensify/domains/Claim-and-Verify-a-Domain.md
rename to docs/articles/new-expensify/workspaces/Claim-and-Verify-a-Domain.md
diff --git a/docs/articles/new-expensify/workspaces/Set-Up-SAML-Single-Sign-On.md b/docs/articles/new-expensify/workspaces/Set-Up-SAML-Single-Sign-On.md
new file mode 100644
index 000000000000..f56aaf9b06f8
--- /dev/null
+++ b/docs/articles/new-expensify/workspaces/Set-Up-SAML-Single-Sign-On.md
@@ -0,0 +1,92 @@
+---
+title: Set Up SAML Single Sign-On (SSO)
+description: Learn how to enable and configure SAML SSO in New Expensify to secure login for domain members using your organization's identity provider.
+keywords: [New Expensify, SAML SSO, domain security, single sign-on, identity provider, verified domain, enable SAML]
+---
+
+
+
+Expensify supports Single Sign-On (SSO) through the SAML protocol, allowing you to secure user authentication across your organization. This guide walks you through setting up and managing SAML SSO for your Expensify account.
+
+
+# Set Up SAML Single Sign-On
+
+⚠️ **Note:** Your domain must be verified before you can enable SAML. [Learn how to claim and verify a domain](https://help.expensify.com/articles/new-expensify/workspaces/Claim-and-Verify-a-Domain)
+
+Once your domain is verified, you can configure SAML-based login for enhanced authentication.
+
+1. Select your domain from **Workspaces > Domains**.
+2. Open the **SAML** section.
+3. Toggle **Enable SAML login**.
+4. Download Expensify’s Service Provider Metadata to provide to your Identity Provider.
+5. Enter the Identity Provider Metadata from your SSO provider. (Contact your provider if unsure how to obtain this).
+6. Toggle on **Require SAML login**, ensuring users sign in via SSO only.
+
+
+# Identity Provider-Specific SAML Setup
+
+You can find provider-specific instructions here:
+
+- [Amazon Web Services (AWS SSO)](https://docs.aws.amazon.com/singlesignon/latest/userguide/)
+- [Google Workspace / SAML](https://support.google.com/a/answer/6087519)
+- [Microsoft Entra ID (formerly Azure AD)](https://learn.microsoft.com/en-us/entra/identity/)
+- [Okta](https://help.okta.com/)
+- [OneLogin](https://support.onelogin.com/)
+- [Oracle Identity Cloud Service](https://docs.oracle.com/)
+- [SAASPASS](https://saaspass.com/)
+- [Microsoft ADFS](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/)
+
+If your provider isn’t listed, contact them directly for metadata and configuration help.
+
+
+# Troubleshooting and Tips
+
+## User login behavior
+- Members with emails matching the verified domain will be prompted to log in via your IdP.
+- If a member has a secondary email (e.g., Gmail), they may need to update their email to match the domain.
+
+[How to add or change your email address](https://help.expensify.com/articles/new-expensify/settings/Update-Email-Address)
+
+## Error during setup?
+- Use [samltool.com](https://samltool.com) to validate your SAML fields.
+- Confirm certificate formatting is correct (PEM format).
+- Double-check that your domain is **verified** and matches the email domain used by your members.
+
+## What is Expensify’s Entity ID?
+- Default: `https://expensify.com`
+- For custom multi-domain setups: `https://expensify.com/yourdomain.com`
+
+## Managing Multiple Domains with One Entity ID
+It is possible to manage multiple domains with one entity ID. Contact Concierge or your Account Manager to enable this feature.
+
+
+
+
+# Advanced Configurations
+
+## Okta SCIM API for User Deactivation
+
+Ensure your domain is verified and the SAML setup is complete. Then, do the following:
+1. Go to **Workspaces > Domains > [Domain Name] > SAML**.
+2. Enable SAML Login and toggle on **Required for login**.
+3. In Okta, add Expensify as an application, and configure user profile mappings.
+4. Request Okta SCIM API activation via concierge@expensify.com.
+5. Integrate the SCIM token with Okta API provisioning.
+
+Refer to the full setup in Okta’s documentation for attribute mapping and provisioning options.
+
+
+## Microsoft ADFS SAML Authentication
+1. Open **ADFS Management Console** and add a new trust.
+2. Import Expensify’s metadata XML from the SAML page.
+3. Configure **LDAP Attributes** for email or UPN.
+4. Add two claim rules:
+ - Send LDAP Attributes as Claims.
+ - Transform Incoming Claim (Name ID).
+
+
+---
+
+Still stuck? Reach out to Concierge from the bottom-left menu in New Expensify.
+
+
diff --git a/docs/articles/new-expensify/workspaces/Workspace-Workflows.md b/docs/articles/new-expensify/workspaces/Workspace-Workflows.md
index b309a9a00559..cc6b7af79843 100644
--- a/docs/articles/new-expensify/workspaces/Workspace-Workflows.md
+++ b/docs/articles/new-expensify/workspaces/Workspace-Workflows.md
@@ -19,9 +19,9 @@ To get started, enable the **Workflows** feature for your workspace.
3. Click **More Features**.
4. Under the **Spend** section, toggle on **Workflows**.
-{:width="100%"}
+{:width="100%"}
-{:width="100%"}
+{:width="100%"}
---
@@ -32,7 +32,7 @@ Once enabled, go to the **Workflows** tab in the left menu to customize your sub
1. Click **Workflows**.
2. Use the toggles to enable the workflows you want to use.
-{:width="100%"}
+{:width="100%"}
## Add Approvals
diff --git a/docs/articles/travel/booking-travel/Arrange-Travel-for-Another-User.md b/docs/articles/travel/booking-travel/Arrange-Travel-for-Another-User.md
index c0702e14ad2f..064e59d4763a 100644
--- a/docs/articles/travel/booking-travel/Arrange-Travel-for-Another-User.md
+++ b/docs/articles/travel/booking-travel/Arrange-Travel-for-Another-User.md
@@ -5,23 +5,24 @@ keywords: [arrange travel, book for others, travel arranger, expensify travel, c
---
-After you're assigned the Travel Arranger role, you can arrange travel for another member of your organization using Expensify Travel.
+Once granted the Travel Arranger role, you can arrange travel for another member of your organization using Expensify Travel.
---
-## Where to find Expensify Travel
+## How to access the travel tool
-Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
+- **In New Expensify:** Click the green **+** button in the bottom-left corner of your screen, then select **Book travel**.
+- **In Classic Expensify:** Click **Travel** in the left-hand menu, then select **Book or manage travel**.
-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.
+**Need to enable travel still?** Reach out to your Account Manager or Concierge to schedule a travel demo and get it enabled for your account.
---
-## How to get the Travel Arranger role
+## How to get access
Before arranging travel for another traveler, a Travel Admin must assign you the **Travel Arranger** role.
-**To set the Travel Arranger role for another member:**
+**To set the Travel Arranger role for another travel member:**
1. Head to Expensify Travel.
2. Click on **Program** > **Users** > search and select the name of the user who needs to arrange travel.
@@ -47,7 +48,7 @@ Before arranging travel for another traveler, a Travel Admin must assign you the
---
-## What happens after booking for another member with Expensify Travel
+## What happens next
- The selected traveler will receive a confirmation email.
- Receipts will automatically be sent to their Expensify account and added to a trip report.
diff --git a/docs/articles/travel/booking-travel/Book-Rail-Travel.md b/docs/articles/travel/booking-travel/Book-Rail-Travel.md
index b7e1c5e88afd..067ff4181a3e 100644
--- a/docs/articles/travel/booking-travel/Book-Rail-Travel.md
+++ b/docs/articles/travel/booking-travel/Book-Rail-Travel.md
@@ -8,11 +8,12 @@ keywords: [New Expensify, book rail, train booking, rail travel, train tickets,
Book train tickets directly in Expensify Travel. This guide covers how to search, book, and view your rail itineraries in New Expensify.
-## Where to find Expensify Travel
+# How to access the travel tool
-Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
+From the left-hand menu, select **Reports > Trips**.
+Click the green **+** button in the bottom-left corner, then select **Book travel**.
-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.
+If you don’t see this option, contact your Account Manager or Concierge to schedule a travel demo and enable travel booking for your account.
# How to book rail travel
@@ -35,19 +36,19 @@ If you don’t see **Book travel**, ask a Workspace Admin to [enable Expensify T
8. Confirm trip details and payment method.
9. Click **Pay** to finalize your booking.
-## What happens after booking with Expensify Travel
+# What happens next
- Your booking may require approval depending on your company’s travel policy.
- You’ll receive a confirmation email after booking.
- The rail booking receipt will sync automatically with your Expensify account.
- If your payment card is connected to Expensify, the receipt will merge with the imported expense.
-**Note:** Rail bookings are not supported in all regions. Availability may vary
+> **Note:** Rail bookings are not supported in all regions. Availability may vary.
# FAQ
## Can I book a train in Expensify Travel?
-Yes. Once Expensify Travel is enabled for your workspace, you can book train tickets directly from the app.
+Yes. If travel is enabled on your account, you can search and book rail journeys directly in Expensify.
## Will I receive a digital ticket?
Yes. Most rail bookings include a digital ticket or QR code in your confirmation email.
diff --git a/docs/articles/travel/booking-travel/Book-Travel-for-a-Guest.md b/docs/articles/travel/booking-travel/Book-Travel-for-a-Guest.md
index b8754f782a59..7f5f3ace5f7c 100644
--- a/docs/articles/travel/booking-travel/Book-Travel-for-a-Guest.md
+++ b/docs/articles/travel/booking-travel/Book-Travel-for-a-Guest.md
@@ -4,33 +4,33 @@ description: Learn how to book travel for guests in Expensify, even if they don
keywords: [book guest travel, guest traveler, expensify travel, book for non-member, travel booking, classic, new expensify]
---
-Any member with guest booking permission can book travel for someone who doesn’t have an Expensify account. This guide explains how to get permission and how to complete a guest booking.
+Any member with guest booking permission can book travel for a guest, aka someone who doesn’t have an Expensify account.
---
-## Where to find Expensify Travel
+## How to access the travel tool
-Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
+- **In New Expensify:** Click the green **+** button in the bottom-left corner of your screen, then select **Book travel**.
+- **In Classic Expensify:** Click **Travel** in the left-hand menu, then select **Book or manage travel**.
-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.
+**Need to enable travel still?** Reach out to your Account Manager or Concierge to schedule a travel demo and get it enabled for your account.
---
-## How to get permission to book travel for a guest
+## How to get access
-To book travel for a guest, a Travel Admin must assign you the **Travel Arranger** role and enable guest booking.
+Before arranging travel for another traveler, a Travel Admin must assign you the **Travel Arranger** role.
-**To give a member guest booking permissions:**
+**To set the Travel Arranger role for another travel member:**
1. Head to Expensify Travel.
-2. Click **Program** > **Users**, then search and select the name of the member.
-3. Click the **Configuration** tab.
-4. Toggle on **Allow employee to book for guests**.
+2. Click on **Program** > **Users** > search and select the name of the user who needs to arrange travel.
+3. Click the **Configuration** tab and then click the toggle for **Allow employee to book for guests**.
---
-# How to book travel for a guest
+## How to book travel for a guest
1. Click **Book for Guest** at the bottom of the booking tool.
2. Choose a travel type:
@@ -46,7 +46,7 @@ To book travel for a guest, a Travel Admin must assign you the **Travel Arranger
---
-## What happens after booking with Expensify Travel
+## What happens next
- Bookings may require approval based on your company’s travel policy.
- The guest will receive a confirmation email with their travel details.
@@ -57,13 +57,9 @@ To book travel for a guest, a Travel Admin must assign you the **Travel Arranger
# FAQ
-## Does the guest need to have an Expensify account?
+## Will the guest need an Expensify account?
No. Guest travelers don’t need an account to receive or use their travel reservation.
-## Will the guest travel booking show up in my reports?
+## Will the travel show up in my reports?
Yes. The booking appears in your Expensify account, and receipts are automatically added to an expense report.
-## Who approves guest travel bookings?
-Guest travel follows the same approval workflow as the member who made the booking. If your company requires approval for flights, hotels, or other travel types, those steps will still apply.
-
-
diff --git a/docs/articles/travel/booking-travel/Book-a-Hotel.md b/docs/articles/travel/booking-travel/Book-a-Hotel.md
index ea35d842b2b1..f25f12b695b7 100644
--- a/docs/articles/travel/booking-travel/Book-a-Hotel.md
+++ b/docs/articles/travel/booking-travel/Book-a-Hotel.md
@@ -6,15 +6,16 @@ keywords: [New Expensify, book hotel, travel booking, hotel receipt, travel itin
-Book hotels easily with Expensify Travel. This guide covers how to search for hotels, filter options, complete your booking, and view your travel details in New Expensify.
+Book hotels easily using Expensify’s integrated travel tool. This guide covers how to search for hotels, filter options, complete your booking, and view your travel details in New Expensify.
-## Where to find Expensify Travel
+# How to access the travel tool
-Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
+From the left-hand menu, select **Reports > Trips**.
+Click the green **+** button in the bottom-left corner, then choose **Book travel**.
-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.
+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.
-# How to book a hotel
+# How to search and book a hotel
1. Click the **Hotel** tab in the booking tool.
2. Enter the search details:
@@ -36,11 +37,12 @@ If you don’t see **Book travel**, ask a Workspace Admin to [enable Expensify T
7. Select an existing trip or create a new one for the booking.
8. Confirm your payment method and click **Book Hotel**.
-## What happens after booking with Expensify Travel
+# What happens next
- Your company’s travel policy may require approval before the reservation is finalized.
- You’ll receive a confirmation email after booking.
-- The hotel receipt will automatically be added to your Expensify account. If your hotel is post-paid, upload the final bill manually after checkout.
+- The hotel receipt will automatically be added to your Expensify account.
+ - Note: If your hotel booking is post-paid, then you'll need to upload your final hotel bill to your Expensify account instead.
- If your payment card is connected to Expensify, the receipt will merge with the imported expense.
# FAQ
@@ -55,13 +57,10 @@ Yes. Use the **Payment Type** filter to choose a payment option that works best
## What if my plans change?
-You can cancel or modify the booking if the hotel’s policy allows.
-Cancellation policies are shown during booking. Changes made through support may incur a fee of up to $25.
+You can cancel or modify your booking if the hotel’s policy allows it.
+Cancellation policies are shown during booking. Changes made via support incur a $25 booking change fee.
-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**
+Manage bookings by going to **Reports > Trips > My Trips**.
## Where will my hotel receipt appear?
diff --git a/docs/articles/travel/booking-travel/Book-a-Rental-Car.md b/docs/articles/travel/booking-travel/Book-a-Rental-Car.md
index a20ec6449aaf..adaf7064cbe3 100644
--- a/docs/articles/travel/booking-travel/Book-a-Rental-Car.md
+++ b/docs/articles/travel/booking-travel/Book-a-Rental-Car.md
@@ -8,11 +8,12 @@ keywords: [New Expensify, book rental car, car rental, trip room, travel itinera
Book rental cars quickly and easily through Expensify’s integrated travel portal. This guide walks you through searching for a rental, booking it, and accessing your trip information in New Expensify.
-## Where to find Expensify Travel
+# How to access the travel tool
-Tap the green ➕ **Create** button at the bottom of your screen, then choose **Book travel**.
+From the left-hand menu, select **Reports > Trips**.
+Click the green **+** button in the bottom-left corner, then choose **Book travel**.
-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.
+If this option isn’t available, reach out to your Account Manager or Concierge to schedule a travel demo and enable travel for your account.
# How to book a rental car
@@ -32,7 +33,7 @@ If you don’t see **Book travel**, ask a Workspace Admin to [enable Expensify T
6. Select an existing trip or create a new one to assign the booking.
7. Confirm your payment method and click **Reserve**.
-## What happens after booking with Expensify Travel
+# What happens next
- Your company’s travel policy may require approval before the reservation is confirmed.
- You’ll receive a confirmation email once your booking is complete.
@@ -53,7 +54,7 @@ Insurance offerings vary by provider. You’ll see available options and add-ons
Yes! Use filters to select your preferred rental provider.
-## Where can I view my trip itinerary?
+## Where can I view my car rental itinerary while on the go?
When a traveler books a trip in Expensify Travel, a **trip itinerary** is automatically created in New Expensify.
To view your trip:
diff --git a/docs/assets/images/ExpensifyHelp-Workflows-1.png b/docs/assets/images/ExpensifyHelp-Workflows-1.png
new file mode 100644
index 000000000000..2eb2a8a79b04
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-Workflows-1.png differ
diff --git a/docs/assets/images/ExpensifyHelp-Workflows-2.png b/docs/assets/images/ExpensifyHelp-Workflows-2.png
new file mode 100644
index 000000000000..64561611c59c
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-Workflows-2.png differ
diff --git a/docs/assets/images/ExpensifyHelp-Workflows-3.png b/docs/assets/images/ExpensifyHelp-Workflows-3.png
new file mode 100644
index 000000000000..e714a221c421
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-Workflows-3.png differ
diff --git a/docs/assets/images/personal-card-01.png b/docs/assets/images/personal-card-01.png
deleted file mode 100644
index 96a6f8120d20..000000000000
Binary files a/docs/assets/images/personal-card-01.png and /dev/null differ
diff --git a/docs/assets/images/personal-card-02.png b/docs/assets/images/personal-card-02.png
deleted file mode 100644
index f664e1ea5b5b..000000000000
Binary files a/docs/assets/images/personal-card-02.png and /dev/null differ
diff --git a/docs/assets/images/submissions-01.png b/docs/assets/images/submissions-01.png
deleted file mode 100644
index 961fd527a634..000000000000
Binary files a/docs/assets/images/submissions-01.png and /dev/null differ
diff --git a/docs/assets/images/submissions-02.png b/docs/assets/images/submissions-02.png
deleted file mode 100644
index edabf944f7f9..000000000000
Binary files a/docs/assets/images/submissions-02.png and /dev/null differ
diff --git a/docs/assets/images/submissions-03.png b/docs/assets/images/submissions-03.png
deleted file mode 100644
index e859096fd82d..000000000000
Binary files a/docs/assets/images/submissions-03.png and /dev/null differ
diff --git a/docs/new-expensify/hubs/domains/index.html b/docs/new-expensify/hubs/domains/index.html
deleted file mode 100644
index 3ed77ad3b188..000000000000
--- a/docs/new-expensify/hubs/domains/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
----
-layout: default
-title: domains
----
-
-{% include hub.html %}
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 737c485d8085..dea29cd1a8f7 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -823,7 +823,7 @@ https://help.expensify.com/new-expensify/hubs/connect-credit-cards/company-cards
https://help.expensify.com/articles/expensify-classic/reports/Track-report-history.html,https://help.expensify.com/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports
https://help.expensify.com/articles/expensify-classic/reports/Report-statuses.html,https://help.expensify.com/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports#report-statuses
https://help.expensify.com/articles/expensify-classic/reports/Edit-a-report.html,https://help.expensify.com/articles/expensify-classic/reports/Edit-and-Submit-Expense-Reports
-https://help.expensify.com/articles/expensify-classic/domains/SAML-SSO.html,https://help.expensify.com/articles/expensify-classic/domains/Set-Up-SAML-SSO.md
+https://help.expensify.com/articles/expensify-classic/domains/SAML-SSO.html,https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify
https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Reimbursements.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments
https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.html,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense#add-an-expense-with-smartscan
https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview.html,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription
@@ -892,9 +892,4 @@ https://help.expensify.com/classic,https://help.expensify.com/expensify-classic/
https://help.expensify.com/articles/new-expensify/expensify-card/Enable-Expensify-Card-notifications,https://help.expensify.com/articles/new-expensify/expensify-card/Expensify-Card-Notifications
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts.md/Add-or-remove-a-business-bank-account,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account
https://help.expensify.com/articles/Unlisted/Compliance-Documentation.md,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security
-https://help.expensify.com/articles/new-expensify/workspaces/Verify-a-Domain,https://help.expensify.com/articles/new-expensify/domains/Claim-and-Verify-a-Domain
-https://help.expensify.com/articles/new-expensify/workspaces/Set-Up-SAML-Single-Sign-On,https://help.expensify.com/articles/new-expensify/domains/Set-Up-SAML-SSO
-https://community.expensify.com/discussion/4751/how-to-add-an-ach-business-bank-account-to-reimburse-employees-us-only,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account
-https://community.expensify.com/discussion/5700/deep-dive-approval-workflow-overview/,https://help.expensify.com/articles/expensify-classic/reports/Create-a-report-approval-workflow
-https://community.expensify.com/discussion/5799/,https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports#date-formats
-https://community.expensify.com/discussion/5677/deep-dive-how-expensify-protects-your-information/,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security
+https://help.expensify.com/articles/new-expensify/workspaces/Verify-a-Domain,https://help.expensify.com/articles/new-expensify/workspaces/Claim-and-Verify-a-Domain
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 0b84522bce4f..f3be8b5704e1 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,7 +1,6 @@
import {FlatCompat} from '@eslint/eslintrc';
import tsParser from '@typescript-eslint/parser';
import expensifyConfig from 'eslint-config-expensify';
-import fileProgress from 'eslint-plugin-file-progress';
import jsdoc from 'eslint-plugin-jsdoc';
import lodash from 'eslint-plugin-lodash';
import react from 'eslint-plugin-react';
@@ -163,7 +162,6 @@ const config = defineConfig([
expensifyConfig,
typescriptEslint.configs.recommendedTypeChecked,
typescriptEslint.configs.stylisticTypeChecked,
- fileProgress.configs['recommended-ci'],
{
extends: new FlatCompat({baseDirectory: dirname}).extends(
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index b9492ea49a9d..a7f6896a15ee 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.3.7
+ 9.3.1CFBundleSignature????CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.3.7.1
+ 9.3.1.0FullStoryOrgId
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 94d5939cf3c3..40a70e84b653 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.3.7
+ 9.3.1CFBundleVersion
- 9.3.7.1
+ 9.3.1.0NSExtensionNSExtensionPointIdentifier
diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist
index 3f8247b8269f..7788fddb8885 100644
--- a/ios/ShareViewController/Info.plist
+++ b/ios/ShareViewController/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.3.7
+ 9.3.1CFBundleVersion
- 9.3.7.1
+ 9.3.1.0NSExtensionNSExtensionAttributes
diff --git a/jest.config.js b/jest.config.js
index 904f49eb1f49..0c60bf77e3aa 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -14,7 +14,7 @@ module.exports = {
'^.+\\.svg?$': 'jest-transformer-svg',
},
transformIgnorePatterns: [
- '/node_modules/(?!.*(react-native|expo|@noble|react-navigation|uuid|@shopify\/flash-list).*/)',
+ '/node_modules/(?!.*(react-native|expo|react-navigation|uuid|@shopify\/flash-list).*/)',
// Prevent Babel from transforming worklets in this file so they are treated as normal functions, otherwise FormatSelectionUtilsTest won't run.
'/node_modules/@expensify/react-native-live-markdown/lib/commonjs/parseExpensiMark.js',
],
@@ -35,7 +35,5 @@ module.exports = {
moduleNameMapper: {
'\\.(lottie)$': '/__mocks__/fileMock.ts',
'^group-ib-fp$': '/__mocks__/group-ib-fp.ts',
- '@noble/ed25519': '/node_modules/@noble/ed25519/index.ts',
- '@noble/hashes/(.*)': '/node_modules/@noble/hashes/src/$1.ts',
},
};
diff --git a/jest/setup.ts b/jest/setup.ts
index 6ed6fcbbbf41..7ae6c9ba6d93 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -9,7 +9,6 @@ import type * as RNKeyboardController from 'react-native-keyboard-controller';
import mockStorage from 'react-native-onyx/dist/storage/__mocks__';
import type Animated from 'react-native-reanimated';
import 'setimmediate';
-import * as MockedSecureStore from '@src/libs/MultifactorAuthentication/Biometrics/SecureStore/index.web';
import mockFSLibrary from './setupMockFullstoryLib';
import setupMockImages from './setupMockImages';
import setupMockReactNativeWorklets from './setupMockReactNativeWorklets';
@@ -35,12 +34,6 @@ jest.mock('react-native-onyx/dist/storage', () => mockStorage);
// Mock NativeEventEmitter as it is needed to provide mocks of libraries which include it
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter');
-// Mock expo-task-manager
-jest.mock('expo-task-manager', () => ({
- defineTask: jest.fn(),
- // Add other methods here if you use them
-}));
-
// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest
jest.mock('react-native/Libraries/LogBox/LogBox', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -99,9 +92,6 @@ jest.mock('react-native-share', () => ({
default: jest.fn(),
}));
-// Jest has no access to the native secure store module, so we mock it with the web implementation.
-jest.mock('@src/libs/MultifactorAuthentication/Biometrics/SecureStore', () => MockedSecureStore);
-
jest.mock('react-native-reanimated', () => ({
...jest.requireActual('react-native-reanimated/mock'),
createAnimatedPropAdapter: jest.fn,
diff --git a/package-lock.json b/package-lock.json
index 1d34dad6ee82..b5b2c1504667 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.3.7-1",
+ "version": "9.3.1-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.3.7-1",
+ "version": "9.3.1-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -74,7 +74,6 @@
"expo-modules-core": "3.0.18",
"expo-secure-store": "~14.2.4",
"expo-store-review": "~9.0.8",
- "expo-task-manager": "~14.0.9",
"fast-equals": "^5.2.2",
"focus-trap-react": "^11.0.3",
"group-ib-fp": "file:modules/group-ib-fp",
@@ -119,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.30",
+ "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",
@@ -244,7 +243,6 @@
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-expensify": "2.0.103",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-file-progress": "3.0.1",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^60.7.0",
"eslint-plugin-lodash": "^7.4.0",
@@ -12531,42 +12529,6 @@
}
}
},
- "node_modules/@rnmapbox/maps/node_modules/@turf/distance": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz",
- "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==",
- "license": "MIT",
- "dependencies": {
- "@turf/helpers": "^6.5.0",
- "@turf/invariant": "^6.5.0"
- },
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
- "node_modules/@rnmapbox/maps/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
- "node_modules/@rnmapbox/maps/node_modules/@turf/length": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/length/-/length-6.5.0.tgz",
- "integrity": "sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig==",
- "license": "MIT",
- "dependencies": {
- "@turf/distance": "^6.5.0",
- "@turf/helpers": "^6.5.0",
- "@turf/meta": "^6.5.0"
- },
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@rnmapbox/maps/node_modules/debounce": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz",
@@ -16002,28 +15964,6 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/along/node_modules/@turf/distance": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz",
- "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==",
- "license": "MIT",
- "dependencies": {
- "@turf/helpers": "^6.5.0",
- "@turf/invariant": "^6.5.0"
- },
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
- "node_modules/@turf/along/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@turf/bbox": {
"version": "6.5.0",
"license": "MIT",
@@ -16035,15 +15975,6 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/bbox/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@turf/bearing": {
"version": "6.5.0",
"license": "MIT",
@@ -16055,16 +15986,18 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/bearing/node_modules/@turf/helpers": {
+ "node_modules/@turf/destination": {
"version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
"license": "MIT",
+ "dependencies": {
+ "@turf/helpers": "^6.5.0",
+ "@turf/invariant": "^6.5.0"
+ },
"funding": {
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/destination": {
+ "node_modules/@turf/distance": {
"version": "6.5.0",
"license": "MIT",
"dependencies": {
@@ -16075,10 +16008,8 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/destination/node_modules/@turf/helpers": {
+ "node_modules/@turf/helpers": {
"version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
"license": "MIT",
"funding": {
"url": "https://opencollective.com/turf"
@@ -16094,11 +16025,14 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/invariant/node_modules/@turf/helpers": {
+ "node_modules/@turf/length": {
"version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
"license": "MIT",
+ "dependencies": {
+ "@turf/distance": "^6.5.0",
+ "@turf/helpers": "^6.5.0",
+ "@turf/meta": "^6.5.0"
+ },
"funding": {
"url": "https://opencollective.com/turf"
}
@@ -16117,15 +16051,6 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/line-intersect/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@turf/line-segment": {
"version": "6.5.0",
"license": "MIT",
@@ -16138,15 +16063,6 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/line-segment/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@turf/meta": {
"version": "6.5.0",
"license": "MIT",
@@ -16157,15 +16073,6 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/meta/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@turf/nearest-point-on-line": {
"version": "6.5.0",
"license": "MIT",
@@ -16182,28 +16089,6 @@
"url": "https://opencollective.com/turf"
}
},
- "node_modules/@turf/nearest-point-on-line/node_modules/@turf/distance": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz",
- "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==",
- "license": "MIT",
- "dependencies": {
- "@turf/helpers": "^6.5.0",
- "@turf/invariant": "^6.5.0"
- },
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
- "node_modules/@turf/nearest-point-on-line/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -22354,23 +22239,6 @@
"node": ">=4"
}
},
- "node_modules/eslint-plugin-file-progress": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-file-progress/-/eslint-plugin-file-progress-3.0.1.tgz",
- "integrity": "sha512-sPUOIifutW3Ehhmujt40IU5ytXL6HnuGVev2n9cp4d5fzVOoQp1K17DJ/I+cRdctmTpMcYit/fOuF9efjmD98A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "nanospinner": "^1.2.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "peerDependencies": {
- "eslint": "^9.0.0"
- }
- },
"node_modules/eslint-plugin-import": {
"version": "2.32.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
@@ -23376,12 +23244,12 @@
}
},
"node_modules/expo-constants": {
- "version": "18.0.10",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
- "integrity": "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw==",
+ "version": "18.0.9",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.9.tgz",
+ "integrity": "sha512-sqoXHAOGDcr+M9NlXzj1tGoZyd3zxYDy215W6E0Z0n8fgBaqce9FAYQE2bu5X4G629AYig5go7U6sQz7Pjcm8A==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~12.0.10",
+ "@expo/config": "~12.0.9",
"@expo/env": "~2.0.7"
},
"peerDependencies": {
@@ -23585,23 +23453,10 @@
}
},
"node_modules/expo-store-review": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-9.0.9.tgz",
- "integrity": "sha512-99vS7edXlKzPcdjrzVlMQWc4zOyq4khQfFjhNqJgpGP+AgRn4U0LaZkHIrVjmzolryD3rcHJSiUQH9Vi0sD0MQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "node_modules/expo-task-manager": {
- "version": "14.0.9",
- "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-14.0.9.tgz",
- "integrity": "sha512-GKWtXrkedr4XChHfTm5IyTcSfMtCPxzx89y4CMVqKfyfROATibrE/8UI5j7UC/pUOfFoYlQvulQEvECMreYuUA==",
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-9.0.8.tgz",
+ "integrity": "sha512-S8ExQRqQBHHmZOX2gabEeRJDrgeAhzBf5FOtnhp5fMinpwT7umNOMHy+PwrTYJ/+AjnnKHgLoM1o/MDgcL3qCw==",
"license": "MIT",
- "dependencies": {
- "unimodules-app-loader": "~6.0.8"
- },
"peerDependencies": {
"expo": "*",
"react-native": "*"
@@ -25267,15 +25122,6 @@
"rbush": "^3.0.1"
}
},
- "node_modules/geojson-rbush/node_modules/@turf/helpers": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
- "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/turf"
- }
- },
"node_modules/geojson-vt": {
"version": "3.2.1",
"license": "ISC"
@@ -31198,16 +31044,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/nanospinner": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz",
- "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picocolors": "^1.1.1"
- }
- },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"license": "MIT",
@@ -33885,9 +33721,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "3.0.30",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.30.tgz",
- "integrity": "sha512-qlaK6UJTdoO26oPWTGCvii3U9vITrBXWaam7pkP+W9tiB56DbTcVVC/nWvJlg7+CENa2NlGwuPBB2DtodpBt8A==",
+ "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",
@@ -37733,12 +37569,6 @@
"node": ">=4"
}
},
- "node_modules/unimodules-app-loader": {
- "version": "6.0.8",
- "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-6.0.8.tgz",
- "integrity": "sha512-fqS8QwT/MC/HAmw1NKCHdzsPA6WaLm0dNmoC5Pz6lL+cDGYeYCNdHMO9fy08aL2ZD7cVkNM0pSR/AoNRe+rslA==",
- "license": "MIT"
- },
"node_modules/union": {
"version": "0.5.0",
"dev": true,
diff --git a/package.json b/package.json
index 951481d288dc..e82cd0e41371 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.3.7-1",
+ "version": "9.3.1-0",
"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=383 --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",
@@ -143,7 +143,6 @@
"expo-modules-core": "3.0.18",
"expo-secure-store": "~14.2.4",
"expo-store-review": "~9.0.8",
- "expo-task-manager": "~14.0.9",
"fast-equals": "^5.2.2",
"focus-trap-react": "^11.0.3",
"group-ib-fp": "file:modules/group-ib-fp",
@@ -188,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.30",
+ "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",
@@ -313,7 +312,6 @@
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-expensify": "2.0.103",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-file-progress": "3.0.1",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^60.7.0",
"eslint-plugin-lodash": "^7.4.0",
diff --git a/src/App.tsx b/src/App.tsx
index 4298c48bf140..15d8ac7ba9be 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -53,7 +53,6 @@ import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import './libs/HybridApp';
import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext';
import ExpensifyCardContextProvider from './pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider';
-import './setup/backgroundLocationTrackingTask';
import './setup/backgroundTask';
import './setup/fraudProtection';
import './setup/hybridApp';
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 2b6e2f223bae..b225d8f8a7d9 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -734,10 +734,8 @@ const CONST = {
NO_OPTIMISTIC_TRANSACTION_THREADS: 'noOptimisticTransactionThreads',
UBER_FOR_BUSINESS: 'uberForBusiness',
CUSTOM_REPORT_NAMES: 'newExpensifyCustomReportNames',
- ZERO_EXPENSES: 'zeroExpenses',
NEW_DOT_DEW: 'newDotDEW',
GPS_MILEAGE: 'gpsMileage',
- NEW_DOT_HOME: 'newDotHome',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -1366,17 +1364,13 @@ const CONST = {
UPDATE_DEFAULT_APPROVER: 'POLICYCHANGELOG_UPDATE_DEFAULT_APPROVER',
UPDATE_SUBMITS_TO: 'POLICYCHANGELOG_UPDATE_SUBMITS_TO',
UPDATE_FORWARDS_TO: 'POLICYCHANGELOG_UPDATE_FORWARDS_TO',
- UPDATE_INVOICE_COMPANY_NAME: 'POLICYCHANGELOG_UPDATE_INVOICE_COMPANY_NAME',
- UPDATE_INVOICE_COMPANY_WEBSITE: 'POLICYCHANGELOG_UPDATE_INVOICE_COMPANY_WEBSITE',
UPDATE_MANUAL_APPROVAL_THRESHOLD: 'POLICYCHANGELOG_UPDATE_MANUAL_APPROVAL_THRESHOLD',
UPDATE_MAX_EXPENSE_AMOUNT: 'POLICYCHANGELOG_UPDATE_MAX_EXPENSE_AMOUNT',
UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT: 'POLICYCHANGELOG_UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT',
- UPDATE_MAX_EXPENSE_AGE: 'POLICYCHANGELOG_UPDATE_MAX_EXPENSE_AGE',
UPDATE_MULTIPLE_TAGS_APPROVER_RULES: 'POLICYCHANGELOG_UPDATE_MULTIPLE_TAGS_APPROVER_RULES',
UPDATE_NAME: 'POLICYCHANGELOG_UPDATE_NAME',
UPDATE_DESCRIPTION: 'POLICYCHANGELOG_UPDATE_DESCRIPTION',
UPDATE_OWNERSHIP: 'POLICYCHANGELOG_UPDATE_OWNERSHIP',
- UPDATE_REIMBURSER: 'POLICYCHANGELOG_UPDATE_REIMBURSER',
UPDATE_PROHIBITED_EXPENSES: 'POLICYCHANGELOG_UPDATE_PROHIBITED_EXPENSES',
UPDATE_REIMBURSEMENT_CHOICE: 'POLICYCHANGELOG_UPDATE_REIMBURSEMENT_CHOICE',
UPDATE_REIMBURSEMENT_ENABLED: 'POLICYCHANGELOG_UPDATE_REIMBURSEMENT_ENABLED',
@@ -1710,7 +1704,6 @@ const CONST = {
SPAN_OPEN_REPORT: 'ManualOpenReport',
SPAN_APP_STARTUP: 'ManualAppStartup',
SPAN_NAVIGATE_TO_REPORTS_TAB: 'ManualNavigateToReportsTab',
- SPAN_NAVIGATE_TO_REPORTS_TAB_RENDER: 'ManualNavigateToReportsTabRender',
SPAN_ON_LAYOUT_SKELETON_REPORTS: 'ManualOnLayoutSkeletonReports',
SPAN_NAVIGATE_TO_INBOX_TAB: 'ManualNavigateToInboxTab',
SPAN_OD_ND_TRANSITION: 'ManualOdNdTransition',
@@ -3347,7 +3340,6 @@ const CONST = {
CONTROL: 'control',
},
DEFAULT_MAX_EXPENSE_AGE: 90,
- DISABLED_MAX_EXPENSE_AGE: 10000000000,
DEFAULT_MAX_EXPENSE_AMOUNT: 200000,
DEFAULT_MAX_AMOUNT_NO_RECEIPT: 2500,
DEFAULT_TAG_LIST: {
@@ -3507,8 +3499,8 @@ const CONST = {
FIXED: 'fixed',
},
LIMIT_VALUE: 21474836,
- STEP_NAMES: ['1', '2', '3', '4', '5'],
- ASSIGNEE_EXCLUDED_STEP_NAMES: ['1', '2', '3', '4'],
+ STEP_NAMES: ['1', '2', '3', '4', '5', '6'],
+ ASSIGNEE_EXCLUDED_STEP_NAMES: ['1', '2', '3', '4', '5'],
STEP: {
ASSIGNEE: 'Assignee',
CARD_TYPE: 'CardType',
@@ -3705,24 +3697,6 @@ const CONST = {
INVOICING: 'invoicing2018',
},
},
- EXPENSE_RULES: {
- FIELDS: {
- BILLABLE: 'billable',
- CATEGORY: 'category',
- DESCRIPTION: 'comment',
- CREATE_REPORT: 'createReport',
- MERCHANT: 'merchantToMatch',
- RENAME_MERCHANT: 'merchant',
- REIMBURSABLE: 'reimbursable',
- REPORT: 'report',
- TAG: 'tag',
- TAX: 'tax',
- },
- BULK_ACTION_TYPES: {
- EDIT: 'edit',
- DELETE: 'delete',
- },
- },
get SUBSCRIPTION_PRICES() {
return {
@@ -5487,10 +5461,6 @@ const CONST = {
DISABLED: 'DISABLED',
DISABLE: 'DISABLE',
},
- MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE: {
- SUCCESS: 'success',
- FAILURE: 'failure',
- },
MERGE_ACCOUNT_RESULTS: {
SUCCESS: 'success',
ERR_2FA: 'err_2fa',
@@ -5753,16 +5723,6 @@ const CONST = {
CAROUSEL: 3,
},
- /**
- * Feature flag to disable the missingAttendees violation feature.
- * Set to true to disable the feature if regressions are found.
- * When true:
- * - Prevents new missingAttendees violations from being created
- * - Removes existing missingAttendees violations from transaction lists
- * - Hides "Require attendees" toggle in category settings
- */
- IS_ATTENDEES_REQUIRED_FEATURE_DISABLED: true,
-
/**
* Constants for types of violation.
*/
@@ -5772,11 +5732,6 @@ const CONST = {
WARNING: 'warning',
},
- /**
- * Constant for prefix of violations.
- */
- VIOLATIONS_PREFIX: 'violations.',
-
/**
* Constants with different types for the modifiedAmount violation
*/
@@ -5831,8 +5786,6 @@ const CONST = {
RECEIPT_GENERATED_WITH_AI: 'receiptGeneratedWithAI',
OVER_TRIP_LIMIT: 'overTripLimit',
COMPANY_CARD_REQUIRED: 'companyCardRequired',
- NO_ROUTE: 'noRoute',
- MISSING_ATTENDEES: 'missingAttendees',
},
RTER_VIOLATION_TYPES: {
BROKEN_CARD_CONNECTION: 'brokenCardConnection',
@@ -5995,31 +5948,31 @@ const CONST = {
/* If we update these values, let's ensure this logic is consistent with the logic in the backend (Auth), since we're using the same method to calculate the rate value in distance requests created via Concierge. */
CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{
"AED": {
- "rate": 428.5,
+ "rate": 414,
"unit": "km"
},
"AFN": {
- "rate": 7703.93,
+ "rate": 8851,
"unit": "km"
},
"ALL": {
- "rate": 9633.54,
+ "rate": 10783,
"unit": "km"
},
"AMD": {
- "rate": 44504.28,
+ "rate": 45116,
"unit": "km"
},
"ANG": {
- "rate": 208.86,
+ "rate": 203,
"unit": "km"
},
"AOA": {
- "rate": 106762.22,
+ "rate": 102929,
"unit": "km"
},
"ARS": {
- "rate": 171174.53,
+ "rate": 118428,
"unit": "km"
},
"AUD": {
@@ -6027,75 +5980,75 @@ const CONST = {
"unit": "km"
},
"AWG": {
- "rate": 210.31,
+ "rate": 203,
"unit": "km"
},
"AZN": {
- "rate": 198.35,
+ "rate": 192,
"unit": "km"
},
"BAM": {
- "rate": 194.86,
+ "rate": 212,
"unit": "km"
},
"BBD": {
- "rate": 233.35,
+ "rate": 225,
"unit": "km"
},
"BDT": {
- "rate": 14251.98,
+ "rate": 13697,
"unit": "km"
},
"BGN": {
- "rate": 195.25,
+ "rate": 211,
"unit": "km"
},
"BHD": {
- "rate": 43.98,
+ "rate": 42,
"unit": "km"
},
"BIF": {
- "rate": 345436.14,
+ "rate": 331847,
"unit": "km"
},
"BMD": {
- "rate": 116.68,
+ "rate": 113,
"unit": "km"
},
"BND": {
- "rate": 149.25,
+ "rate": 153,
"unit": "km"
},
"BOB": {
- "rate": 807.7,
+ "rate": 779,
"unit": "km"
},
"BRL": {
- "rate": 626.95,
+ "rate": 660,
"unit": "km"
},
"BSD": {
- "rate": 116.68,
+ "rate": 113,
"unit": "km"
},
"BTN": {
- "rate": 10516.86,
+ "rate": 9761,
"unit": "km"
},
"BWP": {
- "rate": 1619.97,
+ "rate": 1569,
"unit": "km"
},
"BYN": {
- "rate": 342.9,
+ "rate": 369,
"unit": "km"
},
"BYR": {
- "rate": 2336612.1,
+ "rate": 2255979,
"unit": "km"
},
"BZD": {
- "rate": 234.56,
+ "rate": 227,
"unit": "km"
},
"CAD": {
@@ -6103,7 +6056,7 @@ const CONST = {
"unit": "km"
},
"CDF": {
- "rate": 264869.39,
+ "rate": 321167,
"unit": "km"
},
"CHF": {
@@ -6111,67 +6064,67 @@ const CONST = {
"unit": "km"
},
"CLP": {
- "rate": 104301.62,
+ "rate": 111689,
"unit": "km"
},
"CNY": {
- "rate": 814.84,
+ "rate": 808,
"unit": "km"
},
"COP": {
- "rate": 439901.44,
+ "rate": 473791,
"unit": "km"
},
"CRC": {
- "rate": 57973.09,
+ "rate": 57190,
"unit": "km"
},
"CUC": {
- "rate": 116.68,
+ "rate": 113,
"unit": "km"
},
"CUP": {
- "rate": 3004.45,
+ "rate": 2902,
"unit": "km"
},
"CVE": {
- "rate": 11004.01,
+ "rate": 11961,
"unit": "km"
},
"CZK": {
- "rate": 2413.34,
+ "rate": 2715,
"unit": "km"
},
"DJF": {
- "rate": 20752.5,
+ "rate": 19956,
"unit": "km"
},
"DKK": {
- "rate": 394,
+ "rate": 381,
"unit": "km"
},
"DOP": {
- "rate": 7398.54,
+ "rate": 6948,
"unit": "km"
},
"DZD": {
- "rate": 15153.28,
+ "rate": 15226,
"unit": "km"
},
"EEK": {
- "rate": 1704.36,
+ "rate": 1646,
"unit": "km"
},
"EGP": {
- "rate": 5512.97,
+ "rate": 5657,
"unit": "km"
},
"ERN": {
- "rate": 1750.16,
+ "rate": 1690,
"unit": "km"
},
"ETB": {
- "rate": 18077.09,
+ "rate": 14326,
"unit": "km"
},
"EUR": {
@@ -6179,11 +6132,11 @@ const CONST = {
"unit": "km"
},
"FJD": {
- "rate": 265.25,
+ "rate": 264,
"unit": "km"
},
"FKP": {
- "rate": 86.44,
+ "rate": 90,
"unit": "km"
},
"GBP": {
@@ -6191,55 +6144,55 @@ const CONST = {
"unit": "mi"
},
"GEL": {
- "rate": 313.87,
+ "rate": 323,
"unit": "km"
},
"GHS": {
- "rate": 1246.23,
+ "rate": 1724,
"unit": "km"
},
"GIP": {
- "rate": 86.44,
+ "rate": 90,
"unit": "km"
},
"GMD": {
- "rate": 8575.79,
+ "rate": 8111,
"unit": "km"
},
"GNF": {
- "rate": 1019841.61,
+ "rate": 974619,
"unit": "km"
},
"GTQ": {
- "rate": 893.94,
+ "rate": 872,
"unit": "km"
},
"GYD": {
- "rate": 24400.12,
+ "rate": 23585,
"unit": "km"
},
"HKD": {
- "rate": 908.6,
+ "rate": 877,
"unit": "km"
},
"HNL": {
- "rate": 3080.03,
+ "rate": 2881,
"unit": "km"
},
"HRK": {
- "rate": 752.16,
+ "rate": 814,
"unit": "km"
},
"HTG": {
- "rate": 15266.76,
+ "rate": 14734,
"unit": "km"
},
"HUF": {
- "rate": 38407.92,
+ "rate": 44127,
"unit": "km"
},
"IDR": {
- "rate": 1954232.16,
+ "rate": 1830066,
"unit": "km"
},
"ILS": {
@@ -6247,147 +6200,147 @@ const CONST = {
"unit": "km"
},
"INR": {
- "rate": 10518.19,
+ "rate": 9761,
"unit": "km"
},
"IQD": {
- "rate": 152781.51,
+ "rate": 147577,
"unit": "km"
},
"IRR": {
- "rate": 4910488.26,
+ "rate": 4741290,
"unit": "km"
},
"ISK": {
- "rate": 14694.92,
+ "rate": 15772,
"unit": "km"
},
"JMD": {
- "rate": 18515.8,
+ "rate": 17738,
"unit": "km"
},
"JOD": {
- "rate": 82.72,
+ "rate": 80,
"unit": "km"
},
"JPY": {
- "rate": 18278.93,
+ "rate": 17542,
"unit": "km"
},
"KES": {
- "rate": 15058.57,
+ "rate": 14589,
"unit": "km"
},
"KGS": {
- "rate": 10202.68,
+ "rate": 9852,
"unit": "km"
},
"KHR": {
- "rate": 468558.4,
+ "rate": 453066,
"unit": "km"
},
"KMF": {
- "rate": 49237.89,
+ "rate": 53269,
"unit": "km"
},
"KPW": {
- "rate": 105009.73,
+ "rate": 101389,
"unit": "km"
},
"KRW": {
- "rate": 168722.07,
+ "rate": 162705,
"unit": "km"
},
"KWD": {
- "rate": 35.82,
+ "rate": 35,
"unit": "km"
},
"KYD": {
- "rate": 97.19,
+ "rate": 93,
"unit": "km"
},
"KZT": {
- "rate": 59441.07,
+ "rate": 58319,
"unit": "km"
},
"LAK": {
- "rate": 2520371.89,
+ "rate": 2452802,
"unit": "km"
},
"LBP": {
- "rate": 10444573.37,
+ "rate": 10093809,
"unit": "km"
},
"LKR": {
- "rate": 36161.18,
+ "rate": 33423,
"unit": "km"
},
"LRD": {
- "rate": 20916.15,
+ "rate": 22185,
"unit": "km"
},
"LSL": {
- "rate": 1909.99,
+ "rate": 2099,
"unit": "km"
},
"LTL": {
- "rate": 376.1,
+ "rate": 364,
"unit": "km"
},
"LVL": {
- "rate": 76.56,
+ "rate": 74,
"unit": "km"
},
"LYD": {
- "rate": 631.41,
+ "rate": 554,
"unit": "km"
},
"MAD": {
- "rate": 1070.73,
+ "rate": 1127,
"unit": "km"
},
"MDL": {
- "rate": 1950.82,
+ "rate": 2084,
"unit": "km"
},
"MGA": {
- "rate": 537347.25,
+ "rate": 529635,
"unit": "km"
},
"MKD": {
- "rate": 6144.12,
+ "rate": 6650,
"unit": "km"
},
"MMK": {
- "rate": 245011.43,
+ "rate": 236413,
"unit": "km"
},
"MNT": {
- "rate": 415371.81,
+ "rate": 382799,
"unit": "km"
},
"MOP": {
- "rate": 935.53,
+ "rate": 904,
"unit": "km"
},
"MRO": {
- "rate": 41653.05,
+ "rate": 40234,
"unit": "km"
},
"MRU": {
- "rate": 4631.69,
+ "rate": 4506,
"unit": "km"
},
"MUR": {
- "rate": 5402.17,
+ "rate": 5226,
"unit": "km"
},
"MVR": {
- "rate": 1803.83,
+ "rate": 1735,
"unit": "km"
},
"MWK": {
- "rate": 202409.9,
+ "rate": 195485,
"unit": "km"
},
"MXN": {
@@ -6395,23 +6348,23 @@ const CONST = {
"unit": "km"
},
"MYR": {
- "rate": 472.6,
+ "rate": 494,
"unit": "km"
},
"MZN": {
- "rate": 7456.85,
+ "rate": 7199,
"unit": "km"
},
"NAD": {
- "rate": 1910.94,
+ "rate": 2099,
"unit": "km"
},
"NGN": {
- "rate": 166247.91,
+ "rate": 174979,
"unit": "km"
},
"NIO": {
- "rate": 4291.54,
+ "rate": 4147,
"unit": "km"
},
"NOK": {
@@ -6419,35 +6372,35 @@ const CONST = {
"unit": "km"
},
"NPR": {
- "rate": 16827.52,
+ "rate": 15617,
"unit": "km"
},
"NZD": {
- "rate": 117,
+ "rate": 104,
"unit": "km"
},
"OMR": {
- "rate": 44.87,
+ "rate": 43,
"unit": "km"
},
"PAB": {
- "rate": 116.68,
+ "rate": 113,
"unit": "km"
},
"PEN": {
- "rate": 392.29,
+ "rate": 420,
"unit": "km"
},
"PGK": {
- "rate": 501.36,
+ "rate": 455,
"unit": "km"
},
"PHP": {
- "rate": 6917.8,
+ "rate": 6582,
"unit": "km"
},
"PKR": {
- "rate": 32652.84,
+ "rate": 31411,
"unit": "km"
},
"PLN": {
@@ -6455,43 +6408,43 @@ const CONST = {
"unit": "km"
},
"PYG": {
- "rate": 787561.7,
+ "rate": 890772,
"unit": "km"
},
"QAR": {
- "rate": 424.92,
+ "rate": 410,
"unit": "km"
},
"RON": {
- "rate": 508.07,
+ "rate": 538,
"unit": "km"
},
"RSD": {
- "rate": 11713.4,
+ "rate": 12656,
"unit": "km"
},
"RUB": {
- "rate": 9392.54,
+ "rate": 11182,
"unit": "km"
},
"RWF": {
- "rate": 169805.16,
+ "rate": 156589,
"unit": "km"
},
"SAR": {
- "rate": 437.58,
+ "rate": 423,
"unit": "km"
},
"SBD": {
- "rate": 948.61,
+ "rate": 951,
"unit": "km"
},
"SCR": {
- "rate": 1615.27,
+ "rate": 1611,
"unit": "km"
},
"SDG": {
- "rate": 70123.16,
+ "rate": 67705,
"unit": "km"
},
"SEK": {
@@ -6499,167 +6452,159 @@ const CONST = {
"unit": "km"
},
"SGD": {
- "rate": 149.43,
+ "rate": 151,
"unit": "km"
},
"SHP": {
- "rate": 86.44,
+ "rate": 90,
"unit": "km"
},
"SLL": {
- "rate": 2446668.74,
+ "rate": 2362357,
"unit": "km"
},
"SLE": {
- "rate": 2800.26,
+ "rate": 2363,
"unit": "km"
},
"SOS": {
- "rate": 66591.46,
+ "rate": 64374,
"unit": "km"
},
"SRD": {
- "rate": 4468.75,
+ "rate": 3954,
"unit": "km"
},
"STD": {
- "rate": 2599784.99,
+ "rate": 2510095,
"unit": "km"
},
"STN": {
- "rate": 2440.86,
+ "rate": 2683,
"unit": "km"
},
"SVC": {
- "rate": 1020.49,
+ "rate": 987,
"unit": "km"
},
"SYP": {
- "rate": 1517040.54,
+ "rate": 1464664,
"unit": "km"
},
"SZL": {
- "rate": 1910.2,
+ "rate": 2099,
"unit": "km"
},
"THB": {
- "rate": 3641.51,
+ "rate": 3801,
"unit": "km"
},
"TJS": {
- "rate": 1077.65,
+ "rate": 1228,
"unit": "km"
},
"TMT": {
- "rate": 408.37,
+ "rate": 394,
"unit": "km"
},
"TND": {
- "rate": 336.58,
+ "rate": 360,
"unit": "km"
},
"TOP": {
- "rate": 280.93,
+ "rate": 274,
"unit": "km"
},
"TRY": {
- "rate": 5022.36,
+ "rate": 4035,
"unit": "km"
},
"TTD": {
- "rate": 791.64,
+ "rate": 763,
"unit": "km"
},
"TWD": {
- "rate": 3676.16,
+ "rate": 3703,
"unit": "km"
},
"TZS": {
- "rate": 288716.35,
+ "rate": 286235,
"unit": "km"
},
"UAH": {
- "rate": 4966.74,
+ "rate": 4725,
"unit": "km"
},
"UGX": {
- "rate": 422422.35,
+ "rate": 416016,
"unit": "km"
},
"USD": {
- "rate": 72.5,
+ "rate": 70,
"unit": "mi"
},
"UYU": {
- "rate": 4544.27,
+ "rate": 4888,
"unit": "km"
},
"UZS": {
- "rate": 1395513.79,
+ "rate": 1462038,
"unit": "km"
},
"VEB": {
- "rate": 735113.61,
+ "rate": 709737,
"unit": "km"
},
"VEF": {
- "rate": 28992910.93,
+ "rate": 27993155,
"unit": "km"
},
"VES": {
- "rate": 35909.77,
+ "rate": 6457,
"unit": "km"
},
"VND": {
- "rate": 3065619.41,
+ "rate": 2825526,
"unit": "km"
},
"VUV": {
- "rate": 14152.58,
+ "rate": 13358,
"unit": "km"
},
"WST": {
- "rate": 322.95,
+ "rate": 315,
"unit": "km"
},
"XAF": {
- "rate": 65490.66,
+ "rate": 70811,
"unit": "km"
},
"XCD": {
- "rate": 315.32,
- "unit": "km"
- },
- "XCG": {
- "rate": 210.2,
+ "rate": 304,
"unit": "km"
},
"XOF": {
- "rate": 65490.66,
+ "rate": 70811,
"unit": "km"
},
"XPF": {
- "rate": 11913.98,
+ "rate": 12875,
"unit": "km"
},
"YER": {
- "rate": 27815.91,
+ "rate": 28003,
"unit": "km"
},
"ZAR": {
- "rate": 476,
+ "rate": 484,
"unit": "km"
},
"ZMK": {
- "rate": 612915.63,
+ "rate": 591756,
"unit": "km"
},
"ZMW": {
- "rate": 2440.41,
- "unit": "km"
- },
- "ZWG": {
- "rate": 3023.59,
+ "rate": 3148,
"unit": "km"
}
}`) as Record,
@@ -7017,6 +6962,7 @@ const CONST = {
BANK_ACCOUNT: 'bankAccount',
REPORT_ID: 'reportID',
BASE_62_REPORT_ID: 'base62ReportID',
+ TAX: 'tax',
EXPORTED_TO: 'exportedto',
EXCHANGE_RATE: 'exchangeRate',
REIMBURSABLE_TOTAL: 'reimbursableTotal',
@@ -7161,54 +7107,6 @@ 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',
@@ -7776,11 +7674,6 @@ const CONST = {
REFUND: 'REFUND',
EXCHANGE: 'EXCHANGE',
},
- /**
- * The Travel Invoicing feed type constant.
- * This feed is used for Travel Invoicing cards which are separate from regular Expensify Cards.
- */
- PROGRAM_TRAVEL_US: 'TRAVEL_US',
},
LAST_PAYMENT_METHOD: {
LAST_USED: 'lastUsed',
@@ -7847,15 +7740,6 @@ const CONST = {
INVERTED: 'inverted',
},
- VIOLATION_DISMISSAL: {
- rter: {
- manual: 'marked this receipt as cash',
- },
- duplicatedTransaction: {
- manual: 'resolved the duplicate',
- },
- },
-
SENTRY_LABEL: {
NAVIGATION_TAB_BAR: {
EXPENSIFY_LOGO: 'NavigationTabBar-ExpensifyLogo',
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 345fc8e6f018..4c995bab6dbc 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -7,7 +7,6 @@ import {AppState, Linking, Platform} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import ConfirmModal from './components/ConfirmModal';
-import DelegateNoAccessModalProvider from './components/DelegateNoAccessModalProvider';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import GrowlNotification from './components/GrowlNotification';
import {InitialURLContext} from './components/InitialURLContextProvider';
@@ -363,9 +362,7 @@ function Expensify() {
{shouldInit && (
<>
-
-
-
+
{/* We include the modal for showing a new update at the top level so the option is always present. */}
{updateAvailable && !updateRequired ? : null}
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 3205cb49523a..47dbc171a2b7 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -949,8 +949,6 @@ 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',
@@ -979,14 +977,11 @@ const ONYXKEYS = {
CREATE_DOMAIN_FORM_DRAFT: 'createDomainFormDraft',
SPLIT_EXPENSE_EDIT_DATES: 'splitExpenseEditDates',
SPLIT_EXPENSE_EDIT_DATES_DRAFT: 'splitExpenseEditDatesDraft',
- EXPENSE_RULE_FORM: 'expenseRuleForm',
- EXPENSE_RULE_FORM_DRAFT: 'expenseRuleFormDraft',
},
DERIVED: {
REPORT_ATTRIBUTES: 'reportAttributes',
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID',
- VISIBLE_REPORT_ACTIONS: 'visibleReportActions',
},
/** Stores HybridApp specific state required to interoperate with OldDot */
@@ -1048,7 +1043,6 @@ 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;
@@ -1099,7 +1093,6 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm;
[ONYXKEYS.FORMS.CREATE_DOMAIN_FORM]: FormTypes.CreateDomainForm;
[ONYXKEYS.FORMS.SPLIT_EXPENSE_EDIT_DATES]: FormTypes.SplitExpenseEditDateForm;
- [ONYXKEYS.FORMS.EXPENSE_RULE_FORM]: FormTypes.ExpenseRuleForm;
};
type OnyxFormDraftValuesMapping = {
@@ -1395,7 +1388,6 @@ type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue;
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue;
[ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue;
- [ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS]: OnyxTypes.VisibleReportActionsDerivedValue;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 50d6749e940f..16c126be5dfe 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -13,7 +13,6 @@ import type {IOURequestType} from './libs/actions/IOU';
import Log from './libs/Log';
import type {RootNavigatorParamList} from './libs/Navigation/types';
import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUtils';
-import StringUtils from './libs/StringUtils';
import {getUrlWithParams} from './libs/Url';
import SCREENS from './SCREENS';
import type {Screen} from './SCREENS';
@@ -52,11 +51,6 @@ const PUBLIC_SCREENS_ROUTES = {
// Exported for identifying a url as a verify-account route, associated with a page extending the VerifyAccountPageBase component
const VERIFY_ACCOUNT = 'verify-account';
-const MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES = {
- FACTOR: 'multifactor-authentication/factor',
- PROMPT: 'multifactor-authentication/prompt',
-} as const;
-
const ROUTES = {
...PUBLIC_SCREENS_ROUTES,
// This route renders the list of reports.
@@ -97,17 +91,6 @@ const ROUTES = {
return getUrlWithBackToParam(baseRoute, backTo);
},
},
-
- EXPENSE_REPORT_RHP: {
- route: 'e/:reportID',
- getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => {
- const baseRoute = `e/${reportID}` as const;
-
- // eslint-disable-next-line no-restricted-syntax -- Legacy route generation
- return getUrlWithBackToParam(baseRoute, backTo);
- },
- },
-
SEARCH_REPORT_VERIFY_ACCOUNT: {
route: `search/view/:reportID/${VERIFY_ACCOUNT}`,
getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}` as const,
@@ -173,6 +156,7 @@ const ROUTES = {
ENABLE_PAYMENTS: 'enable-payments',
WALLET_STATEMENT_WITH_DATE: 'statements/:yearMonth',
SIGN_IN_MODAL: 'sign-in-modal',
+ REQUIRE_TWO_FACTOR_AUTH: '2fa-required',
BANK_ACCOUNT: 'bank-account',
BANK_ACCOUNT_VERIFY_ACCOUNT: {
@@ -392,18 +376,6 @@ const ROUTES = {
getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const,
},
SETTINGS_RULES: 'settings/rules',
- SETTINGS_RULES_ADD: {
- route: 'settings/rules/new/:field?',
- getRoute: (field?: ValueOf) => {
- return `settings/rules/new/${field ? StringUtils.camelToHyphenCase(field) : ''}` as const;
- },
- },
- SETTINGS_RULES_EDIT: {
- route: 'settings/rules/edit/:hash/:field?',
- getRoute: (hash?: string, field?: ValueOf) => {
- return `settings/rules/edit/${hash ?? ':hash'}/${field ? StringUtils.camelToHyphenCase(field) : ''}` as const;
- },
- },
SETTINGS_LEGAL_NAME: 'settings/profile/legal-name',
SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth',
SETTINGS_PHONE_NUMBER: 'settings/profile/phone',
@@ -621,10 +593,6 @@ const ROUTES = {
route: `r/:reportID/${VERIFY_ACCOUNT}`,
getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}` as const,
},
- EXPENSE_REPORT_VERIFY_ACCOUNT: {
- route: `e/:reportID/${VERIFY_ACCOUNT}`,
- getRoute: (reportID: string) => `e/${reportID}/${VERIFY_ACCOUNT}` as const,
- },
REPORT_PARTICIPANTS: {
route: 'r/:reportID/participants',
@@ -1213,12 +1181,13 @@ const ROUTES = {
},
MONEY_REQUEST_STEP_DISTANCE_ODOMETER: {
route: ':action/:iouType/distance-odometer/:transactionID/:reportID',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined) => {
+ 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');
}
- return `${action as string}/${iouType as string}/distance-odometer/${transactionID}/${reportID}` as const;
+ // 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: {
@@ -1325,16 +1294,6 @@ const ROUTES = {
label ? `${backTo || state ? '&' : '?'}label=${encodeURIComponent(label)}` : ''
}` as const,
},
- MONEY_REQUEST_STEP_TIME_RATE: {
- route: ':action/:iouType/rate/:transactionID/:reportID/:reportActionID?',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, reportActionID?: string) =>
- `${action as string}/${iouType as string}/rate/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}` as const,
- },
- MONEY_REQUEST_STEP_HOURS: {
- route: ':action/:iouType/hours/:transactionID/:reportID/:reportActionID?',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) =>
- `${action as string}/${iouType as string}/hours/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}` as const,
- },
DISTANCE_REQUEST_CREATE: {
route: ':action/:iouType/start/:transactionID/:reportID/distance-new/:backToReport?',
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) =>
@@ -2049,10 +2008,6 @@ const ROUTES = {
route: 'workspaces/:policyID/category/:categoryName/require-receipts-over',
getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/require-receipts-over` as const,
},
- WORKSPACE_CATEGORY_REQUIRED_FIELDS: {
- route: 'workspaces/:policyID/category/:categoryName/required-fields',
- getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/required-fields` as const,
- },
WORKSPACE_CATEGORY_APPROVER: {
route: 'workspaces/:policyID/category/:categoryName/approver',
getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/approver` as const,
@@ -3698,27 +3653,6 @@ const ROUTES = {
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,
- },
-
- MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.FACTOR}/magic-code`,
- MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test',
-
- // The exact notification & prompt type will be added as a part of Multifactor Authentication config in another PR,
- // for now a string is accepted to avoid blocking this PR.
- MULTIFACTOR_AUTHENTICATION_NOTIFICATION: {
- route: 'multifactor-authentication/notification/:notificationType',
- getRoute: (notificationType: ValueOf) => `multifactor-authentication/notification/${notificationType}` as const,
- },
-
- MULTIFACTOR_AUTHENTICATION_PROMPT: {
- route: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.PROMPT}/:promptType`,
- getRoute: (promptType: string) => `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.PROMPT}/${promptType}` as const,
- },
-
- MULTIFACTOR_AUTHENTICATION_NOT_FOUND: 'multifactor-authentication/not-found',
} as const;
/**
@@ -3734,7 +3668,7 @@ const SHARED_ROUTE_PARAMS: Partial> = {
[SCREENS.WORKSPACE.INITIAL]: ['backTo'],
} as const;
-export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT, MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES};
+export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT};
export default ROUTES;
type ReportAttachmentsRoute = typeof ROUTES.REPORT_ATTACHMENTS.route;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 739c5ac47279..989a9ae3fe92 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -150,26 +150,6 @@ const SCREENS = {
RULES: {
ROOT: 'Settings_Rules',
- ADD: 'Settings_Rules_Add',
- ADD_MERCHANT: 'Settings_Rules_Add_Merchant',
- ADD_RENAME_MERCHANT: 'Settings_Rules_Add_Rename_Merchant',
- ADD_CATEGORY: 'Settings_Rules_Add_Category',
- ADD_TAG: 'Settings_Rules_Add_Tag',
- ADD_TAX: 'Settings_Rules_Add_Tax',
- ADD_DESCRIPTION: 'Settings_Rules_Add_Description',
- ADD_REIMBURSABLE: 'Settings_Rules_Add_Reimbursable',
- ADD_BILLABLE: 'Settings_Rules_Add_Billable',
- ADD_REPORT: 'Settings_Rules_Add_Report',
- EDIT: 'Settings_Rules_Edit',
- EDIT_MERCHANT: 'Settings_Rules_Edit_Merchant',
- EDIT_RENAME_MERCHANT: 'Settings_Rules_Edit_Rename_Merchant',
- EDIT_CATEGORY: 'Settings_Rules_Edit_Category',
- EDIT_TAG: 'Settings_Rules_Edit_Tag',
- EDIT_TAX: 'Settings_Rules_Edit_Tax',
- EDIT_DESCRIPTION: 'Settings_Rules_Edit_Description',
- EDIT_REIMBURSABLE: 'Settings_Rules_Edit_Reimbursable',
- EDIT_BILLABLE: 'Settings_Rules_Edit_Billable',
- EDIT_REPORT: 'Settings_Rules_Edit_Report',
},
WALLET: {
@@ -275,6 +255,8 @@ const SCREENS = {
TRAVEL: 'Travel',
SEARCH_REPORT: 'SearchReport',
SEARCH_REPORT_ACTIONS: 'SearchReportActions',
+ // These two routes will be added in a separate PR adding Super Wide RHP routes
+ EXPENSE_REPORT: 'ExpenseReport',
SEARCH_MONEY_REQUEST_REPORT: 'SearchMoneyRequestReport',
SEARCH_COLUMNS: 'SearchColumns',
@@ -295,8 +277,6 @@ const SCREENS = {
MERGE_TRANSACTION: 'MergeTransaction',
REPORT_CARD_ACTIVATE: 'Report_Card_Activate',
DOMAIN: 'Domain',
- EXPENSE_REPORT: 'ExpenseReport',
- MULTIFACTOR_AUTHENTICATION: 'MultifactorAuthentication',
},
REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root',
PUBLIC_CONSOLE_DEBUG: 'Console_Debug',
@@ -353,8 +333,6 @@ const SCREENS = {
STEP_DISTANCE_GPS: 'Money_Request_Step_Distance_GPS',
STEP_DISTANCE_ODOMETER: 'Money_Request_Step_Distance_Odometer',
RECEIPT_PREVIEW: 'Money_Request_Receipt_preview',
- STEP_TIME_RATE: 'Money_Request_Step_Time_Rate',
- STEP_HOURS: 'Money_Request_Step_Hours',
},
TRANSACTION_DUPLICATE: {
@@ -712,7 +690,6 @@ const SCREENS = {
CATEGORY_DESCRIPTION_HINT: 'Category_Description_Hint',
CATEGORY_APPROVER: 'Category_Approver',
CATEGORY_REQUIRE_RECEIPTS_OVER: 'Category_Require_Receipts_Over',
- CATEGORY_REQUIRED_FIELDS: 'Category_Required_Fields',
CATEGORIES_SETTINGS: 'Categories_Settings',
CATEGORIES_IMPORT: 'Categories_Import',
CATEGORIES_IMPORTED: 'Categories_Imported',
@@ -849,7 +826,6 @@ const SCREENS = {
REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info',
REFERRAL_DETAILS: 'Referral_Details',
REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account',
- EXPENSE_REPORT_VERIFY_ACCOUNT: 'Expense_Report_Verify_Account',
KEYBOARD_SHORTCUTS: 'KeyboardShortcuts',
SHARE: {
ROOT: 'Share_Root',
@@ -902,14 +878,6 @@ const SCREENS = {
ADD_ADMIN: 'Domain_Add_Admin',
MEMBERS: 'Domain_Members',
MEMBER_DETAILS: 'Member_Details',
- RESET_DOMAIN: 'Domain_Reset',
- },
- MULTIFACTOR_AUTHENTICATION: {
- MAGIC_CODE: 'Multifactor_Authentication_Magic_Code',
- BIOMETRICS_TEST: 'Multifactor_Authentication_Biometrics_Test',
- NOTIFICATION: 'Multifactor_Authentication_Notification',
- PROMPT: 'Multifactor_Authentication_Prompt',
- NOT_FOUND: 'Multifactor_Authentication_Not_Found',
},
} as const;
diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx
index b347b69ebb96..20598751629d 100644
--- a/src/components/AccountSwitcher.tsx
+++ b/src/components/AccountSwitcher.tsx
@@ -185,7 +185,7 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) {
{
const timeoutId = setTimeout(() => {
diff --git a/src/components/AddUnreportedExpenseFooter.tsx b/src/components/AddUnreportedExpenseFooter.tsx
index 53c506524f96..58defd361630 100644
--- a/src/components/AddUnreportedExpenseFooter.tsx
+++ b/src/components/AddUnreportedExpenseFooter.tsx
@@ -51,7 +51,7 @@ function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, repor
setErrorMessage(translate('iou.selectUnreportedExpense'));
return;
}
- Navigation.dismissToSuperWideRHP();
+ Navigation.dismissModal();
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
if (report && isIOUReport(report)) {
@@ -75,7 +75,7 @@ function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, repor
policy,
reportNextStep,
policyCategories,
- allTransactions,
+ allTransactionsCollection: allTransactions,
});
}
});
diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx
index 62b3fe7b724e..181190035104 100644
--- a/src/components/AddressForm.tsx
+++ b/src/components/AddressForm.tsx
@@ -134,7 +134,7 @@ function AddressForm({
if (countrySpecificZipRegex) {
if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) {
if (isRequiredFulfilled(values.zipPostCode?.trim())) {
- errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat);
+ errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat});
} else {
errors.zipPostCode = translate('common.error.fieldRequired');
}
diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx
index d9c44dc223ce..58a9d0fcd551 100644
--- a/src/components/AmountTextInput.tsx
+++ b/src/components/AmountTextInput.tsx
@@ -1,7 +1,6 @@
import {useNavigation} from '@react-navigation/native';
import React from 'react';
import type {NativeSyntheticEvent, StyleProp, TextInputKeyPressEvent, TextInputSelectionChangeEvent, TextStyle, ViewStyle} from 'react-native';
-import useLocalize from '@hooks/useLocalize';
import CONST from '@src/CONST';
import type {TextSelection} from './Composer/types';
import TextInput from './TextInput';
@@ -43,7 +42,7 @@ type AmountTextInputProps = {
/** Hide the focus styles on TextInput */
hideFocusedState?: boolean;
-} & Pick;
+} & Pick;
function AmountTextInput({
formattedAmount,
@@ -60,11 +59,9 @@ function AmountTextInput({
shouldApplyPaddingToContainer = false,
ref,
disabled,
- accessibilityLabel,
...rest
}: AmountTextInputProps) {
const navigation = useNavigation();
- const {translate} = useLocalize();
return (
void}
touchableInputWrapperStyle={touchableInputWrapperStyle}
// On iPad, even if the soft keyboard is hidden, the keyboard suggestion is still shown.
diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
index 5c34ce3fee4a..4da71d03330c 100644
--- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
+++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
@@ -80,7 +80,6 @@ function BaseAnchorForCommentsOnly({
onPressIn={onPressIn}
onPressOut={onPressOut}
role={CONST.ROLE.LINK}
- tabIndex={-1}
accessibilityLabel={href}
wrapperStyle={wrapperStyle}
>
@@ -92,7 +91,6 @@ function BaseAnchorForCommentsOnly({
ref={linkRef}
style={StyleSheet.flatten([style, defaultTextStyle])}
role={CONST.ROLE.LINK}
- tabIndex={0}
hrefAttrs={{
rel,
target: isEmail || !linkProps.href ? '_self' : target,
diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx
index 37e614853bf2..a037a6004430 100644
--- a/src/components/ApprovalWorkflowSection.tsx
+++ b/src/components/ApprovalWorkflowSection.tsx
@@ -80,8 +80,6 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CU
descriptionTextStyle={[styles.textNormalThemeText, styles.lineHeightXLarge]}
description={members}
numberOfLinesDescription={4}
- shouldBeAccessible={false}
- tabIndex={-1}
icon={icons.Users}
iconHeight={20}
iconWidth={20}
@@ -101,8 +99,6 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress, currency = CONST.CU
descriptionTextStyle={[styles.textNormalThemeText, styles.lineHeightXLarge]}
description={Str.removeSMSDomain(approver.displayName)}
icon={icons.UserCheck}
- shouldBeAccessible={false}
- tabIndex={-1}
iconHeight={20}
iconWidth={20}
numberOfLinesDescription={1}
diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts
index df0d08946c19..6dffd994d6a7 100644
--- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts
+++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts
@@ -3,11 +3,11 @@ import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {Attachment} from '@components/Attachments/types';
import {getFileName, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils';
-import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, isReportActionVisible} from '@libs/ReportActionsUtils';
+import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction} from '@libs/ReportUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import CONST from '@src/CONST';
-import type {Report, ReportAction, ReportActions, VisibleReportActionsDerivedValue} from '@src/types/onyx';
+import type {Report, ReportAction, ReportActions} from '@src/types/onyx';
import type {Note} from '@src/types/onyx/Report';
/**
@@ -22,7 +22,6 @@ function extractAttachments(
reportActions,
report,
isReportArchived,
- visibleReportActionsData,
}: {
privateNotes?: Record;
accountID?: number;
@@ -30,7 +29,6 @@ function extractAttachments(
reportActions?: OnyxEntry;
report: OnyxEntry;
isReportArchived: boolean | undefined;
- visibleReportActionsData?: VisibleReportActionsDerivedValue;
},
) {
const targetNote = privateNotes?.[Number(accountID)]?.note ?? '';
@@ -117,13 +115,9 @@ function extractAttachments(
return attachments.reverse();
}
- const reportID = report?.reportID;
- if (!reportID) {
- return attachments.reverse();
- }
const actions = [...(parentReportAction ? [parentReportAction] : []), ...getSortedReportActions(Object.values(reportActions ?? {}))];
- for (const action of actions) {
- if (!isReportActionVisible(action, reportID, canUserPerformAction, visibleReportActionsData) || isMoneyRequestAction(action)) {
+ for (const [key, action] of actions.entries()) {
+ if (!shouldReportActionBeVisible(action, key, canUserPerformAction) || isMoneyRequestAction(action)) {
continue;
}
diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
index 0cd97ef55eb6..29787a820a62 100644
--- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
+++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
@@ -40,7 +40,6 @@ function BaseAutoCompleteSuggestions({
onPress={() => onSelect(index)}
onLongPress={() => {}}
accessibilityLabel={accessibilityLabelExtractor(item, index)}
- role={CONST.ROLE.MENUITEM}
>
{renderSuggestionMenuItem(item, index)}
diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx
index b5ba853f3b98..8348d0f5323c 100644
--- a/src/components/Banner.tsx
+++ b/src/components/Banner.tsx
@@ -131,7 +131,7 @@ function Banner({
CONST.ROLE.BUTTON;
+const getButtonRole: GetButtonRole = (isNested) => (isNested ? CONST.ROLE.PRESENTATION : CONST.ROLE.BUTTON);
// eslint-disable-next-line import/prefer-default-export
export {getButtonRole};
diff --git a/src/components/CardFeedIcon.tsx b/src/components/CardFeedIcon.tsx
index fc32a3745d08..cd1b9390294e 100644
--- a/src/components/CardFeedIcon.tsx
+++ b/src/components/CardFeedIcon.tsx
@@ -12,10 +12,9 @@ type CardFeedIconProps = {
isExpensifyCardFeed?: boolean;
selectedFeed?: CompanyCardFeedWithDomainID | undefined;
iconProps?: Partial;
- useSkeletonLoader?: boolean;
};
-function CardFeedIcon({iconProps, selectedFeed, isExpensifyCardFeed = false, useSkeletonLoader = false}: CardFeedIconProps) {
+function CardFeedIcon({iconProps, selectedFeed, isExpensifyCardFeed = false}: CardFeedIconProps) {
const {src, ...restIconProps} = iconProps ?? {};
const illustrations = useThemeIllustrations();
@@ -32,7 +31,6 @@ function CardFeedIcon({iconProps, selectedFeed, isExpensifyCardFeed = false, use
return (
diff --git a/src/components/CaretWrapper.tsx b/src/components/CaretWrapper.tsx
index 6ccfbb6689e9..6518f6a53449 100644
--- a/src/components/CaretWrapper.tsx
+++ b/src/components/CaretWrapper.tsx
@@ -12,10 +12,9 @@ type CaretWrapperProps = ChildrenProps & {
style?: StyleProp;
caretWidth?: number;
caretHeight?: number;
- isActive?: boolean;
};
-function CaretWrapper({children, style, caretWidth, caretHeight, isActive = false}: CaretWrapperProps) {
+function CaretWrapper({children, style, caretWidth, caretHeight}: CaretWrapperProps) {
const theme = useTheme();
const styles = useThemeStyles();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DownArrow'] as const);
@@ -28,7 +27,6 @@ function CaretWrapper({children, style, caretWidth, caretHeight, isActive = fals
fill={theme.icon}
width={caretWidth ?? variables.iconSizeExtraSmall}
height={caretHeight ?? variables.iconSizeExtraSmall}
- additionalStyles={isActive ? styles.flipUpsideDown : []}
/>
);
diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx
index 6cc08379704f..6e05f80574c2 100644
--- a/src/components/Checkbox.tsx
+++ b/src/components/Checkbox.tsx
@@ -139,7 +139,6 @@ function Checkbox({
pressDimmingValue={1}
wrapperStyle={wrapperStyle}
sentryLabel={sentryLabel}
- shouldUseAutoHitSlop
>
{children ?? (
string | undefined;
-};
-
-const CurrencyListContext = createContext({
- currencyList: getEmptyObject(),
- getCurrencySymbol: () => undefined,
-});
-
-function CurrencyListContextProvider({children}: CurrencyListContextProviderProps) {
- const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true});
-
- const getCurrencySymbol = useCallback(
- (currencyCode: string): string | undefined => {
- return currencyList[currencyCode]?.symbol;
- },
- [currencyList],
- );
-
- const contextValue = useMemo(
- () => ({
- currencyList,
- getCurrencySymbol,
- }),
- [currencyList, getCurrencySymbol],
- );
-
- return {children};
-}
-
-export {CurrencyListContext, CurrencyListContextProvider};
-
-export type {CurrencyListContextProps};
diff --git a/src/components/CurrencyPicker.tsx b/src/components/CurrencyPicker.tsx
index 971aebf9163e..d0dcebcdc38f 100644
--- a/src/components/CurrencyPicker.tsx
+++ b/src/components/CurrencyPicker.tsx
@@ -1,8 +1,8 @@
import type {ReactNode} from 'react';
import React, {Fragment, useState} from 'react';
-import useCurrencyList from '@hooks/useCurrencyList';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getCurrencySymbol} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
@@ -41,7 +41,6 @@ type CurrencyPickerProps = {
function CurrencyPicker({label, value, errorText, headerContent, excludeCurrencies, disabled = false, shouldShowFullPageOfflineView = false, onInputChange = () => {}}: CurrencyPickerProps) {
const {translate} = useLocalize();
- const {getCurrencySymbol} = useCurrencyList();
const [isPickerVisible, setIsPickerVisible] = useState(false);
const styles = useThemeStyles();
diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx
index 64f05e8c6cc8..88c494253139 100644
--- a/src/components/CurrencySelectionList/index.tsx
+++ b/src/components/CurrencySelectionList/index.tsx
@@ -4,9 +4,11 @@ import React, {useCallback, useMemo, useState} from 'react';
import SelectionList from '@components/SelectionListWithSections';
import RadioListItem from '@components/SelectionListWithSections/RadioListItem';
import SelectableListItem from '@components/SelectionListWithSections/SelectableListItem';
-import useCurrencyList from '@hooks/useCurrencyList';
import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import {getCurrencySymbol} from '@libs/CurrencyUtils';
import getMatchScore from '@libs/getMatchScore';
+import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {CurrencyListItem, CurrencySelectionListProps} from './types';
@@ -21,12 +23,12 @@ function CurrencySelectionList({
excludedCurrencies = [],
...restProps
}: CurrencySelectionListProps) {
- const {currencyList, getCurrencySymbol} = useCurrencyList();
+ const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: false});
const [searchValue, setSearchValue] = useState('');
const {translate} = useLocalize();
const getUnselectedOptions = useCallback((options: CurrencyListItem[]) => options.filter((option) => !option.isSelected), []);
const {sections, headerMessage} = useMemo(() => {
- const currencyOptions: CurrencyListItem[] = Object.entries(currencyList).reduce((acc, [currencyCode, currencyInfo]) => {
+ const currencyOptions: CurrencyListItem[] = Object.entries(currencyList ?? {}).reduce((acc, [currencyCode, currencyInfo]) => {
const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode || selectedCurrencies.includes(currencyCode);
if (!excludedCurrencies.includes(currencyCode) && (isSelectedCurrency || !currencyInfo?.retired)) {
acc.push({
@@ -92,7 +94,7 @@ function CurrencySelectionList({
}
return {sections: result, headerMessage: isEmpty ? translate('common.noResultsFound') : ''};
- }, [currencyList, recentlyUsedCurrencies, searchValue, getUnselectedOptions, translate, initiallySelectedCurrencyCode, selectedCurrencies, excludedCurrencies, getCurrencySymbol]);
+ }, [currencyList, recentlyUsedCurrencies, searchValue, getUnselectedOptions, translate, initiallySelectedCurrencyCode, selectedCurrencies, excludedCurrencies]);
return (
{currentYearView}
@@ -208,7 +208,7 @@ function CalendarPicker({
{monthNames.at(currentMonthView)}
@@ -219,7 +219,6 @@ function CalendarPicker({
onPress={moveToPrevMonth}
hoverDimmingValue={1}
accessibilityLabel={translate('common.previous')}
- role={CONST.ROLE.BUTTON}
>
@@ -271,18 +269,16 @@ function CalendarPicker({
onDayPressed(day);
};
const key = `${index}_day-${day}`;
- const dateAccessibilityLabel = day ? format(currentDate, 'EEEE, MMMM d, yyyy') : '';
return (
{({hovered, pressed}) => (
{
- // This makes sure that cursor does not appear in the TextInput when we open the DatePicker
- event?.preventDefault();
- calculatePopoverPosition();
- setIsModalVisible(true);
- },
- [calculatePopoverPosition],
- );
+ const handlePress = useCallback(() => {
+ calculatePopoverPosition();
+ setIsModalVisible(true);
+ }, [calculatePopoverPosition]);
const closeDatePicker = useCallback(() => {
textInputRef.current?.blur();
@@ -112,7 +101,7 @@ function DatePicker({
}, [calculatePopoverPosition, windowWidth]);
useEffect(() => {
- if (!autoFocus || isAutoFocused.current || isSmallScreenWidth) {
+ if (!autoFocus || isAutoFocused.current) {
return;
}
isAutoFocused.current = true;
@@ -120,7 +109,7 @@ function DatePicker({
InteractionManager.runAfterInteractions(() => {
handlePress();
});
- }, [handlePress, autoFocus, isSmallScreenWidth]);
+ }, [handlePress, autoFocus]);
const getValidDateForCalendar = useMemo(() => {
if (!selectedDate) {
@@ -150,12 +139,12 @@ function DatePicker({
errorText={errorText}
inputStyle={styles.pointerEventsNone}
disabled={disabled}
+ readOnly
onPress={handlePress}
textInputContainerStyles={isModalVisible ? styles.borderColorFocus : {}}
shouldHideClearButton={shouldHideClearButton}
onClearInput={handleClear}
forwardedFSClass={forwardedFSClass}
- disableKeyboard
/>
diff --git a/src/components/DestinationPicker.tsx b/src/components/DestinationPicker.tsx
index 73b9597fe9bf..8ffee6e20d3f 100644
--- a/src/components/DestinationPicker.tsx
+++ b/src/components/DestinationPicker.tsx
@@ -1,5 +1,4 @@
import React, {useMemo} from 'react';
-import type {ForwardedRef} from 'react';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
@@ -13,17 +12,15 @@ import ONYXKEYS from '@src/ONYXKEYS';
// eslint-disable-next-line no-restricted-imports
import SelectionList from './SelectionListWithSections';
import RadioListItem from './SelectionListWithSections/RadioListItem';
-import type {ListItem, SelectionListHandle} from './SelectionListWithSections/types';
+import type {ListItem} from './SelectionListWithSections/types';
type DestinationPickerProps = {
policyID: string;
selectedDestination?: string;
onSubmit: (item: ListItem & {currency: string}) => void;
- ref?: ForwardedRef;
- textInputAutoFocus?: boolean;
};
-function DestinationPicker({selectedDestination, policyID, onSubmit, ref, textInputAutoFocus = false}: DestinationPickerProps) {
+function DestinationPicker({selectedDestination, policyID, onSubmit}: DestinationPickerProps) {
const policy = usePolicy(policyID);
const customUnit = getPerDiemCustomUnit(policy);
const [policyRecentlyUsedDestinations] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS}${policyID}`, {canBeMissing: true});
@@ -74,7 +71,6 @@ function DestinationPicker({selectedDestination, policyID, onSubmit, ref, textIn
return (
);
diff --git a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx
index 98870d7d928a..7364cd9adf4b 100644
--- a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx
+++ b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx
@@ -1,10 +1,9 @@
import React from 'react';
+import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
-import {isWaypointNullIsland} from '@libs/TransactionUtils';
-import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
@@ -33,7 +32,7 @@ type DistanceRequestProps = {
function DistanceRequestRenderItem({waypoints, item = '', onSecondaryInteraction, getIndex, isActive = false, onPress = () => {}, disabled = false}: DistanceRequestProps) {
const theme = useTheme();
- const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'DotIndicatorUnfilled', 'DotIndicator', 'DragHandles']);
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location']);
const {translate} = useLocalize();
const numberOfWaypoints = Object.keys(waypoints ?? {}).length;
const lastWaypointIndex = numberOfWaypoints - 1;
@@ -43,25 +42,24 @@ function DistanceRequestRenderItem({waypoints, item = '', onSecondaryInteraction
let waypointIcon;
if (index === 0) {
descriptionKey += 'start';
- waypointIcon = expensifyIcons.DotIndicatorUnfilled;
+ waypointIcon = Expensicons.DotIndicatorUnfilled;
} else if (index === lastWaypointIndex) {
descriptionKey += 'stop';
waypointIcon = expensifyIcons.Location;
} else {
descriptionKey += 'stop';
- waypointIcon = expensifyIcons.DotIndicator;
+ waypointIcon = Expensicons.DotIndicator;
}
const waypoint = waypoints?.[`waypoint${index}`] ?? {};
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const title = waypoint.name || waypoint.address;
- const errorText = isWaypointNullIsland(waypoint) ? translate('violations.noRoute') : undefined;
return (
);
}
diff --git a/src/components/Domain/DomainMenuItem.tsx b/src/components/Domain/DomainMenuItem.tsx
index c8f207b9a43a..2e0a5356b9a2 100644
--- a/src/components/Domain/DomainMenuItem.tsx
+++ b/src/components/Domain/DomainMenuItem.tsx
@@ -8,9 +8,7 @@ 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 = {
@@ -39,9 +37,6 @@ 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) {
@@ -73,14 +68,12 @@ function DomainMenuItem({item, index}: DomainMenuItemProps) {
clearDomainErrors(item.accountID)}
+ style={styles.mb2}
>
{({hovered}) => (
diff --git a/src/components/DraggableList/index.tsx b/src/components/DraggableList/index.tsx
index 4df38de83d4b..b244d3a9ba92 100644
--- a/src/components/DraggableList/index.tsx
+++ b/src/components/DraggableList/index.tsx
@@ -1,7 +1,7 @@
import type {DragEndEvent} from '@dnd-kit/core';
-import {closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core';
+import {closestCenter, DndContext, PointerSensor, useSensor} from '@dnd-kit/core';
import {restrictToParentElement, restrictToVerticalAxis} from '@dnd-kit/modifiers';
-import {arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy} from '@dnd-kit/sortable';
+import {arrayMove, SortableContext, verticalListSortingStrategy} from '@dnd-kit/sortable';
import React, {Fragment} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView as RNScrollView} from 'react-native';
@@ -69,16 +69,13 @@ function DraggableList({
);
});
- const sensors = useSensors(
+ const sensors = [
useSensor(PointerSensor, {
activationConstraint: {
distance: minimumActivationDistance,
},
}),
- useSensor(KeyboardSensor, {
- coordinateGetter: sortableKeyboardCoordinates,
- }),
- );
+ ];
const Container = disableScroll ? Fragment : ScrollView;
diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx
index d4cde4306b30..b59911c1d5e6 100644
--- a/src/components/EReceipt.tsx
+++ b/src/components/EReceipt.tsx
@@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
-import useCurrencyList from '@hooks/useCurrencyList';
import useEReceipt from '@hooks/useEReceipt';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -9,7 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {filterPersonalCards, getCardDescription, getCompanyCardDescription} from '@libs/CardUtils';
-import {convertToDisplayString} from '@libs/CurrencyUtils';
+import {convertToDisplayString, getCurrencySymbol} from '@libs/CurrencyUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getTransactionDetails} from '@libs/ReportUtils';
import variables from '@styles/variables';
@@ -41,7 +40,6 @@ function EReceipt({transactionID, transactionItem, onLoad, isThumbnail = false}:
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
- const {getCurrencySymbol} = useCurrencyList();
const theme = useTheme();
const icons = useMemoizedLazyExpensifyIcons(['ReceiptBody', 'ExpensifyWordmark']);
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
@@ -62,8 +60,7 @@ function EReceipt({transactionID, transactionItem, onLoad, isThumbnail = false}:
const formattedAmount = convertToDisplayString(transactionAmount, transactionCurrency);
const currency = getCurrencySymbol(transactionCurrency ?? '');
const amount = currency ? formattedAmount.replace(currency, '') : formattedAmount;
- const cardDescription =
- getCompanyCardDescription(transactionCardName, transactionCardID, cardList) ?? (transactionCardID ? getCardDescription(cardList?.[transactionCardID], translate) : '');
+ const cardDescription = getCompanyCardDescription(transactionCardName, transactionCardID, cardList) ?? (transactionCardID ? getCardDescription(cardList?.[transactionCardID]) : '');
const secondaryBgcolorStyle = secondaryColor ? StyleUtils.getBackgroundColorStyle(secondaryColor) : undefined;
const primaryTextColorStyle = primaryColor ? StyleUtils.getColorStyle(primaryColor) : undefined;
const titleTextColorStyle = titleColor ? StyleUtils.getColorStyle(titleColor) : undefined;
diff --git a/src/components/EmojiPicker/CategoryShortcutButton.tsx b/src/components/EmojiPicker/CategoryShortcutButton.tsx
index 33f59ca73a33..fa53a9b7309b 100644
--- a/src/components/EmojiPicker/CategoryShortcutButton.tsx
+++ b/src/components/EmojiPicker/CategoryShortcutButton.tsx
@@ -41,7 +41,7 @@ function CategoryShortcutButton({code, icon, onPress}: CategoryShortcutButtonPro
onHoverIn={() => setIsHighlighted(true)}
onHoverOut={() => setIsHighlighted(false)}
style={({pressed}) => [StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), styles.categoryShortcutButton, isHighlighted && styles.emojiItemHighlighted]}
- accessibilityLabel={translate(`emojiPicker.headers.${code}` as TranslationPaths)}
+ accessibilityLabel={`emojiPicker.headers.${code}`}
role={CONST.ROLE.BUTTON}
sentryLabel={CONST.SENTRY_LABEL.EMOJI_PICKER.CATEGORY_SHORTCUT}
>
diff --git a/src/components/EmojiPicker/EmojiPickerButton.tsx b/src/components/EmojiPicker/EmojiPickerButton.tsx
index b1010d33dc1a..b90d0447d320 100644
--- a/src/components/EmojiPicker/EmojiPickerButton.tsx
+++ b/src/components/EmojiPicker/EmojiPickerButton.tsx
@@ -82,7 +82,6 @@ function EmojiPickerButton({isDisabled = false, emojiPickerID = '', shiftVertica
onPress={openEmojiPicker}
id={CONST.EMOJI_PICKER_BUTTON_NATIVE_ID}
accessibilityLabel={translate('reportActionCompose.emoji')}
- role={CONST.ROLE.BUTTON}
sentryLabel={CONST.SENTRY_LABEL.EMOJI_PICKER.BUTTON}
>
{({hovered, pressed}) => (
diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.tsx b/src/components/EmojiPicker/EmojiPickerButtonDropdown.tsx
index 71233e7cd28f..4489044fccad 100644
--- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.tsx
+++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.tsx
@@ -72,7 +72,7 @@ function EmojiPickerButtonDropdown(
disabled={isDisabled}
onPress={onPress}
id="emojiDropdownButton"
- accessibilityLabel={value ? `${value}, ${translate('statusPage.status')}` : translate('statusPage.status')}
+ accessibilityLabel="statusEmoji"
role={CONST.ROLE.BUTTON}
sentryLabel={CONST.SENTRY_LABEL.EMOJI_PICKER.BUTTON_DROPDOWN}
>
diff --git a/src/components/ExceededCommentLength.tsx b/src/components/ExceededCommentLength.tsx
index 8996a69c0ba8..9efd896d005c 100644
--- a/src/components/ExceededCommentLength.tsx
+++ b/src/components/ExceededCommentLength.tsx
@@ -20,7 +20,7 @@ function ExceededCommentLength({maxCommentLength = CONST.MAX_COMMENT_LENGTH, isT
style={[styles.textMicro, styles.textDanger, styles.chatItemComposeSecondaryRow, styles.mlAuto, styles.pl2]}
numberOfLines={1}
>
- {translate(translationKey, numberFormat(maxCommentLength))}
+ {translate(translationKey, {formattedMaxLength: numberFormat(maxCommentLength)})}
);
}
diff --git a/src/components/FloatingGPSButton/index.native.tsx b/src/components/FloatingGPSButton/index.native.tsx
deleted file mode 100644
index 0d8d4c2c8b87..000000000000
--- a/src/components/FloatingGPSButton/index.native.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import Icon from '@components/Icon';
-import {PressableWithoutFeedback} from '@components/Pressable';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
-import useLocalize from '@hooks/useLocalize';
-import useOnyx from '@hooks/useOnyx';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
-import {generateReportID} from '@libs/ReportUtils';
-import variables from '@styles/variables';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-
-function FloatingGpsButton() {
- const [gpsDraftDetails] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS, {canBeMissing: true});
- const {translate} = useLocalize();
-
- const icons = useMemoizedLazyExpensifyIcons(['Location'] as const);
- const {textMutedReversed} = useTheme();
- const styles = useThemeStyles();
-
- if (!gpsDraftDetails?.isTracking) {
- return null;
- }
-
- const navigateToGpsScreen = () => {
- const reportID = gpsDraftDetails?.reportID ?? generateReportID();
- Navigation.navigate(ROUTES.DISTANCE_REQUEST_CREATE_TAB_GPS.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.CREATE, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID));
- };
-
- return (
-
-
-
-
-
- );
-}
-
-FloatingGpsButton.displayName = 'FloatingGpsButton';
-
-export default FloatingGpsButton;
diff --git a/src/components/FloatingGPSButton/index.tsx b/src/components/FloatingGPSButton/index.tsx
deleted file mode 100644
index e0f2ea10029d..000000000000
--- a/src/components/FloatingGPSButton/index.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-function FloatingGpsButton() {
- return null;
-}
-
-FloatingGpsButton.displayName = 'FloatingGpsButton';
-
-export default FloatingGpsButton;
diff --git a/src/components/FocusModeNotification.tsx b/src/components/FocusModeNotification.tsx
index 585ce4558a52..2d127e86175b 100644
--- a/src/components/FocusModeNotification.tsx
+++ b/src/components/FocusModeNotification.tsx
@@ -31,7 +31,7 @@ function FocusModeNotification({onClose}: FocusModeNotificationProps) {
onCancel={onClose}
prompt={
-
+
}
isVisible
diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx
index 178a8cddaf87..bc5ca52af418 100644
--- a/src/components/Form/FormWrapper.tsx
+++ b/src/components/Form/FormWrapper.tsx
@@ -1,4 +1,4 @@
-import React, {useRef} from 'react';
+import React, {useCallback, useMemo, useRef} from 'react';
import type {RefObject} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native';
@@ -103,9 +103,9 @@ function FormWrapper({
const [formState] = useOnyx(`${formID}`, {canBeMissing: true});
- const errorMessage = formState ? getLatestErrorMessage(formState) : undefined;
+ const errorMessage = useMemo(() => (formState ? getLatestErrorMessage(formState) : undefined), [formState]);
- const onFixTheErrorsLinkPressed = () => {
+ const onFixTheErrorsLinkPressed = useCallback(() => {
const errorFields = !isEmptyObject(errors) ? errors : (formState?.errorFields ?? {});
const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => key in errorFields);
@@ -134,7 +134,7 @@ function FormWrapper({
// Focus the input after scrolling, as on the Web it gives a slightly better visual result
focusInput?.focus?.();
- };
+ }, [errors, formState?.errorFields, inputRefs]);
// If either of `addBottomSafeAreaPadding` or `shouldSubmitButtonStickToBottom` is explicitly set,
// we expect that the user wants to use the new edge-to-edge mode.
@@ -159,53 +159,88 @@ function FormWrapper({
style: submitButtonStyles,
});
- const SubmitButton = isSubmitButtonVisible && (
-
+ const SubmitButton = useMemo(
+ () =>
+ isSubmitButtonVisible && (
+
+ ),
+ [
+ disablePressOnEnter,
+ enterKeyEventListenerPriority,
+ enabledWhenOffline,
+ errorMessage,
+ errors,
+ footerContent,
+ formState?.errorFields,
+ formState?.isLoading,
+ isLoading,
+ isSubmitActionDangerous,
+ isSubmitButtonVisible,
+ isSubmitDisabled,
+ onFixTheErrorsLinkPressed,
+ onSubmit,
+ shouldHideFixErrorsAlert,
+ shouldSubmitButtonBlendOpacity,
+ shouldSubmitButtonStickToBottom,
+ style,
+ styles.flex1,
+ styles.mh0,
+ styles.mt5,
+ styles.stickToBottom,
+ submitButtonStylesWithBottomSafeAreaPadding,
+ submitButtonText,
+ submitFlexEnabled,
+ shouldRenderFooterAboveSubmit,
+ shouldPreventDefaultFocusOnPressSubmit,
+ ],
);
- const scrollViewContent = () => (
- {
- if (!shouldScrollToEnd) {
- return;
- }
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => {
- requestAnimationFrame(() => {
- formRef.current?.scrollToEnd({animated: true});
+ const scrollViewContent = useCallback(
+ () => (
+ {
+ if (!shouldScrollToEnd) {
+ return;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ InteractionManager.runAfterInteractions(() => {
+ requestAnimationFrame(() => {
+ formRef.current?.scrollToEnd({animated: true});
+ });
});
- });
- }}
- >
- {children}
- {!shouldSubmitButtonStickToBottom && SubmitButton}
-
+ }}
+ >
+ {children}
+ {!shouldSubmitButtonStickToBottom && SubmitButton}
+
+ ),
+ [formID, style, styles.pb5, children, shouldSubmitButtonStickToBottom, SubmitButton, shouldScrollToEnd],
);
if (!shouldUseScrollView) {
diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx
index 9c495aa651ab..f5e21fe762c6 100644
--- a/src/components/FullscreenLoadingIndicator.tsx
+++ b/src/components/FullscreenLoadingIndicator.tsx
@@ -5,7 +5,6 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {ExtraLoadingContext} from '@libs/AppState';
import Navigation from '@libs/Navigation/Navigation';
-import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan';
import CONST from '@src/CONST';
import ActivityIndicator from './ActivityIndicator';
import Button from './Button';
@@ -40,7 +39,6 @@ function FullScreenLoadingIndicator({
const styles = useThemeStyles();
const {translate} = useLocalize();
const [showGoBackButton, setShowGoBackButton] = useState(false);
- useSkeletonSpan('FullScreenLoadingIndicator');
useEffect(() => {
if (!shouldUseGoBackButton) {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
index fe91b8354ed8..f84dbb0f9cdb 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
@@ -106,7 +106,6 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) {
style={linkStyle}
onPress={() => openLink(attrHref, environmentURL, isAttachment)}
suppressHighlighting
- role={CONST.ROLE.LINK}
>
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
index fac38983df18..46943dff4b26 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
@@ -58,11 +58,6 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
accountID = getAccountIDsByLogins([mentionDisplayText])?.at(0) ?? -1;
navigationRoute = ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute(), mentionDisplayText);
mentionDisplayText = Str.removeSMSDomain(mentionDisplayText);
- } else if (!isEmpty(htmlAttribAccountID)) {
- // accountID not found in personal details and mention data not provided
- accountID = parseInt(htmlAttribAccountID, 10);
- mentionDisplayText = getDisplayNameOrDefault();
- navigationRoute = ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute());
} else {
// If neither an account ID or email is provided, don't render anything
return null;
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 65851e064167..84cd803da8db 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -13,7 +13,6 @@ import Approval from '@assets/images/simple-illustrations/simple-illustration__a
import Binoculars from '@assets/images/simple-illustrations/simple-illustration__binoculars.svg';
import BlueShield from '@assets/images/simple-illustrations/simple-illustration__blueshield.svg';
import Buildings from '@assets/images/simple-illustrations/simple-illustration__buildings.svg';
-import CalendarMonthly from '@assets/images/simple-illustrations/simple-illustration__calendar-monthly.svg';
import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg';
import Car from '@assets/images/simple-illustrations/simple-illustration__car.svg';
import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg';
@@ -28,7 +27,6 @@ import EmailAddress from '@assets/images/simple-illustrations/simple-illustratio
import EmptyShelves from '@assets/images/simple-illustrations/simple-illustration__empty-shelves.svg';
import Encryption from '@assets/images/simple-illustrations/simple-illustration__encryption.svg';
import EnvelopeReceipt from '@assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg';
-import FastMoney from '@assets/images/simple-illustrations/simple-illustration__fastmoney.svg';
import Filters from '@assets/images/simple-illustrations/simple-illustration__filters.svg';
import Flash from '@assets/images/simple-illustrations/simple-illustration__flash.svg';
import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg';
@@ -47,51 +45,49 @@ import ExpensifyApprovedLogo from '@assets/images/subscription-details__approved
import TurtleInShell from '@assets/images/turtle-in-shell.svg';
export {
- Abacus,
- Alert,
- Approval,
- Binoculars,
- BlueShield,
- Buildings,
- CalendarMonthly,
- Car,
- CarIce,
+ Encryption,
ChatBubbles,
- CheckmarkCircle,
- Clock,
- CommentBubbles,
Computer,
- ConciergeBot,
- ConciergeBubble,
- CreditCardEyes,
- CreditCardsNewGreen,
+ Clock,
EmailAddress,
EmptyCardState,
- EmptyShelves,
- EmptyStateTravel,
- Encryption,
EnvelopeReceipt,
- ExpensifyApprovedLogo,
ExpensifyCardImage,
- FastMoney,
- Filters,
- Flash,
- Gears,
- HeadSet,
- Hourglass,
- House,
+ Mailbox,
+ CreditCardsNewGreen,
LaptopWithSecondScreenAndHourglass,
LaptopWithSecondScreenSync,
LaptopWithSecondScreenX,
- Lightbulb,
- LockClosed,
- LockClosedOrange,
LockOpen,
Luggage,
MagnifyingGlassReceipt,
- Mailbox,
- Pencil,
- PendingTravel,
+ ConciergeBot,
+ ConciergeBubble,
+ HeadSet,
+ Hourglass,
+ CommentBubbles,
Puzzle,
+ LockClosed,
+ Gears,
+ Approval,
+ House,
+ Buildings,
+ Alert,
+ Abacus,
+ Binoculars,
+ Car,
+ Pencil,
+ CarIce,
+ Lightbulb,
+ ExpensifyApprovedLogo,
+ CheckmarkCircle,
+ CreditCardEyes,
+ LockClosedOrange,
+ Filters,
TurtleInShell,
+ Flash,
+ PendingTravel,
+ EmptyStateTravel,
+ EmptyShelves,
+ BlueShield,
};
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index e51dac50bbc5..02e26682bc7a 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -156,7 +156,6 @@ import MoneySearch from '@assets/images/money-search.svg';
import MoneyWaving from '@assets/images/money-waving.svg';
import Monitor from '@assets/images/monitor.svg';
import MultiTag from '@assets/images/multi-tag.svg';
-import Fingerprint from '@assets/images/multifactorAuthentication/fingerprint.svg';
import Mute from '@assets/images/mute.svg';
import NewWindow from '@assets/images/new-window.svg';
import NewWorkspace from '@assets/images/new-workspace.svg';
@@ -226,7 +225,6 @@ 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 UserShield from '@assets/images/user-shield.svg';
import User from '@assets/images/user.svg';
import Users from '@assets/images/users.svg';
import VideoSlash from '@assets/images/video-slash.svg';
@@ -319,7 +317,6 @@ const Expensicons = {
FlagLevelOne,
FlagLevelTwo,
FlagLevelThree,
- Fingerprint,
Fullscreen,
Folder,
Tag,
@@ -476,7 +473,6 @@ const Expensicons = {
ArrowCircleClockwise,
LuggageWithLines,
TreasureChestGreenWithSparkle,
- UserShield,
};
// 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 f86a9b9bd84a..ce7cf4c06406 100644
--- a/src/components/Icon/chunks/illustrations.chunk.ts
+++ b/src/components/Icon/chunks/illustrations.chunk.ts
@@ -40,10 +40,6 @@ import LaptopOnDeskWithCoffeeAndKey from '@assets/images/laptop-on-desk-with-cof
import LaptopWithSecondScreenAndHourglass from '@assets/images/laptop-with-second-screen-and-hourglass.svg';
import LaptopWithSecondScreenSync from '@assets/images/laptop-with-second-screen-sync.svg';
import LaptopWithSecondScreenX from '@assets/images/laptop-with-second-screen-x.svg';
-// Multifactor Authentication Illustrations
-import HumptyDumpty from '@assets/images/multifactorAuthentication/humpty-dumpty.svg';
-import OpenPadlock from '@assets/images/multifactorAuthentication/open-padlock.svg';
-import RunOutOfTime from '@assets/images/multifactorAuthentication/running-out-of-time.svg';
import PendingTravel from '@assets/images/pending-travel.svg';
// Product Illustrations
import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
@@ -95,7 +91,6 @@ import Binoculars from '@assets/images/simple-illustrations/simple-illustration_
import BlueShield from '@assets/images/simple-illustrations/simple-illustration__blueshield.svg';
import Building from '@assets/images/simple-illustrations/simple-illustration__building.svg';
import Buildings from '@assets/images/simple-illustrations/simple-illustration__buildings.svg';
-import CalendarMonthly from '@assets/images/simple-illustrations/simple-illustration__calendar-monthly.svg';
import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg';
import Car from '@assets/images/simple-illustrations/simple-illustration__car.svg';
import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg';
@@ -111,7 +106,6 @@ import EmailAddress from '@assets/images/simple-illustrations/simple-illustratio
import EmptyShelves from '@assets/images/simple-illustrations/simple-illustration__empty-shelves.svg';
import Encryption from '@assets/images/simple-illustrations/simple-illustration__encryption.svg';
import EnvelopeReceipt from '@assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg';
-import FastMoney from '@assets/images/simple-illustrations/simple-illustration__fastmoney.svg';
import Filters from '@assets/images/simple-illustrations/simple-illustration__filters.svg';
import Flash from '@assets/images/simple-illustrations/simple-illustration__flash.svg';
import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg';
@@ -311,7 +305,6 @@ const Illustrations = {
Approval,
Binoculars,
Buildings,
- CalendarMonthly,
Car,
ChatBubbles,
CheckmarkCircle,
@@ -323,7 +316,6 @@ const Illustrations = {
EmptyShelves,
Encryption,
EnvelopeReceipt,
- FastMoney,
Filters,
Flash,
Gears,
@@ -339,11 +331,6 @@ const Illustrations = {
Clock,
Members,
UserShield,
-
- // Multifactor Authentication Illustrations
- OpenPadlock,
- RunOutOfTime,
- HumptyDumpty,
};
/**
diff --git a/src/components/ImportColumn.tsx b/src/components/ImportColumn.tsx
index 0d97ce652cd0..3f6451882dd9 100644
--- a/src/components/ImportColumn.tsx
+++ b/src/components/ImportColumn.tsx
@@ -178,7 +178,7 @@ function ImportColumn({column, columnName, columnRoles, columnIndex, shouldShowD
// 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', columnName);
+ const columnHeader = containsHeader ? column.at(0) : translate('spreadsheet.column', {name: columnName});
return (
diff --git a/src/components/InteractiveStepSubHeader.tsx b/src/components/InteractiveStepSubHeader.tsx
index 387eb031a73f..15664c591764 100644
--- a/src/components/InteractiveStepSubHeader.tsx
+++ b/src/components/InteractiveStepSubHeader.tsx
@@ -100,8 +100,7 @@ function InteractiveStepSubHeader({stepNames, startStepIndex = 0, onStepSelected
disabled={isLockedStep || !onStepSelected}
onPress={moveToStep}
accessible
- accessibilityLabel={`${index + 1}`}
- aria-current={currentStep === index ? 'step' : undefined}
+ accessibilityLabel={stepName[index]}
role={CONST.ROLE.BUTTON}
>
{isCompletedStep ? (
diff --git a/src/components/InvertedFlatList/index.e2e.tsx b/src/components/InvertedFlatList/index.e2e.tsx
index caa08ea9b023..b0cb84050188 100644
--- a/src/components/InvertedFlatList/index.e2e.tsx
+++ b/src/components/InvertedFlatList/index.e2e.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import type {ScrollViewProps, ViewToken} from 'react-native';
import {DeviceEventEmitter, FlatList} from 'react-native';
import type {ReportAction} from '@src/types/onyx';
@@ -9,25 +9,33 @@ const AUTOSCROLL_TO_TOP_THRESHOLD = 128;
function InvertedFlatListE2E({ref, ...props}: InvertedFlatListProps) {
const {shouldEnableAutoScrollToTopThreshold, ...rest} = props;
- const handleViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => {
- DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems);
- };
+ const handleViewableItemsChanged = useMemo(
+ () =>
+ ({viewableItems}: {viewableItems: ViewToken[]}) => {
+ DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems);
+ },
+ [],
+ );
+
+ const maintainVisibleContentPosition = useMemo(() => {
+ const config: ScrollViewProps['maintainVisibleContentPosition'] = {
+ // This needs to be 1 to avoid using loading views as anchors.
+ minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0,
+ };
- const config: ScrollViewProps['maintainVisibleContentPosition'] = {
- // This needs to be 1 to avoid using loading views as anchors.
- minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0,
- };
+ if (shouldEnableAutoScrollToTopThreshold) {
+ config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD;
+ }
- if (shouldEnableAutoScrollToTopThreshold) {
- config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD;
- }
+ return config;
+ }, [shouldEnableAutoScrollToTopThreshold, rest.data?.length]);
return (
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
ref={ref}
- maintainVisibleContentPosition={config}
+ maintainVisibleContentPosition={maintainVisibleContentPosition}
inverted
onViewableItemsChanged={handleViewableItemsChanged}
/>
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index fdb139f30b43..be3641aa3508 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -7,8 +7,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useParentReportAction from '@hooks/useParentReportAction';
-import usePermissions from '@hooks/usePermissions';
-import useReportTransactions from '@hooks/useReportTransactions';
import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts';
import {completePaymentOnboarding, savePreferredPaymentMethod} from '@libs/actions/IOU';
import {navigateToBankAccountRoute} from '@libs/actions/ReimbursementAccount';
@@ -72,9 +70,6 @@ function KYCWall({
const reportPreviewAction = useParentReportAction(iouReport);
const personalDetails = usePersonalDetails();
const employeeEmail = personalDetails?.[iouReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID]?.login ?? '';
- const reportTransactions = useReportTransactions(iouReport?.reportID);
- const {isBetaEnabled} = usePermissions();
- const isCustomReportNamesBetaEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_REPORT_NAMES);
const anchorRef = useRef(null);
const transferBalanceButtonRef = useRef(null);
@@ -140,7 +135,7 @@ function KYCWall({
if (iouReport && isIOUReport(iouReport)) {
const adminPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`];
if (adminPolicy) {
- const inviteResult = moveIOUReportToPolicyAndInviteSubmitter(iouReport?.reportID, adminPolicy, formatPhoneNumber, reportTransactions, isCustomReportNamesBetaEnabled);
+ const inviteResult = moveIOUReportToPolicyAndInviteSubmitter(iouReport?.reportID, adminPolicy, formatPhoneNumber);
if (inviteResult?.policyExpenseChatReportID) {
setNavigationActionToMicrotaskQueue(() => {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(inviteResult.policyExpenseChatReportID));
@@ -150,7 +145,7 @@ function KYCWall({
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(adminPolicy.id));
});
} else {
- const moveResult = moveIOUReportToPolicy(iouReport?.reportID, adminPolicy, true, reportTransactions, isCustomReportNamesBetaEnabled);
+ const moveResult = moveIOUReportToPolicy(iouReport?.reportID, adminPolicy, true);
savePreferredPaymentMethod(iouReport.policyID, adminPolicy.id, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[adminPolicy.id]);
if (moveResult?.policyExpenseChatReportID && !moveResult.useTemporaryOptimisticExpenseChatReportID) {
@@ -215,8 +210,6 @@ function KYCWall({
reportPreviewAction,
currentUserEmail,
employeeEmail,
- reportTransactions,
- isCustomReportNamesBetaEnabled,
],
);
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index 308bb1599c61..5495efe57950 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -13,7 +13,6 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import TextBlock from '@components/TextBlock';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -29,19 +28,20 @@ import Log from '@libs/Log';
import {getMovedReportID} from '@libs/ModifiedExpenseMessage';
import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils';
import {
- getLastVisibleAction,
getOneTransactionThreadReportID,
getOriginalMessage,
- getReportActionActorAccountID,
+ getSortedReportActions,
+ getSortedReportActionsForDisplay,
isInviteOrRemovedAction,
isMoneyRequestAction,
+ shouldReportActionBeVisibleAsLastAction,
} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetails, Report} from '@src/types/onyx';
+import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import OptionRowLHNData from './OptionRowLHNData';
import OptionRowRendererComponent from './OptionRowRendererComponent';
@@ -71,8 +71,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY, {canBeMissing: true});
- const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true});
- const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const {policyForMovingExpensesID} = usePolicyForMovingExpenses();
const theme = useTheme();
@@ -192,6 +190,9 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
}
const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`];
+ const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item);
+ const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined;
+
const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`];
const transactionID = isMoneyRequestAction(itemParentReportAction)
? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID)
@@ -203,57 +204,54 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const isReportArchived = !!itemReportNameValuePairs?.private_isArchived;
const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived);
- const lastAction = getLastVisibleAction(
- reportID,
- canUserPerformWrite,
- {},
- itemReportActions ? {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: itemReportActions} : undefined,
- visibleReportActionsData,
- );
+ const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite);
+ const lastReportAction = sortedReportActions.at(0);
- const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, visibleReportActionsData, lastAction);
- const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined;
-
- const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID;
+ // Get the transaction for the last report action
+ const lastReportActionTransactionID = isMoneyRequestAction(lastReportAction)
+ ? (getOriginalMessage(lastReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID)
+ : CONST.DEFAULT_NUMBER_ID;
const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`];
- const lastActorAccountID = getReportActionActorAccountID(lastAction, undefined, item) ?? item.lastActorAccountID;
- let lastActorDetails: Partial | null = lastActorAccountID && personalDetails?.[lastActorAccountID] ? personalDetails[lastActorAccountID] : null;
-
- if (!lastActorDetails && lastAction) {
- const lastActorDisplayName = lastAction?.person?.[0]?.text;
+ // SidebarUtils.getOptionData in OptionRowLHNData does not get re-evaluated when the linked task report changes, so we have the lastMessageTextFromReport evaluation logic here
+ let lastActorDetails: Partial | null = item?.lastActorAccountID && personalDetails?.[item.lastActorAccountID] ? personalDetails[item.lastActorAccountID] : null;
+ if (!lastActorDetails && lastReportAction) {
+ const lastActorDisplayName = lastReportAction?.person?.[0]?.text;
lastActorDetails = lastActorDisplayName
? {
displayName: lastActorDisplayName,
- accountID: lastActorAccountID,
+ accountID: item?.lastActorAccountID,
}
: null;
}
-
- const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`];
- const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`];
+ const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.FROM)}`];
+ const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.TO)}`];
const itemReportMetadata = reportMetadataCollection?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`];
-
- // For archived reports, always call getLastMessageTextForReport to get the archive reason message
- // instead of using lastMessageText from the report
- const lastMessageTextFromReport =
- (isReportArchived ? undefined : item.lastMessageText) ??
- getLastMessageTextForReport({
- translate,
- report: item,
- lastActorDetails,
- movedFromReport,
- movedToReport,
- policy: itemPolicy,
- isReportArchived,
- policyForMovingExpensesID,
- reportMetadata: itemReportMetadata,
- visibleReportActionsDataParam: visibleReportActionsData,
- lastAction,
- });
+ const lastMessageTextFromReport = getLastMessageTextForReport({
+ report: item,
+ lastActorDetails,
+ movedFromReport,
+ movedToReport,
+ policy: itemPolicy,
+ isReportArchived: !!itemReportNameValuePairs?.private_isArchived,
+ policyForMovingExpensesID,
+ reportMetadata: itemReportMetadata,
+ });
const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID;
+ let lastAction: ReportAction | undefined;
+ if (!itemReportActions || !item) {
+ lastAction = undefined;
+ } else {
+ const canUserPerformWriteAction = canUserPerformWriteActionUtil(item, isReportArchived);
+ const actionsArray = getSortedReportActions(Object.values(itemReportActions));
+ const reportActionsForDisplay = actionsArray.filter(
+ (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED,
+ );
+ lastAction = reportActionsForDisplay.at(-1);
+ }
+
let lastActionReport: OnyxEntry | undefined;
if (isInviteOrRemovedAction(lastAction)) {
const lastActionOriginalMessage = lastAction?.actionName ? getOriginalMessage(lastAction) : null;
@@ -296,7 +294,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
isReportArchived={isReportArchived}
lastAction={lastAction}
lastActionReport={lastActionReport}
- currentUserAccountID={currentUserAccountID}
/>
);
},
@@ -326,8 +323,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
isScreenFocused,
localeCompare,
translate,
- visibleReportActionsData,
- currentUserAccountID,
],
);
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index b20daa3bc918..362146554059 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -41,7 +41,6 @@ function OptionRowLHNData({
isReportArchived = false,
lastAction,
lastActionReport,
- currentUserAccountID,
...propsToForward
}: OptionRowLHNDataProps) {
const reportID = propsToForward.reportID;
@@ -51,7 +50,6 @@ function OptionRowLHNData({
const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`, {canBeMissing: true});
const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`, {canBeMissing: true});
- const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true});
// Check the report errors equality to avoid re-rendering when there are no changes
const prevReportErrors = usePrevious(reportAttributes?.reportErrors);
const areReportErrorsEqual = useMemo(() => deepEqual(prevReportErrors, reportAttributes?.reportErrors), [prevReportErrors, reportAttributes?.reportErrors]);
@@ -78,8 +76,6 @@ function OptionRowLHNData({
lastActionReport,
movedFromReport,
movedToReport,
- currentUserAccountID,
- visibleReportActionsData,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
@@ -115,8 +111,6 @@ function OptionRowLHNData({
isReportArchived,
movedFromReport,
movedToReport,
- currentUserAccountID,
- visibleReportActionsData,
]);
return (
diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts
index 15d2b0c661bf..b40a3eadc9cf 100644
--- a/src/components/LHNOptionsList/types.ts
+++ b/src/components/LHNOptionsList/types.ts
@@ -141,9 +141,6 @@ type OptionRowLHNDataProps = {
lastAction: ReportAction | undefined;
lastActionReport: OnyxEntry | undefined;
-
- /** The current user's account ID */
- currentUserAccountID: number;
};
type OptionRowLHNProps = {
diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx
index c1238221e472..b9e1410809cf 100644
--- a/src/components/LottieAnimations/index.tsx
+++ b/src/components/LottieAnimations/index.tsx
@@ -94,11 +94,6 @@ const DotLottieAnimations = {
w: 375,
h: 240,
},
- Fingerprint: {
- file: require('@assets/animations/Fingerprint.lottie'),
- w: 204,
- h: 204,
- },
} satisfies Record;
export default DotLottieAnimations;
diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx
index 2b8e42af119c..6741825dd2d2 100644
--- a/src/components/MagicCodeInput.tsx
+++ b/src/components/MagicCodeInput.tsx
@@ -4,7 +4,6 @@ import type {FocusEvent, TextInput as RNTextInput, TextInputKeyPressEvent} from
import {StyleSheet, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import Animated, {useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming} from 'react-native-reanimated';
-import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -101,9 +100,6 @@ type MagicCodeInputProps = {
/** TestID for test */
testID?: string;
- /** Accessibility label for the input */
- accessibilityLabel?: string;
-
/** Reference to the outer element */
ref?: ForwardedRef;
};
@@ -154,12 +150,10 @@ function MagicCodeInput({
autoComplete,
hasError = false,
testID = '',
- accessibilityLabel,
ref,
}: MagicCodeInputProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const {translate} = useLocalize();
const inputRef = useRef(null);
const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE);
const [focusedIndex, setFocusedIndex] = useState(autoFocus ? 0 : undefined);
@@ -492,7 +486,7 @@ function MagicCodeInput({
}}
selectionColor="transparent"
inputStyle={[styles.inputTransparent]}
- accessibilityLabel={`${accessibilityLabel ?? translate('common.magicCode')}, ${maxLength} ${translate('common.digits')}`}
+ role={CONST.ROLE.PRESENTATION}
style={[styles.inputTransparent]}
textInputContainerStyles={[styles.borderTransparent, styles.bgTransparent]}
testID={testID}
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 23d5d73eb3d2..6d6d3a807f98 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,7 +1,7 @@
import type {ImageContentFit} from 'expo-image';
import type {ReactElement, ReactNode, Ref} from 'react';
import React, {useContext, useMemo, useRef} from 'react';
-import type {GestureResponderEvent, Role, StyleProp, TextStyle, ViewStyle} from 'react-native';
+import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -36,6 +36,7 @@ import type {DisplayNameWithTooltip} from './DisplayNames/types';
import FormHelpMessage from './FormHelpMessage';
import Hoverable from './Hoverable';
import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
import {MenuItemGroupContext} from './MenuItemGroup';
import PlaidCardFeedIcon from './PlaidCardFeedIcon';
import type {PressableRef} from './Pressable/GenericPressable/types';
@@ -211,9 +212,6 @@ type MenuItemBaseProps = ForwardedFSClassProps &
/** Text to display for the item */
title?: string;
- /** Accessibility label for the menu item */
- accessibilityLabel?: string;
-
/** Component to display as the title */
titleComponent?: ReactElement;
@@ -403,23 +401,11 @@ type MenuItemBaseProps = ForwardedFSClassProps &
/** Whether the screen containing the item is focused */
isFocused?: boolean;
- /** Additional styles for the root wrapper View */
- rootWrapperStyle?: StyleProp;
-
- /** The accessibility role to use for this menu item */
- role?: Role;
-
/** Whether to show the badge in a separate row */
shouldShowBadgeInSeparateRow?: boolean;
/** Whether to show the badge below the title */
shouldShowBadgeBelow?: boolean;
-
- /** Whether item should be accessible */
- shouldBeAccessible?: boolean;
-
- /** Whether item should be focusable with keyboard */
- tabIndex?: 0 | -1;
};
type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
@@ -487,7 +473,6 @@ function MenuItem({
focused = false,
disabled = false,
title,
- accessibilityLabel,
titleComponent,
titleContainerStyle,
subtitle,
@@ -553,12 +538,8 @@ function MenuItem({
ref,
isFocused,
sentryLabel,
- rootWrapperStyle,
- role = CONST.ROLE.MENUITEM,
- shouldBeAccessible = true,
- tabIndex = 0,
}: MenuItemProps) {
- const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'FallbackAvatar', 'DotIndicator', 'Checkmark']);
+ const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'FallbackAvatar']);
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -571,7 +552,6 @@ function MenuItem({
const isCompact = viewMode === CONST.OPTION_MODE.COMPACT;
const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedbackDeleted) : false;
const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
- const defaultAccessibilityLabel = (shouldShowDescriptionOnTop ? [description, title] : [title, description]).filter(Boolean).join(', ');
const combinedTitleTextStyle = StyleUtils.combineStyles(
[
@@ -709,10 +689,7 @@ function MenuItem({
const isIDPassed = !!iconReportID || !!iconAccountID || iconAccountID === CONST.DEFAULT_NUMBER_ID;
return (
-
+
{!!label && !isLabelHoverable && (
{label}
@@ -758,10 +735,9 @@ function MenuItem({
disabledStyle={shouldUseDefaultCursorWhenDisabled && [styles.cursorDefault]}
disabled={disabled || isExecuting}
ref={mergeRefs(ref, popoverAnchor)}
- role={role}
- accessibilityLabel={accessibilityLabel ?? defaultAccessibilityLabel}
- accessible={shouldBeAccessible}
- tabIndex={tabIndex}
+ role={CONST.ROLE.MENUITEM}
+ accessibilityLabel={title ? title.toString() : ''}
+ accessible
onFocus={onFocus}
sentryLabel={sentryLabel}
>
@@ -1003,7 +979,7 @@ function MenuItem({
{!!brickRoadIndicator && (
@@ -1035,7 +1011,7 @@ function MenuItem({
{shouldShowSelectedState && }
{shouldShowSelectedItemCheck && isSelected && (
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 5ecd5a62ce26..11a7328031ae 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -24,7 +24,6 @@ import usePermissions from '@hooks/usePermissions';
import usePolicy from '@hooks/usePolicy';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals';
import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
import useStrictPolicyRules from '@hooks/useStrictPolicyRules';
@@ -46,7 +45,7 @@ import Log from '@libs/Log';
import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types';
+import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList, SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
import {
buildOptimisticNextStepForDEWOfflineSubmission,
buildOptimisticNextStepForDynamicExternalWorkflowError,
@@ -157,6 +156,7 @@ import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import {useSearchContext} from './Search/SearchContext';
import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton';
import Text from './Text';
+import {WideRHPContext} from './WideRHPContextProvider';
type MoneyReportHeaderProps = {
/** The report currently being looked at */
@@ -197,8 +197,7 @@ function MoneyReportHeader({
const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth;
const route = useRoute<
| PlatformStackRouteProp
- | PlatformStackRouteProp
- | PlatformStackRouteProp
+ | PlatformStackRouteProp
| PlatformStackRouteProp
>();
const {login: currentUserLogin, accountID, email} = useCurrentUserPersonalDetails();
@@ -399,14 +398,13 @@ function MoneyReportHeader({
typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.HOLD | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT | typeof CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_BULK
> | null>(null);
- const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash, currentSearchResults} = useSearchContext();
+ const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash} = useSearchContext();
const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.similarSearchHash, true);
+ const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON?.hash}`, {canBeMissing: true});
+ const {wideRHPRouteKeys} = useContext(WideRHPContext);
const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true});
-
- const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP();
-
- const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;
+ const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || (wideRHPRouteKeys.length > 0 && !isSmallScreenWidth);
const showExportProgressModal = useCallback(() => {
return showConfirmModal({
@@ -511,9 +509,9 @@ function MoneyReportHeader({
const shouldShowLoadingBar = useLoadingBarVisibility();
const kycWallRef = useContext(KYCWallContext);
- const isReportInRHP = route.name !== SCREENS.REPORT;
+ const isReportInRHP = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
- const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT;
+ const isReportInSearch = route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT;
const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport);
const isChatReportDM = isDM(chatReport);
@@ -998,11 +996,6 @@ function MoneyReportHeader({
success
text={translate('iou.unhold')}
onPress={() => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
const parentReportAction = getReportAction(moneyRequestReport?.parentReportID, moneyRequestReport?.parentReportActionID);
const IOUActions = getAllExpensesToHoldIfApplicable(moneyRequestReport, reportActions, transactions, policy);
@@ -1280,11 +1273,6 @@ function MoneyReportHeader({
throw new Error('Parent action does not exist');
}
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
changeMoneyRequestHoldStatus(requestParentReportAction);
},
},
@@ -1413,8 +1401,8 @@ function MoneyReportHeader({
}
const result = await showConfirmModal({
- title: translate('iou.deleteReport'),
- prompt: translate('iou.deleteReportConfirmation'),
+ title: translate('iou.deleteReport', {count: transactionCount}),
+ prompt: translate('iou.deleteReportConfirmation', {count: transactionCount}),
confirmText: translate('common.delete'),
cancelText: translate('common.cancel'),
danger: true,
@@ -1428,7 +1416,7 @@ function MoneyReportHeader({
Navigation.goBack(backToRoute);
// eslint-disable-next-line @typescript-eslint/no-deprecated
InteractionManager.runAfterInteractions(() => {
- deleteAppReport(moneyRequestReport?.reportID, email ?? '', reportTransactions, allTransactionViolations, bankAccountList);
+ deleteAppReport(moneyRequestReport?.reportID, email ?? '', reportTransactions, violations, bankAccountList);
});
});
},
@@ -1561,9 +1549,7 @@ function MoneyReportHeader({
const backToRoute = route.params?.backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined);
Navigation.goBack(backToRoute);
}
- // It has been handled like the rest of the delete cases. It will be refactored along with other cases.
- // eslint-disable-next-line @typescript-eslint/no-deprecated
- InteractionManager.runAfterInteractions(() => handleDeleteTransactions());
+ handleDeleteTransactions();
});
}, [showConfirmModal, translate, selectedTransactionIDs.length, transactions, handleDeleteTransactions, route.params?.backTo, chatReport?.reportID]);
@@ -1745,7 +1731,7 @@ function MoneyReportHeader({
{isReportInSearch && (
)}
@@ -1762,7 +1748,6 @@ function MoneyReportHeader({
paymentType={paymentType}
chatReport={chatReport}
moneyRequestReport={moneyRequestReport}
- hasNonHeldExpenses={!hasOnlyHeldExpenses}
startAnimation={() => {
if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
startApprovedAnimation();
diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx
index 0c30556f7512..f69f60be5dc0 100644
--- a/src/components/MoneyRequestAmountInput.tsx
+++ b/src/components/MoneyRequestAmountInput.tsx
@@ -167,7 +167,7 @@ function MoneyRequestAmountInput({
disabled,
...props
}: MoneyRequestAmountInputProps) {
- const {preferredLocale, translate} = useLocalize();
+ const {preferredLocale} = useLocalize();
const textInput = useRef(null);
const numberFormRef = useRef(null);
const decimals = getCurrencyDecimals(currency);
@@ -258,7 +258,6 @@ function MoneyRequestAmountInput({
toggleNegative={toggleNegative}
clearNegative={clearNegative}
onFocus={props.onFocus}
- accessibilityLabel={`${translate('iou.amount')} (${currency})`}
/>
);
}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index d5e1ebbf7ff6..4da78c1ab818 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -3,7 +3,6 @@ import {deepEqual} from 'fast-equals';
import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import useCurrencyList from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
@@ -31,7 +30,6 @@ import {
setMoneyRequestTaxRate,
setSplitShares,
} from '@libs/actions/IOU';
-import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
import {isCategoryDescriptionRequired} from '@libs/CategoryUtils';
import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -44,7 +42,6 @@ import {getTagLists, isTaxTrackingEnabled} from '@libs/PolicyUtils';
import {isSelectedManagerMcTest} from '@libs/ReportUtils';
import type {OptionData} from '@libs/ReportUtils';
import {hasEnabledTags, hasMatchingTag} from '@libs/TagsOptionsListUtils';
-import {isValidTimeExpenseAmount} from '@libs/TimeTrackingUtils';
import {
areRequiredFieldsEmpty,
calculateTaxAmount,
@@ -119,12 +116,6 @@ type MoneyRequestConfirmationListProps = {
/** IOU isBillable */
iouIsBillable?: boolean;
- /** Time expense's hour count */
- iouTimeCount?: number;
-
- /** Time expense's hourly rate */
- iouTimeRate?: number;
-
/** Callback to toggle the billable state */
onToggleBillable?: (isOn: boolean) => void;
@@ -262,27 +253,22 @@ function MoneyRequestConfirmationList({
onToggleReimbursable,
showRemoveExpenseConfirmModal,
isTimeRequest = false,
- iouTimeCount,
- iouTimeRate,
}: MoneyRequestConfirmationListProps) {
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true});
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true});
const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true});
- const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {canBeMissing: true});
const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, {canBeMissing: true});
const [defaultMileageRateDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, {
selector: mileageRateSelector,
canBeMissing: true,
});
- const {policyForMovingExpenses} = usePolicyForMovingExpenses();
- const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action);
const [defaultMileageRateReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
selector: mileageRateSelector,
canBeMissing: true,
});
const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`, {canBeMissing: true});
const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {canBeMissing: true});
- const {getCurrencySymbol} = useCurrencyList();
+ const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: false});
const {isBetaEnabled} = usePermissions();
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
@@ -303,6 +289,7 @@ function MoneyRequestConfirmationList({
isTestDriveReceipt || isManagerMcTestReceipt,
);
+ const {policyForMovingExpenses} = usePolicyForMovingExpenses();
const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK;
const policy = isTrackExpense ? policyForMovingExpenses : (policyReal ?? policyDraft);
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
@@ -320,6 +307,7 @@ function MoneyRequestConfirmationList({
const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE;
const isScanRequest = useMemo(() => isScanRequestUtil(transaction), [transaction]);
const isCreateExpenseFlow = !!transaction?.isFromGlobalCreate && !isPerDiemRequest;
+ const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action);
const transactionID = transaction?.transactionID;
const customUnitRateID = getRateID(transaction);
@@ -359,22 +347,20 @@ function MoneyRequestConfirmationList({
? !policy || shouldSelectPolicy || hasEnabledOptions(Object.values(policyCategories ?? {}))
: (isPolicyExpenseChat || isTypeInvoice) && (!!iouCategory || hasEnabledOptions(Object.values(policyCategories ?? {})));
- const shouldShowMerchant = (shouldShowSmartScanFields || isTypeSend) && !isDistanceRequest && !isPerDiemRequest && !isTimeRequest;
+ const shouldShowMerchant = (shouldShowSmartScanFields || isTypeSend) && !isDistanceRequest && !isPerDiemRequest;
const policyTagLists = useMemo(() => getTagLists(policyTags), [policyTags]);
- const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat || isTrackExpense, policy, isDistanceRequest, isPerDiemRequest, isTimeRequest);
+ 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) ?? (isMovingTransactionFromTrackExpense ? (getDefaultTaxCode(policyForMovingExpenses, transaction) ?? '') : '');
-
+ const defaultTaxCode = getDefaultTaxCode(policy, transaction) ?? '';
useEffect(() => {
- if (!transactionID || isReadOnly || !shouldShowTax || isMovingTransactionFromTrackExpense) {
+ if (!transactionID || isReadOnly || !shouldShowTax) {
return;
}
setMoneyRequestTaxRate(transactionID, defaultTaxCode);
- // trigger this useEffect also when policyID changes - the defaultTaxCode may stay the same
- }, [defaultTaxCode, isMovingTransactionFromTrackExpense, isReadOnly, transactionID, policyID, shouldShowTax]);
+ }, [defaultTaxCode, transactionID, isReadOnly, shouldShowTax]);
const distance = getDistanceInMeters(transaction, unit);
const prevDistance = usePrevious(distance);
@@ -425,8 +411,8 @@ function MoneyRequestConfirmationList({
return false;
}
- return (!!hasSmartScanFailed && hasMissingSmartscanFields(transaction, transactionReport)) || (didConfirmSplit && areRequiredFieldsEmpty(transaction, transactionReport));
- }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit, transactionReport]);
+ return (!!hasSmartScanFailed && hasMissingSmartscanFields(transaction)) || (didConfirmSplit && areRequiredFieldsEmpty(transaction));
+ }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = useMemo(() => !iouMerchant || isMerchantMissing(transaction), [transaction, iouMerchant]);
const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant;
@@ -447,20 +433,11 @@ function MoneyRequestConfirmationList({
setFormError('iou.receiptScanningFailed');
return;
}
- // Reset the form error whenever the screen gains or loses focus
- // but preserve violation-related errors since those represent real validation issues
- // that can only be fixed by changing the underlying data
- if (!formError.startsWith(CONST.VIOLATIONS_PREFIX)) {
- setFormError('');
- return;
- }
- // Clear missingAttendees violation if user fixed it by changing category or attendees
- const isMissingAttendeesViolation = getIsMissingAttendeesViolation(policyCategories, iouCategory, iouAttendees, currentUserPersonalDetails, policy?.isAttendeeTrackingEnabled);
- if (formError === 'violations.missingAttendees' && !isMissingAttendeesViolation) {
- setFormError('');
- }
+ // reset the form error whenever the screen gains or loses focus
+ setFormError('');
+
// 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, iouCategory, iouAttendees, policyCategories, currentUserPersonalDetails, policy?.isAttendeeTrackingEnabled]);
+ }, [isFocused, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
useEffect(() => {
// We want this effect to run only when the transaction is moving from Self DM to a expense chat
@@ -534,22 +511,15 @@ function MoneyRequestConfirmationList({
// Calculate and set tax amount in transaction draft
const taxableAmount = isDistanceRequest ? DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance) : (transaction?.amount ?? 0);
- // First we'll try to get the tax value from the chosen policy and if not found, we'll try to get it from the policy for moving expenses (only if the transaction is moving from track expense)
- const taxPercentage =
- getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ??
- (isMovingTransactionFromTrackExpense ? getTaxValue(policyForMovingExpenses, transaction, transaction?.taxCode ?? defaultTaxCode) : '');
- const taxAmount =
- isMovingTransactionFromTrackExpense && transaction?.taxAmount
- ? Math.abs(transaction?.taxAmount ?? 0)
- : calculateTaxAmount(taxPercentage, taxableAmount, transaction?.currency ?? CONST.CURRENCY.USD);
-
+ const taxPercentage = getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? '';
+ const taxAmount = calculateTaxAmount(taxPercentage, taxableAmount, transaction?.currency ?? CONST.CURRENCY.USD);
const taxAmountInSmallestCurrencyUnits = convertToBackendAmount(Number.parseFloat(taxAmount.toString()));
useEffect(() => {
- if (!transactionID || isReadOnly || !shouldShowTax || isMovingTransactionFromTrackExpense) {
+ if (!transactionID || isReadOnly || !shouldShowTax) {
return;
}
setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits);
- }, [transactionID, taxAmountInSmallestCurrencyUnits, isReadOnly, shouldShowTax, isMovingTransactionFromTrackExpense]);
+ }, [transactionID, taxAmountInSmallestCurrencyUnits, isReadOnly, shouldShowTax]);
// If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again
if (isEditingSplitBill && didConfirm) {
@@ -582,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 [
{
@@ -693,7 +660,7 @@ function MoneyRequestConfirmationList({
});
}
- const currencySymbol = getCurrencySymbol(iouCurrencyCode ?? '') ?? iouCurrencyCode;
+ const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode;
const formattedTotalAmount = convertToDisplayStringWithoutCurrency(iouAmount, iouCurrencyCode);
return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({
@@ -731,6 +698,7 @@ function MoneyRequestConfirmationList({
isTypeSplit,
payeePersonalDetails,
shouldShowReadOnlySplits,
+ currencyList,
iouCurrencyCode,
iouAmount,
selectedParticipants,
@@ -747,7 +715,6 @@ function MoneyRequestConfirmationList({
transaction?.comment?.splits,
transaction?.splitShares,
onSplitShareChange,
- getCurrencySymbol,
]);
const isSplitModified = useMemo(() => {
@@ -851,7 +818,7 @@ function MoneyRequestConfirmationList({
*/
setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null});
- const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit, getCurrencySymbol);
+ const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit);
setMoneyRequestMerchant(transactionID, distanceMerchant, true);
}, [
isDistanceRequestWithPendingRoute,
@@ -869,7 +836,6 @@ function MoneyRequestConfirmationList({
action,
isReadOnly,
isMovingTransactionFromTrackExpense,
- getCurrencySymbol,
]);
// Auto select the category if there is only one enabled category and it is required
@@ -878,7 +844,7 @@ function MoneyRequestConfirmationList({
if (!transactionID || iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
return;
}
- setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy, isMovingTransactionFromTrackExpense);
+ 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-hooks/exhaustive-deps
}, [shouldShowCategories, policyCategories, isCategoryRequired, policy?.id]);
@@ -962,20 +928,6 @@ function MoneyRequestConfirmationList({
return;
}
- // Since invoices are not expense reports that need attendee tracking, this validation should not apply to invoices
- const isMissingAttendeesViolation =
- iouType !== CONST.IOU.TYPE.INVOICE &&
- getIsMissingAttendeesViolation(policyCategories, iouCategory, iouAttendees, currentUserPersonalDetails, policy?.isAttendeeTrackingEnabled);
- if (isMissingAttendeesViolation) {
- setFormError('violations.missingAttendees');
- return;
- }
-
- if (shouldShowTax && !!transaction.taxCode && !Object.keys(policy?.taxRates?.taxes ?? {}).some((key) => key === transaction.taxCode)) {
- setFormError('violations.taxOutOfPolicy');
- return;
- }
-
if (isPerDiemRequest && (transaction.comment?.customUnit?.subRates ?? []).length === 0) {
setFormError('iou.error.invalidSubrateLength');
return;
@@ -989,11 +941,6 @@ function MoneyRequestConfirmationList({
return;
}
- if (isTimeRequest && !isValidTimeExpenseAmount(iouAmount, iouCurrencyCode)) {
- setFormError('iou.timeTracking.amountTooLargeError');
- return;
- }
-
if (isPerDiemRequest) {
if (!isValidPerDiemExpenseAmount(transaction.comment?.customUnit ?? {}, iouCurrencyCode)) {
setFormError('iou.error.invalidQuantity');
@@ -1001,17 +948,12 @@ function MoneyRequestConfirmationList({
}
}
- if (isEditingSplitBill && areRequiredFieldsEmpty(transaction, transactionReport)) {
+ if (isEditingSplitBill && areRequiredFieldsEmpty(transaction)) {
setDidConfirmSplit(true);
setFormError('iou.error.genericSmartscanFailureMessage');
return;
}
- if (isEditingSplitBill && iouAmount === 0) {
- setFormError('iou.error.invalidAmount');
- return;
- }
-
if (formError) {
return;
}
@@ -1043,7 +985,6 @@ function MoneyRequestConfirmationList({
isMerchantRequired,
isMerchantEmpty,
shouldDisplayFieldError,
- shouldShowTax,
transaction,
policyTags,
isPerDiemRequest,
@@ -1060,10 +1001,6 @@ function MoneyRequestConfirmationList({
showDelegateNoAccessModal,
iouCategory,
policyCategories,
- transactionReport,
- iouAttendees,
- currentUserPersonalDetails,
- isTimeRequest,
],
);
@@ -1087,10 +1024,6 @@ function MoneyRequestConfirmationList({
if (isTypeSplit && !shouldShowReadOnlySplits) {
return debouncedFormError && translate(debouncedFormError);
}
- // Don't show error at the bottom of the form for missing attendees
- if (formError === 'violations.missingAttendees') {
- return;
- }
return formError && translate(formError);
}, [routeError, isTypeSplit, shouldShowReadOnlySplits, debouncedFormError, formError, translate]);
@@ -1213,14 +1146,11 @@ function MoneyRequestConfirmationList({
iouIsBillable={iouIsBillable}
iouMerchant={iouMerchant}
iouType={iouType}
- iouTimeCount={iouTimeCount}
- iouTimeRate={iouTimeRate}
isCategoryRequired={isCategoryRequired}
isDistanceRequest={isDistanceRequest}
isManualDistanceRequest={isManualDistanceRequest}
isOdometerDistanceRequest={isOdometerDistanceRequest}
isPerDiemRequest={isPerDiemRequest}
- isTimeRequest={isTimeRequest}
isMerchantEmpty={isMerchantEmpty}
isMerchantRequired={isMerchantRequired}
isPolicyExpenseChat={isPolicyExpenseChat}
@@ -1308,7 +1238,5 @@ export default memo(
prevProps.reportActionID === nextProps.reportActionID &&
prevProps.action === nextProps.action &&
prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt &&
- prevProps.isTimeRequest === nextProps.isTimeRequest &&
- prevProps.iouTimeCount === nextProps.iouTimeCount &&
- prevProps.iouTimeRate === nextProps.iouTimeRate,
+ prevProps.isTimeRequest === nextProps.isTimeRequest,
);
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index e22ecd75bd80..bce89a190df1 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -6,27 +6,24 @@ import React, {memo, useMemo} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
-import useCurrencyList from '@hooks/useCurrencyList';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
-import useOutstandingReports from '@hooks/useOutstandingReports';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import {getDecodedCategoryName} from '@libs/CategoryUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
-import {isMovingTransactionFromTrackExpense, shouldShowReceiptEmptyState} from '@libs/IOUUtils';
+import {shouldShowReceiptEmptyState} from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getDestinationForDisplay, getSubratesFields, getSubratesForDisplay, getTimeDifferenceIntervals, getTimeForDisplay} from '@libs/PerDiemRequestUtils';
import {canSendInvoice, getPerDiemCustomUnit} from '@libs/PolicyUtils';
import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {computeReportName} from '@libs/ReportNameUtils';
-import {generateReportID, getDefaultWorkspaceAvatar, getOutstandingReportsForUser, isMoneyRequestReport, isReportOutstanding} from '@libs/ReportUtils';
+import {generateReportID, getDefaultWorkspaceAvatar, getOutstandingReportsForUser, isArchivedReport, isMoneyRequestReport, isReportOutstanding} from '@libs/ReportUtils';
import {getTagVisibility, hasEnabledTags} from '@libs/TagsOptionsListUtils';
import {
getTagForDisplay,
@@ -44,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';
@@ -105,12 +103,6 @@ type MoneyRequestConfirmationListFooterProps = {
/** The merchant of the IOU */
iouMerchant: string | undefined;
- /** The hours count of the time request */
- iouTimeCount: number | undefined;
-
- /** The hourly rate of the time request */
- iouTimeRate: number | undefined;
-
/** The type of the IOU */
iouType: Exclude;
@@ -129,9 +121,6 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if it is a per diem request */
isPerDiemRequest: boolean;
- /** Flag indicating if it is a time request */
- isTimeRequest: boolean;
-
/** Flag indicating if the merchant is empty */
isMerchantEmpty: boolean;
@@ -243,14 +232,11 @@ function MoneyRequestConfirmationListFooter({
iouIsBillable,
iouMerchant,
iouType,
- iouTimeCount,
- iouTimeRate,
isCategoryRequired,
isDistanceRequest,
isManualDistanceRequest,
isOdometerDistanceRequest = false,
isPerDiemRequest,
- isTimeRequest,
isMerchantEmpty,
isMerchantRequired,
isPolicyExpenseChat,
@@ -286,7 +272,6 @@ function MoneyRequestConfirmationListFooter({
const icons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'CalendarSolid']);
const styles = useThemeStyles();
const {translate, toLocaleDigit, localeCompare} = useLocalize();
- const {getCurrencySymbol} = useCurrencyList();
const {isOffline} = useNetwork();
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
@@ -295,15 +280,23 @@ function MoneyRequestConfirmationListFooter({
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {
canBeMissing: true,
});
- const {policyForMovingExpensesID, policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses();
+ const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses();
const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector, canBeMissing: true});
- const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const isUnreported = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const isCreatingTrackExpense = action === CONST.IOU.ACTION.CREATE && iouType === CONST.IOU.TYPE.TRACK;
const decodedCategoryName = useMemo(() => getDecodedCategoryName(iouCategory), [iouCategory]);
+ const allOutstandingReports = useMemo(() => {
+ const outstandingReports = Object.values(outstandingReportsByPolicyID ?? {}).flatMap((outstandingReportsPolicy) => Object.values(outstandingReportsPolicy ?? {}));
+
+ return outstandingReports.filter((report) => {
+ const reportNameValuePair = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`];
+ return !isArchivedReport(reportNameValuePair) && isReportOutstanding(report, report?.policyID, reportNameValuePairs, false);
+ });
+ }, [outstandingReportsByPolicyID, reportNameValuePairs]);
+
const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK;
const shouldShowTags = useMemo(
() => (isPolicyExpenseChat || isUnreported || isCreatingTrackExpense) && hasEnabledTags(policyTagLists),
@@ -358,20 +351,20 @@ function MoneyRequestConfirmationListFooter({
}, [allReports, shouldUseTransactionReport, transaction?.reportID, outstandingReportID]);
const reportName = useMemo(() => {
- const name = computeReportName(selectedReport, allReports, allPolicies, undefined, undefined, undefined, undefined, currentUserAccountID);
+ const name = computeReportName(selectedReport, allReports, allPolicies);
if (!name) {
return isUnreported ? translate('common.none') : translate('iou.newReport');
}
return name;
- }, [isUnreported, selectedReport, allReports, allPolicies, translate, currentUserAccountID]);
+ }, [isUnreported, selectedReport, allReports, allPolicies, translate]);
+
+ const shouldReportBeEditableFromFAB = isUnreported ? allOutstandingReports.length >= 1 : allOutstandingReports.length > 1;
- const outstandingReports = useOutstandingReports(undefined, isFromGlobalCreate && !isPerDiemRequest ? undefined : policyID, ownerAccountID, false);
// When creating an expense in an individual report, the report field becomes read-only
// since the destination is already determined and there's no need to show a selectable list.
- const shouldReportBeEditable = (isUnreported ? outstandingReports.length >= 1 : outstandingReports.length > 1) && !isMoneyRequestReport(reportID, allReports);
-
- const isMovingCurrentTransactionFromTrackExpense = isMovingTransactionFromTrackExpense(action);
- const taxRates = policy?.taxRates ?? (isMovingCurrentTransactionFromTrackExpense ? policyForMovingExpenses?.taxRates : null);
+ const shouldReportBeEditable =
+ (isFromGlobalCreate && !isPerDiemRequest ? shouldReportBeEditableFromFAB : availableOutstandingReports.length > 1) && !isMoneyRequestReport(reportID, allReports);
+ const taxRates = policy?.taxRates ?? null;
// In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item
const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest;
// Determines whether the tax fields can be modified.
@@ -386,21 +379,12 @@ function MoneyRequestConfirmationListFooter({
const taxAmount = getTaxAmount(transaction, false);
const formattedTaxAmount = convertToDisplayString(taxAmount, iouCurrencyCode);
// Get the tax rate title based on the policy and transaction
- let taxRateTitle;
- if (getTaxName(policy, transaction)) {
- taxRateTitle = getTaxName(policy, transaction);
- } else if (isMovingCurrentTransactionFromTrackExpense) {
- taxRateTitle = getTaxName(policyForMovingExpenses, transaction);
- } else {
- taxRateTitle = '';
- }
+ const taxRateTitle = getTaxName(policy, transaction);
// Determine if the merchant error should be displayed
const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty;
const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate';
const shouldDisplayTagError = formError === 'violations.tagOutOfPolicy';
- const shouldDisplayTaxRateError = formError === 'violations.taxOutOfPolicy';
const shouldDisplayCategoryError = formError === 'violations.categoryOutOfPolicy';
- const shouldDisplayAttendeesError = formError === 'violations.missingAttendees';
const showReceiptEmptyState = shouldShowReceiptEmptyState(iouType, action, policy, isPerDiemRequest);
// The per diem custom unit
@@ -451,12 +435,12 @@ function MoneyRequestConfirmationListFooter({
item: (
{
- if (isDistanceRequest || isTimeRequest || !transactionID) {
+ if (isDistanceRequest || !transactionID) {
return;
}
@@ -526,7 +510,7 @@ function MoneyRequestConfirmationListFooter({
}
if (isOdometerDistanceRequest) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(action, iouType, transactionID, reportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()) as Route);
return;
}
@@ -543,7 +527,7 @@ function MoneyRequestConfirmationListFooter({
{
- if (!transactionID) {
- return;
- }
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_HOURS.getRoute(action, iouType, transactionID, reportID, reportActionID));
- }}
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- ),
- shouldShow: isTimeRequest,
- },
- {
- item: (
- {
- if (!transactionID) {
- return;
- }
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME_RATE.getRoute(action, iouType, transactionID, reportID, reportActionID));
- }}
- disabled={didConfirm}
- interactive={!isReadOnly}
- />
- ),
- shouldShow: isTimeRequest,
- },
{
item: (
),
shouldShow: shouldShowTax,
@@ -797,7 +737,7 @@ function MoneyRequestConfirmationListFooter({
item: (
item?.displayName ?? item?.login).join(', ')}
description={`${translate('iou.attendees')} ${
iouAttendees?.length && iouAttendees.length > 1 && formattedAmountPerAttendee ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
@@ -811,10 +751,8 @@ function MoneyRequestConfirmationListFooter({
Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
- interactive={!isReadOnly}
+ interactive
shouldRenderAsHTML
- brickRoadIndicator={shouldDisplayAttendeesError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
- errorText={shouldDisplayAttendeesError ? translate(formError) : ''}
/>
),
shouldShow: shouldShowAttendees,
@@ -1168,8 +1106,5 @@ export default memo(
prevProps.shouldShowTax === nextProps.shouldShowTax &&
prevProps.transaction === nextProps.transaction &&
prevProps.transactionID === nextProps.transactionID &&
- prevProps.unit === nextProps.unit &&
- prevProps.isTimeRequest === nextProps.isTimeRequest &&
- prevProps.iouTimeCount === nextProps.iouTimeCount &&
- prevProps.iouTimeRate === nextProps.iouTimeRate,
+ prevProps.unit === nextProps.unit,
);
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index 4d76735f1acb..50808f30e565 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -99,7 +99,7 @@ type MoneyRequestHeaderProps = {
function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPress}: MoneyRequestHeaderProps) {
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {shouldUseNarrowLayout, isSmallScreenWidth, isInNarrowPaneModal} = useResponsiveLayout();
+ const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const route = useRoute<
PlatformStackRouteProp | PlatformStackRouteProp
>();
@@ -117,7 +117,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transaction?.reportID)}`, {canBeMissing: true});
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(transactionReport?.policyID)}`, {canBeMissing: true});
const [allPolicyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: false});
- const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
const transactionViolations = useTransactionViolations(transaction?.transactionID);
const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES, {canBeMissing: true});
const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transaction?.transactionID ? [transaction.transactionID] : []);
@@ -264,11 +263,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
success
text={translate('iou.unhold')}
onPress={() => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
changeMoneyRequestHoldStatus(parentReportAction);
}}
/>
@@ -331,8 +325,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
if (!transaction || !parentReportAction || !parentReport) {
return [];
}
- return getSecondaryTransactionThreadActions(currentUserLogin ?? '', accountID, parentReport, transaction, parentReportAction, originalTransaction, policy, report);
- }, [parentReport, transaction, parentReportAction, currentUserLogin, policy, report, originalTransaction, accountID]);
+ return getSecondaryTransactionThreadActions(currentUserLogin ?? '', parentReport, transaction, parentReportAction, originalTransaction, policy, report);
+ }, [parentReport, transaction, parentReportAction, currentUserLogin, policy, report, originalTransaction]);
const dismissModalAndUpdateUseHold = () => {
setIsHoldEducationalModalVisible(false);
@@ -393,11 +387,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
throw new Error('Parent action does not exist');
}
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
changeMoneyRequestHoldStatus(parentReportAction);
},
},
@@ -576,17 +565,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
isSingleTransactionView: true,
isChatReportArchived: isParentReportArchived,
isChatIOUReportArchived,
- allTransactionViolationsParam: allTransactionViolations,
});
} else {
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true);
removeTransaction(transaction.transactionID);
}
- if (isInNarrowPaneModal) {
- Navigation.navigateBackToLastSuperWideRHPScreen();
- return;
- }
-
onBackButtonPress();
}}
onCancel={() => setIsDeleteModalVisible(false)}
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
index 144bf62433dc..7de13fef2bfb 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx
@@ -2,6 +2,7 @@
import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList';
import {useIsFocused, useRoute} from '@react-navigation/native';
import {isUserValidatedSelector} from '@selectors/Account';
+import {accountIDSelector} from '@selectors/Session';
import {tierNameSelector} from '@selectors/UserWallet';
import isEmpty from 'lodash/isEmpty';
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
@@ -19,7 +20,6 @@ import {PressableWithFeedback} from '@components/Pressable';
import ScrollView from '@components/ScrollView';
import {useSearchContext} from '@components/Search/SearchContext';
import Text from '@components/Text';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@hooks/useFlatListScrollKey';
import useLoadReportActions from '@hooks/useLoadReportActions';
import useLocalize from '@hooks/useLocalize';
@@ -30,7 +30,7 @@ import useParentReportAction from '@hooks/useParentReportAction';
import usePrevious from '@hooks/usePrevious';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useReportScrollManager from '@hooks/useReportScrollManager';
-import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -46,11 +46,11 @@ import {
getMostRecentIOURequestActionID,
getOneTransactionThreadReportID,
hasNextActionMadeBySameActor,
- isActionableWhisperRequiringWritePermission,
isConsecutiveChronosAutomaticTimerAction,
isCurrentActionUnread,
isDeletedParentAction,
isIOUActionMatchingTransactionList,
+ shouldReportActionBeVisible,
wasMessageReceivedWhileOffline,
} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction, chatIncludesChronosWithID, getOriginalReportID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils';
@@ -64,7 +64,7 @@ import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListI
import shouldDisplayNewMarkerOnReportAction from '@pages/home/report/shouldDisplayNewMarkerOnReportAction';
import useReportUnreadMessageScrollTracking from '@pages/home/report/useReportUnreadMessageScrollTracking';
import variables from '@styles/variables';
-import {openReport, readNewestAction, subscribeToNewActionEvent} from '@userActions/Report';
+import {getCurrentUserAccountID, openReport, readNewestAction, subscribeToNewActionEvent} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -167,13 +167,12 @@ function MoneyRequestReportActionsList({
const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], false, reportTransactionIDs);
const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]);
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
- const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
+ const [currentUserAccountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: accountIDSelector});
const isReportArchived = useReportIsArchived(reportID);
const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived);
- const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true});
- const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`, {canBeMissing: true});
@@ -226,34 +225,17 @@ function MoneyRequestReportActionsList({
const visibleReportActions = useMemo(() => {
const filteredActions = reportActions.filter((reportAction) => {
const isActionVisibleOnMoneyReport = isActionVisibleOnMoneyRequestReport(reportAction, shouldShowHarvestCreatedAction);
- if (!isActionVisibleOnMoneyReport) {
- return false;
- }
-
- const passesOfflineCheck = isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors;
- if (!passesOfflineCheck) {
- return false;
- }
- const actionReportID = reportAction.reportID ?? reportID;
- const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID] ?? true;
- if (!isStaticallyVisible) {
- return false;
- }
-
- if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) {
- return false;
- }
-
- if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) {
- return false;
- }
-
- return true;
+ return (
+ isActionVisibleOnMoneyReport &&
+ (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) &&
+ shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) &&
+ isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)
+ );
});
return filteredActions.toReversed();
- }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction, visibleReportActionsData, reportID]);
+ }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction]);
const reportActionSize = useRef(visibleReportActions.length);
const lastAction = visibleReportActions.at(-1);
@@ -367,7 +349,7 @@ function MoneyRequestReportActionsList({
const hasNewMessagesInView = scrollingVerticalBottomOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD;
const hasUnreadReportAction = reportActions.some(
- (reportAction) => newMessageTimeReference && newMessageTimeReference < reportAction.created && reportAction.actorAccountID !== currentUserAccountID,
+ (reportAction) => newMessageTimeReference && newMessageTimeReference < reportAction.created && reportAction.actorAccountID !== getCurrentUserAccountID(),
);
if (!hasNewMessagesInView || !hasUnreadReportAction) {
@@ -417,7 +399,7 @@ function MoneyRequestReportActionsList({
message: reportAction,
nextMessage: nextAction,
isEarliestReceivedOfflineMessage,
- currentUserAccountID,
+ accountID: currentUserAccountID,
prevSortedVisibleReportActionsObjects: prevVisibleActionsMap,
unreadMarkerTime,
scrollingVerticalOffset: scrollingVerticalBottomOffset.current,
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx
index 5d561637bcfe..cd9ba8646637 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx
@@ -4,7 +4,7 @@ import Checkbox from '@components/Checkbox';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils';
@@ -64,7 +64,7 @@ function MoneyRequestReportGroupHeader({
}: MoneyRequestReportGroupHeaderProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const cleanedGroupName = isGroupedByTag && group.groupName ? getCommaSeparatedTagNameWithSanitizedColons(group.groupName) : group.groupName;
const displayName = cleanedGroupName || translate(isGroupedByTag ? 'reportLayout.noTag' : 'reportLayout.uncategorized');
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
index 9a9d656b1c38..c2a0d096c1f6 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx
@@ -56,6 +56,7 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo
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/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx
index 349db531a581..badcc7950045 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx
@@ -8,7 +8,6 @@ import TransactionItemRow from '@components/TransactionItemRow';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
@@ -82,8 +81,7 @@ function MoneyRequestReportTransactionItem({
const {translate} = useLocalize();
const styles = useThemeStyles();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout();
- const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
+ const {isSmallScreenWidth, isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
const theme = useTheme();
const isPendingDelete = isTransactionPendingDelete(transaction);
const pendingAction = getTransactionPendingAction(transaction);
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index 4a9598349e7c..5f8d6ac0eaa9 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -21,7 +21,6 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useOnyx from '@hooks/useOnyx';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
@@ -169,8 +168,7 @@ function MoneyRequestReportTransactionList({
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'CheckSquare', 'ReceiptPlus']);
const {translate, localeCompare} = useLocalize();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout();
- const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
+ const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout();
const {markReportIDAsExpense} = useContext(WideRHPContext);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedTransactionID, setSelectedTransactionID] = useState('');
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
index f96ce35ec9f3..1d821678276d 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
@@ -1,14 +1,10 @@
import {PortalHost} from '@gorhom/portal';
import React, {useCallback, useEffect, useMemo} from 'react';
-// We use Animated for all functionality related to wide RHP to make it easier
-// to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated.
-// eslint-disable-next-line no-restricted-imports
-import {Animated, InteractionManager, ScrollView, View} from 'react-native';
+import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import MoneyReportHeader from '@components/MoneyReportHeader';
import MoneyRequestHeader from '@components/MoneyRequestHeader';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import MoneyRequestReceiptView from '@components/ReportActionItem/MoneyRequestReceiptView';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView';
import useNetwork from '@hooks/useNetwork';
@@ -16,7 +12,6 @@ import useNewTransactions from '@hooks/useNewTransactions';
import useOnyx from '@hooks/useOnyx';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
import useParentReportAction from '@hooks/useParentReportAction';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
import {removeFailedReport} from '@libs/actions/Report';
@@ -61,16 +56,6 @@ function goBackFromSearchMoneyRequest() {
const rootState = navigationRef.getRootState();
const lastRoute = rootState.routes.at(-1);
- if (!lastRoute) {
- Log.hmmm('[goBackFromSearchMoneyRequest()] No last route found in root state.');
- return;
- }
-
- if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
- Navigation.goBack();
- return;
- }
-
if (lastRoute?.name !== NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) {
Log.hmmm('[goBackFromSearchMoneyRequest()] goBackFromSearchMoneyRequest was called from a different navigator than SearchFullscreenNavigator.');
return;
@@ -99,10 +84,6 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
const styles = useThemeStyles();
const {isOffline} = useNetwork();
- // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {isSmallScreenWidth} = useResponsiveLayout();
-
- const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false});
const reportID = report?.reportID;
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {canBeMissing: true});
@@ -143,14 +124,11 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
// Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render
// We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate
- const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata, isOffline);
+ const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata);
const isEmptyTransactionReport = visibleTransactions && visibleTransactions.length === 0 && transactionThreadReportID === undefined;
const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? []);
- const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
- const shouldShowWideRHPReceipt = visibleTransactions.length === 1 && !isSmallScreenWidth && !!transactionThreadReport;
-
const reportHeaderView = useMemo(
() =>
isTransactionThreadView ? (
@@ -245,62 +223,48 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
contentContainerStyle={styles.flex1}
errorRowStyles={[styles.ph5, styles.mv2]}
>
-
- {shouldShowWideRHPReceipt && (
-
-
-
-
-
+
+ {shouldDisplayMoneyRequestActionsList ? (
+
+ ) : (
+
)}
-
- {shouldDisplayMoneyRequestActionsList ? (
-
+
- ) : (
-
- )}
- {shouldDisplayReportFooter ? (
- <>
-
-
- >
- ) : null}
-
+
+ >
+ ) : null}
diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
index 0eb61411e18e..35d413dbff79 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
@@ -6,13 +6,12 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {clearReportFieldKeyErrors} from '@libs/actions/Report';
-import {resolveReportFieldValue} from '@libs/Formula';
import Navigation from '@libs/Navigation/Navigation';
import {
+ getAvailableReportFields,
getFieldViolation,
getFieldViolationTranslation,
getReportFieldKey,
- getReportFieldMaps,
isInvoiceReport as isInvoiceReportUtils,
isPaidGroupPolicyExpenseReport as isPaidGroupPolicyExpenseReportUtils,
isReportFieldDisabled,
@@ -84,15 +83,13 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false,
const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report?.reportID}`, {canBeMissing: true});
const sortedPolicyReportFields = useMemo((): EnrichedPolicyReportField[] => {
- const {fieldValues, fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {});
- const fields = Object.values(fieldsByName);
-
+ const fields = getAvailableReportFields(report, Object.values(policy?.fieldList ?? {}));
return fields
.filter((field) => field.target === report?.type)
.filter((reportField) => !shouldHideSingleReportField(reportField))
.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight)
.map((field): EnrichedPolicyReportField => {
- const fieldValue = resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName);
+ 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);
diff --git a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
deleted file mode 100644
index b1dcab2c7dca..000000000000
--- a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import Text from '@components/Text';
-import TextLink from '@components/TextLink';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import goToSettings from '@libs/goToSettings';
-
-const baseTranslationPath = 'multifactorAuthentication.pleaseEnableInSystemSettings' as const;
-
-const translationPaths = {
- start: `${baseTranslationPath}.start`,
- link: `${baseTranslationPath}.link`,
- end: `${baseTranslationPath}.end`,
-} as const;
-
-function NoEligibleMethodsDescription() {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- const start = translate(translationPaths.start);
- const link = translate(translationPaths.link);
- const end = translate(translationPaths.end);
-
- return (
-
- {start}
- {link}
- {end}
-
- );
-}
-
-NoEligibleMethodsDescription.displayName = 'NoEligibleMethodsDescription';
-
-export default NoEligibleMethodsDescription;
diff --git a/src/components/MultifactorAuthentication/PromptContent.tsx b/src/components/MultifactorAuthentication/PromptContent.tsx
deleted file mode 100644
index 2d91ff96d1d4..000000000000
--- a/src/components/MultifactorAuthentication/PromptContent.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import BlockingView from '@components/BlockingViews/BlockingView';
-import type DotLottieAnimation from '@components/LottieAnimations/types';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import type {TranslationPaths} from '@src/languages/types';
-
-type MultifactorAuthenticationPromptContentProps = {
- animation: DotLottieAnimation;
- title: TranslationPaths;
- subtitle: TranslationPaths;
-};
-
-function MultifactorAuthenticationPromptContent({title, subtitle, animation}: MultifactorAuthenticationPromptContentProps) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- return (
-
-
-
- );
-}
-
-MultifactorAuthenticationPromptContent.displayName = 'MultifactorAuthenticationPromptContent';
-
-export default MultifactorAuthenticationPromptContent;
diff --git a/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
deleted file mode 100644
index 819b38507497..000000000000
--- a/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import ConfirmModal from '@components/ConfirmModal';
-import useLocalize from '@hooks/useLocalize';
-import type {TranslationPaths} from '@src/languages/types';
-
-type MultifactorAuthenticationTriggerCancelConfirmModalProps = {
- isVisible: boolean;
- onConfirm: () => void;
- onCancel: () => void;
-};
-
-// TODO: this config will be part of the scenario configuration, the current implementation is for testing purposes (https://github.com/Expensify/App/issues/79373)
-const mockedConfig = {
- title: 'common.areYouSure',
- description: 'multifactorAuthentication.biometricsTest.areYouSureToReject',
- confirmButtonText: 'multifactorAuthentication.biometricsTest.rejectAuthentication',
- cancelButtonText: 'common.cancel',
-} as const satisfies Record;
-
-function MultifactorAuthenticationTriggerCancelConfirmModal({isVisible, onConfirm, onCancel}: MultifactorAuthenticationTriggerCancelConfirmModalProps) {
- const {translate} = useLocalize();
-
- const title = translate(mockedConfig.title);
- const description = translate(mockedConfig.description);
- const confirmButtonText = translate(mockedConfig.confirmButtonText);
- const cancelButtonText = translate(mockedConfig.cancelButtonText);
-
- return (
-
- );
-}
-
-MultifactorAuthenticationTriggerCancelConfirmModal.displayName = 'MultifactorAuthenticationTriggerCancelConfirmModal';
-
-export default MultifactorAuthenticationTriggerCancelConfirmModal;
diff --git a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
deleted file mode 100644
index e129d4461e18..000000000000
--- a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, {useCallback, useImperativeHandle, useRef, useState} from 'react';
-import {View} from 'react-native';
-import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import Text from '@components/Text';
-import ValidateCodeCountdown from '@components/ValidateCodeCountdown';
-import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types';
-import useLocalize from '@hooks/useLocalize';
-import useNetwork from '@hooks/useNetwork';
-import useStyleUtils from '@hooks/useStyleUtils';
-import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
-
-type MultifactorAuthenticationValidateCodeResendButtonHandle = {
- resetCountdown: () => void;
-};
-
-type MultifactorAuthenticationValidateCodeResendButtonProps = {
- ref?: React.Ref;
- shouldDisableResendCode: boolean;
- hasError: boolean;
- resendButtonText: TranslationPaths;
- onResendValidationCode: () => void;
-};
-
-function MultifactorAuthenticationValidateCodeResendButton({
- ref,
- shouldDisableResendCode,
- hasError,
- resendButtonText,
- onResendValidationCode,
-}: MultifactorAuthenticationValidateCodeResendButtonProps) {
- const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
- const {translate} = useLocalize();
- const {isOffline} = useNetwork();
-
- const [isCountdownRunning, setIsCountdownRunning] = useState(true);
- const countdownRef = useRef(null);
-
- const handleCountdownFinish = useCallback(() => {
- setIsCountdownRunning(false);
- }, []);
-
- useImperativeHandle(ref, () => ({
- resetCountdown: () => {
- countdownRef.current?.resetCountdown();
- setIsCountdownRunning(true);
- },
- }));
-
- return (
-
- {isCountdownRunning && !isOffline ? (
-
-
-
- ) : (
-
-
- {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate(resendButtonText)}
-
-
- )}
-
- );
-}
-
-MultifactorAuthenticationValidateCodeResendButton.displayName = 'MultifactorAuthenticationValidateCodeResendButton';
-
-export default MultifactorAuthenticationValidateCodeResendButton;
-
-export type {MultifactorAuthenticationValidateCodeResendButtonHandle};
diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx
index 2af72a9b1107..19248e166673 100644
--- a/src/components/Navigation/DebugTabView.tsx
+++ b/src/components/Navigation/DebugTabView.tsx
@@ -7,9 +7,9 @@ import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
+import type {IndicatorStatus} from '@hooks/useIndicatorStatus';
import useIndicatorStatus from '@hooks/useIndicatorStatus';
import useLocalize from '@hooks/useLocalize';
-import type {IndicatorStatus} from '@hooks/useNavigationTabBarIndicatorChecks';
import useOnyx from '@hooks/useOnyx';
import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports';
import useStyleUtils from '@hooks/useStyleUtils';
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index bce9af6826ea..67fa08bd3848 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -1,11 +1,10 @@
import {findFocusedRoute, StackActions, useNavigationState} from '@react-navigation/native';
import reportsSelector from '@selectors/Attributes';
-import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
+import React, {memo, useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import FloatingCameraButton from '@components/FloatingCameraButton';
-import FloatingGPSButton from '@components/FloatingGPSButton';
import Icon from '@components/Icon';
// import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
@@ -52,10 +51,10 @@ import NAVIGATION_TABS from './NAVIGATION_TABS';
type NavigationTabBarProps = {
selectedTab: ValueOf;
isTopLevelBar?: boolean;
- shouldShowFloatingButtons?: boolean;
+ shouldShowFloatingCameraButton?: boolean;
};
-function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatingButtons = true}: NavigationTabBarProps) {
+function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatingCameraButton = true}: NavigationTabBarProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -173,7 +172,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
}
clearSelectedText();
interceptAnonymousUser(() => {
- const parentSpan = startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, {
+ startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, {
name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB,
op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB,
});
@@ -181,7 +180,6 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
startSpan(CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, {
name: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS,
op: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS,
- parentSpan,
});
const rootState = navigationRef.getRootState() as State;
@@ -240,10 +238,6 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain});
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]);
- const inboxAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.HOME}), [selectedTab]);
- const searchAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.SEARCH}), [selectedTab]);
- const workspacesAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.WORKSPACES}), [selectedTab]);
-
if (!shouldUseNarrowLayout) {
return (
<>
@@ -274,9 +268,8 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
[styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX}
>
@@ -316,9 +309,8 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
[styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
>
@@ -349,9 +341,8 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
[styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
>
@@ -417,9 +408,8 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
>
- {shouldShowFloatingButtons && (
- <>
-
-
- >
- )}
+ {shouldShowFloatingCameraButton && }
>
);
}
diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx
index 21570acb8c5c..dffea363935b 100644
--- a/src/components/Navigation/SearchSidebar.tsx
+++ b/src/components/Navigation/SearchSidebar.tsx
@@ -1,15 +1,19 @@
import type {ParamListBase} from '@react-navigation/native';
-import React, {useEffect} from 'react';
+import {searchResultsSelector} from '@selectors/Snapshot';
+import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
import {useSearchContext} from '@components/Search/SearchContext';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
import {buildSearchQueryJSON} from '@libs/SearchQueryUtils';
import SearchTypeMenu from '@pages/Search/SearchTypeMenu';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import NavigationTabBar from './NavigationTabBar';
import NAVIGATION_TABS from './NavigationTabBar/NAVIGATION_TABS';
@@ -27,22 +31,31 @@ function SearchSidebar({state}: SearchSidebarProps) {
const route = state.routes.at(-1);
const params = route?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT] | undefined;
- const {lastSearchType, setLastSearchType, currentSearchResults} = useSearchContext();
+ const {lastSearchType, setLastSearchType} = useSearchContext();
- const queryJSON = params?.q ? buildSearchQueryJSON(params.q, params.rawQuery) : undefined;
+ const queryJSON = useMemo(() => {
+ if (!params?.q) {
+ return undefined;
+ }
+
+ return buildSearchQueryJSON(params.q, params.rawQuery);
+ }, [params?.q, params?.rawQuery]);
- const searchType = currentSearchResults?.search?.type;
- const isSearchLoading = currentSearchResults?.search?.isLoading;
+ const currentSearchResultsKey = queryJSON?.hash ?? CONST.DEFAULT_NUMBER_ID;
+ const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchResultsKey}`, {
+ canBeMissing: true,
+ selector: searchResultsSelector,
+ });
useEffect(() => {
- if (!searchType) {
+ if (!currentSearchResults?.type) {
return;
}
- setLastSearchType(searchType);
- }, [lastSearchType, queryJSON, setLastSearchType, searchType]);
+ setLastSearchType(currentSearchResults.type);
+ }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.type]);
- const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!isSearchLoading;
+ const shouldShowLoadingState = route?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading;
if (shouldUseNarrowLayout) {
return null;
diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx
index 10b373a81660..b218de77b702 100644
--- a/src/components/NumberWithSymbolForm.tsx
+++ b/src/components/NumberWithSymbolForm.tsx
@@ -458,7 +458,6 @@ function NumberWithSymbolForm({
isNegative={isNegative}
toggleNegative={toggleNegative}
onFocus={props.onFocus}
- accessibilityLabel={props.accessibilityLabel}
/>
);
diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx
index e04d280a1628..301b09e06ceb 100644
--- a/src/components/ParentNavigationSubtitle.tsx
+++ b/src/components/ParentNavigationSubtitle.tsx
@@ -12,8 +12,8 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
import Navigation from '@libs/Navigation/Navigation';
-import type {RightModalNavigatorParamList} from '@libs/Navigation/types';
-import {getReportAction, isReportActionVisible} from '@libs/ReportActionsUtils';
+import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
+import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import type {ParentNavigationSummaryParams} from '@src/languages/params';
@@ -88,27 +88,11 @@ function ParentNavigationSubtitle({
const {translate} = useLocalize();
const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false});
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, {canBeMissing: false});
- const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true});
const isReportArchived = useReportIsArchived(report?.reportID);
const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived);
const isReportInRHP = currentRoute.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
+ const currentFullScreenRoute = useRootNavigationState((state) => state?.routes?.findLast((route) => isFullScreenName(route.name)));
const hasAccessToParentReport = currentReport?.hasParentAccess !== false;
- const {currentFullScreenRoute, currentFocusedNavigator} = useRootNavigationState((state) => {
- const fullScreenRoute = state?.routes?.findLast((route) => isFullScreenName(route.name));
-
- // We need to track which navigator is focused to handle parent report navigation correctly:
- // if we are in RHP, and parent report is opened in RHP, we want to go back to the parent report
- const focusedNavigator = state?.routes
- ? state.routes.findLast((route) => {
- return route.state?.routes && route.state.routes.length > 0;
- })
- : undefined;
-
- return {
- currentFullScreenRoute: fullScreenRoute,
- currentFocusedNavigator: focusedNavigator,
- };
- });
// We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case)
if (!reportName) {
@@ -117,24 +101,18 @@ function ParentNavigationSubtitle({
const onPress = () => {
const parentAction = getReportAction(parentReportID, parentReportActionID);
- const isVisibleAction = isReportActionVisible(parentAction, parentReportID, canUserPerformWriteAction, visibleReportActionsData);
-
- const focusedNavigatorState = currentFocusedNavigator?.state;
- const currentReportIndex = focusedNavigatorState?.index;
+ const isVisibleAction = shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? CONST.DEFAULT_NUMBER_ID, canUserPerformWriteAction);
if (openParentReportInCurrentTab && isReportInRHP) {
// If the report is displayed in RHP in Reports tab, we want to stay in the current tab after opening the parent report
if (currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR && isMoneyRequestReport(report)) {
- // Dismiss wide RHP and go back to already opened super wide RHP if the parent report is already opened there
- if (currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && currentReportIndex && currentReportIndex > 0) {
- const previousRoute = focusedNavigatorState.routes[currentReportIndex - 1];
-
- if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) {
- const moneyRequestReportID = (previousRoute?.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID;
- if (moneyRequestReportID === parentReportID) {
- Navigation.goBack();
- return;
- }
+ const lastRoute = currentFullScreenRoute?.state?.routes.at(-1);
+ if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) {
+ const moneyRequestReportID = (lastRoute?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID;
+ // If the parent report is already displayed underneath RHP, simply dismiss the modal
+ if (moneyRequestReportID === parentReportID) {
+ Navigation.dismissModal();
+ return;
}
}
@@ -142,11 +120,6 @@ function ParentNavigationSubtitle({
return;
}
- if (Navigation.getTopmostSuperWideRHPReportID() === parentReportID && currentFullScreenRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
- Navigation.dismissToSuperWideRHP();
- return;
- }
-
// If the parent report is already displayed underneath RHP, simply dismiss the modal
if (Navigation.getTopmostReportId() === parentReportID && currentFullScreenRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
Navigation.dismissModal();
@@ -156,27 +129,12 @@ function ParentNavigationSubtitle({
// When viewing a money request in the search navigator, open the parent report in a right-hand pane (RHP)
// to preserve the search context instead of navigating away.
- if (openParentReportInCurrentTab && currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
- const lastRoute = currentFocusedNavigator?.state?.routes.at(-1);
- if (lastRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) {
+ if (openParentReportInCurrentTab && currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) {
+ const lastRoute = currentFullScreenRoute?.state?.routes.at(-1);
+ if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) {
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID, reportActionID: parentReportActionID}));
return;
}
-
- // Specific case: when opening expense report from search report (chat RHP),
- // avoid stacking RHPs by going back to the search report if it's already there
- const previousRoute = currentFocusedNavigator?.state?.routes.at(-2);
-
- if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) {
- if (previousRoute.params && 'reportID' in previousRoute.params) {
- const reportIDFromParams = previousRoute.params.reportID;
-
- if (reportIDFromParams === parentReportID) {
- Navigation.goBack();
- return;
- }
- }
- }
}
if (isVisibleAction) {
diff --git a/src/components/PerDiemEReceipt.tsx b/src/components/PerDiemEReceipt.tsx
index a97c55092a69..6d9542b3f36f 100644
--- a/src/components/PerDiemEReceipt.tsx
+++ b/src/components/PerDiemEReceipt.tsx
@@ -1,12 +1,11 @@
import React from 'react';
import {View} from 'react-native';
-import useCurrencyList from '@hooks/useCurrencyList';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
-import {convertAmountToDisplayString, convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils';
+import {convertAmountToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencySymbol} from '@libs/CurrencyUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getTransactionDetails} from '@libs/ReportUtils';
import variables from '@styles/variables';
@@ -53,7 +52,6 @@ function PerDiemEReceipt({transactionID}: PerDiemEReceiptProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
- const {getCurrencySymbol} = useCurrencyList();
const icons = useMemoizedLazyExpensifyIcons(['ExpensifyWordmark']);
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`, {
canBeMissing: true,
diff --git a/src/components/PlaidCardFeedIcon.tsx b/src/components/PlaidCardFeedIcon.tsx
index c42cd1cbc3a2..a01681d67c4d 100644
--- a/src/components/PlaidCardFeedIcon.tsx
+++ b/src/components/PlaidCardFeedIcon.tsx
@@ -8,17 +8,15 @@ import variables from '@styles/variables';
import ActivityIndicator from './ActivityIndicator';
import Icon from './Icon';
import Image from './Image';
-import CardIconSkeleton from './Skeletons/CardIconSkeleton';
type PlaidCardFeedIconProps = {
plaidUrl: string;
style?: StyleProp;
isLarge?: boolean;
isSmall?: boolean;
- useSkeletonLoader?: boolean;
};
-function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall, useSkeletonLoader = false}: PlaidCardFeedIconProps) {
+function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall}: PlaidCardFeedIconProps) {
const [isBrokenImage, setIsBrokenImage] = useState(false);
const styles = useThemeStyles();
const illustrations = useThemeIllustrations();
@@ -58,18 +56,11 @@ function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall, useSkeletonLoader
onError={() => setIsBrokenImage(true)}
onLoadEnd={() => setLoading(false)}
/>
- {loading && useSkeletonLoader && (
-
- )}
- {loading && !useSkeletonLoader && (
+ {loading ? (
- )}
- {!loading && (
+ ) : (
(isHovered ? theme.iconHovered : theme.icon)}
style={hasBackButtonText ? styles.pv0 : undefined}
additionalIconStyles={[{width: variables.iconSizeSmall, height: variables.iconSizeSmall}, styles.opacitySemiTransparent, styles.mr1]}
- title={backButtonTitle}
- accessibilityLabel={`${translate('common.goBack')}, ${backButtonTitle}`}
+ title={hasBackButtonText ? previouslySelectedItem?.backButtonText : previouslySelectedItem?.text}
titleStyle={hasBackButtonText ? styles.createMenuHeaderText : undefined}
shouldShowBasicTitle={hasBackButtonText}
shouldCheckActionAllowedOnPress={false}
@@ -403,7 +399,6 @@ function BasePopoverMenu({
shouldRemoveHoverBackground={item.isSelected}
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
icon={icon}
- role={CONST.ROLE.BUTTON}
// Spread other props dynamically
{...menuItemProps}
hasSubMenuItems={!!subMenuItems?.length}
diff --git a/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
index 72fe50bd4561..513e3df69bb0 100644
--- a/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
+++ b/src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx
@@ -39,7 +39,6 @@ function GenericPressable({
ref,
dataSet,
forwardedFSClass,
- accessibilityState,
...rest
}: PressableProps) {
const styles = useThemeStyles();
@@ -174,10 +173,9 @@ function GenericPressable({
// accessibility props
accessibilityState={{
disabled: isDisabled,
- ...accessibilityState,
+ ...rest.accessibilityState,
}}
aria-disabled={isDisabled}
- aria-selected={accessibilityState?.selected}
aria-keyshortcuts={keyboardShortcut && `${keyboardShortcut.modifiers.join('')}+${keyboardShortcut.shortcutKey}`}
// ios-only form of inputs
onMagicTap={!isDisabled ? voidOnPressHandler : undefined}
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index f90caa084916..c34f11928ddc 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -1,4 +1,4 @@
-import React, {useContext, useMemo} from 'react';
+import React, {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
@@ -14,7 +14,6 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import DecisionModal from './DecisionModal';
-import {DelegateNoAccessContext} from './DelegateNoAccessModalProvider';
type ActionHandledType = DeepValueOf;
@@ -48,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,
@@ -64,7 +60,6 @@ function ProcessMoneyReportHoldMenu({
moneyRequestReport,
transactionCount,
startAnimation,
- hasNonHeldExpenses,
}: ProcessMoneyReportHoldMenuProps) {
const {translate} = useLocalize();
const isApprove = requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE;
@@ -82,13 +77,7 @@ function ProcessMoneyReportHoldMenu({
const currentUserDetails = useCurrentUserPersonalDetails();
const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, transactionViolations, currentUserDetails.accountID, currentUserDetails.email ?? '');
- const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
const onSubmit = (full: boolean) => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
if (isApprove) {
if (startAnimation) {
startAnimation();
@@ -113,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/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
index 153bfb03a5d7..cca34fd45ae5 100644
--- a/src/components/ReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -51,7 +51,7 @@ function ReferralProgramCTA({referralContentType, style, onDismiss}: ReferralPro
}}
style={[styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5, style]}
isNested
- accessibilityLabel={translate(`referralProgram.${referralContentType}.header`)}
+ accessibilityLabel="referral"
role={getButtonRole(true)}
>
diff --git a/src/components/ReportActionAvatars/useReportPreviewSenderID.ts b/src/components/ReportActionAvatars/useReportPreviewSenderID.ts
index 9bd649b8e8d0..bf8bd20c9258 100644
--- a/src/components/ReportActionAvatars/useReportPreviewSenderID.ts
+++ b/src/components/ReportActionAvatars/useReportPreviewSenderID.ts
@@ -1,6 +1,5 @@
import {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnyx from '@hooks/useOnyx';
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
@@ -8,6 +7,7 @@ import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils';
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import {getOriginalMessage, isMoneyRequestAction, isSentMoneyReportAction} from '@libs/ReportActionsUtils';
import {isDM, isIOUReport} from '@libs/ReportUtils';
+import {getCurrentUserAccountID} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx';
@@ -65,10 +65,9 @@ function useReportPreviewSenderID({iouReport, action, chatReport}: {action: Onyx
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(shouldFetchData ? iouReport?.policyID : undefined)}`, {
canBeMissing: true,
});
- const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
if (isOptimisticReportPreview) {
- return currentUserAccountID;
+ return getCurrentUserAccountID();
}
// 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 70e260c4ec45..2d87221b0d56 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -18,14 +18,13 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
-import {resolveReportFieldValue} from '@libs/Formula';
import Navigation from '@libs/Navigation/Navigation';
import {
+ getAvailableReportFields,
getFieldViolation,
getFieldViolationTranslation,
getMoneyRequestSpendBreakdown,
getReportFieldKey,
- getReportFieldMaps,
hasUpdatedTotal,
isClosedExpenseReportWithNoExpenses as isClosedExpenseReportWithNoExpensesReportUtils,
isInvoiceReport as isInvoiceReportUtils,
@@ -42,7 +41,7 @@ import CONST from '@src/CONST';
import {clearReportFieldKeyErrors} from '@src/libs/actions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Policy, Report} from '@src/types/onyx';
+import type {Policy, PolicyReportField, Report} from '@src/types/onyx';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
type MoneyReportViewProps = {
@@ -90,12 +89,9 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report?.reportID}`, {canBeMissing: true});
- const {sortedPolicyReportFields, fieldValues, fieldsByName} = useMemo(() => {
- const {fieldValues: values, fieldsByName: byName} = getReportFieldMaps(report, policy?.fieldList ?? {});
- const sorted = Object.values(byName)
- .filter((field) => field.target === report?.type)
- .sort(({orderWeight: a}, {orderWeight: b}) => a - b);
- return {sortedPolicyReportFields: sorted, fieldValues: values, fieldsByName: byName};
+ const sortedPolicyReportFields = useMemo((): PolicyReportField[] => {
+ const fields = getAvailableReportFields(report, Object.values(policy?.fieldList ?? {}));
+ return fields.filter((field) => field.target === report?.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
}, [policy?.fieldList, report]);
const enabledReportFields = sortedPolicyReportFields.filter(
@@ -142,7 +138,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
return null;
}
- const fieldValue = resolveReportFieldValue(reportField, report, policy, fieldValues, fieldsByName);
+ const fieldValue = reportField.value ?? reportField.defaultValue;
const isFieldDisabled = isReportFieldDisabledForUser(report, reportField, policy);
const fieldKey = getReportFieldKey(reportField.fieldID);
diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
index 07fc3fa7d0d3..f8d4347c71ac 100644
--- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
@@ -136,8 +136,7 @@ function MoneyRequestReceiptView({
const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable;
const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`;
- const canEditReceipt =
- isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy);
+ const canEditReceipt = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived);
const iouType = useMemo(() => {
if (isTrackExpense) {
@@ -160,8 +159,7 @@ function MoneyRequestReceiptView({
const transactionToCheck = updatedTransaction ?? transaction;
const doesTransactionHaveReceipt = !!transactionToCheck?.receipt && !isEmptyObject(transactionToCheck?.receipt);
- // Empty state for invoices should be displayed only in WideRHP
- const shouldShowReceiptEmptyState = (isDisplayedInWideRHP || !isInvoice) && !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt;
+ const shouldShowReceiptEmptyState = !isInvoice && !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt;
const [receiptImageViolations, receiptViolations] = useMemo(() => {
const imageViolations = [];
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
index d36c1dad5985..c2819921fd91 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
@@ -143,8 +143,7 @@ function MoneyRequestReportPreviewContent({
const StyleUtils = useStyleUtils();
const {translate, formatPhoneNumber} = useLocalize();
const {isOffline} = useNetwork();
- // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const currentUserDetails = useCurrentUserPersonalDetails();
const currentUserAccountID = currentUserDetails.accountID;
const currentUserEmail = currentUserDetails.email ?? '';
@@ -384,8 +383,6 @@ function MoneyRequestReportPreviewContent({
[action?.childStateNum, action?.childStatusNum, iouReport?.stateNum, iouReport?.statusNum, translate],
);
- const shouldShowReportStatus = !!reportStatus && !!expenseCount;
-
const reportStatusColorStyle = useMemo(
() => getReportStatusColorStyle(theme, iouReport?.stateNum ?? action?.childStateNum, iouReport?.statusNum ?? action?.childStatusNum),
[action?.childStateNum, action?.childStatusNum, iouReport?.stateNum, iouReport?.statusNum, theme],
@@ -520,14 +517,9 @@ function MoneyRequestReportPreviewContent({
name: 'MoneyRequestReportPreviewContent',
op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
});
- // Small screens navigate to full report view since super wide RHP
- // is not available on narrow layouts and would break the navigation logic.
- if (isSmallScreenWidth) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
- } else {
- Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()}));
- }
- }, [iouReportID, isSmallScreenWidth]);
+
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
+ }, [iouReportID]);
const isDEWPolicy = hasDynamicExternalWorkflow(policy);
const isDEWSubmitPending = hasPendingDEWSubmit(iouReportMetadata, isDEWPolicy);
@@ -645,7 +637,7 @@ function MoneyRequestReportPreviewContent({
) : null,
[CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW]: (
)}
{!!item.accountID && (
= {
/** Alternate text to display */
alternateText?: string | null;
- /** Accessibility label for screen readers */
- accessibilityLabel?: string;
-
/** Key used internally by React */
keyForList: K;
diff --git a/src/components/SelectionListWithSections/BaseListItem.tsx b/src/components/SelectionListWithSections/BaseListItem.tsx
index efd068359c06..ea490d7e6c3e 100644
--- a/src/components/SelectionListWithSections/BaseListItem.tsx
+++ b/src/components/SelectionListWithSections/BaseListItem.tsx
@@ -80,9 +80,6 @@ function BaseListItem({
return rightHandSideComponent;
};
- const defaultAccessibilityLabel = item.text === item.alternateText ? (item.text ?? '') : [item.text, item.alternateText].filter(Boolean).join(', ');
- const accessibilityLabel = item.accessibilityLabel ?? defaultAccessibilityLabel;
-
return (
onDismissError(item)}
@@ -110,7 +107,7 @@ function BaseListItem({
}}
disabled={isDisabled && !item.isSelected}
interactive={item.isInteractive}
- accessibilityLabel={accessibilityLabel}
+ accessibilityLabel={item.text ?? ''}
role={getButtonRole(true)}
isNested
hoverDimmingValue={1}
diff --git a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
index 24020a1e7988..cb49ef3ac3f5 100644
--- a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
+++ b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
@@ -104,7 +104,6 @@ function BaseSelectionListWithSections({
includeSafeAreaPaddingBottom = true,
shouldTextInputInterceptSwipe = false,
listHeaderContent,
- showSectionTitleWithListHeaderContent = false,
onEndReached,
onEndReachedThreshold,
windowSize = 5,
@@ -594,7 +593,7 @@ function BaseSelectionListWithSections({
return ;
}
- if (!section.title || isEmptyObject(section.data) || (!showSectionTitleWithListHeaderContent && listHeaderContent)) {
+ if (!section.title || isEmptyObject(section.data) || listHeaderContent) {
return null;
}
diff --git a/src/components/SelectionListWithSections/Search/CardListItemHeader.tsx b/src/components/SelectionListWithSections/Search/CardListItemHeader.tsx
index 00e68035562f..71406c829325 100644
--- a/src/components/SelectionListWithSections/Search/CardListItemHeader.tsx
+++ b/src/components/SelectionListWithSections/Search/CardListItemHeader.tsx
@@ -15,7 +15,7 @@ import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import CONST from '@src/CONST';
import type {CompanyCardFeed} from '@src/types/onyx/CardFeeds';
import ExpandCollapseArrowButton from './ExpandCollapseArrowButton';
-import TextCell from './TextCell';
+import ExpensesCell from './ExpensesCell';
import TotalCell from './TotalCell';
type CardListItemHeaderProps = {
@@ -98,7 +98,7 @@ function CardListItemHeader({
key={CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPENSES)}
>
-
+
),
[CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL]: (
diff --git a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx
index 5d7dbe67bb81..0a0859f50931 100644
--- a/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx
+++ b/src/components/SelectionListWithSections/Search/ExpenseReportListItem.tsx
@@ -41,31 +41,26 @@ function ExpenseReportListItem({
const theme = useTheme();
const {translate} = useLocalize();
const {isLargeScreenWidth} = useResponsiveLayout();
- const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchContext();
+ const {currentSearchHash, currentSearchKey} = useSearchContext();
const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true});
const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true});
+ const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true});
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector});
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
- const searchData = currentSearchResults?.data;
+ const snapshotData = snapshot?.data;
const snapshotReport = useMemo(() => {
- return (searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
- }, [searchData, reportItem.reportID]);
+ return (snapshotData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
+ }, [snapshotData, reportItem.reportID]);
const snapshotPolicy = useMemo(() => {
- return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
- }, [searchData, reportItem.policyID]);
-
- const reportActions = useMemo(() => {
- const actionsData = searchData?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`];
- return actionsData ? Object.values(actionsData) : [];
- }, [searchData, reportItem.reportID]);
+ return (snapshotData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
+ }, [snapshotData, reportItem.policyID]);
const isDisabledCheckbox = useMemo(() => {
- const isEmpty = reportItem.transactions.length === 0;
- return isEmpty ?? reportItem.isDisabled ?? reportItem.isDisabledCheckbox;
- }, [reportItem.isDisabled, reportItem.isDisabledCheckbox, reportItem.transactions.length]);
+ return reportItem.isDisabled ?? reportItem.isDisabledCheckbox;
+ }, [reportItem.isDisabled, reportItem.isDisabledCheckbox]);
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
@@ -187,10 +182,10 @@ function ExpenseReportListItem({
{(hovered) => (
{},
onButtonPress = () => {},
isActionLoading,
@@ -182,7 +182,10 @@ function ExpenseReportListItemRow({
),
[CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO]: (
-
+
),
[CONST.SEARCH.TABLE_COLUMNS.ACTION]: (
diff --git a/src/components/SelectionListWithSections/Search/ExpensesCell.tsx b/src/components/SelectionListWithSections/Search/ExpensesCell.tsx
new file mode 100644
index 000000000000..f030e1070124
--- /dev/null
+++ b/src/components/SelectionListWithSections/Search/ExpensesCell.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import TextWithTooltip from '@components/TextWithTooltip';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+type ExpensesDataCellProps = {
+ count: number;
+
+ shouldShowTooltip?: boolean;
+};
+
+function ExpensesDataCell({count, shouldShowTooltip = true}: ExpensesDataCellProps) {
+ const styles = useThemeStyles();
+
+ return (
+
+ );
+}
+
+ExpensesDataCell.displayName = 'ExpensesDataCell';
+
+export default ExpensesDataCell;
diff --git a/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx b/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx
index 559ec617a5c2..7e9a49c8f5e0 100644
--- a/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx
+++ b/src/components/SelectionListWithSections/Search/ExportedIconCell.tsx
@@ -3,21 +3,38 @@ import {View} from 'react-native';
import Avatar from '@components/Avatar';
import Icon from '@components/Icon';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getOriginalMessage, isExportedToIntegrationAction} from '@libs/ReportActionsUtils';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportAction} from '@src/types/onyx';
type ExportedIconCellProps = {
- reportActions?: ReportAction[];
+ reportID?: string;
+ hash?: number;
};
-function ExportedIconCell({reportActions}: ExportedIconCellProps) {
+function ExportedIconCell({reportID, hash}: ExportedIconCellProps) {
const theme = useTheme();
const styles = useThemeStyles();
- const actions = reportActions ?? [];
+ // We need to subscribe directly to the snapshot to get the report actions because this can be rendered in either a group
+ // list (which has a separate hash than the current top-level search query) or in the top-level search query.
+ // This selector is specific to this edge-case (and thus is not in the selectors folder) and should be used in other places where the snapshot needs to be accessed
+ // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector
+ const reportActions = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {
+ canBeMissing: true,
+ selector: (snapshot) => {
+ return Object.entries(snapshot?.data ?? {})
+ .filter(([key]) => key === `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`)
+ .map(([, value]) => Object.values(value ?? {}) as ReportAction[])
+ .flat();
+ },
+ });
+
+ const actions = Object.values(reportActions[0] ?? {});
const icons = useMemoizedLazyExpensifyIcons(['NetSuiteSquare', 'XeroSquare', 'IntacctSquare', 'QBOSquare', 'Table', 'ZenefitsSquare', 'BillComSquare', 'CertiniaSquare']);
let isExportedToCsv = false;
diff --git a/src/components/SelectionListWithSections/Search/MemberListItemHeader.tsx b/src/components/SelectionListWithSections/Search/MemberListItemHeader.tsx
index 8827f31ae807..005c7721a33b 100644
--- a/src/components/SelectionListWithSections/Search/MemberListItemHeader.tsx
+++ b/src/components/SelectionListWithSections/Search/MemberListItemHeader.tsx
@@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import CONST from '@src/CONST';
import ExpandCollapseArrowButton from './ExpandCollapseArrowButton';
-import TextCell from './TextCell';
+import ExpensesCell from './ExpensesCell';
import TotalCell from './TotalCell';
type MemberListItemHeaderProps = {
@@ -86,7 +86,7 @@ function MemberListItemHeader({
key={CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPENSES)}
>
-
+
),
[CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL]: (
diff --git a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx
index 9119f45c9a21..e013f656fe85 100644
--- a/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx
+++ b/src/components/SelectionListWithSections/Search/ReportListItemHeader.tsx
@@ -211,12 +211,13 @@ function ReportListItemHeader({
const StyleUtils = useStyleUtils();
const styles = useThemeStyles();
const theme = useTheme();
- const {currentSearchHash, currentSearchKey, currentSearchResults: snapshot} = useSearchContext();
+ const {currentSearchHash, currentSearchKey} = useSearchContext();
const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true});
const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true});
const thereIsFromAndTo = !!reportItem?.from && !!reportItem?.to;
const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from);
+ const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true});
const snapshotReport = useMemo(() => {
return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
}, [snapshot, reportItem.reportID]);
diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx
index 874021d576e2..58fadb2469f4 100644
--- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx
@@ -1,4 +1,4 @@
-import React, {useContext} from 'react';
+import React, {useCallback, useContext, useMemo} from 'react';
import {View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import Button from '@components/Button';
@@ -56,22 +56,33 @@ function TransactionGroupListExpanded({
const [isMobileSelectionModeEnabled] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE, {canBeMissing: true});
const [visibleColumns] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true, selector: columnsSelector});
- const transactionsSnapshotMetadata = transactionsSnapshot?.search;
+ const transactionsSnapshotMetadata = useMemo(() => {
+ return transactionsSnapshot?.search;
+ }, [transactionsSnapshot?.search]);
- const visibleTransactions = isExpenseReportType ? transactions.slice(0, transactionsVisibleLimit) : transactions;
+ const visibleTransactions = useMemo(() => {
+ if (isExpenseReportType) {
+ return transactions.slice(0, transactionsVisibleLimit);
+ }
+ return transactions;
+ }, [transactions, transactionsVisibleLimit, isExpenseReportType]);
- const isLastTransaction = (index: number) => {
- return index === visibleTransactions.length - 1;
- };
+ const isLastTransaction = useCallback(
+ (index: number) => {
+ return index === visibleTransactions.length - 1;
+ },
+ [visibleTransactions.length],
+ );
- let currentColumns = columns ?? [];
- if (!isExpenseReportType) {
+ const currentColumns = useMemo(() => {
+ if (isExpenseReportType) {
+ return columns ?? [];
+ }
if (!transactionsSnapshot?.data) {
- currentColumns = [];
- } else {
- currentColumns = getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type);
+ return [];
}
- }
+ return getColumnsToShow(accountID, transactionsSnapshot?.data, visibleColumns, false, transactionsSnapshot?.search.type);
+ }, [accountID, columns, isExpenseReportType, transactionsSnapshot?.data, transactionsSnapshot?.search.type, visibleColumns]);
// Currently only the transaction report groups have transactions where the empty view makes sense
const shouldDisplayShowMoreButton = isExpenseReportType ? transactions.length > transactionsVisibleLimit : !!transactionsSnapshotMetadata?.hasMoreResults && !isOffline;
@@ -80,12 +91,16 @@ function TransactionGroupListExpanded({
const shouldDisplayLoadingIndicator = !isExpenseReportType && !!transactionsSnapshotMetadata?.isLoading && shouldShowLoadingOnSearch;
const {isLargeScreenWidth} = useResponsiveLayout();
- const isAmountColumnWide = transactions.some((transaction) => transaction.isAmountColumnWide);
- const isTaxAmountColumnWide = transactions.some((transaction) => transaction.isTaxAmountColumnWide);
- const shouldShowYearForSomeTransaction = transactions.some((transaction) => transaction.shouldShowYear);
- const amountColumnSize = isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const taxAmountColumnSize = isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const dateColumnSize = shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
+ const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => {
+ const isAmountColumnWide = transactions.some((transaction) => transaction.isAmountColumnWide);
+ const isTaxAmountColumnWide = transactions.some((transaction) => transaction.isTaxAmountColumnWide);
+ const shouldShowYearForSomeTransaction = transactions.some((transaction) => transaction.shouldShowYear);
+ return {
+ amountColumnSize: isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ taxAmountColumnSize: isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ dateColumnSize: shouldShowYearForSomeTransaction ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ };
+ }, [transactions]);
const {markReportIDAsExpense} = useContext(WideRHPContext);
const openReportInRHP = (transactionItem: TransactionListItemType) => {
@@ -119,13 +134,13 @@ function TransactionGroupListExpanded({
});
};
- const onShowMoreButtonPress = () => {
+ const onShowMoreButtonPress = useCallback(() => {
if (isExpenseReportType) {
setTransactionsVisibleLimit((currentPageSize) => currentPageSize + CONST.TRANSACTION.RESULTS_PAGE_SIZE);
} else if (!isOffline && transactionsQueryJSON) {
searchTransactions(CONST.SEARCH.RESULTS_PAGE_SIZE);
}
- };
+ }, [isExpenseReportType, isOffline, transactionsQueryJSON, setTransactionsVisibleLimit, searchTransactions]);
if (shouldDisplayEmptyView) {
return (
@@ -173,10 +188,9 @@ function TransactionGroupListExpanded({
)}
{visibleTransactions.map((transaction, index) => {
const shouldShowBottomBorder = !isLastTransaction(index) && !isLargeScreenWidth;
- const exportedReportActions = Object.values(transactionsSnapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction?.reportID}`] ?? {});
-
const transactionRow = (
({
shouldShowBottomBorder={shouldShowBottomBorder}
onArrowRightPress={() => openReportInRHP(transaction)}
shouldShowArrowRightOnNarrowLayout
- reportActions={exportedReportActions}
/>
);
return (
diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
index bf5d45394495..c369b094c74b 100644
--- a/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionGroupListItem.tsx
@@ -145,10 +145,14 @@ function TransactionGroupListItem({
return transactions.filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
}, [transactions]);
- const isSelectAllChecked = selectedItemsLength === transactions.length && transactions.length > 0;
+ const isEmpty = transactions.length === 0;
+
+ const isEmptyReportSelected = isEmpty && item?.keyForList && selectedTransactions[item.keyForList]?.isSelected;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const isSelectAllChecked = isEmptyReportSelected || (selectedItemsLength === transactionsWithoutPendingDelete.length && transactionsWithoutPendingDelete.length > 0);
+
const isIndeterminate = selectedItemsLength > 0 && selectedItemsLength !== transactionsWithoutPendingDelete.length;
- const isEmpty = transactions.length === 0;
// Currently only the transaction report groups have transactions where the empty view makes sense
const shouldDisplayEmptyView = isEmpty && isExpenseReportType;
const isDisabledOrEmpty = isEmpty || isDisabled;
@@ -208,11 +212,8 @@ function TransactionGroupListItem({
}, [isExpenseReportType, transactions.length, onSelectRow, transactionPreviewData, item, handleToggle]);
const onLongPress = useCallback(() => {
- if (isEmpty) {
- return;
- }
onLongPressRow?.(item, isExpenseReportType ? undefined : transactions);
- }, [isEmpty, isExpenseReportType, item, onLongPressRow, transactions]);
+ }, [isExpenseReportType, item, onLongPressRow, transactions]);
const onExpandedRowLongPress = useCallback(
(transaction: TransactionListItemType) => {
@@ -288,7 +289,7 @@ function TransactionGroupListItem({
report={groupItem as TransactionReportGroupListItemType}
onSelectRow={(listItem) => onSelectRow(listItem, transactionPreviewData)}
onCheckboxPress={onCheckboxPress}
- isDisabled={isDisabledOrEmpty}
+ isDisabled={isDisabled}
isFocused={isFocused}
canSelectMultiple={canSelectMultiple}
isSelectAllChecked={isSelectAllChecked}
@@ -323,6 +324,9 @@ function TransactionGroupListItem({
onExpandIconPress,
isFocused,
searchType,
+ groupBy,
+ isDisabled,
+ onDEWModalOpen,
onSelectRow,
transactionPreviewData,
],
diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx
index 21331dd7d5cf..761b2fd5747a 100644
--- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx
+++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx
@@ -1,4 +1,4 @@
-import React, {useContext, useRef} from 'react';
+import React, {useCallback, useContext, useMemo, useRef} from 'react';
import type {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
// Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot
@@ -21,7 +21,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type {TransactionPreviewData} from '@libs/actions/Search';
import {handleActionButtonPress as handleActionButtonPressUtil} from '@libs/actions/Search';
-import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {isViolationDismissed, mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils';
import variables from '@styles/variables';
@@ -30,6 +29,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import {isActionLoadingSelector} from '@src/selectors/ReportMetaData';
import type {Policy, Report, ReportAction, ReportActions} from '@src/types/onyx';
import type {TransactionViolation} from '@src/types/onyx/TransactionViolation';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import UserInfoAndActionButtonRow from './UserInfoAndActionButtonRow';
function TransactionListItem({
@@ -55,50 +55,38 @@ function TransactionListItem({
const theme = useTheme();
const {isLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
- const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchContext();
- const snapshotReport = (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report;
+ const {currentSearchHash, currentSearchKey} = useSearchContext();
+ const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true});
+ const snapshotReport = useMemo(() => {
+ return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] ?? {}) as Report;
+ }, [snapshot, transactionItem.reportID]);
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionItem.reportID}`, {canBeMissing: true, selector: isActionLoadingSelector});
- // Use active policy (user's current workspace) as fallback for self DM tracking expenses
- // This matches MoneyRequestView's approach via usePolicyForMovingExpenses()
- const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
-
- // Use report's policyID as fallback when transaction doesn't have policyID directly
- // Use active policy as final fallback for SelfDM (tracking expenses)
- // NOTE: Using || instead of ?? to treat empty string "" as falsy
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const policyID = transactionItem.policyID || snapshotReport?.policyID || activePolicyID;
- const [parentPolicy] = originalUseOnyx(ONYXKEYS.COLLECTION.POLICY, {
- canBeMissing: true,
- selector: (policy) => policy?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`],
- });
- const snapshotPolicy = (currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy;
-
- const actionsData = currentSearchResults?.data?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionItem.reportID}`];
- const exportedReportActions = actionsData ? Object.values(actionsData) : [];
-
- // Fetch policy categories directly from Onyx since they are not included in the search snapshot
- const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(policyID)}`, {canBeMissing: true});
+ const snapshotPolicy = useMemo(() => {
+ return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy;
+ }, [snapshot, transactionItem.policyID]);
const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true});
const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true});
const [parentReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionItem.reportID)}`, {canBeMissing: true});
+ const [parentPolicy] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(transactionItem.policyID)}`, {canBeMissing: true});
const [transactionThreadReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionItem?.reportAction?.childReportID}`, {canBeMissing: true});
const [transaction] = originalUseOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionItem.transactionID)}`, {canBeMissing: true});
- const parentReportActionSelector = (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${transactionItem?.reportAction?.reportActionID}`];
+ const parentReportActionSelector = useCallback(
+ (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${transactionItem?.reportAction?.reportActionID}`],
+ [transactionItem?.reportAction?.reportActionID],
+ );
const [parentReportAction] = originalUseOnyx(
`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(transactionItem.reportID)}`,
{selector: parentReportActionSelector, canBeMissing: true},
[transactionItem],
);
const currentUserDetails = useCurrentUserPersonalDetails();
- const transactionPreviewData: TransactionPreviewData = {
- hasParentReport: !!parentReport,
- hasTransaction: !!transaction,
- hasParentReportAction: !!parentReportAction,
- hasTransactionThreadReport: !!transactionThreadReport,
- };
+ const transactionPreviewData: TransactionPreviewData = useMemo(
+ () => ({hasParentReport: !!parentReport, hasTransaction: !!transaction, hasParentReportAction: !!parentReportAction, hasTransactionThreadReport: !!transactionThreadReport}),
+ [parentReport, transaction, parentReportAction, transactionThreadReport],
+ );
const pressableStyle = [
styles.transactionListItemStyle,
@@ -114,44 +102,44 @@ function TransactionListItem({
backgroundColor: theme.highlightBG,
});
- const amountColumnSize = transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const taxAmountColumnSize = transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const dateColumnSize = transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const submittedColumnSize = transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const approvedColumnSize = transactionItem.shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const postedColumnSize = transactionItem.shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
- const exportedColumnSize = transactionItem.shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL;
-
- // Prefer live Onyx policy data over snapshot to ensure fresh policy settings
- // like isAttendeeTrackingEnabled is not missing
- // Use snapshotReport/snapshotPolicy as fallbacks to fix offline issues where
- // newly created reports aren't in the search snapshot yet
- const policyForViolations = parentPolicy ?? snapshotPolicy;
- const reportForViolations = parentReport ?? snapshotReport;
-
- const onyxViolations = (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter(
- (violation: TransactionViolation) =>
- !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, reportForViolations, policyForViolations) &&
- shouldShowViolation(reportForViolations, policyForViolations, violation.name, currentUserDetails.email ?? '', false, transactionItem),
- );
-
- // Sync missingAttendees violation with current policy category settings (can be removed later when BE handles this)
- // Use live transaction data (attendees, category) to ensure we check against current state, not stale snapshot
- const attendeeOnyxViolations = syncMissingAttendeesViolation(
- onyxViolations,
- policyCategories,
- transaction?.category ?? transactionItem.category ?? '',
- transaction?.comment?.attendees ?? transactionItem.attendees,
- currentUserDetails,
- policyForViolations?.isAttendeeTrackingEnabled ?? false,
- policyForViolations?.type === CONST.POLICY.TYPE.CORPORATE,
- );
-
- const transactionViolations = mergeProhibitedViolations(attendeeOnyxViolations);
+ const {amountColumnSize, dateColumnSize, taxAmountColumnSize, submittedColumnSize, approvedColumnSize, postedColumnSize, exportedColumnSize} = useMemo(() => {
+ return {
+ amountColumnSize: transactionItem.isAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ taxAmountColumnSize: transactionItem.isTaxAmountColumnWide ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ dateColumnSize: transactionItem.shouldShowYear ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ submittedColumnSize: transactionItem.shouldShowYearSubmitted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ approvedColumnSize: transactionItem.shouldShowYearApproved ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ postedColumnSize: transactionItem.shouldShowYearPosted ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ exportedColumnSize: transactionItem.shouldShowYearExported ? CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE : CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL,
+ };
+ }, [
+ transactionItem.isAmountColumnWide,
+ transactionItem.isTaxAmountColumnWide,
+ transactionItem.shouldShowYear,
+ transactionItem.shouldShowYearSubmitted,
+ transactionItem.shouldShowYearApproved,
+ transactionItem.shouldShowYearPosted,
+ transactionItem.shouldShowYearExported,
+ ]);
+
+ // Use parentReport/parentPolicy as fallbacks when snapshotReport/snapshotPolicy are empty
+ // to fix offline issues where newly created reports aren't in the search snapshot yet
+ const reportForViolations = isEmptyObject(snapshotReport) ? parentReport : snapshotReport;
+ const policyForViolations = isEmptyObject(snapshotPolicy) ? parentPolicy : snapshotPolicy;
+
+ const transactionViolations = useMemo(() => {
+ return mergeProhibitedViolations(
+ (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter(
+ (violation: TransactionViolation) =>
+ !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, reportForViolations, policyForViolations) &&
+ shouldShowViolation(reportForViolations, policyForViolations, violation.name, currentUserDetails.email ?? '', false, transactionItem),
+ ),
+ );
+ }, [policyForViolations, reportForViolations, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]);
const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
- const handleActionButtonPress = () => {
+ const handleActionButtonPress = useCallback(() => {
handleActionButtonPressUtil({
hash: currentSearchHash,
item: transactionItem,
@@ -166,7 +154,34 @@ function TransactionListItem({
onDelegateAccessRestricted: showDelegateNoAccessModal,
personalPolicyID,
});
- };
+ }, [
+ currentSearchHash,
+ transactionItem,
+ transactionPreviewData,
+ snapshotReport,
+ snapshotPolicy,
+ lastPaymentMethod,
+ personalPolicyID,
+ currentSearchKey,
+ onSelectRow,
+ item,
+ onDEWModalOpen,
+ isDEWBetaEnabled,
+ isDelegateAccessRestricted,
+ showDelegateNoAccessModal,
+ ]);
+
+ const handleCheckboxPress = useCallback(() => {
+ onCheckboxPress?.(item);
+ }, [item, onCheckboxPress]);
+
+ const onPress = useCallback(() => {
+ onSelectRow(item, transactionPreviewData);
+ }, [item, onSelectRow, transactionPreviewData]);
+
+ const onLongPress = useCallback(() => {
+ onLongPressRow?.(item);
+ }, [item, onLongPressRow]);
const StyleUtils = useStyleUtils();
const pressableRef = useRef(null);
@@ -177,8 +192,8 @@ function TransactionListItem({
onLongPressRow?.(item)}
- onPress={() => onSelectRow(item, transactionPreviewData)}
+ onLongPress={onLongPress}
+ onPress={onPress}
disabled={isDisabled && !item.isSelected}
accessibilityLabel={item.text ?? ''}
role={getButtonRole(true)}
@@ -205,11 +220,12 @@ function TransactionListItem({
/>
)}
onCheckboxPress?.(item)}
+ onCheckboxPress={handleCheckboxPress}
shouldUseNarrowLayout={!isLargeScreenWidth}
columns={columns}
isActionLoading={isLoading ?? isActionLoading}
@@ -224,10 +240,9 @@ function TransactionListItem({
shouldShowCheckbox={!!canSelectMultiple}
style={[styles.p3, styles.pv2, shouldUseNarrowLayout ? styles.pt2 : {}]}
violations={transactionViolations}
- onArrowRightPress={() => onSelectRow(item, transactionPreviewData)}
+ onArrowRightPress={onPress}
isHover={hovered}
customCardNames={customCardNames}
- reportActions={exportedReportActions}
/>
>
)}
diff --git a/src/components/SelectionListWithSections/Search/WithdrawalIDListItemHeader.tsx b/src/components/SelectionListWithSections/Search/WithdrawalIDListItemHeader.tsx
index 7ab5f4e94796..813db1b1c2f3 100644
--- a/src/components/SelectionListWithSections/Search/WithdrawalIDListItemHeader.tsx
+++ b/src/components/SelectionListWithSections/Search/WithdrawalIDListItemHeader.tsx
@@ -21,7 +21,7 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import ExpandCollapseArrowButton from './ExpandCollapseArrowButton';
-import TextCell from './TextCell';
+import ExpensesCell from './ExpensesCell';
import TotalCell from './TotalCell';
type WithdrawalIDListItemHeaderProps = {
@@ -132,7 +132,7 @@ function WithdrawalIDListItemHeader({
key={CONST.SEARCH.TABLE_COLUMNS.EXPENSES}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPENSES)}
>
-
+
),
[CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL]: (
diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts
index 15464aebe076..3a13e47e4fca 100644
--- a/src/components/SelectionListWithSections/types.ts
+++ b/src/components/SelectionListWithSections/types.ts
@@ -125,9 +125,6 @@ type ListItem = {
/** Text to display */
text?: string;
- /** Text to be announced by screen reader */
- accessibilityLabel?: string;
-
/** Alternate text to display */
alternateText?: string | null;
@@ -873,9 +870,6 @@ type SelectionListProps = Partial & {
/** Custom content to display in the header of list component. */
listHeaderContent?: React.JSX.Element | null;
- /** By default, when `listHeaderContent` is passed, the section title will not be rendered. This flag allows the section title to still be rendered in certain cases. */
- showSectionTitleWithListHeaderContent?: boolean;
-
/** Custom content to display in the footer */
footerContent?: ReactNode;
diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx
index ab5ae68d523c..758f9b21335e 100644
--- a/src/components/SettlementButton/index.tsx
+++ b/src/components/SettlementButton/index.tsx
@@ -103,12 +103,11 @@ function SettlementButton({
// The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here.
// eslint-disable-next-line rulesdir/no-default-id-values
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true});
- const [conciergeReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
const [iouReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport?.reportID}`, {canBeMissing: true});
const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: true});
const policyEmployeeAccountIDs = getPolicyEmployeeAccountIDs(policy, accountID);
- const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(chatReport, policyEmployeeAccountIDs, policyID, conciergeReportID) : false;
+ const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(chatReport, policyEmployeeAccountIDs, policyID) : false;
const policyIDKey = reportBelongsToWorkspace ? policyID : (iouReport?.policyID ?? CONST.POLICY.ID_FAKE);
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true});
const hasActivatedWallet = ([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM] as string[]).includes(userWallet?.tierName ?? '');
diff --git a/src/components/SidePanel/HelpComponents/HelpContent.tsx b/src/components/SidePanel/HelpComponents/HelpContent.tsx
index 2d83cadef093..ddcfb78ebb94 100644
--- a/src/components/SidePanel/HelpComponents/HelpContent.tsx
+++ b/src/components/SidePanel/HelpComponents/HelpContent.tsx
@@ -53,7 +53,6 @@ function HelpContent({closeSidePanel}: HelpContentProps) {
});
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${params?.reportID || String(CONST.DEFAULT_NUMBER_ID)}`, {canBeMissing: true});
- const [conciergeReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {canBeMissing: true});
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, {canBeMissing: true});
@@ -90,7 +89,7 @@ function HelpContent({closeSidePanel}: HelpContentProps) {
const cleanedPath = path.replaceAll('?', '');
const expenseType = getExpenseType(transaction);
- const reportType = getHelpPaneReportType(report, conciergeReportID);
+ const reportType = getHelpPaneReportType(report);
if (expenseType && reportType !== CONST.REPORT.HELP_TYPE.EXPENSE_REPORT) {
return cleanedPath.replaceAll(':reportID', `:${CONST.REPORT.HELP_TYPE.EXPENSE}/:${expenseType}`);
@@ -101,7 +100,7 @@ function HelpContent({closeSidePanel}: HelpContentProps) {
}
return cleanedPath;
- }, [routeName, transaction, report, conciergeReportID]);
+ }, [routeName, transaction, report]);
const wasPreviousNarrowScreen = useRef(!isExtraLargeScreenWidth);
useEffect(() => {
diff --git a/src/components/SidePanel/SidePanelModal/index.tsx b/src/components/SidePanel/SidePanelModal/index.tsx
index 60fb40444e7c..467da113af98 100644
--- a/src/components/SidePanel/SidePanelModal/index.tsx
+++ b/src/components/SidePanel/SidePanelModal/index.tsx
@@ -27,9 +27,8 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi
const [isRHPVisible = false] = useOnyx(ONYXKEYS.MODAL, {selector: isRHPVisibleSelector, canBeMissing: true});
const uniqueModalId = ComposerFocusManager.getId();
- const {wideRHPRouteKeys, isWideRHPFocused, superWideRHPRouteKeys, isSuperWideRHPFocused} = useContext(WideRHPContext);
-
- const shouldOverlayBeVisible = (!!wideRHPRouteKeys.length && isWideRHPFocused) || (!!superWideRHPRouteKeys.length && isSuperWideRHPFocused) || !isRHPVisible;
+ const {wideRHPRouteKeys, isWideRHPFocused} = useContext(WideRHPContext);
+ const isWideRHPVisible = !!wideRHPRouteKeys.length;
const onCloseSidePanelOnSmallScreens = () => {
if (isExtraLargeScreenWidth) {
@@ -66,7 +65,7 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi
{!shouldHideSidePanelBackdrop && (
)}
diff --git a/src/components/Skeletons/CardIconSkeleton.tsx b/src/components/Skeletons/CardIconSkeleton.tsx
deleted file mode 100644
index f84af6cd708d..000000000000
--- a/src/components/Skeletons/CardIconSkeleton.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import {Rect} from 'react-native-svg';
-import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader';
-import useTheme from '@hooks/useTheme';
-import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan';
-
-type CardIconSkeletonProps = {
- width: number;
- height: number;
-};
-
-function CardIconSkeleton({width, height}: CardIconSkeletonProps) {
- const theme = useTheme();
- useSkeletonSpan('CardIconSkeleton');
-
- return (
-
-
-
- );
-}
-
-export default CardIconSkeleton;
diff --git a/src/components/Skeletons/TableRowSkeleton.tsx b/src/components/Skeletons/TableRowSkeleton.tsx
index 6276033161ef..41efa7a0d9c4 100644
--- a/src/components/Skeletons/TableRowSkeleton.tsx
+++ b/src/components/Skeletons/TableRowSkeleton.tsx
@@ -8,43 +8,36 @@ type TableListItemSkeletonProps = {
shouldAnimate?: boolean;
fixedNumItems?: number;
gradientOpacityEnabled?: boolean;
- useCompanyCardsLayout?: boolean;
};
const barHeight = '8';
const shortBarWidth = '60';
const longBarWidth = '124';
-function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false, useCompanyCardsLayout = false}: TableListItemSkeletonProps) {
+function TableListItemSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: TableListItemSkeletonProps) {
const styles = useThemeStyles();
useSkeletonSpan('TableRowSkeleton');
- const circleX = useCompanyCardsLayout ? 36 : 40;
- const circleY = useCompanyCardsLayout ? 36 : 32;
- const rectX = useCompanyCardsLayout ? 68 : 80;
- const rectY1 = useCompanyCardsLayout ? 24 : 20;
- const rectY2 = useCompanyCardsLayout ? 40 : 36;
-
return (
(
<>
diff --git a/src/components/SubStepForms/ConfirmationStep.tsx b/src/components/SubStepForms/ConfirmationStep.tsx
index fe12036fa120..113570a2d8c6 100644
--- a/src/components/SubStepForms/ConfirmationStep.tsx
+++ b/src/components/SubStepForms/ConfirmationStep.tsx
@@ -3,15 +3,16 @@ import {View} from 'react-native';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import RenderHTML from '@components/RenderHTML';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
+import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useThemeStyles from '@hooks/useThemeStyles';
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
+import CONST from '@src/CONST';
type SummaryItem = {
description: string;
@@ -79,9 +80,29 @@ function ConfirmationStep({
))}
{showOnfidoLinks && (
-
-
-
+
+ {onfidoLinksTitle}
+
+ {translate('onfidoStep.facialScan')}
+
+ {', '}
+
+ {translate('common.privacy')}
+
+ {` ${translate('common.and')} `}
+
+ {translate('common.termsOfService')}
+
+
)}
diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx
index 55961c9e1ac8..ceed1828500c 100644
--- a/src/components/TabSelector/TabSelectorItem.tsx
+++ b/src/components/TabSelector/TabSelectorItem.tsx
@@ -1,4 +1,4 @@
-import React, {useLayoutEffect, useMemo, useRef, useState} from 'react';
+import React, {useLayoutEffect, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import type {View} from 'react-native';
@@ -112,12 +112,9 @@ function TabSelectorItem({
};
}, [isActive, childRef, isSmallScreenWidth, parentX, parentWidth]);
- const accessibilityState = useMemo(() => ({selected: isActive}), [isActive]);
-
const children = (
+
{shouldUseNarrowLayout
diff --git a/src/components/TestDrive/TestDriveDemo.tsx b/src/components/TestDrive/TestDriveDemo.tsx
index 52ad6b33aea4..902bef99817a 100644
--- a/src/components/TestDrive/TestDriveDemo.tsx
+++ b/src/components/TestDrive/TestDriveDemo.tsx
@@ -5,7 +5,6 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli
import EmbeddedDemo from '@components/EmbeddedDemo';
import Modal from '@components/Modal';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
-import {shouldOpenRHPVariant} from '@components/SidePanel/RHPVariantTest';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin';
import useOnboardingMessages from '@hooks/useOnboardingMessages';
@@ -15,7 +14,6 @@ import useParentReportAction from '@hooks/useParentReportAction';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {completeTestDriveTask} from '@libs/actions/Task';
-import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {isAdminRoom} from '@libs/ReportUtils';
import {getTestDriveURL} from '@libs/TourUtils';
@@ -76,10 +74,6 @@ function TestDriveDemo() {
InteractionManager.runAfterInteractions(() => {
Navigation.goBack();
- if (shouldOpenRHPVariant()) {
- Log.hmmm('[AdminTestDriveModal] User was redirected to Workspace Editor, skipping navigation to admin room');
- return;
- }
if (isAdminRoom(onboardingReport)) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(onboardingReport?.reportID));
}
diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index f4f83bfab000..d31450a02250 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -1,20 +1,15 @@
import React from 'react';
-import {View} from 'react-native';
import useIsAuthenticated from '@hooks/useIsAuthenticated';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports';
-import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
-import useWaitForNavigation from '@hooks/useWaitForNavigation';
import {isUsingStagingApi} from '@libs/ApiUtils';
-import Navigation from '@libs/Navigation/Navigation';
import {setShouldFailAllRequests, setShouldForceOffline, setShouldSimulatePoorConnection} from '@userActions/Network';
import {expireSessionWithDelay, invalidateAuthToken, invalidateCredentials} from '@userActions/Session';
import {setIsDebugModeEnabled, setShouldUseStagingServer} from '@userActions/User';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
import Button from './Button';
import SoftKillTestToolRow from './SoftKillTestToolRow';
import Switch from './Switch';
@@ -31,20 +26,9 @@ function TestToolMenu() {
const {translate} = useLocalize();
const {clearLHNCache} = useSidebarOrderedReports();
- const {singleExecution} = useSingleExecution();
- const waitForNavigate = useWaitForNavigation();
- const navigateToBiometricsTestPage = singleExecution(
- waitForNavigate(() => {
- Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST);
- }),
- );
-
// Check if the user is authenticated to show options that require authentication
const isAuthenticated = useIsAuthenticated();
- // Temporary hardcoded false, expected behavior: status fetched from the MultifactorAuthenticationContext
- const biometricsTitle = translate('multifactorAuthentication.biometricsTest.troubleshootBiometricsStatus', {registered: false});
-
return (
<>
-
- {/* Allows you to test the Biometrics flow */}
-
-
- navigateToBiometricsTestPage()}
- />
-
-
>
)}
diff --git a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx
index e8ea3cb0b2ff..821f9d6d5f01 100644
--- a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx
@@ -280,7 +280,6 @@ function BaseTextInput({
// Height fix is needed only for Text single line inputs
const shouldApplyHeight = !shouldUseFullInputHeight && !isMultiline && !isMarkdownEnabled;
- const accessibilityLabel = [label, hint].filter(Boolean).join(', ');
return (
<>
@@ -292,7 +291,7 @@ function BaseTextInput({
// When autoGrowHeight is true we calculate the width for the text input, so it will break lines properly
// or if multiline is not supplied we calculate the text input height, using onLayout.
onLayout={onLayout}
- accessibilityLabel={accessibilityLabel}
+ accessibilityLabel={label}
style={[
autoGrowHeight &&
!isAutoGrowHeightMarkdown &&
@@ -368,7 +367,6 @@ function BaseTextInput({
}}
// eslint-disable-next-line
{...inputProps}
- accessibilityLabel={inputProps.accessibilityLabel ?? accessibilityLabel}
autoCorrect={inputProps.secureTextEntry ? false : autoCorrect}
placeholder={placeholderValue}
placeholderTextColor={placeholderTextColor ?? theme.placeholderText}
@@ -430,10 +428,15 @@ function BaseTextInput({
style={[StyleUtils.getTextInputIconContainerStyles(hasLabel, false, verticalPaddingDiff)]}
/>
)}
- {!!inputProps.isLoading && !shouldShowClearButton && (
+ {inputProps.isLoading !== undefined && !shouldShowClearButton && (
)}
{!!inputProps.secureTextEntry && (
diff --git a/src/components/TextInput/BaseTextInput/implementation/index.tsx b/src/components/TextInput/BaseTextInput/implementation/index.tsx
index 6fc2988bee93..91e097dffd3c 100644
--- a/src/components/TextInput/BaseTextInput/implementation/index.tsx
+++ b/src/components/TextInput/BaseTextInput/implementation/index.tsx
@@ -289,7 +289,6 @@ function BaseTextInput({
// This is workaround for https://github.com/Expensify/App/issues/47939: in case when user is using Chrome on Android we set inputMode to 'search' to disable autocomplete bar above the keyboard.
// If we need some other inputMode (eg. 'decimal'), then the autocomplete bar will show, but we can do nothing about it as it's a known Chrome bug.
const inputMode = inputProps.inputMode ?? (isMobileChrome() ? 'search' : undefined);
- const accessibilityLabel = [label, hint].filter(Boolean).join(', ');
return (
<>
{!!suffixCharacter && (
@@ -481,10 +479,15 @@ function BaseTextInput({
/>
)}
- {!!inputProps.isLoading && !shouldShowClearButton && (
+ {inputProps.isLoading !== undefined && !shouldShowClearButton && (
)}
{!!inputProps.secureTextEntry && (
diff --git a/src/components/TextInputWithSymbol/types.ts b/src/components/TextInputWithSymbol/types.ts
index 11243020d90a..9fa0bda44e4c 100644
--- a/src/components/TextInputWithSymbol/types.ts
+++ b/src/components/TextInputWithSymbol/types.ts
@@ -103,7 +103,6 @@ type BaseTextInputWithSymbolProps = {
| 'onBlur'
| 'disabled'
| 'ref'
- | 'accessibilityLabel'
>;
type TextInputWithSymbolProps = Omit & {
diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx
index 9a34aa635712..372cf9066ff8 100644
--- a/src/components/ThreeDotsMenu/index.tsx
+++ b/src/components/ThreeDotsMenu/index.tsx
@@ -45,7 +45,6 @@ function ThreeDotsMenu({
shouldSelfPosition = false,
threeDotsMenuRef,
sentryLabel,
- isContainerFocused = true,
}: ThreeDotsMenuProps) {
const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});
@@ -101,12 +100,13 @@ function ThreeDotsMenu({
hidePopoverMenu,
onThreeDotsPress,
}));
+
useEffect(() => {
- if ((!isBehindModal || !isPopupMenuVisible) && isContainerFocused) {
+ if (!isBehindModal || !isPopupMenuVisible) {
return;
}
hidePopoverMenu();
- }, [hidePopoverMenu, isBehindModal, isPopupMenuVisible, isContainerFocused]);
+ }, [hidePopoverMenu, isBehindModal, isPopupMenuVisible]);
useLayoutEffect(() => {
if (!getMenuPosition || !isPopupMenuVisible) {
@@ -166,7 +166,7 @@ function ThreeDotsMenu({
setRestoreFocusType(undefined)}
- isVisible={isPopupMenuVisible && !isBehindModal && isContainerFocused}
+ isVisible={isPopupMenuVisible && !isBehindModal}
anchorPosition={position ?? anchorPosition ?? {horizontal: 0, vertical: 0}}
anchorAlignment={anchorAlignment}
onItemSelected={(item) => {
diff --git a/src/components/ThreeDotsMenu/types.ts b/src/components/ThreeDotsMenu/types.ts
index ec38b6074239..6617d65b4771 100644
--- a/src/components/ThreeDotsMenu/types.ts
+++ b/src/components/ThreeDotsMenu/types.ts
@@ -51,9 +51,6 @@ type ThreeDotsMenuProps = WithSentryLabel & {
/** Ref to the menu */
threeDotsMenuRef?: React.RefObject<{hidePopoverMenu: () => void; isPopupMenuVisible: boolean} | null>;
-
- /** Whether the menu is focused */
- isContainerFocused?: boolean;
};
type ThreeDotsMenuWithOptionalAnchorProps =
diff --git a/src/components/TransactionItemRow/ReceiptPreview/index.tsx b/src/components/TransactionItemRow/ReceiptPreview/index.tsx
index 9a7da1e77eba..be3c27af9884 100644
--- a/src/components/TransactionItemRow/ReceiptPreview/index.tsx
+++ b/src/components/TransactionItemRow/ReceiptPreview/index.tsx
@@ -8,7 +8,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation';
import type {ImageOnLoadEvent} from '@components/Image/types';
import useDebouncedState from '@hooks/useDebouncedState';
-import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {hasReceiptSource, isDistanceRequest, isManualDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils';
@@ -40,7 +40,7 @@ function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}:
const [imageAspectRatio, setImageAspectRatio] = useState(undefined);
const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState(undefined);
const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY);
- const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const hasMeasured = useRef(false);
const {windowHeight} = useWindowDimensions();
const [isLoading, setIsLoading] = useState(true);
diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx
index e366a9f1ee8e..14cf550fac18 100644
--- a/src/components/TransactionItemRow/index.tsx
+++ b/src/components/TransactionItemRow/index.tsx
@@ -16,7 +16,6 @@ import AmountCell from '@components/SelectionListWithSections/Search/TotalCell';
import UserInfoCell from '@components/SelectionListWithSections/Search/UserInfoCell';
import WorkspaceCell from '@components/SelectionListWithSections/Search/WorkspaceCell';
import Text from '@components/Text';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -45,7 +44,7 @@ import {
} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
-import type {PersonalDetails, Policy, Report, ReportAction, TransactionViolation} from '@src/types/onyx';
+import type {PersonalDetails, Policy, Report, TransactionViolation} from '@src/types/onyx';
import type {SearchTransactionAction} from '@src/types/onyx/SearchResults';
import CategoryCell from './DataCells/CategoryCell';
import ChatBubbleCell from './DataCells/ChatBubbleCell';
@@ -102,6 +101,7 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & {
};
type TransactionItemRowProps = {
+ hash?: number;
transactionItem: TransactionWithOptionalSearchFields;
report?: Report;
shouldUseNarrowLayout: boolean;
@@ -133,7 +133,6 @@ type TransactionItemRowProps = {
isHover?: boolean;
shouldShowArrowRightOnNarrowLayout?: boolean;
customCardNames?: Record;
- reportActions?: ReportAction[];
};
function getMerchantName(transactionItem: TransactionWithOptionalSearchFields, translate: (key: TranslationPaths) => string) {
@@ -146,10 +145,11 @@ 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({
+ hash,
transactionItem,
report,
shouldUseNarrowLayout,
@@ -181,14 +181,12 @@ function TransactionItemRow({
isHover = false,
shouldShowArrowRightOnNarrowLayout,
customCardNames,
- reportActions,
}: TransactionItemRowProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const StyleUtils = useStyleUtils();
const theme = useTheme();
const {isLargeScreenWidth} = useResponsiveLayout();
- const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const hasCategoryOrTag = !isCategoryMissing(transactionItem?.category) || !!transactionItem.tag;
const createdAt = getTransactionCreated(transactionItem);
const expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
@@ -201,16 +199,6 @@ function TransactionItemRow({
const isAmountColumnWide = amountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE;
const isTaxAmountColumnWide = taxAmountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE;
- const filteredViolations = useMemo(() => {
- if (!violations) {
- return undefined;
- }
- if (CONST.IS_ATTENDEES_REQUIRED_FEATURE_DISABLED) {
- return violations.filter((violation) => violation.name !== CONST.VIOLATIONS.MISSING_ATTENDEES);
- }
- return violations;
- }, [violations]);
-
const bgActiveStyles = useMemo(() => {
if (!isSelected || !shouldHighlightItemWhenSelected) {
return [];
@@ -553,11 +541,7 @@ function TransactionItemRow({
[CONST.SEARCH.TABLE_COLUMNS.TITLE]: (
@@ -572,7 +556,10 @@ function TransactionItemRow({
),
[CONST.SEARCH.TABLE_COLUMNS.EXPORTED_TO]: (
-
+
),
}),
@@ -604,8 +591,7 @@ function TransactionItemRow({
isAmountColumnWide,
isTaxAmountColumnWide,
isLargeScreenWidth,
- currentUserAccountID,
- reportActions,
+ hash,
],
);
const shouldRenderChatBubbleCell = useMemo(() => {
@@ -717,7 +703,7 @@ function TransactionItemRow({
{shouldShowErrors && (
diff --git a/src/components/UploadFile.tsx b/src/components/UploadFile.tsx
index 385648ab87dc..09737c4fbcf0 100644
--- a/src/components/UploadFile.tsx
+++ b/src/components/UploadFile.tsx
@@ -83,7 +83,7 @@ function UploadFile({
}
if (fileLimit && resultedFiles.length > 0 && resultedFiles.length > fileLimit) {
- setError(translate('attachmentPicker.tooManyFiles', fileLimit));
+ setError(translate('attachmentPicker.tooManyFiles', {fileLimit}));
return;
}
diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
index 0c8e725f30f0..62fa9a1e17d2 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -10,8 +10,6 @@ import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicC
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
-import ValidateCodeCountdown from '@components/ValidateCodeCountdown';
-import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types';
import {WideRHPContext} from '@components/WideRHPContextProvider';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -22,6 +20,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {isMobileSafari} from '@libs/Browser';
import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils';
import {isValidValidateCode} from '@libs/ValidationUtils';
+import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown';
+import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown/types';
import {clearValidateCodeActionError} from '@userActions/User';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
diff --git a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts
index f406e03c6218..aec9590bd640 100644
--- a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts
+++ b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts
@@ -47,13 +47,7 @@ const getCurrentRouteReportID: (url: string) => string | ProtectedCurrentRouteRe
return isFocusedRouteAChatThread ? firstReportThatHasURLInAttachments : focusedRouteReportID;
};
-const screensWithReportID = [
- SCREENS.RIGHT_MODAL.SEARCH_REPORT,
- SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
- SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
- SCREENS.REPORT,
- SCREENS.REPORT_ATTACHMENTS,
-];
+const screensWithReportID = [SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.REPORT, SCREENS.SEARCH.MONEY_REQUEST_REPORT, SCREENS.REPORT_ATTACHMENTS];
function hasReportIdInRouteParams(route: SearchRoute): route is RouteWithReportIDInParams {
return !!route && !!route.params && !!screensWithReportID.find((screen) => screen === route.name) && 'reportID' in route.params;
diff --git a/src/components/WideRHPContextProvider/default.ts b/src/components/WideRHPContextProvider/default.ts
index 10a359bfcb9c..7add883bb4a1 100644
--- a/src/components/WideRHPContextProvider/default.ts
+++ b/src/components/WideRHPContextProvider/default.ts
@@ -20,8 +20,6 @@ const defaultWideRHPContextValue: WideRHPContextType = {
removeSuperWideRHPRouteKey: () => {},
syncRHPKeys: () => {},
clearWideRHPKeys: () => {},
- setIsWideRHPClosing: () => {},
- setIsSuperWideRHPClosing: () => {},
};
export default defaultWideRHPContextValue;
diff --git a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts
index 4d1ff9b4c210..6c945a7fa5a3 100644
--- a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts
+++ b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts
@@ -23,6 +23,7 @@ function getVisibleRHPKeys(allSuperWideRHPKeys: string[], allWideRHPKeys: string
}
const rootState = navigationRef.getRootState();
+
if (!rootState) {
return emptyRHPKeysState;
}
diff --git a/src/components/WideRHPContextProvider/index.tsx b/src/components/WideRHPContextProvider/index.tsx
index c697c1a846b4..941c3d20b6df 100644
--- a/src/components/WideRHPContextProvider/index.tsx
+++ b/src/components/WideRHPContextProvider/index.tsx
@@ -1,5 +1,5 @@
import {findFocusedRoute} from '@react-navigation/native';
-import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {createContext, useCallback, useEffect, useMemo, useState} from 'react';
// We use Animated for all functionality related to wide RHP to make it easier
// to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated.
// eslint-disable-next-line no-restricted-imports
@@ -111,17 +111,6 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: expenseReportSelector, canBeMissing: true});
- const isWideRHPClosingRef = useRef(false);
- const isSuperWideRHPClosingRef = useRef(false);
-
- const setIsWideRHPClosing = useCallback((isClosing: boolean) => {
- isWideRHPClosingRef.current = isClosing;
- }, []);
-
- const setIsSuperWideRHPClosing = useCallback((isClosing: boolean) => {
- isSuperWideRHPClosingRef.current = isClosing;
- }, []);
-
const {focusedRoute, focusedNavigator} = useRootNavigationState((state) => {
if (!state) {
return {focusedRoute: undefined, focusedNavigator: undefined};
@@ -344,8 +333,6 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
isSuperWideRHPFocused,
syncRHPKeys,
clearWideRHPKeys,
- setIsWideRHPClosing,
- setIsSuperWideRHPClosing,
}),
[
wideRHPRouteKeys,
@@ -367,8 +354,6 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) {
isSuperWideRHPFocused,
syncRHPKeys,
clearWideRHPKeys,
- setIsWideRHPClosing,
- setIsSuperWideRHPClosing,
],
);
diff --git a/src/components/WideRHPContextProvider/types.ts b/src/components/WideRHPContextProvider/types.ts
index 20300f48287e..8ffb8e7dca87 100644
--- a/src/components/WideRHPContextProvider/types.ts
+++ b/src/components/WideRHPContextProvider/types.ts
@@ -57,12 +57,6 @@ type WideRHPContextType = {
// Clear the arrays of wide and super wide rhp keys
clearWideRHPKeys: () => void;
-
- // Set that wide rhp is closing
- setIsWideRHPClosing: (isClosing: boolean) => void;
-
- // Set that super wide rhp is closing
- setIsSuperWideRHPClosing: (isClosing: boolean) => void;
};
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/components/WorkspaceMembersSelectionList.tsx b/src/components/WorkspaceMembersSelectionList.tsx
index ec60837ad5a9..3bf855ae4d1f 100644
--- a/src/components/WorkspaceMembersSelectionList.tsx
+++ b/src/components/WorkspaceMembersSelectionList.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import useDebouncedState from '@hooks/useDebouncedState';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
@@ -43,53 +43,67 @@ function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover}
const policy = usePolicy(policyID);
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
- const approvers: SelectionListApprover[] = [];
-
- if (policy?.employeeList) {
- for (const employee of Object.values(policy.employeeList)) {
- const email = employee.email;
-
- if (!email || employee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- continue;
- }
-
- const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList);
- const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
- const {avatar, displayName = email, login} = personalDetails?.[accountID] ?? {};
-
- approvers.push({
- text: displayName,
- alternateText: email,
- keyForList: email,
- isSelected: selectedApprover === email,
- login: email,
- icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
- rightElement: (
-
- ),
- });
+ const orderedApprovers = useMemo(() => {
+ const approvers: SelectionListApprover[] = [];
+
+ if (policy?.employeeList) {
+ const availableApprovers = Object.values(policy.employeeList)
+ .map((employee): SelectionListApprover | null => {
+ const email = employee.email;
+
+ if (!email || employee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return null;
+ }
+
+ const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList);
+ const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? '');
+ const {avatar, displayName = email, login} = personalDetails?.[accountID] ?? {};
+
+ return {
+ text: displayName,
+ alternateText: email,
+ keyForList: email,
+ isSelected: selectedApprover === email,
+ login: email,
+ icons: [{source: avatar ?? icons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}],
+ rightElement: (
+
+ ),
+ };
+ })
+ .filter((approver): approver is SelectionListApprover => !!approver);
+
+ approvers.push(...availableApprovers);
}
- }
- const filteredApprovers = tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (approver) => [approver.text ?? '', approver.login ?? '']);
- const orderedApprovers = sortAlphabetically(filteredApprovers, 'text', localeCompare);
+ const filteredApprovers = tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (approver) => [approver.text ?? '', approver.login ?? '']);
+
+ return sortAlphabetically(filteredApprovers, 'text', localeCompare);
+ }, [policy?.employeeList, policy?.owner, debouncedSearchTerm, countryCode, localeCompare, personalDetails, selectedApprover, icons.FallbackAvatar]);
- const textInputOptions = {
- label: translate('selectionList.nameEmailOrPhoneNumber'),
- value: searchTerm,
- headerMessage: searchTerm && !orderedApprovers.length ? translate('common.noResultsFound') : '',
- onChangeText: setSearchTerm,
+ const handleOnSelectRow = (approver: SelectionListApprover) => {
+ setApprover(approver.login);
};
+ const textInputOptions = useMemo(
+ () => ({
+ label: translate('selectionList.nameEmailOrPhoneNumber'),
+ value: searchTerm,
+ headerMessage: searchTerm && !orderedApprovers.length ? translate('common.noResultsFound') : '',
+ onChangeText: setSearchTerm,
+ }),
+ [searchTerm, orderedApprovers.length, setSearchTerm, translate],
+ );
+
return (
setApprover(approver.login)}
+ onSelectRow={handleOnSelectRow}
textInputOptions={textInputOptions}
showLoadingPlaceholder={!didScreenTransitionEnd}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
diff --git a/src/hooks/useAccountTabIndicatorStatus.ts b/src/hooks/useAccountTabIndicatorStatus.ts
index 480a814f41b9..ee3835b2fc12 100644
--- a/src/hooks/useAccountTabIndicatorStatus.ts
+++ b/src/hooks/useAccountTabIndicatorStatus.ts
@@ -1,19 +1,79 @@
-import useNavigationTabBarIndicatorChecks from './useNavigationTabBarIndicatorChecks';
-import type {IndicatorStatus} from './useNavigationTabBarIndicatorChecks';
+import type {ValueOf} from 'type-fest';
+import {hasPaymentMethodError} from '@libs/actions/PaymentMethods';
+import {hasPartiallySetupBankAccount} from '@libs/BankAccountUtils';
+import {checkIfFeedConnectionIsBroken, hasPendingExpensifyCardAction} from '@libs/CardUtils';
+import {hasSubscriptionGreenDotInfo, hasSubscriptionRedDotError} from '@libs/SubscriptionUtils';
+import {hasLoginListError, hasLoginListInfo} from '@libs/UserUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useOnyx from './useOnyx';
import useTheme from './useTheme';
+type AccountTabIndicatorStatus = ValueOf;
+
type AccountTabIndicatorStatusResult = {
indicatorColor: string;
- status: IndicatorStatus | undefined;
+ status: ValueOf | undefined;
};
function useAccountTabIndicatorStatus(): AccountTabIndicatorStatusResult {
const theme = useTheme();
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true});
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true});
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true});
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true});
+ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true});
+ const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true});
+ const [allCards] = useOnyx(`${ONYXKEYS.CARD_LIST}`, {canBeMissing: true});
+ const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(allCards, CONST.EXPENSIFY_CARD.BANK);
+ const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
+ const [stripeCustomerId] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, {canBeMissing: true});
+ const [retryBillingSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {canBeMissing: true});
+ const [billingDisputePending] = useOnyx(ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING, {canBeMissing: true});
+ const [retryBillingFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, {canBeMissing: true});
+ const [billingStatus] = useOnyx(ONYXKEYS.NVP_PRIVATE_BILLING_STATUS, {canBeMissing: true});
+ // All of the error & info-checking methods are put into an array. This is so that using _.some() will return
+ // early as soon as the first error / info condition is returned. This makes the checks very efficient since
+ // we only care if a single error / info condition exists anywhere.
+ const errorChecking: Partial> = {
+ [CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0,
+ [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList, allCards),
+ [CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
+ [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && hasLoginListError(loginList),
+ // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
+ [CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
+ [CONST.INDICATOR_STATUS.HAS_CARD_CONNECTION_ERROR]: hasBrokenFeedConnection,
+ [CONST.INDICATOR_STATUS.HAS_PHONE_NUMBER_ERROR]: !!privatePersonalDetails?.errorFields?.phoneNumber,
+ [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(
+ stripeCustomerId,
+ retryBillingSuccessful,
+ billingDisputePending,
+ retryBillingFailed,
+ fundList,
+ billingStatus,
+ ),
+ };
+
+ const infoChecking: Partial> = {
+ [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && hasLoginListInfo(loginList, session?.email),
+ [CONST.INDICATOR_STATUS.HAS_PENDING_CARD_INFO]: hasPendingExpensifyCardAction(allCards, privatePersonalDetails),
+ [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(
+ stripeCustomerId,
+ retryBillingSuccessful,
+ billingDisputePending,
+ retryBillingFailed,
+ fundList,
+ billingStatus,
+ ),
+ [CONST.INDICATOR_STATUS.HAS_PARTIALLY_SETUP_BANK_ACCOUNT_INFO]: hasPartiallySetupBankAccount(bankAccountList),
+ };
- const {accountStatus, infoStatus} = useNavigationTabBarIndicatorChecks();
+ const [error] = Object.entries(errorChecking).find(([, value]) => value) ?? [];
+ const [info] = Object.entries(infoChecking).find(([, value]) => value) ?? [];
- const status = accountStatus ?? infoStatus;
- const indicatorColor = accountStatus ? theme.danger : theme.success;
+ const status = (error ?? info) as AccountTabIndicatorStatus | undefined;
+ const indicatorColor = error ? theme.danger : theme.success;
return {indicatorColor, status};
}
diff --git a/src/hooks/useAllTransactions.ts b/src/hooks/useAllTransactions.ts
index 34a2fb0037b5..f907458ba3a3 100644
--- a/src/hooks/useAllTransactions.ts
+++ b/src/hooks/useAllTransactions.ts
@@ -1,6 +1,7 @@
import {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useSearchContext} from '@components/Search/SearchContext';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Transaction} from '@src/types/onyx';
import useOnyx from './useOnyx';
@@ -9,7 +10,9 @@ import useOnyx from './useOnyx';
* Hook that returns all transactions, filtered by current search results if a search data is available
*/
function useAllTransactions() {
- const {currentSearchResults} = useSearchContext();
+ const searchContext = useSearchContext();
+ const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID;
+ const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true});
const [allTransactionsCollection] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
const allTransactions = useMemo(() => {
diff --git a/src/hooks/useAssignCard.ts b/src/hooks/useAssignCard.ts
index 09df344ac255..7e51bd999a29 100644
--- a/src/hooks/useAssignCard.ts
+++ b/src/hooks/useAssignCard.ts
@@ -6,7 +6,6 @@ import {
filterInactiveCards,
getCompanyCardFeed,
getCompanyFeeds,
- getDefaultCardName,
getDomainOrWorkspaceAccountID,
getPlaidCountry,
getPlaidInstitutionId,
@@ -20,11 +19,11 @@ import {clearAddNewCardFlow, clearAssignCardStepAndData, openPolicyCompanyCardsP
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {CompanyCardFeedWithDomainID} from '@src/types/onyx';
+import type {CompanyCardFeedWithDomainID, CurrencyList} from '@src/types/onyx';
import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard';
+import {getEmptyObject} from '@src/types/utils/EmptyObject';
import useCardFeeds from './useCardFeeds';
import type {CombinedCardFeed} from './useCardFeeds';
-import useCurrencyList from './useCurrencyList';
import useIsAllowedToIssueCompanyCard from './useIsAllowedToIssueCompanyCard';
import useNetwork from './useNetwork';
import useOnyx from './useOnyx';
@@ -73,12 +72,7 @@ function useAssignCard({feedName, policyID, setShouldShowOfflineModal}: UseAssig
const getInitialAssignCardStep = useInitialAssignCardStep({policyID, selectedFeed: feedName});
- /**
- * Initiates the card assignment flow.
- * @param cardName - The masked card number displayed to users (e.g., "XXXX1234")
- * @param cardID - The identifier sent to backend (equals cardName for direct feeds)
- */
- const assignCard = (cardName?: string, cardID?: string) => {
+ const assignCard = (cardID?: string) => {
if (isAssigningCardDisabled) {
return;
}
@@ -104,7 +98,7 @@ function useAssignCard({feedName, policyID, setShouldShowOfflineModal}: UseAssig
clearAddNewCardFlow();
clearAssignCardStepAndData();
- const initialAssignCardStep = getInitialAssignCardStep(cardName, cardID);
+ const initialAssignCardStep = getInitialAssignCardStep(cardID);
if (!initialAssignCardStep) {
return;
@@ -143,9 +137,9 @@ function useInitialAssignCardStep({policyID, selectedFeed}: UseInitialAssignCard
const {isOffline} = useNetwork();
const policy = usePolicy(policyID);
- const {currencyList} = useCurrencyList();
const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false});
+ const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true});
const [cardFeeds] = useCardFeeds(policyID);
const companyCards = getCompanyFeeds(cardFeeds);
@@ -155,19 +149,14 @@ function useInitialAssignCardStep({policyID, selectedFeed}: UseInitialAssignCard
const plaidAccessToken = feedData?.plaidAccessToken;
const hasImportedPlaidAccounts = useRef(false);
- /**
- * Gets the initial step and card data for the assignment flow.
- * @param cardName - The masked card number displayed to users
- * @param cardID - The identifier sent to backend (equals cardName for direct feeds)
- */
- const getInitialAssignCardStep = (cardName: string | undefined, cardID?: string): {initialStep: AssignCardStep; cardToAssign: Partial} | undefined => {
+ const getInitialAssignCardStep = (cardID: string | undefined): {initialStep: AssignCardStep; cardToAssign: Partial} | undefined => {
if (!selectedFeed) {
return;
}
const cardToAssign: Partial = {
bankName,
- cardName,
+ cardNumber: cardID,
encryptedCardNumber: cardID,
};
@@ -206,7 +195,7 @@ function useInitialAssignCardStep({policyID, selectedFeed}: UseInitialAssignCard
cardToAssign.email = userEmail;
const personalDetails = getPersonalDetailByEmail(userEmail);
const memberName = personalDetails?.firstName ? personalDetails.firstName : personalDetails?.login;
- cardToAssign.customCardName = getDefaultCardName(memberName);
+ cardToAssign.cardName = `${memberName}'s card`;
return {
initialStep: CONST.COMPANY_CARD.STEP.CONFIRMATION,
diff --git a/src/hooks/useBulkPayOptions.ts b/src/hooks/useBulkPayOptions.ts
index 29b70bea45be..8beeb9099f2f 100644
--- a/src/hooks/useBulkPayOptions.ts
+++ b/src/hooks/useBulkPayOptions.ts
@@ -1,4 +1,5 @@
import truncate from 'lodash/truncate';
+import {useCallback, useMemo} from 'react';
import type {TupleToUnion} from 'type-fest';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {BankAccountMenuItem} from '@components/Search/types';
@@ -98,65 +99,72 @@ function useBulkPayOptions({
});
}
- const getPaymentSubitems = (payAsBusiness: boolean) => {
- const requiredAccountType = payAsBusiness ? CONST.BANK_ACCOUNT.TYPE.BUSINESS : CONST.BANK_ACCOUNT.TYPE.PERSONAL;
- return formattedPaymentMethods
- .filter((method) => {
- const accountData = method?.accountData as AccountData;
- return accountData?.type === requiredAccountType;
- })
- .map((formattedPaymentMethod) => ({
- text: formattedPaymentMethod?.title ?? '',
- description: formattedPaymentMethod?.description ?? '',
- icon: formattedPaymentMethod?.icon,
- shouldUpdateSelectedIndex: true,
- iconStyles: formattedPaymentMethod?.iconStyles,
- iconHeight: formattedPaymentMethod?.iconSize,
- iconWidth: formattedPaymentMethod?.iconSize,
- key: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
- additionalData: {
- payAsBusiness,
- methodID: formattedPaymentMethod.methodID,
- paymentMethod: formattedPaymentMethod.accountType,
- },
- }));
- };
+ const getPaymentSubitems = useCallback(
+ (payAsBusiness: boolean) => {
+ const requiredAccountType = payAsBusiness ? CONST.BANK_ACCOUNT.TYPE.BUSINESS : CONST.BANK_ACCOUNT.TYPE.PERSONAL;
+
+ return formattedPaymentMethods
+ .filter((method) => {
+ const accountData = method?.accountData as AccountData;
+ return accountData?.type === requiredAccountType;
+ })
+ .map((formattedPaymentMethod) => ({
+ text: formattedPaymentMethod?.title ?? '',
+ description: formattedPaymentMethod?.description ?? '',
+ icon: formattedPaymentMethod?.icon,
+ shouldUpdateSelectedIndex: true,
+ iconStyles: formattedPaymentMethod?.iconStyles,
+ iconHeight: formattedPaymentMethod?.iconSize,
+ iconWidth: formattedPaymentMethod?.iconSize,
+ key: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
+ additionalData: {
+ payAsBusiness,
+ methodID: formattedPaymentMethod.methodID,
+ paymentMethod: formattedPaymentMethod.accountType,
+ },
+ }));
+ },
+ [formattedPaymentMethods],
+ );
const latestBankItems = getLatestBankAccountItem();
const personalBankAccountList = formattedPaymentMethods.filter((ba) => (ba.accountData as AccountData)?.type === CONST.BANK_ACCOUNT.TYPE.PERSONAL);
- let bulkPayButtonOptions;
- if (!selectedReportID || !selectedPolicyID) {
- bulkPayButtonOptions = undefined;
- } else if (onlyShowPayElsewhere) {
- bulkPayButtonOptions = [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]];
- } else {
- bulkPayButtonOptions = [];
+ const bulkPayButtonOptions = useMemo(() => {
+ const buttonOptions = [];
+
+ if (!selectedReportID || !selectedPolicyID) {
+ return undefined;
+ }
+
+ if (onlyShowPayElsewhere) {
+ return [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]];
+ }
if (shouldShowBusinessBankAccountOptions) {
- bulkPayButtonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]);
+ buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]);
}
if (canUseWallet) {
if (personalBankAccountList.length && canUsePersonalBankAccount) {
- bulkPayButtonOptions.push({
+ buttonOptions.push({
text: translate('iou.settleWallet', {formattedAmount: ''}),
key: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT,
icon: icons.Wallet,
});
} else if (canUsePersonalBankAccount) {
- bulkPayButtonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]);
+ buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT]);
}
if (activeAdminPolicies.length === 0 && !isPersonalOnlyOption) {
- bulkPayButtonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]);
+ buttonOptions.push(paymentMethods[CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT]);
}
}
if ((hasMultiplePolicies || hasSinglePolicy) && canUseWallet && !isPersonalOnlyOption) {
for (const activePolicy of activeAdminPolicies) {
const policyName = activePolicy.name;
- bulkPayButtonOptions.push({
+ buttonOptions.push({
text: translate('iou.payWithPolicy', {policyName: truncate(policyName, {length: CONST.ADDITIONAL_ALLOWED_CHARACTERS}), formattedAmount: ''}),
icon: icons.Building,
key: activePolicy.id,
@@ -166,7 +174,7 @@ function useBulkPayOptions({
}
if (shouldShowPayElsewhereOption) {
- bulkPayButtonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]);
+ buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]);
}
if (isInvoiceReport) {
@@ -195,13 +203,13 @@ function useBulkPayOptions({
};
if (isIndividualInvoiceRoomUtil(chatReport)) {
- bulkPayButtonOptions.push({
+ buttonOptions.push({
text: translate('iou.settlePersonal', {formattedAmount}),
icon: icons.User,
backButtonText: translate('iou.individual'),
subMenuItems: getInvoicesOptions(false),
});
- bulkPayButtonOptions.push({
+ buttonOptions.push({
text: translate('iou.settleBusiness', {formattedAmount}),
icon: icons.Building,
backButtonText: translate('iou.business'),
@@ -209,10 +217,34 @@ function useBulkPayOptions({
});
} else {
// If there is pay as business option, we should show the submenu items instead.
- bulkPayButtonOptions.push(...getInvoicesOptions(true));
+ buttonOptions.push(...getInvoicesOptions(true));
}
}
- }
+
+ return buttonOptions;
+ }, [
+ translate,
+ icons.Building,
+ icons.User,
+ selectedReportID,
+ selectedPolicyID,
+ shouldShowBusinessBankAccountOptions,
+ canUseWallet,
+ hasMultiplePolicies,
+ hasSinglePolicy,
+ isPersonalOnlyOption,
+ shouldShowPayElsewhereOption,
+ isInvoiceReport,
+ paymentMethods,
+ personalBankAccountList.length,
+ canUsePersonalBankAccount,
+ activeAdminPolicies,
+ currency,
+ chatReport,
+ getPaymentSubitems,
+ formattedAmount,
+ onlyShowPayElsewhere,
+ ]);
return {
bulkPayButtonOptions,
diff --git a/src/hooks/useCardFeedsForDisplay.ts b/src/hooks/useCardFeedsForDisplay.ts
index 0248d41d9f6f..ec83214640f2 100644
--- a/src/hooks/useCardFeedsForDisplay.ts
+++ b/src/hooks/useCardFeedsForDisplay.ts
@@ -1,3 +1,4 @@
+import {useMemo} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import {getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy} from '@libs/CardFeedUtils';
import {filterPersonalCards, isCustomFeed, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils';
@@ -26,42 +27,40 @@ const useCardFeedsForDisplay = () => {
canBeMissing: true,
});
- const cardFeedsByPolicy = getCardFeedsForDisplayPerPolicy(allFeeds);
+ const cardFeedsByPolicy = useMemo(() => getCardFeedsForDisplayPerPolicy(allFeeds), [allFeeds]);
+
+ const defaultCardFeed = useMemo(() => {
+ if (!eligiblePoliciesIDs) {
+ return undefined;
+ }
- let defaultCardFeed;
- if (eligiblePoliciesIDs) {
// Prioritize the active policy if eligible
if (activePolicyID && eligiblePoliciesIDs.has(activePolicyID)) {
const policyCardFeeds = cardFeedsByPolicy[activePolicyID];
if (policyCardFeeds?.length) {
- defaultCardFeed = policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0);
+ return policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0);
}
}
- if (!defaultCardFeed) {
- // If the active policy doesn't have card feeds, use the first eligible policy that does
- for (const eligiblePolicyID of eligiblePoliciesIDs) {
- const policyCardFeeds = cardFeedsByPolicy[eligiblePolicyID];
- if (policyCardFeeds?.length) {
- defaultCardFeed = policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0);
- break;
- }
+ // If the active policy doesn't have card feeds, use the first eligible policy that does
+ for (const eligiblePolicyID of eligiblePoliciesIDs) {
+ const policyCardFeeds = cardFeedsByPolicy[eligiblePolicyID];
+ if (policyCardFeeds?.length) {
+ return policyCardFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0);
}
}
- if (!defaultCardFeed) {
- // Commercial feeds don't have preferred policies, so we need to include these in the list
- const commercialFeeds = Object.values(cardFeedsByPolicy)
- .flat()
- .filter((feed) => !isCustomFeed(feed.name as CompanyCardFeed));
+ // Commercial feeds don't have preferred policies, so we need to include these in the list
+ const commercialFeeds = Object.values(cardFeedsByPolicy)
+ .flat()
+ .filter((feed) => !isCustomFeed(feed.name as CompanyCardFeed));
- defaultCardFeed = commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0);
- }
- }
+ return commercialFeeds.sort((a, b) => localeCompare(a.name, b.name)).at(0);
+ }, [eligiblePoliciesIDs, activePolicyID, cardFeedsByPolicy, localeCompare]);
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
- const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList);
+ const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
const expensifyCards = getCardFeedsForDisplay({}, allCards);
const defaultExpensifyCard = Object.values(expensifyCards)?.at(0);
diff --git a/src/hooks/useCurrencyList.ts b/src/hooks/useCurrencyList.ts
deleted file mode 100644
index 01d89fc95324..000000000000
--- a/src/hooks/useCurrencyList.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import {useContext} from 'react';
-import type {CurrencyListContextProps} from '@components/CurrencyListContextProvider';
-import {CurrencyListContext} from '@components/CurrencyListContextProvider';
-
-export default function useCurrencyList(): CurrencyListContextProps {
- return useContext(CurrencyListContext);
-}
diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts
index 6cb199928627..c98dbafbe8b2 100644
--- a/src/hooks/useDeleteTransactions.ts
+++ b/src/hooks/useDeleteTransactions.ts
@@ -1,8 +1,7 @@
import {useCallback} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
-import {deleteMoneyRequest, getIOURequestPolicyID, initSplitExpenseItemData} from '@libs/actions/IOU';
+import {deleteMoneyRequest, getIOURequestPolicyID, initSplitExpenseItemData, updateSplitTransactions} from '@libs/actions/IOU';
import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate';
-import {updateSplitTransactions} from '@libs/actions/IOU/Split';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getChildTransactions, getOriginalTransactionWithSplitInfo} from '@libs/TransactionUtils';
@@ -37,7 +36,6 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
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 [iouReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(report?.reportID)}`, {canBeMissing: true});
const {isBetaEnabled} = usePermissions();
const archivedReportsIdSet = useArchivedReportsIdSet();
@@ -151,7 +149,6 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
transactionViolations,
policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
quickAction,
- iouReportNextStep,
});
}
@@ -176,7 +173,6 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
transactionIDsPendingDeletion: deletedTransactionIDs,
selectedTransactionIDs: transactionIDs,
hash: currentSearchHash,
- allTransactionViolationsParam: transactionViolations,
});
deletedTransactionIDs.push(transactionID);
if (action.childReportID) {
@@ -187,21 +183,20 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
return Array.from(deletedTransactionThreadReportIDs);
},
[
+ reportActions,
+ allTransactions,
+ allReports,
+ report,
allPolicyRecentlyUsedCategories,
allReportNameValuePairs,
- allReports,
- allTransactions,
- archivedReportsIdSet,
- currentUserPersonalDetails,
- iouReportNextStep,
- isBetaEnabled,
- policy,
policyCategories,
+ policy,
+ isBetaEnabled,
+ currentUserPersonalDetails,
+ transactionViolations,
policyRecentlyUsedCurrencies,
quickAction,
- report,
- reportActions,
- transactionViolations,
+ archivedReportsIdSet,
],
);
diff --git a/src/hooks/useFilterFormValues.tsx b/src/hooks/useFilterFormValues.tsx
index bd355d966b6d..f26fa41f8d2c 100644
--- a/src/hooks/useFilterFormValues.tsx
+++ b/src/hooks/useFilterFormValues.tsx
@@ -6,13 +6,12 @@ import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SearchAdvancedFiltersForm} from '@src/types/form';
+import type {CurrencyList} from '@src/types/onyx';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
-import useCurrencyList from './useCurrencyList';
import useOnyx from './useOnyx';
const useFilterFormValues = (queryJSON?: SearchQueryJSON) => {
const personalDetails = usePersonalDetails();
- const {currencyList} = useCurrencyList();
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true});
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
@@ -20,6 +19,7 @@ const useFilterFormValues = (queryJSON?: SearchQueryJSON) => {
const [policyTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {canBeMissing: true});
const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES, {canBeMissing: true});
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true});
+ const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true});
const taxRates = getAllTaxRates(policies);
const allCards = mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList);
diff --git a/src/hooks/useIndicatorStatus.ts b/src/hooks/useIndicatorStatus.ts
index 3d5fc0021f53..17b3cf6ba191 100644
--- a/src/hooks/useIndicatorStatus.ts
+++ b/src/hooks/useIndicatorStatus.ts
@@ -1,8 +1,19 @@
+import {useMemo} from 'react';
import type {ValueOf} from 'type-fest';
-import type CONST from '@src/CONST';
-import useNavigationTabBarIndicatorChecks from './useNavigationTabBarIndicatorChecks';
+import {isConnectionInProgress} from '@libs/actions/connections';
+import {shouldShowQBOReimbursableExportDestinationAccountError} from '@libs/actions/connections/QuickbooksOnline';
+import {hasPaymentMethodError} from '@libs/actions/PaymentMethods';
+import {checkIfFeedConnectionIsBroken, hasPendingExpensifyCardAction} from '@libs/CardUtils';
+import {shouldShowCustomUnitsError, shouldShowEmployeeListError, shouldShowPolicyError, shouldShowSyncError} from '@libs/PolicyUtils';
+import {hasSubscriptionGreenDotInfo, hasSubscriptionRedDotError} from '@libs/SubscriptionUtils';
+import {hasLoginListError, hasLoginListInfo} from '@libs/UserUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useOnyx from './useOnyx';
import useTheme from './useTheme';
+type IndicatorStatus = ValueOf;
+
type IndicatorStatusResult = {
indicatorColor: string;
status: ValueOf | undefined;
@@ -11,14 +22,84 @@ type IndicatorStatusResult = {
function useIndicatorStatus(): IndicatorStatusResult {
const theme = useTheme();
+ const [allConnectionSyncProgresses] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS, {canBeMissing: true});
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true});
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true});
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true});
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true});
+ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true});
+ const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true});
+ const [allCards] = useOnyx(`${ONYXKEYS.CARD_LIST}`, {canBeMissing: true});
+ const [stripeCustomerId] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, {canBeMissing: true});
+ const [retryBillingSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {canBeMissing: true});
+ const [billingDisputePending] = useOnyx(ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING, {canBeMissing: true});
+ const [retryBillingFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, {canBeMissing: true});
+ const [billingStatus] = useOnyx(ONYXKEYS.NVP_PRIVATE_BILLING_STATUS, {canBeMissing: true});
+ const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(allCards, CONST.EXPENSIFY_CARD.BANK);
+ const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
+
+ // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
+ // those should be cleaned out before doing any error checking
+ const cleanPolicies = useMemo(() => Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)), [policies]);
+
+ const policyErrors = {
+ [CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS]: Object.values(cleanPolicies).find(shouldShowPolicyError),
+ [CONST.INDICATOR_STATUS.HAS_CUSTOM_UNITS_ERROR]: Object.values(cleanPolicies).find(shouldShowCustomUnitsError),
+ [CONST.INDICATOR_STATUS.HAS_EMPLOYEE_LIST_ERROR]: Object.values(cleanPolicies).find(shouldShowEmployeeListError),
+ [CONST.INDICATOR_STATUS.HAS_SYNC_ERRORS]: Object.values(cleanPolicies).find((cleanPolicy) =>
+ shouldShowSyncError(cleanPolicy, isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy)),
+ ),
+ [CONST.INDICATOR_STATUS.HAS_QBO_EXPORT_ERROR]: Object.values(cleanPolicies).find(shouldShowQBOReimbursableExportDestinationAccountError),
+ };
- const {accountStatus, infoStatus, policyStatus, policyIDWithErrors} = useNavigationTabBarIndicatorChecks();
+ // All of the error & info-checking methods are put into an array. This is so that using _.some() will return
+ // early as soon as the first error / info condition is returned. This makes the checks very efficient since
+ // we only care if a single error / info condition exists anywhere.
+ const errorChecking: Partial> = {
+ [CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0,
+ [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList, allCards),
+ ...(Object.fromEntries(Object.entries(policyErrors).map(([error, policy]) => [error, !!policy])) as Record),
+ [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(
+ stripeCustomerId,
+ retryBillingSuccessful,
+ billingDisputePending,
+ retryBillingFailed,
+ fundList,
+ billingStatus,
+ ),
+ [CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
+ [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && hasLoginListError(loginList),
+ // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
+ [CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
+ [CONST.INDICATOR_STATUS.HAS_CARD_CONNECTION_ERROR]: hasBrokenFeedConnection,
+ [CONST.INDICATOR_STATUS.HAS_PHONE_NUMBER_ERROR]: !!privatePersonalDetails?.errorFields?.phoneNumber,
+ };
- const errorStatus = accountStatus ?? policyStatus;
- const status = errorStatus ?? infoStatus;
- const indicatorColor = errorStatus ? theme.danger : theme.success;
+ const infoChecking: Partial> = {
+ [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && hasLoginListInfo(loginList, session?.email),
+ [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(
+ stripeCustomerId,
+ retryBillingSuccessful,
+ billingDisputePending,
+ retryBillingFailed,
+ fundList,
+ billingStatus,
+ ),
+ [CONST.INDICATOR_STATUS.HAS_PENDING_CARD_INFO]: hasPendingExpensifyCardAction(allCards, privatePersonalDetails),
+ };
+
+ const [error] = Object.entries(errorChecking).find(([, value]) => value) ?? [];
+ const [info] = Object.entries(infoChecking).find(([, value]) => value) ?? [];
+
+ const status = (error ?? info) as IndicatorStatus | undefined;
+ const policyIDWithErrors = Object.values(policyErrors).find(Boolean)?.id;
+ const indicatorColor = error ? theme.danger : theme.success;
return {indicatorColor, status, policyIDWithErrors};
}
export default useIndicatorStatus;
+
+export type {IndicatorStatus};
diff --git a/src/hooks/useIsPaidPolicyAdmin.ts b/src/hooks/useIsPaidPolicyAdmin.ts
index dcbef1526eaf..fcdfdc8e11dc 100644
--- a/src/hooks/useIsPaidPolicyAdmin.ts
+++ b/src/hooks/useIsPaidPolicyAdmin.ts
@@ -1,3 +1,4 @@
+import {useCallback} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -11,9 +12,12 @@ import useOnyx from './useOnyx';
function useIsPaidPolicyAdmin() {
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
- const isUserPaidPolicyAdminSelector = (policies: OnyxCollection) => {
- return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login));
- };
+ const isUserPaidPolicyAdminSelector = useCallback(
+ (policies: OnyxCollection) => {
+ return Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserPersonalDetails.login));
+ },
+ [currentUserPersonalDetails?.login],
+ );
const [isCurrentUserPolicyAdmin = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
canBeMissing: true,
diff --git a/src/hooks/useMergeTransactions.ts b/src/hooks/useMergeTransactions.ts
index b82f1afcb742..d33eb28b3f80 100644
--- a/src/hooks/useMergeTransactions.ts
+++ b/src/hooks/useMergeTransactions.ts
@@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import {useSearchContext} from '@components/Search/SearchContext';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getTransactionFromMergeTransaction} from '@libs/MergeTransactionUtils';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {MergeTransaction, Report, SearchResults, Transaction} from '@src/types/onyx';
import useOnyx from './useOnyx';
@@ -36,7 +37,9 @@ function getTransaction(
}
function useMergeTransactions({mergeTransaction}: UseMergeTransactionsProps): UseMergeTransactionsReturn {
- const {currentSearchHash, currentSearchResults} = useSearchContext();
+ const searchContext = useSearchContext();
+ const searchHash = searchContext?.currentSearchHash ?? CONST.DEFAULT_NUMBER_ID;
+ const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, {canBeMissing: true});
const [onyxTargetTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(mergeTransaction?.targetTransactionID)}`, {
canBeMissing: true,
@@ -56,7 +59,7 @@ function useMergeTransactions({mergeTransaction}: UseMergeTransactionsProps): Us
});
// If we're on search and main collection reports are not available, get them from the search snapshot
- if (currentSearchHash && currentSearchResults?.data) {
+ if (searchHash && currentSearchResults?.data) {
targetTransactionReport = targetTransactionReport ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${targetTransaction?.reportID}`];
sourceTransactionReport = sourceTransactionReport ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${sourceTransaction?.reportID}`];
}
diff --git a/src/hooks/useMultipleSnapshots.ts b/src/hooks/useMultipleSnapshots.ts
deleted file mode 100644
index c6b6ee28cdf8..000000000000
--- a/src/hooks/useMultipleSnapshots.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import {useMemo} from 'react';
-import type {OnyxCollection} from 'react-native-onyx';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {SearchResults} from '@src/types/onyx';
-import {getEmptyObject} from '@src/types/utils/EmptyObject';
-import useOnyx from './useOnyx';
-
-type SnapshotMap = Record;
-
-/**
- * Hook to subscribe to multiple search snapshots by their hashes.
- * Returns an object mapping each hash to its corresponding SearchResults.
- */
-function useMultipleSnapshots(hashes: string[]): SnapshotMap {
- const selector = useMemo(() => {
- return (snapshots: OnyxCollection): SnapshotMap => {
- if (!snapshots || hashes.length === 0) {
- return {};
- }
-
- const result: SnapshotMap = {};
- for (const hash of hashes) {
- const snapshot = snapshots[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`];
- if (snapshot) {
- result[hash] = snapshot;
- }
- }
- return result;
- };
- }, [hashes]);
-
- const [snapshotMap = getEmptyObject()] = useOnyx(ONYXKEYS.COLLECTION.SNAPSHOT, {
- canBeMissing: true,
- selector,
- });
-
- return snapshotMap;
-}
-
-export type {SnapshotMap};
-
-export default useMultipleSnapshots;
diff --git a/src/hooks/useNavigationTabBarIndicatorChecks.ts b/src/hooks/useNavigationTabBarIndicatorChecks.ts
deleted file mode 100644
index 16448379a732..000000000000
--- a/src/hooks/useNavigationTabBarIndicatorChecks.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import type {ValueOf} from 'type-fest';
-import {isConnectionInProgress} from '@libs/actions/connections';
-import {shouldShowQBOReimbursableExportDestinationAccountError} from '@libs/actions/connections/QuickbooksOnline';
-import {hasPaymentMethodError} from '@libs/actions/PaymentMethods';
-import {hasPartiallySetupBankAccount} from '@libs/BankAccountUtils';
-import {checkIfFeedConnectionIsBroken, hasPendingExpensifyCardAction} from '@libs/CardUtils';
-import {getUberConnectionErrorDirectlyFromPolicy, shouldShowCustomUnitsError, shouldShowEmployeeListError, shouldShowPolicyError, shouldShowSyncError} from '@libs/PolicyUtils';
-import {hasSubscriptionGreenDotInfo, hasSubscriptionRedDotError} from '@libs/SubscriptionUtils';
-import {hasLoginListError, hasLoginListInfo} from '@libs/UserUtils';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy} from '@src/types/onyx';
-import useOnyx from './useOnyx';
-
-type IndicatorStatus = ValueOf;
-
-type NavigationTabBarChecksResult = {
- accountStatus: IndicatorStatus | undefined;
- policyStatus: IndicatorStatus | undefined;
- infoStatus: IndicatorStatus | undefined;
- policyIDWithErrors: string | undefined;
-};
-
-function useNavigationTabBarIndicatorChecks(): NavigationTabBarChecksResult {
- const [allConnectionSyncProgresses] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS, {canBeMissing: true});
- const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
- const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
- const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true});
- const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true});
- const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true});
- const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true});
- const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true});
- const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true});
- const [allCards] = useOnyx(`${ONYXKEYS.CARD_LIST}`, {canBeMissing: true});
- const [stripeCustomerId] = useOnyx(ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, {canBeMissing: true});
- const [retryBillingSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {canBeMissing: true});
- const [billingDisputePending] = useOnyx(ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING, {canBeMissing: true});
- const [retryBillingFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, {canBeMissing: true});
- const [billingStatus] = useOnyx(ONYXKEYS.NVP_PRIVATE_BILLING_STATUS, {canBeMissing: true});
- const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
-
- const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(allCards, CONST.EXPENSIFY_CARD.BANK);
-
- // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
- // those should be cleaned out before doing any error checking
- const cleanPolicies = Object.values(policies ?? {}).filter((policy) => policy?.id);
-
- const policyChecks: Partial> = {
- [CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS]: cleanPolicies.find(shouldShowPolicyError),
- [CONST.INDICATOR_STATUS.HAS_CUSTOM_UNITS_ERROR]: cleanPolicies.find(shouldShowCustomUnitsError),
- [CONST.INDICATOR_STATUS.HAS_EMPLOYEE_LIST_ERROR]: cleanPolicies.find(shouldShowEmployeeListError),
- [CONST.INDICATOR_STATUS.HAS_SYNC_ERRORS]: cleanPolicies.find((cleanPolicy) =>
- shouldShowSyncError(cleanPolicy, isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy)),
- ),
- [CONST.INDICATOR_STATUS.HAS_QBO_EXPORT_ERROR]: cleanPolicies.find(shouldShowQBOReimbursableExportDestinationAccountError),
- [CONST.INDICATOR_STATUS.HAS_UBER_CREDENTIALS_ERROR]: cleanPolicies.find(getUberConnectionErrorDirectlyFromPolicy),
- };
-
- // All the error & info-checking methods are put into an array. This is so that using _.some() will return
- // early as soon as the first error / info condition is returned. This makes the checks very efficient since
- // we only care if a single error / info condition exists anywhere.
- const accountChecks: Partial> = {
- [CONST.INDICATOR_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0,
- [CONST.INDICATOR_STATUS.HAS_PAYMENT_METHOD_ERROR]: hasPaymentMethodError(bankAccountList, fundList, allCards),
- [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS]: hasSubscriptionRedDotError(
- stripeCustomerId,
- retryBillingSuccessful,
- billingDisputePending,
- retryBillingFailed,
- fundList,
- billingStatus,
- ),
- [CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0,
- [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && hasLoginListError(loginList),
- // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead)
- [CONST.INDICATOR_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID,
- [CONST.INDICATOR_STATUS.HAS_PHONE_NUMBER_ERROR]: !!privatePersonalDetails?.errorFields?.phoneNumber,
- [CONST.INDICATOR_STATUS.HAS_CARD_CONNECTION_ERROR]: hasBrokenFeedConnection,
- };
-
- const infoChecks: Partial> = {
- [CONST.INDICATOR_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && hasLoginListInfo(loginList, session?.email),
- [CONST.INDICATOR_STATUS.HAS_PENDING_CARD_INFO]: hasPendingExpensifyCardAction(allCards, privatePersonalDetails),
- [CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO]: hasSubscriptionGreenDotInfo(
- stripeCustomerId,
- retryBillingSuccessful,
- billingDisputePending,
- retryBillingFailed,
- fundList,
- billingStatus,
- ),
- [CONST.INDICATOR_STATUS.HAS_PARTIALLY_SETUP_BANK_ACCOUNT_INFO]: hasPartiallySetupBankAccount(bankAccountList),
- };
-
- const [accountStatus] = Object.entries(accountChecks).find(([, value]) => value) ?? [];
- const [policyStatus] = Object.entries(policyChecks).find(([, value]) => value) ?? [];
- const [infoStatus] = Object.entries(infoChecks).find(([, value]) => value) ?? [];
-
- const policyIDWithErrors = Object.values(policyChecks).find(Boolean)?.id;
-
- return {
- accountStatus: accountStatus as IndicatorStatus | undefined,
- policyStatus: policyStatus as IndicatorStatus | undefined,
- infoStatus: infoStatus as IndicatorStatus | undefined,
- policyIDWithErrors,
- };
-}
-
-export default useNavigationTabBarIndicatorChecks;
-
-export type {IndicatorStatus};
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 5b3009214593..2f1134ffd8a8 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -38,6 +38,7 @@ function useOnboardingFlowRouter() {
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true, selector: emailSelector});
const isLoggingInAsNewSessionUser = isLoggingInAsNewUser(currentUrl, sessionEmail);
const startedOnboardingFlowRef = useRef(false);
+ const started2FAFlowRef = useRef(false);
const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {
selector: tryNewDotOnyxSelector,
canBeMissing: true,
@@ -78,6 +79,16 @@ function useOnboardingFlowRouter() {
return;
}
+ if (shouldShowRequire2FAPage) {
+ if (started2FAFlowRef.current) {
+ startedOnboardingFlowRef.current = false;
+ return;
+ }
+ started2FAFlowRef.current = true;
+ Navigation.navigate(ROUTES.REQUIRE_TWO_FACTOR_AUTH);
+ return;
+ }
+
if (hasBeenAddedToNudgeMigration && !isProductTrainingElementDismissed('migratedUserWelcomeModal', dismissedProductTraining)) {
const navigationState = navigationRef.getRootState();
const lastRoute = navigationState.routes.at(-1);
@@ -162,6 +173,7 @@ function useOnboardingFlowRouter() {
currentOnboardingPurposeSelected,
onboardingInitialPath,
isOnboardingLoading,
+ shouldShowRequire2FAPage,
]);
return {
diff --git a/src/hooks/useOutstandingReports.ts b/src/hooks/useOutstandingReports.ts
deleted file mode 100644
index 1ce5855d8b7d..000000000000
--- a/src/hooks/useOutstandingReports.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import {getOutstandingReportsForUser, isSelfDM} from '@libs/ReportUtils';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import {createPoliciesSelector} from '@src/selectors/Policy';
-import type {Policy} from '@src/types/onyx';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import useOnyx from './useOnyx';
-
-const policyIdSelector = (policy: OnyxEntry) => policy?.id;
-
-const policiesSelector = (policies: OnyxCollection) => createPoliciesSelector(policies, policyIdSelector);
-
-export default function useOutstandingReports(selectedReportID: string | undefined, selectedPolicyID: string | undefined, ownerAccountID: number | undefined, isEditing: boolean) {
- const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true});
- const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true});
- const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policiesSelector, canBeMissing: false});
- const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true});
- const [selectedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`, {canBeMissing: true});
-
- // Early return if no reports are available to prevent useless loop
- if (!outstandingReportsByPolicyID || isEmptyObject(outstandingReportsByPolicyID)) {
- return [];
- }
-
- if (!selectedPolicyID || selectedPolicyID === personalPolicyID || isSelfDM(selectedReport)) {
- return Object.values(allPoliciesID ?? {})
- .filter((policyID) => personalPolicyID !== policyID)
- .flatMap((policyID) => {
- if (!policyID) {
- return [];
- }
- const reports = getOutstandingReportsForUser(
- policyID,
- ownerAccountID,
- outstandingReportsByPolicyID?.[policyID ?? CONST.DEFAULT_NUMBER_ID] ?? {},
- reportNameValuePairs,
- isEditing,
- );
-
- return reports;
- });
- }
-
- return getOutstandingReportsForUser(selectedPolicyID, ownerAccountID, outstandingReportsByPolicyID?.[selectedPolicyID ?? CONST.DEFAULT_NUMBER_ID] ?? {}, reportNameValuePairs, isEditing);
-}
diff --git a/src/hooks/usePaymentOptions.ts b/src/hooks/usePaymentOptions.ts
index a397cf284bcc..c260240d767d 100644
--- a/src/hooks/usePaymentOptions.ts
+++ b/src/hooks/usePaymentOptions.ts
@@ -68,11 +68,10 @@ function usePaymentOptions({
// The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here.
// eslint-disable-next-line rulesdir/no-default-id-values
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true});
- const [conciergeReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true});
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true});
const hasActivatedWallet = ([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM] as string[]).includes(userWallet?.tierName ?? '');
const policyEmployeeAccountIDs = getPolicyEmployeeAccountIDs(policy, accountID);
- const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(chatReport, policyEmployeeAccountIDs, policyID, conciergeReportID) : false;
+ const reportBelongsToWorkspace = policyID ? doesReportBelongToWorkspace(chatReport, policyEmployeeAccountIDs, policyID) : false;
const policyIDKey = reportBelongsToWorkspace ? policyID : CONST.POLICY.ID_FAKE;
const lastPaymentMethodSelector = useCallback(
(paymentMethod: OnyxEntry) => {
diff --git a/src/hooks/usePolicyForTransaction.ts b/src/hooks/usePolicyForTransaction.ts
deleted file mode 100644
index 31f33c9b5c81..000000000000
--- a/src/hooks/usePolicyForTransaction.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import {isExpenseUnreported} from '@libs/TransactionUtils';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy, Report, Transaction} from '@src/types/onyx';
-import useOnyx from './useOnyx';
-import usePolicyForMovingExpenses from './usePolicyForMovingExpenses';
-
-type UsePolicyForTransactionParams = {
- /** The transaction to determine the policy for */
- transaction: OnyxEntry;
-
- /** The report associated with the transaction */
- report: OnyxEntry;
-
- /** The current action being performed */
- action: string;
-
- /** The type of IOU (split, track, submit, etc.) */
- iouType: string;
-};
-
-type UsePolicyForTransactionResult = {
- /** The policy to use for the transaction */
- policy: OnyxEntry;
-};
-
-function usePolicyForTransaction({transaction, report, action, iouType}: UsePolicyForTransactionParams): UsePolicyForTransactionResult {
- const {policyForMovingExpenses} = usePolicyForMovingExpenses();
- const isUnreportedExpense = isExpenseUnreported(transaction);
- const isCreatingTrackExpense = action === CONST.IOU.ACTION.CREATE && iouType === CONST.IOU.TYPE.TRACK;
-
- const [reportPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true});
- const policy = isUnreportedExpense || isCreatingTrackExpense ? policyForMovingExpenses : reportPolicy;
-
- return {policy};
-}
-
-export default usePolicyForTransaction;
diff --git a/src/hooks/usePrivateIsArchivedMap.ts b/src/hooks/usePrivateIsArchivedMap.ts
deleted file mode 100644
index 2a325ad474ca..000000000000
--- a/src/hooks/usePrivateIsArchivedMap.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type {PrivateIsArchivedMap} from '@selectors/ReportNameValuePairs';
-import {privateIsArchivedMapSelector} from '@selectors/ReportNameValuePairs';
-import ONYXKEYS from '@src/ONYXKEYS';
-import {getEmptyObject} from '@src/types/utils/EmptyObject';
-import useDeepCompareRef from './useDeepCompareRef';
-import useOnyx from './useOnyx';
-
-/**
- * Hook that returns a map of report IDs to their private_isArchived values
- */
-function usePrivateIsArchivedMap(): PrivateIsArchivedMap {
- const [privateIsArchivedMap = getEmptyObject()] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {
- canBeMissing: true,
- selector: privateIsArchivedMapSelector,
- });
-
- return useDeepCompareRef(privateIsArchivedMap) ?? {};
-}
-
-export default usePrivateIsArchivedMap;
diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts b/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts
deleted file mode 100644
index 505b966964c1..000000000000
--- a/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import type ResponsiveLayoutOnWideRHPResult from './types';
-
-// Super Wide and Wide RHPs are not displayed on native platforms.
-export default function useResponsiveLayoutOnWideRHP(): ResponsiveLayoutOnWideRHPResult {
- const responsiveLayoutValues = useResponsiveLayout();
-
- return {
- ...responsiveLayoutValues,
- isWideRHPDisplayedOnWideLayout: false,
- isSuperWideRHPDisplayedOnWideLayout: false,
- };
-}
diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/index.ts b/src/hooks/useResponsiveLayoutOnWideRHP/index.ts
deleted file mode 100644
index 8919d15bdfe6..000000000000
--- a/src/hooks/useResponsiveLayoutOnWideRHP/index.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import {useRoute} from '@react-navigation/native';
-import {useContext} from 'react';
-import {WideRHPContext} from '@components/WideRHPContextProvider';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import type ResponsiveLayoutOnWideRHPResult from './types';
-
-/**
- * useResponsiveLayoutOnWideRHP is a wrapper on useResponsiveLayout. shouldUseNarrowLayout on a wide screen is true when the screen is displayed in RHP.
- * In this hook this value is modified when the screen is displayed in Wide/Super Wide RHP, then in wide screen this value is false.
- */
-export default function useResponsiveLayoutOnWideRHP(): ResponsiveLayoutOnWideRHPResult {
- const route = useRoute();
-
- const responsiveLayoutValues = useResponsiveLayout();
-
- // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {isSmallScreenWidth, isInNarrowPaneModal} = responsiveLayoutValues;
-
- const {superWideRHPRouteKeys, wideRHPRouteKeys} = useContext(WideRHPContext);
-
- const isWideRHPDisplayedOnWideLayout = !isSmallScreenWidth && wideRHPRouteKeys.includes(route?.key);
-
- const isSuperWideRHPDisplayedOnWideLayout = !isSmallScreenWidth && superWideRHPRouteKeys.includes(route?.key);
-
- const shouldUseNarrowLayout = (isSmallScreenWidth || isInNarrowPaneModal) && !isSuperWideRHPDisplayedOnWideLayout && !isWideRHPDisplayedOnWideLayout;
-
- return {
- ...responsiveLayoutValues,
- shouldUseNarrowLayout,
- isWideRHPDisplayedOnWideLayout,
- isSuperWideRHPDisplayedOnWideLayout,
- };
-}
diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/types.ts b/src/hooks/useResponsiveLayoutOnWideRHP/types.ts
deleted file mode 100644
index ca889fb4b263..000000000000
--- a/src/hooks/useResponsiveLayoutOnWideRHP/types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
-
-type ResponsiveLayoutOnWideRHPResult = ResponsiveLayoutResult & {
- isWideRHPDisplayedOnWideLayout: boolean;
- isSuperWideRHPDisplayedOnWideLayout: boolean;
-};
-
-export default ResponsiveLayoutOnWideRHPResult;
diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts
index 87d08877018a..bb2826105dc4 100644
--- a/src/hooks/useSearchHighlightAndScroll.ts
+++ b/src/hooks/useSearchHighlightAndScroll.ts
@@ -1,5 +1,5 @@
import {useIsFocused} from '@react-navigation/native';
-import {useEffect, useRef, useState} from 'react';
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {SearchQueryJSON} from '@components/Search/types';
@@ -51,22 +51,36 @@ function useSearchHighlightAndScroll({
const initializedRef = useRef(false);
const hasPendingSearchRef = useRef(false);
const isChat = queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT;
- const searchResultsData = searchResults?.data;
- const prevTransactionsIDs = Object.keys(previousTransactions ?? {});
- const newTransactions: Transaction[] = [];
- if (prevTransactionsIDs.length > 0) {
- const previousIDs = new Set(prevTransactionsIDs);
+ const existingSearchResultIDs = useMemo(() => {
+ if (!searchResults?.data) {
+ return [];
+ }
+ return isChat ? extractReportActionIDsFromSearchResults(searchResults.data) : extractTransactionIDsFromSearchResults(searchResults.data);
+ }, [searchResults?.data, isChat]);
+
+ const newTransactions = useMemo(() => {
+ const previousTransactionsIDs = Object.keys(previousTransactions ?? {});
+
+ if (previousTransactionsIDs.length === 0) {
+ return [];
+ }
+
+ const previousIDs = new Set(previousTransactionsIDs);
+ const result: Transaction[] = [];
+
for (const [id, transaction] of Object.entries(transactions ?? {})) {
if (!previousIDs.has(id) && transaction) {
- newTransactions.push(transaction);
+ result.push(transaction);
}
}
- }
+
+ return result;
+ }, [previousTransactions, transactions]);
// Trigger search when a new report action is added while on chat or when a new transaction is added for the other search types.
useEffect(() => {
- const previousTransactionIDsLocal = Object.keys(previousTransactions ?? {});
+ const previousTransactionsIDs = Object.keys(previousTransactions ?? {});
const transactionsIDs = Object.keys(transactions ?? {});
const reportActionsIDs = Object.values(reportActions ?? {})
@@ -78,13 +92,13 @@ function useSearchHighlightAndScroll({
// Only proceed if we have previous data to compare against
// This prevents triggering on initial data load
- if ((previousTransactionIDsLocal.length === 0 && previousReportActionsIDs.length === 0) || searchTriggeredRef.current) {
+ if ((previousTransactionsIDs.length === 0 && previousReportActionsIDs.length === 0) || searchTriggeredRef.current) {
return;
}
- const previousTransactionsIDsSet = new Set(previousTransactionIDsLocal);
+ const previousTransactionsIDsSet = new Set(previousTransactionsIDs);
const previousReportActionsIDsSet = new Set(previousReportActionsIDs);
- const hasTransactionsIDsChange = transactionsIDs.length !== previousTransactionIDsLocal.length || transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id));
+ const hasTransactionsIDsChange = transactionsIDs.length !== previousTransactionsIDs.length || transactionsIDs.some((id) => !previousTransactionsIDsSet.has(id));
const hasReportActionsIDsChange = reportActionsIDs.some((id) => !previousReportActionsIDsSet.has(id));
// Check if there is a change in the transactions or report actions list
@@ -97,18 +111,14 @@ function useSearchHighlightAndScroll({
hasPendingSearchRef.current = false;
const newIDs = isChat ? reportActionsIDs : transactionsIDs;
- let currentSearchResultIDs: string[] = [];
- if (searchResultsData) {
- currentSearchResultIDs = isChat ? extractReportActionIDsFromSearchResults(searchResultsData) : extractTransactionIDsFromSearchResults(searchResultsData);
- }
- const existingSearchResultIDsSet = new Set(currentSearchResultIDs);
+ const existingSearchResultIDsSet = new Set(existingSearchResultIDs);
const hasAGenuinelyNewID = newIDs.some((id) => !existingSearchResultIDsSet.has(id));
// Only skip search if there are no new items AND search results aren't empty
// This ensures deletions that result in empty data still trigger search
- if (!hasAGenuinelyNewID && currentSearchResultIDs.length > 0) {
+ if (!hasAGenuinelyNewID && existingSearchResultIDs.length > 0) {
const newIDsSet = new Set(newIDs);
- const hasDeletedID = currentSearchResultIDs.some((id) => !newIDsSet.has(id));
+ const hasDeletedID = existingSearchResultIDs.some((id) => !newIDsSet.has(id));
if (!hasDeletedID) {
return;
}
@@ -116,7 +126,7 @@ function useSearchHighlightAndScroll({
// We only want to highlight new items if the addition of transactions or report actions triggered the search.
// This is because, on deletion of items, the backend sometimes returns old items in place of the deleted ones.
// We don't want to highlight these old items, even if they appear new in the current search results.
- hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionIDsLocal.length;
+ hasNewItemsRef.current = isChat ? reportActionsIDs.length > previousReportActionsIDs.length : transactionsIDs.length > previousTransactionsIDs.length;
// Set the flag indicating the search is triggered by the hook
triggeredByHookRef.current = true;
@@ -141,7 +151,8 @@ function useSearchHighlightAndScroll({
reportActions,
previousReportActions,
isChat,
- searchResultsData,
+ searchResults?.data,
+ existingSearchResultIDs,
isOffline,
searchResults?.search?.isLoading,
]);
@@ -156,14 +167,13 @@ function useSearchHighlightAndScroll({
// Initialize the set with existing IDs only once
useEffect(() => {
- if (initializedRef.current || !searchResultsData) {
+ if (initializedRef.current || !searchResults?.data) {
return;
}
- const initialIDs = isChat ? extractReportActionIDsFromSearchResults(searchResultsData) : extractTransactionIDsFromSearchResults(searchResultsData);
- highlightedIDs.current = new Set(initialIDs);
+ highlightedIDs.current = new Set(existingSearchResultIDs);
initializedRef.current = true;
- }, [searchResultsData, isChat]);
+ }, [searchResults?.data, isChat, existingSearchResultIDs]);
// Detect new items (transactions or report actions)
useEffect(() => {
@@ -225,48 +235,51 @@ function useSearchHighlightAndScroll({
/**
* Callback to handle scrolling to the new search result.
*/
- const handleSelectionListScroll = (data: SearchListItem[], ref: SelectionListHandle | null) => {
- // Early return if there's no ref, new transaction wasn't brought in by this hook
- // or there's no new search result key
- const newSearchResultKey = newSearchResultKeys?.values().next().value;
- if (!ref || !triggeredByHookRef.current || !newSearchResultKey) {
- return;
- }
-
- // Extract the transaction/report action ID from the newSearchResultKey
- const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, '');
-
- // Find the index of the new transaction/report action in the data array
- const indexOfNewItem = data.findIndex((item) => {
- if (isChat) {
- if ('reportActionID' in item && item.reportActionID === newID) {
- return true;
- }
- } else {
- // Handle TransactionListItemType
- if ('transactionID' in item && item.transactionID === newID) {
- return true;
- }
+ const handleSelectionListScroll = useCallback(
+ (data: SearchListItem[], ref: SelectionListHandle | null) => {
+ // Early return if there's no ref, new transaction wasn't brought in by this hook
+ // or there's no new search result key
+ const newSearchResultKey = newSearchResultKeys?.values().next().value;
+ if (!ref || !triggeredByHookRef.current || !newSearchResultKey) {
+ return;
+ }
- // Handle TransactionGroupListItemType with transactions array
- if ('transactions' in item && Array.isArray(item.transactions)) {
- return item.transactions.some((transaction) => transaction?.transactionID === newID);
+ // Extract the transaction/report action ID from the newSearchResultKey
+ const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, '');
+
+ // Find the index of the new transaction/report action in the data array
+ const indexOfNewItem = data.findIndex((item) => {
+ if (isChat) {
+ if ('reportActionID' in item && item.reportActionID === newID) {
+ return true;
+ }
+ } else {
+ // Handle TransactionListItemType
+ if ('transactionID' in item && item.transactionID === newID) {
+ return true;
+ }
+
+ // Handle TransactionGroupListItemType with transactions array
+ if ('transactions' in item && Array.isArray(item.transactions)) {
+ return item.transactions.some((transaction) => transaction?.transactionID === newID);
+ }
}
- }
- return false;
- });
+ return false;
+ });
- // Early return if the new item is not found in the data array
- if (indexOfNewItem <= 0) {
- return;
- }
+ // Early return if the new item is not found in the data array
+ if (indexOfNewItem <= 0) {
+ return;
+ }
- // Perform the scrolling action
- ref.scrollToIndex(indexOfNewItem);
- // Reset the trigger flag to prevent unintended future scrolls and highlights
- triggeredByHookRef.current = false;
- };
+ // Perform the scrolling action
+ ref.scrollToIndex(indexOfNewItem);
+ // Reset the trigger flag to prevent unintended future scrolls and highlights
+ triggeredByHookRef.current = false;
+ },
+ [newSearchResultKeys, isChat],
+ );
return {newSearchResultKeys, handleSelectionListScroll, newTransactions};
}
diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts
index 87b5eaefd18b..cd87176342b2 100644
--- a/src/hooks/useSearchSelector.base.ts
+++ b/src/hooks/useSearchSelector.base.ts
@@ -162,10 +162,8 @@ function useSearchSelectorBase({
const [maxResults, setMaxResults] = useState(maxResultsPerPage);
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true});
- const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true});
const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
- const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true});
const onListEndReached = useDebounce(
useCallback(() => {
@@ -197,10 +195,9 @@ function useSearchSelectorBase({
includeUserToInvite,
countryCode,
loginList,
- visibleReportActionsData,
});
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE:
- return getValidOptions(optionsWithContacts, allPolicies, draftComments, nvpDismissedProductTraining, loginList, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, loginList, {
betas: betas ?? [],
includeP2P: true,
includeSelectedOptions: false,
@@ -212,7 +209,7 @@ function useSearchSelectorBase({
includeUserToInvite,
});
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL:
- return getValidOptions(optionsWithContacts, allPolicies, draftComments, nvpDismissedProductTraining, loginList, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, loginList, {
...getValidOptionsConfig,
betas: betas ?? [],
searchString: computedSearchTerm,
@@ -224,7 +221,6 @@ function useSearchSelectorBase({
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_SHARE_LOG:
return getValidOptions(
optionsWithContacts,
- allPolicies,
draftComments,
nvpDismissedProductTraining,
loginList,
@@ -244,7 +240,7 @@ function useSearchSelectorBase({
countryCode,
);
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_SHARE_DESTINATION:
- return getValidOptions(optionsWithContacts, allPolicies, draftComments, nvpDismissedProductTraining, loginList, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, loginList, {
betas,
selectedOptions,
includeMultipleParticipantReports: true,
@@ -262,7 +258,7 @@ function useSearchSelectorBase({
includeUserToInvite,
});
case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_ATTENDEES:
- return getValidOptions(optionsWithContacts, allPolicies, draftComments, nvpDismissedProductTraining, loginList, {
+ return getValidOptions(optionsWithContacts, draftComments, nvpDismissedProductTraining, loginList, {
...getValidOptionsConfig,
betas: betas ?? [],
includeP2P: true,
@@ -284,7 +280,6 @@ function useSearchSelectorBase({
areOptionsInitialized,
searchContext,
optionsWithContacts,
- allPolicies,
draftComments,
nvpDismissedProductTraining,
betas,
@@ -299,7 +294,6 @@ function useSearchSelectorBase({
getValidOptionsConfig,
selectedOptions,
includeCurrentUser,
- visibleReportActionsData,
]);
const isOptionSelected = useMemo(() => {
diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts
index bd1980bfe565..6a640914a309 100644
--- a/src/hooks/useSelectedTransactionsActions.ts
+++ b/src/hooks/useSelectedTransactionsActions.ts
@@ -1,6 +1,4 @@
-import {useContext, useState} from 'react';
-import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
-import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
+import {useCallback, useMemo, useState} from 'react';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {useSearchContext} from '@components/Search/SearchContext';
import {initSplitExpense} from '@libs/actions/IOU';
@@ -68,7 +66,6 @@ function useSelectedTransactionsActions({
isOnSearch?: boolean;
}) {
const {isOffline} = useNetworkWithOfflineStatus();
- const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext);
const {selectedTransactionIDs, clearSelectedTransactions, currentSearchHash, selectedTransactions: selectedTransactionsMeta} = useSearchContext();
const allTransactions = useAllTransactions();
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false});
@@ -82,45 +79,60 @@ function useSelectedTransactionsActions({
const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs);
const isReportArchived = useReportIsArchived(report?.reportID);
const {deleteTransactions} = useDeleteTransactions({report, reportActions, policy});
- const {login, accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
- const selectedTransactionsList = selectedTransactionIDs.reduce((acc, transactionID) => {
- const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- if (transaction) {
- acc.push(transaction);
- }
- return acc;
- }, [] as Transaction[]);
-
- const knownOwnerIDs = new Set();
- let hasUnknownOwner = false;
-
- for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) {
- const ownerAccountID = selectedTransactionInfo?.ownerAccountID;
- if (typeof ownerAccountID === 'number') {
- knownOwnerIDs.add(ownerAccountID);
- } else {
- hasUnknownOwner = true;
+ const {login} = useCurrentUserPersonalDetails();
+ const selectedTransactionsList = useMemo(
+ () =>
+ selectedTransactionIDs.reduce((acc, transactionID) => {
+ const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ if (transaction) {
+ acc.push(transaction);
+ }
+ return acc;
+ }, [] as Transaction[]),
+ [allTransactions, selectedTransactionIDs],
+ );
+ const hasTransactionsFromMultipleOwners = useMemo(() => {
+ const knownOwnerIDs = new Set();
+ let hasUnknownOwner = false;
+
+ for (const selectedTransactionInfo of Object.values(selectedTransactionsMeta ?? {})) {
+ const ownerAccountID = selectedTransactionInfo?.ownerAccountID;
+ if (typeof ownerAccountID === 'number') {
+ knownOwnerIDs.add(ownerAccountID);
+ if (knownOwnerIDs.size > 1) {
+ return true;
+ }
+ } else {
+ hasUnknownOwner = true;
+ }
}
- }
- for (const selectedTransaction of selectedTransactionsList) {
- const reportID = selectedTransaction?.reportID;
- if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
- hasUnknownOwner = true;
- continue;
- }
+ for (const selectedTransaction of selectedTransactionsList) {
+ const reportID = selectedTransaction?.reportID;
+ if (!reportID || reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
+ hasUnknownOwner = true;
+ continue;
+ }
- const parentReport = getReportOrDraftReport(reportID);
- const ownerAccountID = parentReport?.ownerAccountID;
+ const parentReport = getReportOrDraftReport(reportID);
+ const ownerAccountID = parentReport?.ownerAccountID;
- if (typeof ownerAccountID === 'number') {
- knownOwnerIDs.add(ownerAccountID);
- } else {
- hasUnknownOwner = true;
+ if (typeof ownerAccountID === 'number') {
+ knownOwnerIDs.add(ownerAccountID);
+ if (knownOwnerIDs.size > 1) {
+ return true;
+ }
+ } else {
+ hasUnknownOwner = true;
+ }
+ }
+
+ if (hasUnknownOwner) {
+ return knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1;
}
- }
- const hasTransactionsFromMultipleOwners = hasUnknownOwner ? knownOwnerIDs.size > 0 || selectedTransactionIDs.length > 1 : knownOwnerIDs.size > 1;
+ return false;
+ }, [selectedTransactionsList, selectedTransactionsMeta, selectedTransactionIDs.length]);
const {translate, localeCompare} = useLocalize();
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
@@ -138,23 +150,25 @@ function useSelectedTransactionsActions({
iouType = CONST.IOU.TYPE.INVOICE;
}
- const handleDeleteTransactions = () => {
+ const handleDeleteTransactions = useCallback(() => {
const deletedThreadReportIDs = deleteTransactions(selectedTransactionIDs, duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false);
clearSelectedTransactions(true);
setIsDeleteModalVisible(false);
Navigation.removeReportScreen(new Set(deletedThreadReportIDs));
- };
+ }, [deleteTransactions, selectedTransactionIDs, duplicateTransactions, duplicateTransactionViolations, currentSearchHash, clearSelectedTransactions]);
- const showDeleteModal = () => {
+ const showDeleteModal = useCallback(() => {
setIsDeleteModalVisible(true);
- };
+ }, []);
- const hideDeleteModal = () => {
+ const hideDeleteModal = useCallback(() => {
setIsDeleteModalVisible(false);
- };
+ }, []);
- let computedOptions: Array> = [];
- if (selectedTransactionIDs.length) {
+ const computedOptions = useMemo(() => {
+ if (!selectedTransactionIDs.length) {
+ return [];
+ }
const options = [];
const isMoneyRequestReport = isMoneyRequestReportUtils(report);
const isReportReimbursed = report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
@@ -186,11 +200,6 @@ function useSelectedTransactionsActions({
icon: expensifyIcons.Stopwatch,
value: HOLD,
onSelected: () => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
if (!report?.reportID) {
return;
}
@@ -205,11 +214,6 @@ function useSelectedTransactionsActions({
icon: expensifyIcons.Stopwatch,
value: UNHOLD,
onSelected: () => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
for (const transactionID of selectedTransactionIDs) {
const action = getIOUActionForTransactionID(reportActions, transactionID);
if (!action?.childReportID) {
@@ -231,11 +235,6 @@ function useSelectedTransactionsActions({
icon: expensifyIcons.ThumbsDown,
value: CONST.REPORT.SECONDARY_ACTIONS.REJECT,
onSelected: () => {
- if (isDelegateAccessRestricted) {
- showDelegateNoAccessModal();
- return;
- }
-
Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT_REJECT_TRANSACTIONS.getRoute({reportID: report.reportID}));
},
});
@@ -334,8 +333,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, login ?? '', currentUserAccountID, policy);
+ const canSplitTransaction = selectedTransactionsList.length === 1 && report && !isExpenseSplit && isSplitAction(report, [firstTransaction], originalTransaction, login ?? '', policy);
if (canSplitTransaction) {
options.push({
@@ -367,8 +365,46 @@ function useSelectedTransactionsActions({
onSelected: showDeleteModal,
});
}
- computedOptions = options;
- }
+ return options;
+ }, [
+ session?.email,
+ selectedTransactionIDs,
+ report,
+ selectedTransactionsList,
+ translate,
+ isReportArchived,
+ hasTransactionsFromMultipleOwners,
+ policy,
+ reportActions,
+ clearSelectedTransactions,
+ allTransactionsLength,
+ integrationsExportTemplates,
+ csvExportLayouts,
+ isOffline,
+ onExportOffline,
+ onExportFailed,
+ beginExportWithTemplate,
+ outstandingReportsByPolicyID,
+ iouType,
+ lastVisitedPath,
+ allTransactions,
+ allReports,
+ session?.accountID,
+ showDeleteModal,
+ allTransactionViolations,
+ expensifyIcons.Stopwatch,
+ expensifyIcons.ThumbsDown,
+ expensifyIcons.Table,
+ expensifyIcons.Export,
+ expensifyIcons.ArrowRight,
+ expensifyIcons.ArrowSplit,
+ expensifyIcons.DocumentMerge,
+ expensifyIcons.ArrowCollapse,
+ expensifyIcons.Trashcan,
+ localeCompare,
+ isOnSearch,
+ login,
+ ]);
return {
options: computedOptions,
diff --git a/src/hooks/useShowNotFoundPageInIOUStep.ts b/src/hooks/useShowNotFoundPageInIOUStep.ts
index e576ba1d40d2..443dcfe29ffe 100644
--- a/src/hooks/useShowNotFoundPageInIOUStep.ts
+++ b/src/hooks/useShowNotFoundPageInIOUStep.ts
@@ -54,7 +54,7 @@ const useShowNotFoundPageInIOUStep = (action: IOUAction, iouType: IOUType, repor
// eslint-disable-next-line rulesdir/no-negated-variables
let shouldShowNotFoundPage = false;
- const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && areRequiredFieldsEmpty(transaction, iouReport);
+ const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && areRequiredFieldsEmpty(transaction);
const canEditSplitExpense = isSplitExpense && !!transaction;
if (isEditing) {
diff --git a/src/hooks/useSidePanelDisplayStatus.tsx b/src/hooks/useSidePanelDisplayStatus.tsx
index 730aa2f19f57..e24ee963bb2e 100644
--- a/src/hooks/useSidePanelDisplayStatus.tsx
+++ b/src/hooks/useSidePanelDisplayStatus.tsx
@@ -1,10 +1,8 @@
-import {getDeepestFocusedScreenName, isTwoFactorSetupScreen} from '@libs/Navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
import {hasCompletedGuidedSetupFlowSelector} from '@src/selectors/Onboarding';
import useIsAnonymousUser from './useIsAnonymousUser';
import useOnyx from './useOnyx';
import useResponsiveLayout from './useResponsiveLayout';
-import useRootNavigationState from './useRootNavigationState';
/**
* Hook to get the display status of the Side Panel
@@ -18,10 +16,6 @@ function useSidePanelDisplayStatus() {
});
const isAnonymousUser = useIsAnonymousUser();
const isSidePanelVisible = isExtraLargeScreenWidth ? sidePanelNVP?.open : sidePanelNVP?.openNarrowScreen;
- const isIn2FASetupFlow = useRootNavigationState((state) => {
- const focusedScreenName = getDeepestFocusedScreenName(state);
- return isTwoFactorSetupScreen(focusedScreenName);
- });
// The Side Panel is hidden when:
// - NVP is not set or it is false
@@ -33,8 +27,7 @@ function useSidePanelDisplayStatus() {
// - Side Panel is displayed currently
// - Onboarding is not completed
// - User is anonymous (not signed in)
- // - User is setting up 2FA - if their current token is a TWO_FACTOR_SETUP token, any messages to concierge will fail to send
- const shouldHideHelpButton = !shouldHideSidePanel || !isOnboardingCompleted || isAnonymousUser || isIn2FASetupFlow;
+ const shouldHideHelpButton = !shouldHideSidePanel || !isOnboardingCompleted || isAnonymousUser;
const shouldHideSidePanelBackdrop = shouldHideSidePanel || isExtraLargeScreenWidth || shouldUseNarrowLayout;
return {
diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx
index 2f939b36d5d6..8307061e1353 100644
--- a/src/hooks/useSidebarOrderedReports.tsx
+++ b/src/hooks/useSidebarOrderedReports.tsx
@@ -34,7 +34,7 @@ type SidebarOrderedReportsContextValue = {
clearLHNCache: () => void;
};
-type ReportsToDisplayInLHN = Record;
+type ReportsToDisplayInLHN = Record;
const SidebarOrderedReportsContext = createContext({
orderedReports: [],
@@ -230,7 +230,7 @@ function SidebarOrderedReportsContextProvider({
}, [reportsToDisplayInLHN]);
const getOrderedReportIDs = useCallback(
- () => SidebarUtils.sortReportsToDisplayInLHN(deepComparedReportsToDisplayInLHN ?? {}, priorityMode, localeCompare, deepComparedReportsDrafts, reportNameValuePairs),
+ () => 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-hooks/exhaustive-deps
[deepComparedReportsToDisplayInLHN, localeCompare, deepComparedReportsDrafts],
diff --git a/src/hooks/useTodos.ts b/src/hooks/useTodos.ts
index 609621bfc4eb..485bea8bbf2c 100644
--- a/src/hooks/useTodos.ts
+++ b/src/hooks/useTodos.ts
@@ -13,7 +13,7 @@ export default function useTodos() {
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false});
const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: false});
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
- const {login = '', accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
+ const {login = '', accountID} = useCurrentUserPersonalDetails();
return useMemo(() => {
const reportsToSubmit: Report[] = [];
@@ -48,10 +48,10 @@ export default function useTodos() {
if (isSubmitAction(report, reportTransactions, policy, reportNameValuePair)) {
reportsToSubmit.push(report);
}
- if (isApproveAction(report, reportTransactions, currentUserAccountID, policy)) {
+ if (isApproveAction(report, reportTransactions, policy)) {
reportsToApprove.push(report);
}
- if (isPrimaryPayAction(report, currentUserAccountID, login, bankAccountList, policy, reportNameValuePair)) {
+ if (isPrimaryPayAction(report, accountID, login, bankAccountList, policy, reportNameValuePair)) {
reportsToPay.push(report);
}
if (isExportAction(report, login, policy, reportActions)) {
@@ -60,5 +60,5 @@ export default function useTodos() {
}
return {reportsToSubmit, reportsToApprove, reportsToPay, reportsToExport};
- }, [allReports, allTransactions, allPolicies, allReportNameValuePairs, allReportActions, currentUserAccountID, login, bankAccountList]);
+ }, [allReports, allTransactions, allPolicies, allReportNameValuePairs, allReportActions, accountID, login, bankAccountList]);
}
diff --git a/src/hooks/useTransactionViolations.ts b/src/hooks/useTransactionViolations.ts
index eaaf3a5ac24d..71ab0554f952 100644
--- a/src/hooks/useTransactionViolations.ts
+++ b/src/hooks/useTransactionViolations.ts
@@ -1,7 +1,6 @@
import {useMemo} from 'react';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {isViolationDismissed, mergeProhibitedViolations, shouldShowViolation} from '@libs/TransactionUtils';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {TransactionViolation, TransactionViolations} from '@src/types/onyx';
import getEmptyArray from '@src/types/utils/getEmptyArray';
@@ -29,8 +28,7 @@ function useTransactionViolations(transactionID?: string, shouldShowRterForSettl
transactionViolations.filter(
(violation: TransactionViolation) =>
!isViolationDismissed(transaction, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, iouReport, policy) &&
- shouldShowViolation(iouReport, policy, violation.name, currentUserDetails.email ?? '', shouldShowRterForSettledReport, transaction) &&
- (!CONST.IS_ATTENDEES_REQUIRED_FEATURE_DISABLED || violation.name !== CONST.VIOLATIONS.MISSING_ATTENDEES),
+ shouldShowViolation(iouReport, policy, violation.name, currentUserDetails.email ?? '', shouldShowRterForSettledReport, transaction),
),
),
[transaction, transactionViolations, iouReport, policy, shouldShowRterForSettledReport, currentUserDetails.email, currentUserDetails.accountID],
diff --git a/src/hooks/useTransactionsAndViolationsForReport.ts b/src/hooks/useTransactionsAndViolationsForReport.ts
index 512030a1f3ed..d92ec801bdd4 100644
--- a/src/hooks/useTransactionsAndViolationsForReport.ts
+++ b/src/hooks/useTransactionsAndViolationsForReport.ts
@@ -1,3 +1,4 @@
+import {useMemo} from 'react';
import {useAllReportsTransactionsAndViolations} from '@components/OnyxListItemProvider';
import {getTransactionViolations} from '@libs/TransactionUtils';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -16,14 +17,25 @@ function useTransactionsAndViolationsForReport(reportID?: string) {
const {transactions, violations} = reportID ? (allReportsTransactionsAndViolations?.[reportID] ?? DEFAULT_RETURN_VALUE) : DEFAULT_RETURN_VALUE;
- const filteredViolations: Record = {};
- for (const transactionViolationKey of Object.keys(violations)) {
- const transactionID = transactionViolationKey.split('_').at(1) ?? '';
- const transaction = transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- filteredViolations[transactionViolationKey] = getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? [];
- }
+ const transactionsAndViolations = useMemo(() => {
+ const filteredViolations = Object.keys(violations).reduce(
+ (filteredTransactionViolations, transactionViolationKey) => {
+ const transactionID = transactionViolationKey.split('_').at(1) ?? '';
+ const transaction = transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- return {transactions, violations: filteredViolations};
+ // This is our accumulator, it's okay to reassign
+ // eslint-disable-next-line no-param-reassign
+ filteredTransactionViolations[transactionViolationKey] =
+ getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? [];
+ return filteredTransactionViolations;
+ },
+ {} as Record,
+ );
+
+ return {transactions, violations: filteredViolations};
+ }, [transactions, violations, currentUserDetails?.email, currentUserDetails?.accountID, report, policy]);
+
+ return transactionsAndViolations;
}
export default useTransactionsAndViolationsForReport;
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index bbad1d83c2fa..f86a875b35ed 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -6,7 +6,7 @@ import type {TransactionViolation, ViolationName} from '@src/types/onyx';
/**
* Names of Fields where violations can occur.
*/
-const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'attendees', 'customUnitRateID', 'waypoints', 'none'] as const;
+const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'customUnitRateID', 'none'] as const;
type ViolationField = TupleToUnion;
@@ -28,7 +28,6 @@ const violationNameToField: Record 'date',
missingCategory: () => 'category',
missingComment: () => 'comment',
- missingAttendees: () => 'attendees',
missingTag: () => 'tag',
modifiedAmount: () => 'amount',
modifiedDate: () => 'date',
@@ -61,7 +60,6 @@ const violationNameToField: Record 'none',
receiptGeneratedWithAI: () => 'receipt',
companyCardRequired: () => 'none',
- noRoute: () => 'waypoints',
};
type ViolationsMap = Map;
diff --git a/src/hooks/useWorkspacesTabIndicatorStatus.ts b/src/hooks/useWorkspacesTabIndicatorStatus.ts
index d5a705202a7c..5c5c56269d80 100644
--- a/src/hooks/useWorkspacesTabIndicatorStatus.ts
+++ b/src/hooks/useWorkspacesTabIndicatorStatus.ts
@@ -1,20 +1,51 @@
-import useNavigationTabBarIndicatorChecks from './useNavigationTabBarIndicatorChecks';
-import type {IndicatorStatus} from './useNavigationTabBarIndicatorChecks';
+import {useMemo} from 'react';
+import type {ValueOf} from 'type-fest';
+import {isConnectionInProgress} from '@libs/actions/connections';
+import {shouldShowQBOReimbursableExportDestinationAccountError} from '@libs/actions/connections/QuickbooksOnline';
+import {getUberConnectionErrorDirectlyFromPolicy, shouldShowCustomUnitsError, shouldShowEmployeeListError, shouldShowPolicyError, shouldShowSyncError} from '@libs/PolicyUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useOnyx from './useOnyx';
import useTheme from './useTheme';
+type WorkspacesTabIndicatorStatus = ValueOf;
+
type WorkspacesTabIndicatorStatusResult = {
indicatorColor: string;
- status: IndicatorStatus | undefined;
+ status: ValueOf | undefined;
policyIDWithErrors: string | undefined;
};
function useWorkspacesTabIndicatorStatus(): WorkspacesTabIndicatorStatusResult {
const theme = useTheme();
+ const [allConnectionSyncProgresses] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS, {canBeMissing: true});
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
+ // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and
+ // those should be cleaned out before doing any error checking
+ const cleanPolicies = useMemo(() => Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)), [policies]);
+ const policyErrors = {
+ [CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS]: Object.values(cleanPolicies).find(shouldShowPolicyError),
+ [CONST.INDICATOR_STATUS.HAS_CUSTOM_UNITS_ERROR]: Object.values(cleanPolicies).find(shouldShowCustomUnitsError),
+ [CONST.INDICATOR_STATUS.HAS_EMPLOYEE_LIST_ERROR]: Object.values(cleanPolicies).find(shouldShowEmployeeListError),
+ [CONST.INDICATOR_STATUS.HAS_SYNC_ERRORS]: Object.values(cleanPolicies).find((cleanPolicy) =>
+ shouldShowSyncError(cleanPolicy, isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy)),
+ ),
+ [CONST.INDICATOR_STATUS.HAS_QBO_EXPORT_ERROR]: Object.values(cleanPolicies).find(shouldShowQBOReimbursableExportDestinationAccountError),
+ [CONST.INDICATOR_STATUS.HAS_UBER_CREDENTIALS_ERROR]: Object.values(cleanPolicies).find(getUberConnectionErrorDirectlyFromPolicy),
+ };
+
+ // All of the error & info-checking methods are put into an array. This is so that using _.some() will return
+ // early as soon as the first error / info condition is returned. This makes the checks very efficient since
+ // we only care if a single error / info condition exists anywhere.
+ const errorChecking: Partial> = {
+ ...(Object.fromEntries(Object.entries(policyErrors).map(([error, policy]) => [error, !!policy])) as Record),
+ };
- const {policyStatus, policyIDWithErrors} = useNavigationTabBarIndicatorChecks();
+ const [error] = Object.entries(errorChecking).find(([, value]) => value) ?? [];
- const status = policyStatus;
- const indicatorColor = policyStatus ? theme.danger : theme.success;
+ const status = error as WorkspacesTabIndicatorStatus | undefined;
+ const policyIDWithErrors = Object.values(policyErrors).find(Boolean)?.id;
+ const indicatorColor = error ? theme.danger : theme.success;
return {indicatorColor, status, policyIDWithErrors};
}
diff --git a/src/languages/de.ts b/src/languages/de.ts
index c9531841315e..27bc9d97b453 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -28,6 +28,24 @@ import type {
EditActionParams,
ExportAgainModalDescriptionParams,
ExportIntegrationSelectedParams,
+ FileLimitParams,
+ FileTypeParams,
+ FiltersAmountBetweenParams,
+ FlightLayoverParams,
+ FlightParams,
+ FocusModeUpdateParams,
+ FormattedMaxLengthParams,
+ GoBackMessageParams,
+ HarvestCreatedExpenseReportParams,
+ ImportedTagsMessageParams,
+ ImportedTypesParams,
+ ImportFieldParams,
+ ImportMembersSuccessfulDescriptionParams,
+ ImportPerDiemRatesSuccessfulDescriptionParams,
+ ImportTagsSuccessfulDescriptionParams,
+ IncorrectZipFormatParams,
+ IndividualExpenseRulesSubtitleParams,
+ InstantSummaryParams,
IntacctMappingTitleParams,
IntegrationExportParams,
IntegrationSyncFailedParams,
@@ -115,6 +133,9 @@ import type {
SplitDateRangeParams,
SplitExpenseEditTitleParams,
SplitExpenseSubtitleParams,
+ SpreadCategoriesParams,
+ SpreadFieldNameParams,
+ SpreadSheetColumnParams,
StatementTitleParams,
StepCounterParams,
StripePaidParams,
@@ -164,7 +185,6 @@ import type {
UpdatedPolicyManualApprovalThresholdParams,
UpdatedPolicyPreventSelfApprovalParams,
UpdatedPolicyReimbursementEnabledParams,
- UpdatedPolicyReimburserParams,
UpdatedPolicyReportFieldDefaultValueParams,
UpdatedPolicyTagFieldParams,
UpdatedPolicyTagNameParams,
@@ -259,6 +279,7 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Suchen …',
next: 'Weiter',
previous: 'Zurück',
+ // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: 'Zurück',
create: 'Erstellen',
add: 'Hinzufügen',
@@ -276,7 +297,6 @@ const translations: TranslationDeepObject = {
zoom: 'Zoom',
password: 'Passwort',
magicCode: 'Magischer Code',
- digits: 'Ziffern',
twoFactorCode: 'Zwei-Faktor-Code',
workspaces: 'Arbeitsbereiche',
inbox: 'Posteingang',
@@ -532,6 +552,10 @@ const translations: TranslationDeepObject = {
value: 'Wert',
downloadFailedTitle: 'Download fehlgeschlagen',
downloadFailedDescription: 'Ihr Download konnte nicht abgeschlossen werden. Bitte versuchen Sie es später noch einmal.',
+ downloadFailedEmptyReportDescription: () => ({
+ one: 'Sie können keinen leeren Bericht exportieren.',
+ other: () => 'Sie können keine leeren Berichte exportieren.',
+ }),
filterLogs: 'Protokolle filtern',
network: 'Netzwerk',
reportID: 'Berichts-ID',
@@ -627,6 +651,7 @@ const translations: TranslationDeepObject = {
copyToClipboard: 'In die Zwischenablage kopieren',
thisIsTakingLongerThanExpected: 'Das dauert länger als erwartet ...',
domains: 'Domänen',
+ viewReport: 'Bericht anzeigen',
actionRequired: 'Aktion erforderlich',
duplicate: 'Duplizieren',
duplicated: 'Dupliziert',
@@ -687,12 +712,12 @@ const translations: TranslationDeepObject = {
protectedPDFNotSupported: 'Passwortgeschützte PDF-Datei wird nicht unterstützt',
attachmentImageResized: 'Dieses Bild wurde für die Vorschau verkleinert. Für die volle Auflösung herunterladen.',
attachmentImageTooLarge: 'Dieses Bild ist zu groß, um vor dem Hochladen eine Vorschau anzuzeigen.',
- tooManyFiles: (fileLimit: number) => `Sie können jeweils nur bis zu ${fileLimit} Dateien hochladen.`,
+ tooManyFiles: ({fileLimit}: FileLimitParams) => `Sie können jeweils nur bis zu ${fileLimit} Dateien hochladen.`,
sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Datei überschreitet ${maxUploadSizeInMB} MB. Bitte versuche es erneut.`,
someFilesCantBeUploaded: 'Einige Dateien können nicht hochgeladen werden',
sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Dateien müssen kleiner als ${maxUploadSizeInMB} MB sein. Größere Dateien werden nicht hochgeladen.`,
maxFileLimitExceeded: 'Sie können bis zu 30 Belege auf einmal hochladen. Alle weiteren werden nicht hochgeladen.',
- unsupportedFileType: (fileType: string) => `${fileType}-Dateien werden nicht unterstützt. Es werden nur unterstützte Dateitypen hochgeladen.`,
+ unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType}-Dateien werden nicht unterstützt. Es werden nur unterstützte Dateitypen hochgeladen.`,
learnMoreAboutSupportedFiles: 'Erfahren Sie mehr über unterstützte Formate.',
passwordProtected: 'Passwortgeschützte PDFs werden nicht unterstützt. Es werden nur unterstützte Dateien hochgeladen.',
},
@@ -717,8 +742,8 @@ const translations: TranslationDeepObject = {
composer: {
noExtensionFoundForMimeType: 'Keine Erweiterung für diesen MIME-Typ gefunden',
problemGettingImageYouPasted: 'Es gab ein Problem beim Abrufen des von dir eingefügten Bildes',
- commentExceededMaxLength: (formattedMaxLength: string) => `Die maximale Kommentarlänge beträgt ${formattedMaxLength} Zeichen.`,
- taskTitleExceededMaxLength: (formattedMaxLength: string) => `Die maximale Aufgabenüberschrift darf ${formattedMaxLength} Zeichen lang sein.`,
+ commentExceededMaxLength: ({formattedMaxLength}: FormattedMaxLengthParams) => `Die maximale Kommentarlänge beträgt ${formattedMaxLength} Zeichen.`,
+ taskTitleExceededMaxLength: ({formattedMaxLength}: FormattedMaxLengthParams) => `Die maximale Aufgabenüberschrift darf ${formattedMaxLength} Zeichen lang sein.`,
},
baseUpdateAppModal: {
updateApp: 'App aktualisieren',
@@ -729,35 +754,6 @@ const translations: TranslationDeepObject = {
expired: 'Ihre Sitzung ist abgelaufen.',
signIn: 'Bitte melden Sie sich erneut an.',
},
- multifactorAuthentication: {
- biometricsTest: {
- biometricsTest: 'Biometrie-Test',
- authenticationSuccessful: 'Authentifizierung erfolgreich',
- successfullyAuthenticatedUsing: ({authType}) => `Du hast dich erfolgreich mit ${authType} authentifiziert.`,
- troubleshootBiometricsStatus: ({registered}) => `Biometrie (${registered ? 'Registriert' : 'Nicht registriert'})`,
- yourAttemptWasUnsuccessful: 'Dein Authentifizierungsversuch war nicht erfolgreich.',
- youCouldNotBeAuthenticated: 'Sie konnten nicht authentifiziert werden',
- areYouSureToReject: 'Bist du sicher? Der Authentifizierungsversuch wird abgelehnt, wenn du diesen Bildschirm schließt.',
- rejectAuthentication: 'Authentifizierung ablehnen',
- test: 'Test',
- biometricsAuthentication: 'Biometrie-Authentifizierung',
- },
- pleaseEnableInSystemSettings: {
- start: 'Bitte aktivieren Sie die Gesichts-/Fingerabdrucküberprüfung oder setzen Sie eine Geräte-Passcode in Ihren ',
- link: 'Systemeinstellungen',
- end: '.',
- },
- oops: 'Hoppla, etwas ist schief gelaufen',
- looksLikeYouRanOutOfTime: 'Sieht aus, als wäre deine Zeit abgelaufen! Bitte versuche es erneut beim Händler.',
- youRanOutOfTime: 'Die Zeit ist abgelaufen',
- letsVerifyItsYou: 'Lass uns überprüfen, ob du es bist',
- verifyYourself: {
- biometrics: 'Verifiziere dich mit deinem Gesicht oder Fingerabdruck',
- },
- enableQuickVerification: {
- biometrics: 'Schnelle, sichere Verifizierung mit deinem Gesicht oder Fingerabdruck aktivieren. Keine Passwörter oder Codes erforderlich.',
- },
- },
validateCodeModal: {
successfulSignInTitle: dedent(`
Abrakadabra,
@@ -928,7 +924,7 @@ const translations: TranslationDeepObject = {
adminOnlyCanPost: 'Nur Administratoren können Nachrichten in diesem Raum senden.',
reportAction: {
asCopilot: 'als Copilot für',
- harvestCreatedExpenseReport: (reportUrl: string, reportName: string) =>
+ 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`,
@@ -971,7 +967,7 @@ const translations: TranslationDeepObject = {
buttonFind: 'Etwas suchen …',
buttonMySettings: 'Meine Einstellungen',
fabNewChat: 'Chat starten',
- fabNewChatExplained: 'Aktionsmenü öffnen',
+ fabNewChatExplained: 'Chat starten (Schwebende Aktion)',
fabScanReceiptExplained: 'Beleg scannen (Schwebende Aktion)',
chatPinned: 'Chat angeheftet',
draftedMessage: 'Entwurfsnachricht',
@@ -998,13 +994,15 @@ const translations: TranslationDeepObject = {
chooseSpreadsheet: 'Wählen Sie eine Tabellenkalkulationsdatei zum Importieren aus. Unterstützte Formate: .csv, .txt, .xls und .xlsx.',
chooseSpreadsheetMultiLevelTag: `Wähle eine Tabellenkalkulationsdatei zum Importieren aus. Erfahre mehr über unterstützte Dateiformate.`,
fileContainsHeader: 'Datei enthält Spaltenüberschriften',
- column: (name: string) => `Spalte ${name}`,
- fieldNotMapped: (fieldName: string) => `Ups! Ein erforderliches Feld („${fieldName}“) wurde nicht zugeordnet. Bitte überprüfen und erneut versuchen.`,
- singleFieldMultipleColumns: (fieldName: string) => `Ups! Du hast ein einzelnes Feld („${fieldName}“) mehreren Spalten zugeordnet. Bitte überprüfe dies und versuche es erneut.`,
- emptyMappedField: (fieldName: string) => `Ups! Das Feld („${fieldName}“) enthält einen oder mehrere leere Werte. Bitte überprüfen Sie es und versuchen Sie es erneut.`,
+ column: ({name}: SpreadSheetColumnParams) => `Spalte ${name}`,
+ fieldNotMapped: ({fieldName}: SpreadFieldNameParams) => `Ups! Ein erforderliches Feld („${fieldName}“) wurde nicht zugeordnet. Bitte überprüfen und erneut versuchen.`,
+ singleFieldMultipleColumns: ({fieldName}: SpreadFieldNameParams) =>
+ `Ups! Du hast ein einzelnes Feld („${fieldName}“) mehreren Spalten zugeordnet. Bitte überprüfe dies und versuche es erneut.`,
+ emptyMappedField: ({fieldName}: SpreadFieldNameParams) =>
+ `Ups! Das Feld („${fieldName}“) enthält einen oder mehrere leere Werte. Bitte überprüfen Sie es und versuchen Sie es erneut.`,
importSuccessfulTitle: 'Import erfolgreich',
- importCategoriesSuccessfulDescription: ({categories}: {categories: number}) => (categories > 1 ? `${categories} Kategorien wurden hinzugefügt.` : '1 Kategorie wurde hinzugefügt.'),
- importMembersSuccessfulDescription: ({added, updated}: {added: number; updated: number}) => {
+ importCategoriesSuccessfulDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `${categories} Kategorien wurden hinzugefügt.` : '1 Kategorie wurde hinzugefügt.'),
+ importMembersSuccessfulDescription: ({added, updated}: ImportMembersSuccessfulDescriptionParams) => {
if (!added && !updated) {
return 'Es wurden keine Mitglieder hinzugefügt oder aktualisiert.';
}
@@ -1016,9 +1014,10 @@ const translations: TranslationDeepObject = {
}
return added > 1 ? `${added} Mitglieder wurden hinzugefügt.` : '1 Mitglied wurde hinzugefügt.';
},
- importTagsSuccessfulDescription: ({tags}: {tags: number}) => (tags > 1 ? `${tags} Tags wurden hinzugefügt.` : '1 Tag wurde hinzugefügt.'),
+ importTagsSuccessfulDescription: ({tags}: ImportTagsSuccessfulDescriptionParams) => (tags > 1 ? `${tags} Tags wurden hinzugefügt.` : '1 Tag wurde hinzugefügt.'),
importMultiLevelTagsSuccessfulDescription: 'Hierarchische Tags wurden hinzugefügt.',
- importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `${rates} Übernachtungspauschalen wurden hinzugefügt.` : '1 Pauschale wurde hinzugefügt.'),
+ importPerDiemRatesSuccessfulDescription: ({rates}: ImportPerDiemRatesSuccessfulDescriptionParams) =>
+ rates > 1 ? `${rates} Übernachtungspauschalen wurden hinzugefügt.` : '1 Pauschale wurde hinzugefügt.',
importFailedTitle: 'Import fehlgeschlagen',
importFailedDescription: 'Bitte stelle sicher, dass alle Felder korrekt ausgefüllt sind, und versuche es erneut. Wenn das Problem weiterhin besteht, wende dich bitte an Concierge.',
importDescription: 'Wählen Sie aus, welche Felder aus Ihrer Tabelle zugeordnet werden sollen, indem Sie auf das Dropdown-Menü neben jeder der importierten Spalten unten klicken.',
@@ -1199,8 +1198,14 @@ const translations: TranslationDeepObject = {
one: 'Sind Sie sicher, dass Sie diese Ausgabe löschen möchten?',
other: 'Sind Sie sicher, dass Sie diese Ausgaben löschen möchten?',
}),
- deleteReport: 'Bericht löschen',
- deleteReportConfirmation: 'Sind Sie sicher, dass Sie diesen Bericht löschen möchten?',
+ deleteReport: () => ({
+ one: 'Bericht löschen',
+ other: 'Berichte löschen',
+ }),
+ deleteReportConfirmation: () => ({
+ one: 'Möchten Sie diesen Bericht wirklich löschen?',
+ other: 'Möchten Sie diese Berichte wirklich löschen?',
+ }),
settledExpensify: 'Bezahlt',
done: 'Fertig',
settledElsewhere: 'Anderswo bezahlt',
@@ -1451,8 +1456,9 @@ const translations: TranslationDeepObject = {
moveExpensesError: 'Sie können Per Diem-Ausgaben nicht in Berichte auf anderen Arbeitsbereichen verschieben, da sich die Tagessätze zwischen Arbeitsbereichen unterscheiden können.',
changeApprover: {
title: 'Genehmigenden ändern',
- header: ({workflowSettingLink}: WorkflowSettingsParam) =>
- `Wählen Sie eine Option, um den Genehmiger für diesen Bericht zu ändern. (Aktualisieren Sie Ihre Arbeitsbereichseinstellungen, um diese Änderung dauerhaft für alle Berichte zu übernehmen.)`,
+ subtitle: 'Wählen Sie eine Option, um den Genehmiger für diesen Bericht zu ändern.',
+ description: ({workflowSettingLink}: WorkflowSettingsParam) =>
+ `Sie können den Genehmiger auch dauerhaft für alle Berichte in Ihren Workflow-Einstellungen ändern.`,
changedApproverMessage: (managerID: number) => `Genehmigenden in geändert`,
actions: {
addApprover: 'Genehmiger hinzufügen',
@@ -1470,13 +1476,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.',
- hours: 'Stunden',
- ratePreview: (rate: string) => `${rate} / Stunde`,
- amountTooLargeError: 'Der Gesamtbetrag ist zu hoch. Verringere die Stunden oder reduziere den Satz.',
- },
+ timeTracking: {hoursAt: (hours: number, rate: string) => `${hours} ${hours === 1 ? 'Stunde' : 'Stunden'} @ ${rate} / Stunde`, hrs: 'Std.'},
},
transactionMerge: {
listPage: {
@@ -1955,6 +1955,13 @@ const translations: TranslationDeepObject = {
chatToConciergeToUnlock: 'Chatte mit Concierge, um Sicherheitsbedenken zu klären und dein Konto wieder freizuschalten.',
chatWithConcierge: 'Chat mit Concierge',
},
+ passwordPage: {
+ changePassword: 'Passwort ändern',
+ changingYourPasswordPrompt: 'Wenn Sie Ihr Passwort ändern, wird es sowohl für Ihr Expensify.com-Konto als auch für Ihr New-Expensify-Konto aktualisiert.',
+ currentPassword: 'Aktuelles Passwort',
+ newPassword: 'Neues Passwort',
+ newPasswordPrompt: 'Ihr neues Passwort muss sich von Ihrem alten Passwort unterscheiden und mindestens 8 Zeichen, 1 Großbuchstaben, 1 Kleinbuchstaben und 1 Zahl enthalten.',
+ },
twoFactorAuth: {
headerTitle: 'Zwei-Faktor-Authentifizierung',
twoFactorAuthEnabled: 'Zwei-Faktor-Authentifizierung aktiviert',
@@ -2354,7 +2361,7 @@ ${amount} für ${merchant} – ${date}`,
transferAmountPage: {
transfer: ({amount}: TransferParams) => `Überweisen${amount ? ` ${amount}` : ''}`,
instant: 'Sofort (Debitkarte)',
- instantSummary: (rate: string, minAmount: string) => `${rate}% Gebühr (mindestens ${minAmount})`,
+ instantSummary: ({rate, minAmount}: InstantSummaryParams) => `${rate}% Gebühr (mindestens ${minAmount})`,
ach: '1–3 Werktage (Bankkonto)',
achSummary: 'Keine Gebühr',
whichAccount: 'Welches Konto?',
@@ -2389,31 +2396,10 @@ ${amount} für ${merchant} – ${date}`,
comment: (value: string) => `Beschreibung in „${value}“ ändern`,
merchant: (value: string) => `Händler aktualisieren auf „${value}“`,
reimbursable: (value: boolean) => `Ausgabe ${value ? 'erstattungsfähig' : 'nicht erstattungsfähig'} aktualisieren`,
- report: (value: string) => `Zu einem Bericht mit dem Namen „${value}“ hinzufügen`,
+ report: (value: string) => `Einen Bericht mit dem Namen „${value}“ hinzufügen`,
tag: (value: string) => `Tag auf „${value}“ aktualisieren`,
tax: (value: string) => `Steuersatz auf ${value} aktualisieren`,
},
- newRule: 'Neue Regel',
- addRule: {
- title: 'Regel hinzufügen',
- expenseContains: 'Wenn Ausgabe enthält:',
- applyUpdates: 'Wenden Sie dann diese Aktualisierungen an:',
- merchantHint: 'Gib * ein, um eine Regel zu erstellen, die für alle Händler gilt',
- addToReport: 'Zu einem Bericht mit dem Namen hinzufügen',
- createReport: 'Bericht bei Bedarf erstellen',
- applyToExistingExpenses: 'Auf passende vorhandene Spesen anwenden',
- confirmError: 'Geben Sie einen Händler ein und nehmen Sie mindestens eine Änderung vor',
- confirmErrorMerchant: 'Bitte Händler eingeben',
- confirmErrorUpdate: 'Bitte wende mindestens eine Aktualisierung an',
- saveRule: 'Regel speichern',
- },
- editRule: {title: 'Regel bearbeiten'},
- deleteRule: {
- deleteSingle: 'Regel löschen',
- deleteMultiple: 'Regeln löschen',
- deleteSinglePrompt: 'Sind Sie sicher, dass Sie diese Regel löschen möchten?',
- deleteMultiplePrompt: 'Möchten Sie diese Regeln wirklich löschen?',
- },
},
preferencesPage: {
appSection: {
@@ -2910,7 +2896,7 @@ ${
dateShouldBeBefore: (dateString: string) => `Das Datum sollte vor dem ${dateString} liegen`,
dateShouldBeAfter: (dateString: string) => `Datum muss nach ${dateString} liegen`,
hasInvalidCharacter: 'Name darf nur lateinische Zeichen enthalten',
- incorrectZipFormat: (zipFormat?: string) => `Ungültiges Postleitzahlformat${zipFormat ? `Akzeptables Format: ${zipFormat}` : ''}`,
+ incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Ungültiges Postleitzahlformat${zipFormat ? `Akzeptables Format: ${zipFormat}` : ''}`,
invalidPhoneNumber: `Bitte stelle sicher, dass die Telefonnummer gültig ist (z. B. ${CONST.EXAMPLE_PHONE_NUMBER})`,
},
},
@@ -2994,7 +2980,7 @@ ${
},
focusModeUpdateModal: {
title: 'Willkommen im #Focus-Modus!',
- prompt: (priorityModePageUrl: string) =>
+ prompt: ({priorityModePageUrl}: FocusModeUpdateParams) =>
`Behalte den Überblick, indem du nur ungelesene Chats oder Chats siehst, die deine Aufmerksamkeit erfordern. Keine Sorge, du kannst dies jederzeit in den Einstellungen ändern.`,
},
notFound: {
@@ -3014,6 +3000,15 @@ ${
subtitle: 'Ihre Anfrage konnte nicht abgeschlossen werden. Bitte versuchen Sie es später noch einmal.',
wrongTypeSubtitle: 'Diese Suche ist ungültig. Versuche, deine Suchkriterien anzupassen.',
},
+ setPasswordPage: {
+ enterPassword: 'Passwort eingeben',
+ setPassword: 'Passwort festlegen',
+ newPasswordPrompt: 'Dein Passwort muss mindestens 8 Zeichen, 1 Großbuchstaben, 1 Kleinbuchstaben und 1 Zahl enthalten.',
+ passwordFormTitle: 'Willkommen zurück bei New Expensify! Bitte lege dein Passwort fest.',
+ passwordNotSet: 'Wir konnten Ihr neues Passwort nicht festlegen. Wir haben Ihnen einen neuen Passwort-Link gesendet, damit Sie es erneut versuchen können.',
+ setPasswordLinkInvalid: 'Dieser Link zum Festlegen des Passworts ist ungültig oder abgelaufen. Ein neuer wartet bereits in deinem E-Mail-Posteingang auf dich!',
+ validateAccount: 'Konto verifizieren',
+ },
statusPage: {
status: 'Status',
statusExplanation: 'Füge ein Emoji hinzu, damit deine Kolleg:innen und Freund:innen leicht sehen können, was los ist. Du kannst optional auch eine Nachricht hinzufügen!',
@@ -3157,13 +3152,10 @@ ${
errorMessageInvalidPhone: `Bitte gib eine gültige Telefonnummer ohne Klammern oder Bindestriche ein. Wenn du außerhalb der USA bist, gib bitte deine Ländervorwahl an (z. B. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
errorMessageInvalidEmail: 'Ungültige E-Mail',
userIsAlreadyMember: ({login, name}: UserIsAlreadyMemberParams) => `${login} ist bereits Mitglied von ${name}`,
- userIsAlreadyAnAdmin: ({login, name}: UserIsAlreadyMemberParams) => `${login} ist bereits ein Admin von ${name}`,
},
onfidoStep: {
acceptTerms: 'Indem Sie mit der Anfrage zur Aktivierung Ihres Expensify Wallets fortfahren, bestätigen Sie, dass Sie gelesen, verstanden und akzeptiert haben',
facialScan: 'Onfidos Richtlinie und Einverständniserklärung zur Gesichtserfassung',
- onfidoLinks: (onfidoTitle: string) =>
- `${onfidoTitle} Onfidos Richtlinie und Einverständniserklärung zur Gesichtserfassung, Datenschutz und Nutzungsbedingungen.`,
tryAgain: 'Erneut versuchen',
verifyIdentity: 'Identität bestätigen',
letsVerifyIdentity: 'Lass uns deine Identität bestätigen',
@@ -3662,7 +3654,7 @@ ${
flight: 'Flug',
flightDetails: {
passenger: 'Passagier',
- layover: (layover: string) => `Sie haben einen ${layover} Zwischenstopp vor diesem Flug`,
+ layover: ({layover}: FlightLayoverParams) => `Sie haben einen ${layover} Zwischenstopp vor diesem Flug`,
takeOff: 'Abflug',
landing: 'Startseite',
seat: 'Sitz',
@@ -3753,18 +3745,17 @@ ${
`Aktivierung von Reisen für die Domain ${domain} fehlgeschlagen. Bitte überprüfen Sie diese Domain und aktivieren Sie Reisen dafür.`,
},
updates: {
- bookingTicketed: (airlineCode: string, origin: string, destination: string, startDate: string, confirmationID = '') =>
+ bookingTicketed: ({airlineCode, origin, destination, startDate, confirmationID = ''}: FlightParams) =>
`Ihr Flug ${airlineCode} (${origin} → ${destination}) am ${startDate} wurde gebucht. Bestätigungscode: ${confirmationID}`,
- ticketVoided: (airlineCode: string, origin: string, destination: string, startDate: string) =>
+ ticketVoided: ({airlineCode, origin, destination, startDate}: FlightParams) =>
`Ihr Ticket für den Flug ${airlineCode} (${origin} → ${destination}) am ${startDate} wurde storniert.`,
- ticketRefunded: (airlineCode: string, origin: string, destination: string, startDate: string) =>
+ ticketRefunded: ({airlineCode, origin, destination, startDate}: FlightParams) =>
`Ihr Ticket für Flug ${airlineCode} (${origin} → ${destination}) am ${startDate} wurde erstattet oder umgebucht.`,
- flightCancelled: (airlineCode: string, origin: string, destination: string, startDate: string) =>
+ flightCancelled: ({airlineCode, origin, destination, startDate}: FlightParams) =>
`Ihr Flug ${airlineCode} (${origin} → ${destination}) am ${startDate}} wurde von der Fluggesellschaft storniert.`,
flightScheduleChangePending: (airlineCode: string) => `Die Fluggesellschaft hat eine Flugplanänderung für Flug ${airlineCode} vorgeschlagen; wir warten auf die Bestätigung.`,
flightScheduleChangeClosed: (airlineCode: string, startDate?: string) => `Flugplanänderung bestätigt: Flug ${airlineCode} startet jetzt um ${startDate}.`,
- flightUpdated: (airlineCode: string, origin: string, destination: string, startDate: string) =>
- `Ihr Flug ${airlineCode} (${origin} → ${destination}) am ${startDate} wurde aktualisiert.`,
+ flightUpdated: ({airlineCode, origin, destination, startDate}: FlightParams) => `Ihr Flug ${airlineCode} (${origin} → ${destination}) am ${startDate} wurde aktualisiert.`,
flightCabinChanged: (airlineCode: string, cabinClass?: string) => `Ihre Kabinenklasse wurde für den Flug ${airlineCode} auf ${cabinClass} aktualisiert.`,
flightSeatConfirmed: (airlineCode: string) => `Ihre Sitzplatzzuweisung auf Flug ${airlineCode} wurde bestätigt.`,
flightSeatChanged: (airlineCode: string) => `Ihre Sitzplatzzuweisung auf Flug ${airlineCode} wurde geändert.`,
@@ -4581,7 +4572,7 @@ ${
importTaxDescription: 'Steuergruppen aus NetSuite importieren.',
importCustomFields: {
chooseOptionBelow: 'Wähle eine der folgenden Optionen:',
- label: (importedTypes: string[]) => `Importiert als ${importedTypes.join('und')}`,
+ label: ({importedTypes}: ImportedTypesParams) => `Importiert als ${importedTypes.join('und')}`,
requiredFieldError: ({fieldName}: RequiredFieldParams) => `Bitte geben Sie das ${fieldName} ein`,
customSegments: {
title: 'Benutzerdefinierte Segmente/Einträge',
@@ -4696,18 +4687,18 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
[CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT]: {
label: 'Standard-NetSuite-Mitarbeiter',
description: 'Nicht in Expensify importiert, beim Export angewendet',
- footerContent: (importField: string) =>
+ footerContent: ({importField}: ImportFieldParams) =>
`Wenn du ${importField} in NetSuite verwendest, wenden wir den Standardwert an, der im Mitarbeitendendatensatz festgelegt ist, sobald nach „Expense Report“ oder „Journal Entry“ exportiert wird.`,
},
[CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: {
label: 'Stichwörter',
description: 'Auf Positionsebene',
- footerContent: (importField: string) => `${startCase(importField)} wird für jede einzelne Ausgabe im Bericht eines Mitarbeiters auswählbar sein.`,
+ footerContent: ({importField}: ImportFieldParams) => `${startCase(importField)} wird für jede einzelne Ausgabe im Bericht eines Mitarbeiters auswählbar sein.`,
},
[CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: {
label: 'Berichtsfelder',
description: 'Berichtsebene',
- footerContent: (importField: string) => `${startCase(importField)}-Auswahl gilt für alle Ausgaben auf dem Bericht eines Mitarbeiters.`,
+ footerContent: ({importField}: ImportFieldParams) => `${startCase(importField)}-Auswahl gilt für alle Ausgaben auf dem Bericht eines Mitarbeiters.`,
},
},
},
@@ -4785,7 +4776,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
commercialFeedPlaidDetails: `Erfordert die Einrichtung mit Ihrer Bank, aber wir führen Sie durch den Prozess. Dies ist in der Regel auf größere Unternehmen beschränkt.`,
directFeedDetails: 'Der einfachste Ansatz. Verbinde dich direkt mit deinen Master-Zugangsdaten. Diese Methode ist am gebräuchlichsten.',
enableFeed: {
- title: (provider: string) => `Aktiviere deinen ${provider}-Feed`,
+ title: ({provider}: GoBackMessageParams) => `Aktiviere deinen ${provider}-Feed`,
heading:
'Wir verfügen über eine direkte Integration mit Ihrem Kartenaussteller und können Ihre Transaktionsdaten schnell und genau in Expensify importieren.\n\nUm zu beginnen, gehen Sie einfach wie folgt vor:',
visa: 'Wir verfügen über globale Integrationen mit Visa, wobei die Berechtigung je nach Bank und Kartenprogramm variiert.\n\nUm loszulegen, gehen Sie einfach wie folgt vor:',
@@ -5061,25 +5052,6 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
subtitle: 'Nutzen Sie Expensify Travel für die besten Reiseangebote und verwalten Sie alle Ihre Geschäftsausgaben an einem Ort.',
ctaText: 'Buchen oder verwalten',
},
- travelInvoicing: {
- travelBookingSection: {
- title: 'Reisebuchung',
- subtitle: 'Glückwunsch! Du kannst jetzt in diesem Workspace Reisen buchen und verwalten.',
- manageTravelLabel: 'Reisen verwalten',
- },
- centralInvoicingSection: {
- title: 'Zentrale Rechnungsstellung',
- subtitle: 'Zentralisieren Sie alle Reisekosten in einer monatlichen Rechnung, anstatt zum Zeitpunkt des Kaufs zu bezahlen.',
- learnHow: "So funktioniert's.",
- subsections: {
- currentTravelSpendLabel: 'Aktuelle Reisekosten',
- currentTravelSpendCta: 'Saldo bezahlen',
- currentTravelLimitLabel: 'Aktuelles Reiselimit',
- settlementAccountLabel: 'Ausgleichskonto',
- settlementFrequencyLabel: 'Abrechnungshäufigkeit',
- },
- },
- },
},
expensifyCard: {
title: 'Expensify Card',
@@ -5368,7 +5340,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
prompt3: 'Ein Backup herunterladen',
prompt4: 'zuerst.',
},
- importedTagsMessage: (columnCounts: number) =>
+ importedTagsMessage: ({columnCounts}: ImportedTagsMessageParams) =>
`Wir haben *${columnCounts} Spalten* in Ihrer Tabelle gefunden. Wählen Sie *Name* neben der Spalte aus, die die Tag-Namen enthält. Sie können außerdem *Aktiviert* neben der Spalte auswählen, die den Tag-Status festlegt.`,
cannotDeleteOrDisableAllTags: {
title: 'Es können nicht alle Tags gelöscht oder deaktiviert werden',
@@ -5539,8 +5511,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
giveItNameInstruction: 'Mach sie eindeutig genug, um sie von anderen Karten unterscheiden zu können. Konkrete Anwendungsfälle sind sogar noch besser!',
cardName: 'Kartenname',
letsDoubleCheck: 'Lass uns noch einmal überprüfen, ob alles richtig aussieht.',
- willBeReadyToUse: 'Diese Karte ist sofort einsatzbereit.',
- willBeReadyToShip: 'Diese Karte ist sofort versandbereit.',
+ willBeReady: 'Diese Karte ist sofort einsatzbereit.',
cardholder: 'Karteninhaber',
cardType: 'Kartentyp',
limit: 'Limit',
@@ -6198,7 +6169,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
rules: {
individualExpenseRules: {
title: 'Ausgaben',
- subtitle: (categoriesPageLink: string, tagsPageLink: string) =>
+ subtitle: ({categoriesPageLink, tagsPageLink}: IndividualExpenseRulesSubtitleParams) =>
`Legen Sie Ausgabenkontrollen und Standardwerte für einzelne Ausgaben fest. Sie können auch Regeln für Kategorien und Tags erstellen.`,
receiptRequiredAmount: 'Erforderlicher Belegbetrag',
receiptRequiredAmountDescription: 'Belege verlangen, wenn die Ausgaben diesen Betrag überschreiten, sofern dies nicht durch eine Kategorienregel außer Kraft gesetzt wird.',
@@ -6269,10 +6240,6 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
title: 'Kategorierichtlinien',
approver: 'Genehmiger',
requireDescription: 'Beschreibung erforderlich',
- requireFields: 'Felder verpflichtend machen',
- requiredFieldsTitle: 'Pflichtfelder',
- requiredFieldsDescription: (categoryName: string) => `Dies gilt für alle Ausgaben, die als ${categoryName} kategorisiert sind.`,
- requireAttendees: 'Teilnehmer erforderlich machen',
descriptionHint: 'Hinweis zur Beschreibung',
descriptionHintDescription: (categoryName: string) =>
`Mitarbeitende daran erinnern, zusätzliche Informationen für Ausgaben der Kategorie „${categoryName}“ anzugeben. Dieser Hinweis erscheint im Beschreibungsfeld von Ausgaben.`,
@@ -6506,6 +6473,11 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
deleteReportField: (fieldType: string, fieldName?: string) => `${fieldType}-Berichtsfeld „${fieldName}“ entfernt`,
preventSelfApproval: ({oldValue, newValue}: UpdatedPolicyPreventSelfApprovalParams) =>
`„Verhinderung der Selbstgenehmigung“ aktualisiert auf „${newValue === 'true' ? 'Aktiviert' : 'Deaktiviert'}“ (zuvor „${oldValue === 'true' ? 'Aktiviert' : 'Deaktiviert'}“)`,
+ updateMaxExpenseAmountNoReceipt: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
+ `den maximalen Belegbetrag für erforderliche Ausgaben auf ${newValue} geändert (zuvor ${oldValue})`,
+ updateMaxExpenseAmount: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `den maximalen Spesenbetrag für Verstöße auf ${newValue} geändert (zuvor ${oldValue})`,
+ updateMaxExpenseAge: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
+ `„Maximales Spesenalter (Tage)“ aktualisiert auf „${newValue}“ (zuvor „${oldValue === 'false' ? CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE : oldValue}“)`,
updateMonthlyOffset: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => {
if (!oldValue) {
return `Das Datum für die monatliche Berichtseinreichung auf „${newValue}“ festlegen`;
@@ -6658,40 +6630,8 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
}
}
},
- setDefaultBankAccount: ({bankAccountName, maskedBankAccountNumber}: {bankAccountName: string; maskedBankAccountNumber: string}) =>
- `das Standardgeschäftsbankkonto auf „${bankAccountName ? `${bankAccountName}: ` : ''}${maskedBankAccountNumber}“ festlegen`,
- removedDefaultBankAccount: ({bankAccountName, maskedBankAccountNumber}: {bankAccountName: string; maskedBankAccountNumber: string}) =>
- `das Standard-Geschäftsbankkonto „${bankAccountName ? `${bankAccountName}: ` : ''}${maskedBankAccountNumber}“ entfernt`,
- changedDefaultBankAccount: ({
- bankAccountName,
- maskedBankAccountNumber,
- oldBankAccountName,
- oldMaskedBankAccountNumber,
- }: {
- bankAccountName: string;
- maskedBankAccountNumber: string;
- oldBankAccountName: string;
- oldMaskedBankAccountNumber: string;
- }) =>
- `Standard-Geschäftsbankkonto geändert zu „${bankAccountName ? `${bankAccountName}: ` : ''}${maskedBankAccountNumber}“ (zuvor „${oldBankAccountName ? `${oldBankAccountName}: ` : ''}${oldMaskedBankAccountNumber}“)`,
- changedInvoiceCompanyName: ({newValue, oldValue}: {newValue: string; oldValue?: string}) =>
- oldValue ? `den Firmennamen der Rechnung in „${newValue}“ geändert (zuvor „${oldValue}“)` : `den Rechnungsfirmennamen auf „${newValue}“ festlegen`,
- changedInvoiceCompanyWebsite: ({newValue, oldValue}: {newValue: string; oldValue?: string}) =>
- oldValue ? `die Firmenwebsite der Rechnung wurde in „${newValue}“ geändert (zuvor „${oldValue}“)` : `setzen Sie die Firmenwebsite der Rechnung auf „${newValue}“`,
changedCustomReportNameFormula: ({newValue, oldValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
`benutzerdefinierte Berichtsnamensformel in „${newValue}“ geändert (zuvor „${oldValue}“)`,
- changedReimburser: ({newReimburser, previousReimburser}: UpdatedPolicyReimburserParams) =>
- previousReimburser ? `hat den autorisierten Zahler in „${newReimburser}“ geändert (zuvor „${previousReimburser}“)` : `den autorisierten Zahler in „${newReimburser}“ geändert`,
- setReceiptRequiredAmount: ({newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `erforderlichen Belegbetrag auf „${newValue}“ festlegen`,
- changedReceiptRequiredAmount: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
- `Betrag für erforderlichen Beleg auf „${newValue}“ geändert (zuvor „${oldValue}“)`,
- removedReceiptRequiredAmount: ({oldValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `Betrag für erforderlichen Beleg entfernt (zuvor „${oldValue}“)`,
- setMaxExpenseAmount: ({newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `maximalen Spesenbetrag auf „${newValue}“ festlegen`,
- changedMaxExpenseAmount: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `maximalen Ausgabenbetrag auf „${newValue}“ geändert (zuvor „${oldValue}“)`,
- removedMaxExpenseAmount: ({oldValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `maximalen Ausgabenbetrag entfernt (zuvor „${oldValue}“)`,
- setMaxExpenseAge: ({newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `maximales Ausgabenalter auf „${newValue}“ Tage festlegen`,
- changedMaxExpenseAge: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `maximales Ausgabenalter auf „${newValue}“ Tage geändert (zuvor „${oldValue}“)`,
- removedMaxExpenseAge: ({oldValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `maximales Ausgabenalter entfernt (zuvor „${oldValue}“ Tage)`,
},
roomMembersPage: {
memberNotFound: 'Mitglied nicht gefunden.',
@@ -6858,7 +6798,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
amount: {
lessThan: ({amount}: OptionalParam = {}) => `Weniger als ${amount ?? ''}`,
greaterThan: ({amount}: OptionalParam = {}) => `Größer als ${amount ?? ''}`,
- between: (greaterThan: string, lessThan: string) => `Zwischen ${greaterThan} und ${lessThan}`,
+ between: ({greaterThan, lessThan}: FiltersAmountBetweenParams) => `Zwischen ${greaterThan} und ${lessThan}`,
equalTo: ({amount}: OptionalParam = {}) => `Gleich ${amount ?? ''}`,
},
card: {
@@ -7287,7 +7227,6 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Datum älter als ${maxAge} Tage`,
missingCategory: 'Fehlende Kategorie',
missingComment: 'Beschreibung für ausgewählte Kategorie erforderlich',
- missingAttendees: 'Für diese Kategorie sind mehrere Teilnehmer erforderlich',
missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Fehlende ${tagName ?? 'Tag'}`,
modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => {
switch (type) {
@@ -7395,7 +7334,6 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
hold: 'Diese Ausgabe wurde zurückgestellt',
resolvedDuplicates: 'Duplikat behoben',
companyCardRequired: 'Firmenkartenkäufe erforderlich',
- noRoute: 'Bitte wählen Sie eine gültige Adresse aus',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} ist erforderlich`,
@@ -8022,18 +7960,9 @@ 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 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: