diff --git a/.claude/commands/review-code-pr.md b/.claude/commands/review-code-pr.md index f2cf709276083..6e4364d4518b3 100644 --- a/.claude/commands/review-code-pr.md +++ b/.claude/commands/review-code-pr.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(gh pr diff:*),Bash(gh pr view:*) +allowed-tools: Bash(gh pr diff:*),Bash(gh pr view:*),Bash(check-compiler.sh:*) description: Review a code contribution pull request --- diff --git a/.claude/scripts/check-compiler.sh b/.claude/scripts/check-compiler.sh new file mode 100755 index 0000000000000..a1279c942bcac --- /dev/null +++ b/.claude/scripts/check-compiler.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Secure proxy script to run React Compiler compliance check on a single file. +# Validates the filepath before passing it to the underlying npm script. +set -eu + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +readonly FILEPATH="$1" + +# Strict filepath validation - reject shell metacharacters +if ! [[ "$FILEPATH" =~ ^[a-zA-Z0-9_./@-]+$ ]]; then + echo "Error: Invalid filepath (contains disallowed characters)" >&2 + exit 1 +fi + +npm run react-compiler-compliance-check -- check "$FILEPATH" diff --git a/.claude/skills/coding-standards/rules/clean-react-0-compiler.md b/.claude/skills/coding-standards/rules/clean-react-0-compiler.md index af697f1499458..c2a5283346b94 100644 --- a/.claude/skills/coding-standards/rules/clean-react-0-compiler.md +++ b/.claude/skills/coding-standards/rules/clean-react-0-compiler.md @@ -98,7 +98,7 @@ function Avatar({source, size}: AvatarProps) { Before flagging, verify that the file actually compiles with React Compiler: ```bash -npx react-compiler-healthcheck --src "" --verbose +check-compiler.sh ``` If the output contains **"Failed to compile"** for the file under review, the rule **does not apply** — the author may have no alternative to manual memoization until the compilation issue is resolved. diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index da5a2b15460fe..ed5756f5d8cce 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -211,6 +211,79 @@ jobs: git push origin ${{ inputs.TARGET }} fi + - name: Find deploy workflow run + # Also runs for version-bump-only CPs where HAS_CONFLICTS is unset + if: ${{ steps.cherryPick.outputs.HAS_CONFLICTS != 'true' }} + id: findDeployRun + run: | + PUSH_SHA=$(git rev-parse HEAD) + echo "Push SHA: $PUSH_SHA" + + DEPLOY_RUN_URL="" + for i in 1 2 3 4 5 6; do + echo "Polling for deploy run (attempt $i)..." + sleep 10 + DEPLOY_RUN_URL=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/deploy.yml/runs?head_sha=$PUSH_SHA&per_page=1" \ + --jq '.workflow_runs[0].html_url // empty' 2>/dev/null || true) + if [ -n "$DEPLOY_RUN_URL" ]; then + echo "Found deploy run: $DEPLOY_RUN_URL" + break + fi + done + + FALLBACK_URL="https://github.com/${{ github.repository }}/actions/workflows/deploy.yml" + if [ -z "$DEPLOY_RUN_URL" ]; then + echo "::warning::Could not find deploy workflow run for SHA $PUSH_SHA" + { + echo "DEPLOY_RUN_FOUND=false" + echo "DEPLOY_RUN_URL=$FALLBACK_URL" + echo "DEPLOY_RUN_MESSAGE=⚠️ Could not locate deploy run — check $FALLBACK_URL" + } >> "$GITHUB_OUTPUT" + else + { + echo "DEPLOY_RUN_FOUND=true" + echo "DEPLOY_RUN_URL=$DEPLOY_RUN_URL" + echo "DEPLOY_RUN_MESSAGE=$DEPLOY_RUN_URL" + } >> "$GITHUB_OUTPUT" + fi + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Announce successful CP in #deployer + # Also runs for version-bump-only CPs where HAS_CONFLICTS is unset + if: ${{ steps.cherryPick.outputs.HAS_CONFLICTS != 'true' }} + uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: 'good', + text: `🍒 Cherry-pick to *${{ inputs.TARGET }}* successful\nPR: ${{ inputs.PULL_REQUEST_URL || '(version bump only)' }}\nDeploy workflow: ${{ steps.findDeployRun.outputs.DEPLOY_RUN_MESSAGE }}` + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + - name: Write workflow summary + # Also runs for version-bump-only CPs where HAS_CONFLICTS is unset + if: ${{ steps.cherryPick.outputs.HAS_CONFLICTS != 'true' }} + run: | + { + echo "## Cherry-pick successful" + echo "" + echo "**Target:** \`${{ inputs.TARGET }}\`" + echo "**PR:** ${{ inputs.PULL_REQUEST_URL || 'N/A (version bump only)' }}" + if [[ "${{ steps.findDeployRun.outputs.DEPLOY_RUN_FOUND }}" == "true" ]]; then + echo "**Deploy workflow:** ${{ steps.findDeployRun.outputs.DEPLOY_RUN_URL }}" + else + echo "**Deploy workflow:** :warning: Could not locate deploy run — check [${{ inputs.TARGET }} deploy runs](${{ steps.findDeployRun.outputs.DEPLOY_RUN_URL }})" + fi + } >> "$GITHUB_STEP_SUMMARY" + - name: Create Pull Request to manually finish CP if: steps.cherryPick.outputs.HAS_CONFLICTS == 'true' id: createPullRequest diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 98578ba2bc78c..89d2256ec0a65 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -74,7 +74,7 @@ jobs: prompt: "/review-code-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}" claude_args: | --model claude-opus-4-6 - --allowedTools "Task,Glob,Grep,Read,Bash(gh pr diff:*),Bash(gh pr view:*)" --json-schema '${{ steps.schema.outputs.json }}' + --allowedTools "Task,Glob,Grep,Read,Bash(gh pr diff:*),Bash(gh pr view:*),Bash(check-compiler.sh:*)" --json-schema '${{ steps.schema.outputs.json }}' - name: Post code review results if: steps.code-review.outcome == 'success' && steps.filter.outputs.code == 'true' diff --git a/Mobile-Expensify b/Mobile-Expensify index 65761aa22cb01..84e475f1405d4 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 65761aa22cb01930272fd06a557d3dffcdc99c8a +Subproject commit 84e475f1405d4ad7a2d568bdd2a1a13624d74cbb diff --git a/android/app/build.gradle b/android/app/build.gradle index 1b5773244181f..a19bfe474cb1b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,8 +111,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009033709 - versionName "9.3.37-9" + versionCode 1009033903 + versionName "9.3.39-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/car-plus.svg b/assets/images/car-plus.svg new file mode 100644 index 0000000000000..e43db1ecf1c2d --- /dev/null +++ b/assets/images/car-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg new file mode 100644 index 0000000000000..045c80d03f570 --- /dev/null +++ b/assets/images/document-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/envelope-open-star.svg b/assets/images/envelope-open-star.svg deleted file mode 100644 index 74652c126f5fe..0000000000000 --- a/assets/images/envelope-open-star.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cspell.json b/cspell.json index 1e1382d4174f9..d55ae14f4fede 100644 --- a/cspell.json +++ b/cspell.json @@ -667,6 +667,8 @@ "setuptools", "shareeEmail", "Sharees", + "sharee", + "sharees", "Sharons", "shellcheck", "shellenv", diff --git a/docs/HELPSITE_NAMING_CONVENTIONS.md b/docs/HELPSITE_NAMING_CONVENTIONS.md index 270f9d64aa3d7..356f7965df2fa 100644 --- a/docs/HELPSITE_NAMING_CONVENTIONS.md +++ b/docs/HELPSITE_NAMING_CONVENTIONS.md @@ -10,6 +10,8 @@ This document governs UI language conventions. It does not define article struct docs/HELP_AUTHORING_GUIDELINES.md +Note: All article headings (# and ##) must follow the task-based heading rules in HELP_AUTHORING_GUIDELINES.md Section 2, except for `# FAQ` which is exempt. This includes section headings that reference UI features — they must still be task-based, not just feature labels. + --- # Core UI Referencing Rules diff --git a/docs/HELP_AUTHORING_GUIDELINES.md b/docs/HELP_AUTHORING_GUIDELINES.md index 1907b04f1a40d..a282f60e28953 100644 --- a/docs/HELP_AUTHORING_GUIDELINES.md +++ b/docs/HELP_AUTHORING_GUIDELINES.md @@ -34,11 +34,14 @@ If multiple workflows are detected → split into multiple articles. - Setup - Options - Step 1 +- Noun-only or topic-only headings that describe a category rather than a task +- Platform-only labels used as headings (e.g., "iPhone", "Android", "Desktop", "Web") +- Any heading that does not describe an action the user takes or a question the user has ## Requirements -- All headings must be: - - Task-based +- All headings must be (except `# FAQ`, which is exempt from task-based rules): + - Task-based — must describe what the user will do or learn. Start with an action verb or question word (How, What, Where, Who, Why, When) - Searchable - Explicit - Feature-specific @@ -48,6 +51,15 @@ If multiple workflows are detected → split into multiple articles. # Who can connect a business bank account in Expensify ## Where to enable ACH reimbursements in a Workspace ## How to troubleshoot bank connection errors in Expensify +## How to enable Expensify Card notifications on iPhone + +**Invalid → Corrected Examples** + +- ❌ `## Transaction decline notifications` → ✅ `## How to understand why your Expensify Card transaction was declined` +- ❌ `## iPhone` → ✅ `## How to enable notifications on iPhone` +- ❌ `## Suspected fraud notifications` → ✅ `## What to do when you receive a fraud alert` +- ❌ `## Banking and settlement notifications` → ✅ `## How banking and settlement alerts work for admins` +- ❌ `## Card lifecycle notifications` → ✅ `## What card lifecycle notifications you'll receive` --- @@ -72,11 +84,15 @@ Metadata must reflect real search queries. Each article must include: - YAML metadata - - One primary # heading - - Task-based ## sections + - Exactly one # heading (the article title). No other # headings are allowed except # FAQ + - Task-based ## sections for all content below the title - Sequential numbered steps (if procedural) - An FAQ section (if needed) +Do not use multiple # headings to organize an article into major sections. If content feels like it needs its own # heading, either: + - Demote it to ## under the single # title, or + - Split it into a separate article (per Section 1: one workflow per article) + If including an FAQ section, it must comply with: /docs/HELPSITE_NAMING_CONVENTIONS.md @@ -224,8 +240,12 @@ Prioritize screenshots for: # 9. Pre-Publish Validation Checklist Before outputting an article, confirm: - - Only # and ## used - - No generic headings + - Exactly one # heading (the article title), plus optional # FAQ — no other # headings + - Only ## used for all content sections (no ### or deeper) + - Every ## heading starts with an action verb or question word (How, What, Where, Who, Why, When) + - `# FAQ` is exempt from task-based heading rules + - No noun-only, topic-only, or platform-only headings + - No generic headings (Overview, Introduction, Notes, Setup, Options, Step 1) - Feature names match UI - Metadata aligns with search intent - Navigation included (if applicable) diff --git a/docs/articles/expensify-classic/expenses/Expense-Types.md b/docs/articles/expensify-classic/expenses/Expense-Types.md index 9012cc77d8e5e..3962608cf0927 100644 --- a/docs/articles/expensify-classic/expenses/Expense-Types.md +++ b/docs/articles/expensify-classic/expenses/Expense-Types.md @@ -69,7 +69,7 @@ Use filters to narrow down the data: 2. Adjust the filters at the top of the page: - **Date Range** – Select a specific time frame. - **Merchant Name** – Find expenses from a particular vendor (partial searches work). - - **Workspace** – View expenses for a specific Group or Individual Workspace. + - **Workspace** – View expenses for a specific workspace. - **Categories** – Filter by category to refine your search. - **Tags** – Locate expenses based on assigned tags. - **Submitters** – Find expenses by employee or vendor. diff --git a/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md index 16bc8d4dbaf7f..5dbc2f8647080 100644 --- a/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md +++ b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md @@ -5,35 +5,14 @@ keywords: [Expensify Classic, workspace plan, subscription] --- -Expensify offers several plans based on your needs: **Track, Submit, Collect, Control,** and **Free**. Your choice depends on whether you manage expenses individually, for a group, or for a company. You may need to upgrade if you hire employees who need access to a **Group Workspace** or require features exclusive to paid plans. +Expensify offers several plans based on your needs: **Collect, Control,** and **Free**. You may need to upgrade if you hire employees who need access to your workspace or require features exclusive to paid plans. --- -# Change a Subscription on an Individual Plan +# Changing Your Subscription **Web:** -1. Go to **Settings > Workspaces > Individual > [Workspace Name]**. -2. Click **Overview** and select **Switch** under your desired plan. - -**Mobile:** -1. Open the Expensify app. -2. Tap the **hamburger menu** (three lines) on the top left. -3. Tap **Settings**. -4. Tap **View All** under your Workspace. -5. Select the Workspace under the **Individual** tab. -6. Tap **Current Plan**. -7. Tap **Switch** to change plans. - -## Upgrade to a Group Workspace -1. Go to **Settings > Workspaces > Group**. -2. Select a **Collect** or **Control** plan. - ---- - -# Changing a Subscription on a Group Plan - -**Web:** -1. Go to **Settings > Workspaces > Group > [Your Group Workspace]**. +1. Go to **Settings > Workspaces > [Your Workspace]**. 2. Click **Overview** and select **Switch** under your desired plan. **Mobile:** @@ -42,48 +21,32 @@ Expensify offers several plans based on your needs: **Track, Submit, Collect, Co --- -## Adjust Subscription Size -1. Go to **Settings > Workspaces > Group > Subscription**. +## Adjust Annual Subscription Size +1. Go to **Settings > Workspaces > Subscription**. 2. Enter the desired number in the **Subscription size** field. - If left blank, your subscription size will be set automatically: - **New Workspaces**: Based on active users in the first month. - **Existing Workspaces Switching to Annual**: Based on the last month's active users. ## Auto-Increase Subscription Size -1. Go to **Settings > Workspaces > Group > Subscription**. -2. Toggle **Auto-increase annual seats**. +1. Go to **Settings > Workspaces > Subscription**. +2. Toggle **Auto Increase Subscription Size**. 3. When enabled, your subscription size will adjust automatically based on usage, triggering a new 12-month commitment for the updated size. ## Auto-Renew Subscription -1. Go to **Settings > Workspaces > Group > Subscription**. -2. Toggle **Auto-renew** off before your current subscription ends if you do not want it to renew. - - If **Auto-renew** is disabled, your final bill at the annual rate will be issued on the date listed under **Subscriptions**. - ---- - -# Downgrading to a Free Account from an Individual Plan - -**Web:** -1. Log in via a web browser. -2. Go to **Settings > Workspaces > Individual > Subscription**. -3. Click **Cancel Subscription**. - - **Note**: The subscription is prepaid for 30 days of unlimited **SmartScanning**. No refunds are issued, but you retain access until the period ends. - -**App Store (iOS Users):** -1. Go to the **App Store**. -2. Tap your **Apple ID** > **Subscriptions**. -3. Cancel your Expensify subscription. - - **Note**: This cannot be done within Expensify. Downgrading to a free account must happen from the App Store. +1. Go to **Settings > Workspaces > Subscription**. +2. Toggle **Auto Renew** off before your current subscription ends if you do not want it to renew. + - If **Auto Renew** is disabled, your final bill at the annual rate will be issued on the date listed under **Subscriptions**. --- -# Downgrading to a Free Account from a Group Plan +# Downgrading to a Free Account ## Pay-Per-Use Plan -1. Go to **Settings > Workspaces > Group**. +1. Go to **Settings > Workspaces**. 2. Click the **cog icon** next to your Workspace name. 3. Select **Delete**. - - **Note**: Deleting a Workspace removes its settings and members but does not delete their Expensify accounts. + - **Note**: Only the Billing Owner can delete a Workspace. Deleting a Workspace removes its settings and members but does not delete members' Expensify accounts. - If any members were active that month (submitted, approved, or edited reports), you will be billed for their usage. ## Annual Subscription @@ -99,11 +62,11 @@ Once downgraded, your Workspace will be deleted, and a refund line item will app # FAQ -## Will I be charged for a monthly subscription even if I don't use SmartScans? +## Will I be charged for my subscription even if I don't use SmartScans? -Yes, monthly subscriptions are prepaid and not usage-based, so you will be charged regardless of activity. +Yes, Collect and Control subscriptions are prepaid and not usage-based, so you will be charged regardless of activity. -## I'm in a Group Workspace. Do I need the monthly subscription too? +## I'm in a workspace. Do I need my own subscription too? -No, Group Workspace members already have unlimited **SmartScans**. However, you can keep a subscription for personal use if you leave your company's Workspace. +No, workspace members already have unlimited **SmartScans**. However, you can keep a subscription for personal use if you leave your company's workspace. diff --git a/docs/articles/expensify-classic/expensify-billing/Personal-and-Corporate-Karma.md b/docs/articles/expensify-classic/expensify-billing/Personal-and-Corporate-Karma.md index 49c69dfda8e8c..911ba5e1409de 100644 --- a/docs/articles/expensify-classic/expensify-billing/Personal-and-Corporate-Karma.md +++ b/docs/articles/expensify-classic/expensify-billing/Personal-and-Corporate-Karma.md @@ -20,13 +20,7 @@ The fund from your Personal Karma is determined by the expense's MCC (Merchant C ## Setting Up Personal Karma Donations -You can enable Personal Karma donations from your personal workspace settings. - -1. [Sign in](www.expensify.com) to your web account. -2. Go to **Settings > Workspace > Individual**. -3. Under the **Subscription** section, enable Karma donations. - -![Settings > Workspaces > Individual workspace > enable Personal Karma in settings](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png){:width="100%"} +Personal Karma is now managed in New Expensify. To enable or disable Personal Karma donations, see the [Personal Karma](https://help.expensify.com/articles/new-expensify/settings/Personal-Karma) help page. --- @@ -40,11 +34,11 @@ The fund to which your Corporate Karma goes is determined by the expense's MCC ( ## Setting Up Corporate Karma Donations -As a [workspace billing owner](https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account), you can enable Corporate Karma from the group workspace settings. +As a [workspace billing owner](https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account), you can enable Corporate Karma from your workspace settings. -1. [Sign in](www.expensify.com) to your web account. -2. Go to **Settings > Workspace > Group**. -3. Under the **Subscription** section, enable Karma donations. +1. [Sign in](https://www.expensify.com) to your web account. +2. Go to **Settings > Workspace > [Your Workspace Name] > Subscription**. +3. Enable Karma donations. ![Settings > Workspaces > Group > enable Corporate Karma in subscription settings](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_Karma_Group.png){:width="100%"} diff --git a/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md b/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md index 7879530999a92..fff6fcab9ce13 100644 --- a/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md +++ b/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md @@ -6,7 +6,7 @@ keywords: [Expensify Classic, create workspace, new workspace, getting started] Creating a workspace in Expensify Classic is the first step to organizing your expenses and managing your finances efficiently. This guide walks you through setting up your workspace so you can seamlessly track receipts, submit reports, and stay on top of your spending. -All Expensify accounts include an Individual Workspace, which allows you to track personal expenses. If you want to connect your personal expenses to an accounting or travel integration, you can create a Group Workspace — even if you are the only member. +You can create a workspace to track personal expenses and connect to accounting or travel integrations — even if you are the only member. --- @@ -29,24 +29,9 @@ Your personal assistant, **Concierge**, is available on your Expensify Home page # 3. Create a Workspace -## Individual vs. Group Workspaces -- **Individual Workspace**: A private space for managing your personal expenses. -- **Group Workspace**: Designed for teams, even if you’re the only member. Group workspaces include features like expense approval flows and integrations with accounting, HR, and travel tools. - -## Set Up Your Individual Workspace -1. Go to **Settings** > **Workspaces**. -2. Click the **Individual** tab on the left. -3. Click the workspace name to set up your Individual Workspace. -4. Go to Plan on the workspace menu to select the workspace type that fits your needs. -5. Configure your workspace details (e.g., workspace name, categories, tags, etc). - -Note: The Individual Workspace is an underlying component of Expensify Classic. It cannot be deleted, and you can't add additional Individual Workspaces. Only additional Group Workspaces can be added manually (see below). Individual workspaces don't connect directly to accounting software or have the ability to add users. - -## Create a Group Workspace 1. Go to **Settings** > **Workspaces**. -2. Click the **Group** tab on the left. -3. Click **New Workspace**. -4. Set up the workspace details (e.g., name, expense rules, categories). +2. Click **New Workspace**. +3. Set up the workspace details (e.g., name, expense rules, categories). --- @@ -127,13 +112,13 @@ When logging in, use the Magic Code from your email and the 6-digit code from yo # FAQ -## Is the Individual workspace free? +## Can I use Expensify for free? -Yes, individuals can use Expensify for free to track expenses. +Yes, you can use Expensify for free to track expenses. -## Can I create a Group workspace to use just for myself? +## Can I create a workspace to use just for myself? -Yes, group workspaces include key features such as more robust approval rules, direct connections to accounting integrations, and access to Expensify Travel. These are helpful features for automating expense tracking, even if you are the only member of the workspace. +Yes, a workspace includes features such as approval rules, direct connections to accounting integrations, and access to Expensify Travel -- even if you are the only member. See pricing details [here](https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing). diff --git a/docs/articles/expensify-classic/workspaces/Expense-Settings.md b/docs/articles/expensify-classic/workspaces/Expense-Settings.md index 8e773bb237d0b..8e93c3a83f9fb 100644 --- a/docs/articles/expensify-classic/workspaces/Expense-Settings.md +++ b/docs/articles/expensify-classic/workspaces/Expense-Settings.md @@ -4,7 +4,7 @@ description: Customize and manage expense settings at the workspace level, inclu keywords: [Expensify Classic, expenses, violations, reimbursable, billable, mileage, eReceipts, taxes, Concierge Receipt Audit, expense rules] --- -Expensify offers multiple ways to customize how expenses are created and managed at the workspace level. Whether you’re using an individual workspace or managing expenses in a group workspace, you can configure various settings to suit your needs. +Expensify offers multiple ways to customize how expenses are created and managed at the workspace level. You can configure various settings to suit your needs. You can manage expense rules and distance rates at the workspace level. The following expense-level settings are customizable: diff --git a/docs/articles/expensify-classic/workspaces/Navigate-multiple-workspaces.md b/docs/articles/expensify-classic/workspaces/Navigate-multiple-workspaces.md index 4913da8146439..a5d5bfeed7df6 100644 --- a/docs/articles/expensify-classic/workspaces/Navigate-multiple-workspaces.md +++ b/docs/articles/expensify-classic/workspaces/Navigate-multiple-workspaces.md @@ -4,7 +4,7 @@ description: Using more than one Expensify workspace keywords: [Expensify Classic, default workspace] --- -If you have multiple workspaces—whether an individual workspace and a group workspace or multiple group workspaces—you’ll want to: +If you have multiple workspaces, you’ll want to: - Set a default workspace. (*Note: Some domains automatically set your default workspace, and in these cases, you cannot change it.*) - Select your workspace before creating an expense or report to ensure it’s posted to the correct workspace. diff --git a/docs/articles/new-expensify/settings/Personal-Karma.md b/docs/articles/new-expensify/settings/Personal-Karma.md new file mode 100644 index 0000000000000..40d8ebd650a77 --- /dev/null +++ b/docs/articles/new-expensify/settings/Personal-Karma.md @@ -0,0 +1,65 @@ +--- +title: Personal Karma +description: Learn how Personal Karma works, how donation amounts are calculated, and how to enable or disable automatic donations to Expensify.org. +keywords: [New Expensify, personal karma, donations, expensify.org, save the world, Expensify.org donation, automatic donation, billing card charge] +--- + +# Personal Karma + +Personal Karma lets you automatically donate a small percentage of your monthly added expenses to [Expensify.org](https://www.expensify.org/about). + +For every $500 in expenses you add, $1 is donated to a related Expensify.org fund. Donations are calculated monthly and charged to the billing card on file. You’ll receive a donation receipt by email after each charge. + +--- + +## How to enable or disable Personal Karma + +1. In the navigation tabs (on the left on Web, at the bottom on Mobile), choose **Account**. +2. Select **Save the World** +3. Toggle **Personal Karma** to enable or disable it. + +--- + +## How Personal Karma calculates your donation amount + +Your donation is based on the total amount of expenses added during the month. + +This includes: + - Reported expenses + - Unreported expenses + - Invoice expenses + +For every $500 in total expenses, $1 is donated. For example: + + - $500 in expenses = $1 donation + - $2,500 in expenses = $5 donation + +The total donation amount is calculated at the end of each month. + +--- + +## How Expensify charges you for Personal Karma donations + +At the end of each month, Expensify calculates your total donation amount and charges your billing card on file. A donation receipt is sent to your email. + +To change the card where Personal Karma is charged, [update your Expensify billing card](https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription#how-to-update-your-expensify-billing-card). + +--- + +# FAQ + +## Does Personal Karma include unreported expenses? + +Yes. All expenses on the Expenses page — reported and unreported — are included in the monthly total used to calculate your donation. + +## Where does my donation go? + +Donations are directed to one of Expensify.org's five funds — Climate Justice, Food Security, Housing Equity, Reentry Services, or Youth Advocacy — based on the Merchant Category Code of each expense. + +## Can I choose which Expensify.org fund receives my donation? + +No. The fund is automatically selected based on the Merchant Category Code (MCC) of each expense. + +## How can I learn more about Expensify.org's fund categories? + +To learn more about Expensify.org funding categories, visit [expensify.org/funds](https://www.expensify.org/funds). diff --git a/eslint-plugin-report-name-utils/index.mjs b/eslint-plugin-report-name-utils/index.mjs new file mode 100644 index 0000000000000..78bbc70504e0f --- /dev/null +++ b/eslint-plugin-report-name-utils/index.mjs @@ -0,0 +1,30 @@ +/** + * ESLint plugin that enforces architectural constraints on ReportNameUtils.ts. + * + * `getReportName` must remain a pure read-only function — it reads from + * pre-computed `reportAttributesDerivedValue` and must never call other + * functions. All computation belongs in `computeReportName`. + */ + +const noFunctionCallInGetReportName = { + meta: { + type: 'problem', + docs: {description: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.'}, + messages: {noFunctionCall: 'getReportName must be a pure read-only function. Move any computation to computeReportName instead.'}, + schema: [], + }, + create(context) { + return { + 'FunctionDeclaration[id.name="getReportName"] CallExpression': function (node) { + context.report({node, messageId: 'noFunctionCall'}); + }, + }; + }, +}; + +export default { + meta: {name: 'eslint-plugin-report-name-utils'}, + rules: { + 'no-function-call-in-get-report-name': noFunctionCallInGetReportName, + }, +}; diff --git a/eslint.changed.config.mjs b/eslint.changed.config.mjs index 79114b59a65eb..87417ae72a2ab 100644 --- a/eslint.changed.config.mjs +++ b/eslint.changed.config.mjs @@ -1,4 +1,5 @@ import {defineConfig} from 'eslint/config'; +import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs'; import mainConfig from './eslint.config.mjs'; const restrictedIconImportPaths = [ @@ -106,6 +107,12 @@ const config = defineConfig([ ], }, }, + + { + files: ['src/libs/ReportNameUtils.ts'], + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, + }, ]); export default config; diff --git a/eslint.config.mjs b/eslint.config.mjs index 361e2b63b90ae..6051af243bcde 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import typescriptEslint from 'typescript-eslint'; import reactCompilerCompat from './eslint-plugin-react-compiler-compat/index.mjs'; +import reportNameUtilsPlugin from './eslint-plugin-report-name-utils/index.mjs'; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -592,6 +593,12 @@ const config = defineConfig([ }, }, + { + files: ['src/libs/ReportNameUtils.ts'], + plugins: {'report-name-utils': reportNameUtilsPlugin}, + rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}, + }, + { files: ['src/**/*'], ignores: ['src/languages/**', 'src/CONST/index.ts', 'src/NAICS.ts'], diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 39a7381c60b76..9a067f8e2067d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.3.37 + 9.3.39 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.37.9 + 9.3.39.3 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index bae659acf92c0..990267605871c 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.3.37 + 9.3.39 CFBundleVersion - 9.3.37.9 + 9.3.39.3 NSExtension NSExtensionPointIdentifier diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index 4f24716fc927d..919573a5e3ecf 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.3.37 + 9.3.39 CFBundleVersion - 9.3.37.9 + 9.3.39.3 NSExtension NSExtensionAttributes diff --git a/jest/setup.ts b/jest/setup.ts index a452f7d1e945e..2f7c5a52a5983 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -43,6 +43,35 @@ jest.mock('expo-task-manager', () => ({ // Add other methods here if you use them })); +// Mock expo-location — the jest-expo preset replaces all native module methods with jest.fn(async () => {}), +// which returns undefined instead of a proper PermissionResponse. This causes crashes when code reads .status +// from the result of requestForegroundPermissionsAsync(). +jest.mock('expo-location', () => ({ + requestForegroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), + requestBackgroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), + getForegroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), + getBackgroundPermissionsAsync: jest.fn(() => Promise.resolve({status: 'granted', granted: true, canAskAgain: true, expires: 0})), + getCurrentPositionAsync: jest.fn(() => Promise.resolve({coords: {latitude: 0, longitude: 0, altitude: 0, accuracy: 0, altitudeAccuracy: 0, heading: 0, speed: 0}, timestamp: 0})), + hasStartedLocationUpdatesAsync: jest.fn(() => Promise.resolve(false)), + startLocationUpdatesAsync: jest.fn(() => Promise.resolve()), + stopLocationUpdatesAsync: jest.fn(() => Promise.resolve()), + reverseGeocodeAsync: jest.fn(() => Promise.resolve([])), + hasServicesEnabledAsync: jest.fn(() => Promise.resolve(true)), + PermissionStatus: { + GRANTED: 'granted', + DENIED: 'denied', + UNDETERMINED: 'undetermined', + }, + Accuracy: { + Lowest: 1, + Low: 2, + Balanced: 3, + High: 4, + Highest: 5, + BestForNavigation: 6, + }, +})); + // 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 diff --git a/package-lock.json b/package-lock.json index bddd3b82363fb..a67b91f2fd154 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.37-9", + "version": "9.3.39-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.37-9", + "version": "9.3.39-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -60,7 +60,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.171", + "expensify-common": "2.0.173", "expo": "54.0.22", "expo-asset": "12.0.8", "expo-audio": "1.1.1", @@ -117,7 +117,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.45", + "react-native-onyx": "3.0.46", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -238,7 +238,7 @@ "dotenv": "^16.0.3", "eslint": "^9.36.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "2.0.105", + "eslint-config-expensify": "2.0.107", "eslint-config-prettier": "^9.1.0", "eslint-plugin-file-progress": "3.0.1", "eslint-plugin-jest": "^29.0.1", @@ -273,7 +273,7 @@ "react-native-clean-project": "^4.0.0-alpha4.0", "react-refresh": "^0.14.2", "react-test-renderer": "19.1.0", - "reassure": "^1.0.0-rc.4", + "reassure": "^1.0.0", "rock": "0.12.10", "semver": "7.5.2", "setimmediate": "^1.0.5", @@ -3769,19 +3769,14 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.25.7", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.14.0", - "license": "MIT" - }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -3862,33 +3857,26 @@ "license": "Apache-2.0" }, "node_modules/@callstack/reassure-cli": { - "version": "1.0.0-rc.4", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@callstack/reassure-cli/-/reassure-cli-1.4.1.tgz", + "integrity": "sha512-NKJi/WoLiDkn3FnYJhOk+FhWsdvrytrAfHRqexrCeyVfwpgbM+laPWto2+EKbfKdrHWBqWgbBOWyFoMiOAXcVg==", "dev": true, "license": "MIT", "dependencies": { - "@callstack/reassure-compare": "1.0.0-rc.4", - "@callstack/reassure-logger": "1.0.0-rc.4", + "@callstack/reassure-compare": "1.4.1", + "@callstack/reassure-logger": "1.4.1", "chalk": "4.1.2", - "simple-git": "^3.24.0", + "simple-git": "^3.32.3", "yargs": "^17.7.2" }, "bin": { "reassure": "lib/commonjs/bin.js" } }, - "node_modules/@callstack/reassure-cli/node_modules/@callstack/reassure-compare": { - "version": "1.0.0-rc.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@callstack/reassure-logger": "1.0.0-rc.4", - "markdown-table": "^2.0.0", - "ts-regex-builder": "^1.7.1", - "zod": "^3.23.8" - } - }, "node_modules/@callstack/reassure-cli/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3903,6 +3891,8 @@ }, "node_modules/@callstack/reassure-cli/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -3918,6 +3908,8 @@ }, "node_modules/@callstack/reassure-cli/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3929,11 +3921,15 @@ }, "node_modules/@callstack/reassure-cli/node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/@callstack/reassure-cli/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -3942,6 +3938,8 @@ }, "node_modules/@callstack/reassure-cli/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3952,26 +3950,49 @@ } }, "node_modules/@callstack/reassure-compare": { - "version": "1.4.0", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@callstack/reassure-compare/-/reassure-compare-1.4.1.tgz", + "integrity": "sha512-qJgASbKlBWA37XSN5b/uVAvc524dd9s3grumCKabI2GHkA5w5G6WWI11DnJPzjBnoZ/oIgNSrOm2sm6UMEvsVQ==", "dev": true, "license": "MIT", "dependencies": { - "@callstack/reassure-logger": "1.4.0", - "ts-markdown-builder": "0.4.1", + "@callstack/reassure-logger": "1.4.1", + "ts-markdown-builder": "0.5.0", "ts-regex-builder": "^1.8.2", - "zod": "^3.24.2" + "zod": "^4.1.12" } }, - "node_modules/@callstack/reassure-compare/node_modules/@callstack/reassure-logger": { - "version": "1.4.0", + "node_modules/@callstack/reassure-compare/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@callstack/reassure-danger": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@callstack/reassure-danger/-/reassure-danger-1.4.1.tgz", + "integrity": "sha512-TTfliolOt8Tfq4yu8vdj4lRdki5Vuvwxj22K2+xpAjxiF2rT4BL6gwM3YIPVUwuQMN3kQ8d8TuGJ4Wlc3zpAUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@callstack/reassure-logger": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@callstack/reassure-logger/-/reassure-logger-1.4.1.tgz", + "integrity": "sha512-2uB6OBk0/IdSXUpdTsMcN40d3ly5xWWPRjs0G4zEr37tyHB0lmJVuQIR5elUjIs+fTuK48PVuqW/4+Yce7WlFA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2" } }, - "node_modules/@callstack/reassure-compare/node_modules/ansi-styles": { + "node_modules/@callstack/reassure-logger/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -3984,8 +4005,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@callstack/reassure-compare/node_modules/chalk": { + "node_modules/@callstack/reassure-logger/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -3999,8 +4022,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@callstack/reassure-compare/node_modules/color-convert": { + "node_modules/@callstack/reassure-logger/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4010,21 +4035,27 @@ "node": ">=7.0.0" } }, - "node_modules/@callstack/reassure-compare/node_modules/color-name": { + "node_modules/@callstack/reassure-logger/node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, - "node_modules/@callstack/reassure-compare/node_modules/has-flag": { + "node_modules/@callstack/reassure-logger/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/@callstack/reassure-compare/node_modules/supports-color": { + "node_modules/@callstack/reassure-logger/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -4034,93 +4065,67 @@ "node": ">=8" } }, - "node_modules/@callstack/reassure-danger": { - "version": "1.0.0-rc.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@callstack/reassure-logger": { - "version": "1.0.0-rc.4", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2" - } - }, - "node_modules/@callstack/reassure-logger/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@callstack/reassure-logger/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@callstack/reassure-measure": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@callstack/reassure-measure/-/reassure-measure-1.4.1.tgz", + "integrity": "sha512-EPUKuMgzz0bccf/nn+6A5PhunZS/K3h0rpvhdOsPzGl3abjROoPi7bQN/ipURsLwle+OonqLCigcOK/CnndLEg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" + "@callstack/reassure-logger": "1.4.1", + "mathjs": "^15.1.0", + "pretty-format": "^30.2.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "react": ">=18.0.0" } }, - "node_modules/@callstack/reassure-logger/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/@callstack/reassure-measure/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=7.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@callstack/reassure-logger/node_modules/color-name": { - "version": "1.1.4", + "node_modules/@callstack/reassure-measure/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, - "node_modules/@callstack/reassure-logger/node_modules/has-flag": { - "version": "4.0.0", + "node_modules/@callstack/reassure-measure/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/@callstack/reassure-logger/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" + "node": ">=10" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@callstack/reassure-measure": { - "version": "1.0.0-rc.4", + "node_modules/@callstack/reassure-measure/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "@callstack/reassure-logger": "1.0.0-rc.4", - "mathjs": "^12.4.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, - "peerDependencies": { - "react": ">=18.0.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@clack/core": { @@ -9858,6 +9863,8 @@ }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", "dev": true, "license": "MIT", "dependencies": { @@ -9866,6 +9873,8 @@ }, "node_modules/@kwsites/promise-deferred": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "dev": true, "license": "MIT" }, @@ -19941,15 +19950,17 @@ "license": "MIT" }, "node_modules/complex.js": { - "version": "2.1.1", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.3.tgz", + "integrity": "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==", "dev": true, "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, "node_modules/compressible": { @@ -22288,6 +22299,8 @@ }, "node_modules/escape-latex": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", "dev": true, "license": "MIT" }, @@ -22447,9 +22460,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.105", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.105.tgz", - "integrity": "sha512-fH8w9XSN53xsTKPt6qxqq+L7B/6Pd6cNyzUL8TexfJRiG9SM9BCAE8Jitlrp/PhDbyfDsZxWPTGbLlbuYLm7Yw==", + "version": "2.0.107", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.107.tgz", + "integrity": "sha512-zRqSigtxbE1WA2pIUMtW1Cuq5pjsHj69G/3h9rKf8QW6TvUgSsRyHsbWZUn2rvGGg43irS1SjtPnmGHJNTVbpw==", "dev": true, "license": "ISC", "dependencies": { @@ -23462,9 +23475,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.171", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.171.tgz", - "integrity": "sha512-2Pvnm+VNOV0v2+5Q2NRiOQiTgStZLI4r5f8XCccg3k2fz+kgKERibAUaA3FRWZavmu1/Fydekqs35q+bklFRdA==", + "version": "2.0.173", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.173.tgz", + "integrity": "sha512-fRGgm8OblMA9BrsLGBxZ3WAwHRISlVwoDLhU9JGuJ+bm6WZBPJbfaqlg8uWrzTxkZEgSgpV1IfDspx5ac/cn2g==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", @@ -25366,14 +25379,16 @@ } }, "node_modules/fraction.js": { - "version": "4.3.4", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -30984,18 +30999,6 @@ "vt-pbf": "^3.1.3" } }, - "node_modules/markdown-table": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/marky": { "version": "1.2.5", "license": "Apache-2.0" @@ -31019,19 +31022,21 @@ } }, "node_modules/mathjs": { - "version": "12.4.3", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.1.1.tgz", + "integrity": "sha512-rM668DTtpSzMVoh/cKAllyQVEbBApM5g//IMGD8vD7YlrIz9ITRr3SrdhjaDxcBNTdyETWwPebj2unZyHD7ZdA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.24.4", - "complex.js": "^2.1.1", + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", - "fraction.js": "4.3.4", + "fraction.js": "^5.2.1", "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", - "typed-function": "^4.1.1" + "typed-function": "^4.2.1" }, "bin": { "mathjs": "bin/cli.js" @@ -34562,9 +34567,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.45", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.45.tgz", - "integrity": "sha512-oJyizoazptOzKfY8ENDu0Y+QcFvCvRFySIEgQGOMZ0SDvmuni9MJ57zPd6/C/3n84GzTCJ5yDLmOFQRiHuPoRQ==", + "version": "3.0.46", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.46.tgz", + "integrity": "sha512-/dB5PrK+AZ4QiaFdIdb2iC1q5tu7L3n6pxh+QZJiMhDORlQT6jK832b/6sqwgQ3qIZCrrsnqmyLVKCqEbYTrlQ==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -35198,31 +35203,22 @@ } }, "node_modules/reassure": { - "version": "1.0.0-rc.4", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/reassure/-/reassure-1.4.1.tgz", + "integrity": "sha512-EeW23/ci4mGHsv4WDSK9jTzzi5yCu3Gdf3GgugKvi5NrqxCiVubiTlcz8bjfpipSFEUK+mpDtlhyJI/6Kmkgjg==", "dev": true, "license": "MIT", "dependencies": { - "@callstack/reassure-cli": "1.0.0-rc.4", - "@callstack/reassure-compare": "1.0.0-rc.4", - "@callstack/reassure-danger": "1.0.0-rc.4", - "@callstack/reassure-measure": "1.0.0-rc.4", - "import-local": "^3.1.0" + "@callstack/reassure-cli": "1.4.1", + "@callstack/reassure-compare": "1.4.1", + "@callstack/reassure-danger": "1.4.1", + "@callstack/reassure-measure": "1.4.1", + "import-local": "^3.2.0" }, "bin": { "reassure": "lib/commonjs/bin/reassure.js" } }, - "node_modules/reassure/node_modules/@callstack/reassure-compare": { - "version": "1.0.0-rc.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@callstack/reassure-logger": "1.0.0-rc.4", - "markdown-table": "^2.0.0", - "ts-regex-builder": "^1.7.1", - "zod": "^3.23.8" - } - }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -35460,14 +35456,6 @@ "entities": "^2.0.0" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -36371,13 +36359,15 @@ "optional": true }, "node_modules/simple-git": { - "version": "3.24.0", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", "dev": true, "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "debug": "^4.3.4" + "debug": "^4.4.0" }, "funding": { "type": "github", @@ -37946,7 +37936,9 @@ } }, "node_modules/ts-markdown-builder": { - "version": "0.4.1", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ts-markdown-builder/-/ts-markdown-builder-0.5.0.tgz", + "integrity": "sha512-/2pzAFjGwk5fUR8ftFMhCOm/zmoqtpYWK6209j3YPfgWF2I+eYY6PKpR0mBJte6s8McnkQ/T92fV+IJg6bdxyw==", "dev": true, "license": "MIT", "engines": { @@ -38200,7 +38192,9 @@ } }, "node_modules/typed-function": { - "version": "4.2.1", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.2.tgz", + "integrity": "sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index c632c8519cce3..4ec8bb100af6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.37-9", + "version": "9.3.39-3", "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.", @@ -45,7 +45,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "typecheck-tsgo": "tsgo --project tsconfig.tsgo.json", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=316 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=315 --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", @@ -124,7 +124,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.171", + "expensify-common": "2.0.173", "expo": "54.0.22", "expo-asset": "12.0.8", "expo-audio": "1.1.1", @@ -181,7 +181,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.45", + "react-native-onyx": "3.0.46", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -302,7 +302,7 @@ "dotenv": "^16.0.3", "eslint": "^9.36.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "2.0.105", + "eslint-config-expensify": "2.0.107", "eslint-config-prettier": "^9.1.0", "eslint-plugin-file-progress": "3.0.1", "eslint-plugin-jest": "^29.0.1", @@ -337,7 +337,7 @@ "react-native-clean-project": "^4.0.0-alpha4.0", "react-refresh": "^0.14.2", "react-test-renderer": "19.1.0", - "reassure": "^1.0.0-rc.4", + "reassure": "^1.0.0", "rock": "0.12.10", "semver": "7.5.2", "setimmediate": "^1.0.5", diff --git a/patches/react-native-web/details.md b/patches/react-native-web/details.md index 49ca1a391a819..df0cad52449ef 100644 --- a/patches/react-native-web/details.md +++ b/patches/react-native-web/details.md @@ -134,13 +134,3 @@ - Upstream PR/issue: https://github.com/necolas/react-native-web/issues/2817 - E/App issue: https://github.com/Expensify/App/issues/73782 - PR introducing patch: https://github.com/Expensify/App/pull/76332 - -### [react-native-web+0.21.2+013+fix-selection-bug.patch](react-native-web+0.21.2+013+fix-selection-bug.patch) - -- Reason: - ``` - Fix selection bug for InvertedFlatlist by reversing the DOM tree elements using `pushOrUnshift` method - ``` -- Upstream PR/issue: https://github.com/necolas/react-native-web/issues/1807, it has been closed because of [this](https://github.com/necolas/react-native-web/issues/1807#issuecomment-725689704) -- E/App issue: https://github.com/Expensify/App/issues/37447 -- PR introducing patch: https://github.com/Expensify/App/pull/82507 \ No newline at end of file diff --git a/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch b/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch deleted file mode 100644 index 6ddc31e8e80ee..0000000000000 --- a/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch +++ /dev/null @@ -1,194 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/ScrollView/index.js b/node_modules/react-native-web/dist/exports/ScrollView/index.js -index c4f9b5b..fa32056 100644 ---- a/node_modules/react-native-web/dist/exports/ScrollView/index.js -+++ b/node_modules/react-native-web/dist/exports/ScrollView/index.js -@@ -558,8 +558,9 @@ class ScrollView extends React.Component { - var children = hasStickyHeaderIndices || pagingEnabled ? React.Children.map(this.props.children, (child, i) => { - var isSticky = hasStickyHeaderIndices && stickyHeaderIndices.indexOf(i) > -1; - if (child != null && (isSticky || pagingEnabled)) { -+ var stickyItemIndex = (this.props.children.length - 1) - i + 10; - return /*#__PURE__*/React.createElement(View, { -- style: [isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild] -+ style: [isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild, isSticky && {zIndex: stickyItemIndex}] - }, child); - } else { - return child; -@@ -636,7 +637,6 @@ var styles = StyleSheet.create({ - stickyHeader: { - position: 'sticky', - top: 0, -- zIndex: 10 - }, - pagingEnabledHorizontal: { - scrollSnapType: 'x mandatory' -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index 42c4984..caf22bb 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -108,6 +108,15 @@ function windowSizeOrDefault(windowSize) { - * - */ - class VirtualizedList extends StateSafePureComponent { -+ // reverse push order logic when props.inverted = true -+ pushOrUnshift(input, item) { -+ if (this.props.inverted) { -+ input.unshift(item); -+ } else { -+ input.push(item); -+ } -+ } -+ - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params) { - var animated = params ? params.animated : true; -@@ -343,6 +352,7 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._defaultRenderScrollComponent = props => { - var onRefresh = props.onRefresh; -+ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return /*#__PURE__*/React.createElement(View, props); -@@ -354,6 +364,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] - React.createElement(ScrollView, _extends({}, props, { -+ contentContainerStyle: StyleSheet.compose(inversionStyle, this.props.contentContainerStyle), - refreshControl: props.refreshControl == null ? /*#__PURE__*/React.createElement(RefreshControl - // $FlowFixMe[incompatible-type] - , { -@@ -366,7 +377,9 @@ class VirtualizedList extends StateSafePureComponent { - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return /*#__PURE__*/React.createElement(ScrollView, props); -+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { -+ contentContainerStyle: StyleSheet.compose(inversionStyle, this.props.contentContainerStyle) -+ })); - } - }; - this._onCellLayout = (e, cellKey, index) => { -@@ -568,6 +581,14 @@ class VirtualizedList extends StateSafePureComponent { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { - var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); -+ -+ // revert the state if calculations are off -+ // this would only happen on the inverted flatlist (probably a bug with overscroll-behavior) -+ // when scrolled from bottom all the way up until onEndReached is triggered -+ if (cellsAroundViewport.first === cellsAroundViewport.last) { -+ cellsAroundViewport = state.cellsAroundViewport; -+ } -+ - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -679,7 +700,7 @@ class VirtualizedList extends StateSafePureComponent { - onViewableItemsChanged = _this$props3.onViewableItemsChanged, - viewabilityConfig = _this$props3.viewabilityConfig; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged - }); -@@ -991,15 +1012,15 @@ class VirtualizedList extends StateSafePureComponent { - var end = getItemCount(data) - 1; - var prevCellKey; - last = Math.min(end, last); -- var _loop = function _loop() { -+ var _loop = () => { - var item = getItem(data, ii); - var key = VirtualizedList._keyExtractor(item, ii, _this.props); - _this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); -- cells.push(/*#__PURE__*/React.createElement(CellRenderer, _extends({ -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ - CellRendererComponent: CellRendererComponent, - ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, - ListItemComponent: ListItemComponent, -@@ -1074,14 +1095,14 @@ class VirtualizedList extends StateSafePureComponent { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : - /*#__PURE__*/ - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListHeaderComponent, null); -- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-header', - key: "$header" - }, /*#__PURE__*/React.createElement(View -@@ -1105,7 +1126,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListEmptyComponent, null); -- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-empty', - key: "$empty" - }, /*#__PURE__*/React.cloneElement(_element2, { -@@ -1145,7 +1166,7 @@ class VirtualizedList extends StateSafePureComponent { - var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); - var lastMetrics = this.__getFrameMetricsApprox(last, this.props); - var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push(/*#__PURE__*/React.createElement(View, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { - key: "$spacer-" + section.first, - style: { - [spacerKey]: spacerSize -@@ -1168,7 +1189,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListFooterComponent, null); -- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getFooterCellKey(), - key: "$footer" - }, /*#__PURE__*/React.createElement(View, { -@@ -1179,6 +1200,14 @@ class VirtualizedList extends StateSafePureComponent { - _element3))); - } - -+ if (this.props.inverted && stickyHeaderIndices.length > 0) { -+ var totalCells = cells.length; -+ stickyHeaderIndices = stickyHeaderIndices.map(function(recordedIndex) { -+ return totalCells - 1 - recordedIndex; -+ }); -+ } -+ -+ - // 4. Render the ScrollView - var scrollProps = _objectSpread(_objectSpread({}, this.props), {}, { - onContentSizeChange: this._onContentSizeChange, -@@ -1353,7 +1382,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } - } - var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; -@@ -1526,6 +1555,12 @@ var styles = StyleSheet.create({ - horizontallyInverted: { - transform: 'scaleX(-1)' - }, -+ rowReverse: { -+ flexDirection: 'row-reverse', -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse', -+ }, - debug: { - flex: 1 - }, diff --git a/scripts/clean.sh b/scripts/clean.sh index 780805e52ecca..166f570daee1f 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -10,8 +10,8 @@ IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) # See if we should force standalone NewDot build NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" -# Clean rock cache -npx rock clean --include rock +# Clean rock and ccache cache +npx rock clean --include rock,ccache if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then echo -e "${BLUE}Cleaning HybridApp project...${NC}" diff --git a/src/CONST/index.ts b/src/CONST/index.ts index f2d66c6656c65..27466d9f12abf 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -22,6 +22,8 @@ import {LOCALES} from './LOCALES'; const EMPTY_ARRAY = Object.freeze([]); const EMPTY_OBJECT = Object.freeze({}); const EMPTY_SET = new Set(); +// Shared immutable map used in hot paths that only read from the instance. +const EMPTY_MAP = new Map(); const EMPTY_TODOS_REPORT_COUNTS = Object.freeze({ submit: 0, approve: 0, @@ -796,6 +798,7 @@ const CONST = { AU: 'AU', CA: 'CA', GB: 'GB', + GI: 'GI', IT: 'IT', PR: 'PR', GU: 'GU', @@ -1017,6 +1020,7 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, EMPTY_SET, + EMPTY_MAP, EMPTY_TODOS_REPORT_COUNTS, DEFAULT_NUMBER_ID, DEFAULT_MISSING_ID, @@ -1124,6 +1128,7 @@ const CONST = { EMPLOYEE_TOUR_MOBILE: 'https://expensify.storylane.io/share/v8uwkznocw0g', EMPLOYEE_MIGRATED: 'https://app.storylane.io/share/v9dr1rjqsd9y', EMPLOYEE_MIGRATED_MOBILE: 'https://app.storylane.io/share/qbbob6zvapqo', + IFRAME_SANDBOX: 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox', }, OLD_DOT_PUBLIC_URLS: { TERMS_URL: `${EXPENSIFY_URL}/terms`, @@ -1224,6 +1229,7 @@ const CONST = { MERGE: 'merge', DUPLICATE: 'duplicate', DUPLICATE_REPORT: 'duplicateReport', + MOVE_EXPENSE: 'moveExpense', }, PRIMARY_ACTIONS: { SUBMIT: 'submit', @@ -1260,6 +1266,7 @@ const CONST = { REJECT_BULK: 'rejectBulk', MERGE: 'merge', DUPLICATE: 'duplicate', + MOVE_EXPENSE: 'moveExpense', }, ADD_EXPENSE_OPTIONS: { CREATE_NEW_EXPENSE: 'createNewExpense', @@ -1729,6 +1736,17 @@ const CONST = { FAB_OUT: 200, }, }, + FAB_MENU_ITEM_IDS: { + QUICK_ACTION: 'quick-action', + EXPENSE: 'expense', + TRACK_DISTANCE: 'track-distance', + CREATE_REPORT: 'create-report', + NEW_CHAT: 'new-chat', + INVOICE: 'invoice', + TRAVEL: 'travel', + TEST_DRIVE: 'test-drive', + NEW_WORKSPACE: 'new-workspace', + }, TIMING: { REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, SHOW_LOADING_SPINNER_DEBOUNCE_TIME: 250, @@ -1736,6 +1754,7 @@ const CONST = { TOOLTIP_SENSE: 1000, COMMENT_LENGTH_DEBOUNCE_TIME: 1500, SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, + ACCESSIBILITY_ANNOUNCEMENT_DEBOUNCE_TIME: 1000, SUGGESTION_DEBOUNCE_TIME: 100, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, @@ -2174,6 +2193,7 @@ const CONST = { KEYBOARD_TYPE: { VISIBLE_PASSWORD: 'visible-password', ASCII_CAPABLE: 'ascii-capable', + PHONE_PAD: 'phone-pad', NUMBER_PAD: 'number-pad', DECIMAL_PAD: 'decimal-pad', NUMBERS_AND_PUNCTUATION: 'numbers-and-punctuation', @@ -2198,7 +2218,6 @@ const CONST = { YOUR_LOCATION_TEXT: 'Your Location', ATTACHMENT_MESSAGE_TEXT: '[Attachment]', - ATTACHMENT_REGEX: /