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. ![Click Add bank account on the Wallet page]({{site.url}}/assets/images/ExpensiHelp-DepositAccount-1.png){: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**. -![Click Account Settings > Workspaces > click on the workspace]({{site.url}}/assets/images/submissions-01.png){:width="100%"} +![Click Account Settings > Workspaces > click on the workspace]({{site.url}}/assets/images/ExpensifyHelp-Workflows-1.png){:width="100%"} -![Click More Features > Enable Workflows]({{site.url}}/assets/images/submissions-02.png){:width="100%"} +![Click More Features > Enable Workflows]({{site.url}}/assets/images/ExpensifyHelp-Workflows-2.png){: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. -![Enable workflow features]({{site.url}}/assets/images/submissions-03.png){:width="100%"} +![Enable workflow features]({{site.url}}/assets/images/ExpensifyHelp-Workflows-3.png){: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 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.3.7 + 9.3.1 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.7.1 + 9.3.1.0 FullStory OrgId 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.1 CFBundleVersion - 9.3.7.1 + 9.3.1.0 NSExtension NSExtensionPointIdentifier 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.1 CFBundleVersion - 9.3.7.1 + 9.3.1.0 NSExtension NSExtensionAttributes 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]: (