diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c1238d6805aa..459a780ca8b4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -51,7 +51,10 @@ For example: 1. Click on the text input to bring it into focus 2. Upload an image via copy paste 3. Verify a modal appears displaying a preview of that image + +It's acceptable to write "Same as tests" if the QA team is able to run the tests in the above "Tests" section. ---> +// TODO: These must be filled out, or the issue title must include "[No QA]." - [ ] Verify that no errors appear in the JS console diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 59f41bd12526..d578621930a7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -182,6 +182,7 @@ jobs: submodules: true path: 'Mobile-Expensify' token: ${{ secrets.OS_BOTIFY_TOKEN }} + # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - name: Update submodule @@ -276,6 +277,13 @@ jobs: name: android-hybrid-build-artifact path: ${{ env.aabPath }} + - name: Upload Android sourcemap artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: android-hybrid-sourcemap-artifact + path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map + - name: Set current App version in Env run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" @@ -482,6 +490,7 @@ jobs: submodules: true path: 'Mobile-Expensify' token: ${{ secrets.OS_BOTIFY_TOKEN }} + # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - name: Update submodule @@ -597,6 +606,13 @@ jobs: name: ios-hybrid-build-artifact path: ${{ env.ipaPath }} + - name: Upload iOS sourcemap artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: ios-hybrid-sourcemap-artifact + path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map + - name: Warn deployers if iOS production deploy failed if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: 8398a7/action-slack@v3 @@ -802,19 +818,25 @@ jobs: "./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap.js.map" "./android-aab-artifact/app-production-release.aab#android.aab" "./android-hybrid-build-artifact/Expensify-release.aab#android-hybrid.aab" + "./android-hybrid-sourcemap-artifact/index.android.bundle.map#android-hybrid-sourcemap.js.map" "./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap.js.map" "./desktop-staging-build-artifact/NewExpensify.dmg#desktop-staging.dmg" "./ios-sourcemaps-artifact/main.jsbundle.map#ios-sourcemap.js.map" "./ios-build-artifact/New Expensify.ipa#ios.ipa" "./ios-hybrid-build-artifact/Expensify.ipa#ios-hybrid.ipa" - "./web-staging-sourcemaps-artifact/web-staging-sourcemap.js.map#web-staging-sourcemap.js.map" + "./ios-hybrid-sourcemap-artifact/main.jsbundle.map#ios-hybrid-sourcemap.js.map" + "./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map#web-staging-sourcemap.js.map" "./web-staging-build-tar-gz-artifact/webBuild.tar.gz#web-staging.tar.gz" "./web-staging-build-zip-artifact/webBuild.zip#web-staging.zip" ) - + # Loop through each file and upload individually (so if one fails, we still have other platforms uploaded) for file_entry in "${files[@]}"; do - gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber "$file_entry" + gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber "$file_entry" || { + echo "Failed to upload $file_entry. Continuing with the next file." + continue + } + echo "Successfully uploaded $file_entry." done env: GITHUB_TOKEN: ${{ github.token }} @@ -870,10 +892,14 @@ jobs: "./web-build-tar-gz-artifact/webBuild.tar.gz#web-production.tar.gz" "./web-build-zip-artifact/webBuild.zip#web-production.zip" ) - + # Loop through each file and upload individually (so if one fails, we still have other platforms uploaded) for file_entry in "${files[@]}"; do - gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber "$file_entry" + gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber "$file_entry" || { + echo "Failed to upload $file_entry. Continuing with the next file." + continue + } + echo "Successfully uploaded $file_entry." done env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/deployNewHelp.yml b/.github/workflows/deployNewHelp.yml index 2d2f551482d2..8e455979a50e 100644 --- a/.github/workflows/deployNewHelp.yml +++ b/.github/workflows/deployNewHelp.yml @@ -53,7 +53,7 @@ jobs: # Install Node for _scripts/*.js - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20.18.0' diff --git a/Gemfile.lock b/Gemfile.lock index 35920fc3e988..71f4fd64bc0c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,20 +20,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.979.0) - aws-sdk-core (3.209.1) + aws-partitions (1.1001.0) + aws-sdk-core (3.211.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) + aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.94.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.166.0) - aws-sdk-core (~> 3, >= 3.207.0) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.0) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -89,7 +89,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.111.0) + excon (0.112.0) faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -119,7 +119,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.222.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -135,6 +135,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -164,6 +165,8 @@ GEM apktools (~> 0.7) aws-sdk-s3 (~> 1) mime-types (~> 3.3) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) ffi (1.17.0) ffi (1.17.0-arm64-darwin) ffi (1.17.0-x86_64-darwin) @@ -214,8 +217,8 @@ GEM i18n (1.14.5) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.2) - jwt (2.9.1) + json (2.7.6) + jwt (2.9.3) base64 mime-types (3.5.1) mime-types-data (~> 3.2015) @@ -226,7 +229,7 @@ GEM molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) @@ -241,7 +244,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.7) + rexml (3.3.9) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -255,6 +258,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -270,13 +274,13 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.25.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/android/app/build.gradle b/android/app/build.gradle index 271311c543fb..60429549fbec 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009005700 - versionName "9.0.57-0" + versionCode 1009005709 + versionName "9.0.57-9" // 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/binoculars.svg b/assets/images/binoculars.svg new file mode 100644 index 000000000000..64977dee38b5 --- /dev/null +++ b/assets/images/binoculars.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 75cb080f1349..926fb1e24d22 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -9,7 +9,7 @@ "dependencies": { "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", - "electron-updater": "^6.3.8", + "electron-updater": "^6.3.9", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12" }, @@ -59,9 +59,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.9", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz", - "integrity": "sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw==", + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -156,12 +156,12 @@ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "node_modules/electron-updater": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.8.tgz", - "integrity": "sha512-OFsA2vyuOZgsqq4EW6tgW8X8e521ybDmQyIYLqss7HdXev+Ak90YatzpIECOBJXpmro5YDq4yZ2xFsKXqPt1DQ==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", + "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", "license": "MIT", "dependencies": { - "builder-util-runtime": "9.2.9", + "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -469,9 +469,9 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, "builder-util-runtime": { - "version": "9.2.9", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz", - "integrity": "sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw==", + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", "requires": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -538,11 +538,11 @@ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "electron-updater": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.8.tgz", - "integrity": "sha512-OFsA2vyuOZgsqq4EW6tgW8X8e521ybDmQyIYLqss7HdXev+Ak90YatzpIECOBJXpmro5YDq4yZ2xFsKXqPt1DQ==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", + "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", "requires": { - "builder-util-runtime": "9.2.9", + "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", diff --git a/desktop/package.json b/desktop/package.json index 326d6f24f740..ac66df7e9aed 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -6,7 +6,7 @@ "dependencies": { "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", - "electron-updater": "^6.3.8", + "electron-updater": "^6.3.9", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12" }, diff --git a/docs/Hidden/Expensify-Lounge.md b/docs/Hidden/Expensify-Lounge.md new file mode 100644 index 000000000000..716040ba2078 --- /dev/null +++ b/docs/Hidden/Expensify-Lounge.md @@ -0,0 +1,66 @@ +--- +title: Expensify Lounge +description: Explore the Expensify Lounge - A stylish space to work, relax, and connect. +--- + +The Expensify Lounge is a place where people come to Get Shit Done. With beautiful surroundings, great coffee, and a collaborative community, it's the perfect environment to fuel productivity. Check out this guide on how to make the most of the Expensify Lounge! + +# The Two Rules + +## Rule #1 - Get Shit Done +The Lounge is designed to help you focus, collaborate, and bring your boldest ideas to life. To keep this environment productive, we ask our members to remember: + +- **#focus** - Use the space as it’s intended, without disrupting others. The Lounge is social and collaborative but ultimately meant to support productive work. +- **#urgency** - Remote work is fantastic, but face-to-face collaboration is unmatched. Use the Lounge to meet co-workers in person and drive your projects forward. +- **#results** - Don’t confuse time spent with effort or effort with results. Visualize what you want to accomplish and don’t leave until it’s done. + +## Rule #2 - Don’t Ruin It for Everyone Else +We want the Lounge to be an incredible, ever-evolving space. To achieve this, please follow these guidelines: + +- **#writeitdown** - If you can share knowledge, do it! Write a blog post, document, or post in Expensify Chat to help others learn from your experience. Suggestions to improve the Lounge are always welcome. +- **#showup** - Be fully present when you’re here. Engage with others and collaborate in social spaces. This is a community built to get shit done; the more you contribute, the more you gain. +- **#oneteam** - Inclusivity is a priority. We do not tolerate any form of discrimination. Make an effort to include those who want to join. +- **#nocreeps** - Don’t make others feel uncomfortable with your words or actions. If you feel uncomfortable or notice it happening to someone else, use the escalation process in the FAQ. + +--- + +# How to Use the Expensify Lounge +With these two rules in mind, here’s how to get the most from the Lounge: + +## Rule #1 - Getting Shit Done +- **Order drinks from Concierge** - Contact Concierge here to ask questions or order beverages, and they’ll deliver your order to you. +- **Using an office** - Offices are first-come, first-serve, and ideal for brief calls or meetings. Please keep usage to under an hour. Offices cannot be reserved. +- **Lounge hours** - The Lounge is open from 8am-6pm PT, Monday through Friday, and closed on some major holidays. Check our Google Maps profile for holiday hours. +- **Suggest improvements** - Post any ideas to enhance the Lounge experience in #announce - Expensify Lounge. + +## Rule #2 - Not Ruining It for Everyone Else +- **Offices are for calls** - Only use an office if you have a call or meeting, and try to keep it under an hour. +- **Respect others** - Avoid being too loud or distracting while others work. When collaborating in Expensify Chat, be respectful and maintain a positive environment. +- **Stay home if you’re sick** - If you’re feeling unwell, please skip the Lounge or wear a mask in public areas. +- **If you see something, say something** - If you feel uncomfortable or notice others in discomfort, notify Concierge. In Expensify Chat, you can also use our moderation tools (outlined in the FAQ). + +We’re thrilled to have you here to live richly, have fun, and help save the world with us. Now, go enjoy the Expensify Lounge, and let’s Get Shit Done! + +--- + +{% include faq-begin.md %} + +## What is Concierge? +Concierge is our automated system that answers member questions in real-time. Local lounge questions are routed to the Lounge’s Concierge. Message Concierge for drink requests or general inquiries—they’ll handle it for you! + +## Who is invited to the Expensify Lounge? +Everyone is invited! Whether you’re a current customer or just need a productive space, we’d love to have you. + +## How do I escalate something that’s making me or someone else uncomfortable? +In Expensify Chat, use the escalation feature to flag messages as: + +- **Spam or Inconsiderate**: This sends a whisper to the sender and flags the message. These flags are visible to all users but not reviewed by Concierge. +- **Intimidating or Bullying**: The message is hidden and reviewed. If confirmed, it will remain hidden, and we’ll communicate the violation to the sender. +- **Harassment or Assault**: The message is hidden immediately, and our team reviews it. The sender receives a warning, and Concierge may block the user if needed. + +In person, please notify Concierge with your lounge location, and they’ll escalate the issue accordingly. + +## Where are other Expensify Lounge locations? +Currently, we only have the San Francisco Lounge, but stay tuned for more locations coming soon! +{% include faq-end.md %} + diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md index afe366fb1dbe..41dc52a4239c 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md @@ -14,7 +14,7 @@ Before a report can be reimbursed via direct deposit: To reimburse a report via direct deposit (USD): 1. Open the report. -2. Click the **Reimburse** button and select **Via Direct Deposit (ACH)**. +2. Click the **Reimburse** button and select **Via Direct Deposit**. 3. Confirm that the correct bank account is listed in the dropdown menu. 4. Click **Accept Terms & Pay**. @@ -27,7 +27,7 @@ Before a report can be reimbursed via global reimbursement: To reimburse a report via global reimbursement: 1. Open the report. -2. Click the **Reimburse** button and select **Via Direct Deposit (ACH)**. +2. Click the **Reimburse** button and select **Via Direct Deposit**. 3. Confirm that the correct bank account is listed in the dropdown menu. 4. Click **Accept Terms & Pay**. diff --git a/docs/articles/expensify-classic/expenses/Add-an-expense.md b/docs/articles/expensify-classic/expenses/Add-an-expense.md index 92a96e989013..5f40ff377be6 100644 --- a/docs/articles/expensify-classic/expenses/Add-an-expense.md +++ b/docs/articles/expensify-classic/expenses/Add-an-expense.md @@ -2,7 +2,6 @@ title: Add an expense description: Create a new expense in Expensify --- -
You can add an expense automatically with SmartScan or enter the expense details manually. @@ -41,63 +40,189 @@ You can open any receipt and click **Fill out details myself** to add or edit th {% include end-selector.html %} -# Email a receipt - You can also email receipts to SmartScan by sending them to receipts@expensify.com from an email address tied to your Expensify account (either a primary or secondary email). SmartScan will automatically pull all of the details from the receipt, fill them in for you, and add the receipt to the Expenses tab on your account. {% include info.html %} **For copilots**: To ensure a receipt is routed to the Expensify account you are copiloting instead of your own account, email the receipt to receipts@expensify.com with the email address of the account you are copiloting as the subject line of the email. {% include end-info.html %} -# Add an expense manually +# Add a per diem expense + +A per diem (also called “per diem allowance” or “daily allowance”) is a fixed daily payment provided by an employer to cover expenses during business or work-related travel. These allowances simplify travel expense tracking and reimbursement for meals, lodging, and incidental expenses. + +{% include info.html %} +Before you can add a per diem expense, a Workspace Admin must [enable per diem expenses](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses) for the workspace and add the per diem rates. If you do not see an option for per diem rates, it is currently unavailable for your workspace, and you’ll need to reach out to one of your Workspace Admins for guidance. +{% include end-info.html %} + +To add a per diem expense, + +1. Click the **Expenses** tab. +2. Click **New Expense** and choose **Per Diem**. +3. Select your travel destination. + - If your trip involves multiple stops, create a separate per diem expense for each destination. +4. Select the start date, end date, start time, and end time for the trip. +5. Select a sub-rate. The available sub-rates are dependent on the trip duration. + - You can include meal deductions or overnight lodging costs if allowed by your workspace. +6. Enter any other required coding information, such as the category, description, or report, and click **Save**. + +# Add a mileage expense + +You can track your mileage-related expenses by logging your trips in Expensify. You have a couple of different options for logging distance: + +- Web app: + - **Manually create**: Manually enter the number of miles for the trip + - **Create from map**: Automatically determine the trip distance based on the start and end location. +- Mobile app: + - **Manually create**: Manually enter the miles for the trip and your mileage rate + - **Odometer**: Enter your odometer reading before and after the trip + - **Start GPS**: Currently under development and unavailable for use. + +{% include info.html %} +When adding a distance expense, the rates available are determined by the rates set in your [workspace rate settings](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). To update these rates or add a new rate, you must be a Workspace Admin. +{% include end-info.html %} {% include selector.html values="desktop, mobile" %} {% include option.html value="desktop" %} 1. Click the **Expenses** tab. -2. Click the + icon in the top right. -3. Select the type of expense. - - **Manually create**: Manually enter receipt details. - - **Scan receipt**: Upload a saved image of a receipt. - - **Create multiple**: Manually enter multiple expenses at once. - - **Time**: Create an expense based on hours. - - **Distance**: Create an expense based on distance. - - Manually Create: Manually enter the distance details for the expense. - - Create from Map: Enter the start and end destination and Expensify will help you create a receipt for the trip. -4. Click **Save**. +2. Click **New Expense**. +3. Select the expense type. + - **Manually create**: + - Enter the number of miles for the trip. + - Select your rate. + - If desired, select the category, add a description, or select a report to add the expense to. + - Click **Save**. + - **Create from map**: + - Add your start location as point A. + - Add your end location as point B. + - If applicable, click **Add Destination** to add additional stops. + - To generate a map receipt, leave the Create Receipt checkbox selected. + - Click **Save**. + - Select your rate. + - If desired, select the category, add a description, or select a report to add the expense to. + - Click **Save**. + {% include end-option.html %} {% include option.html value="mobile" %} -1. Tap the ☰ menu icon in the top left. -2. Tap **Expenses**. -3. Tap the + icon in the top right. -4. Tap the correct expense type and enter the expense details. - - **Manually create**: Manually enter receipt details. - - **Time**: Enter work time and rate. - - **Manually create (Distance)**: Manually enter trip details by total distance. - - **Odometer**: Manually enter trip details by start and end odometer readings. - - **Start GPS**: Track distance while using the Expensify app to automatically calculate the distance in real time during the trip. -5. Tap **Save**. +1. Click the + icon in the top right corner. +2. Under the Distance section, select the expense type. + - **Manually create**: + - Enter your mileage. + - Select your rate. + - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. + - Click **Save**. + - **Odometer**: + - Enter your vehicle’s odometer reading before the trip. + - Enter your vehicle’s odometer reading after the trip. + - Select your rate. + - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. + - Click **Save**. {% include end-option.html %} {% include end-selector.html %} +# Add a group expense + +Capture group and event expenses with Attendee Tracking by documenting who attended and the cost per attendee. The amount is always divided evenly between all attendees—different amounts cannot be allocated to specific attendees. To divide the amounts differently, you’ll first have to split the expense. + {% include info.html %} -If you are an employee under a company workspace, you may not see all of the different expense type options depending on your company’s workspace settings. +Attendees added to an expense will not be notified that they were added to an expense, nor will they share in the expense or be requested to pay for any portion of the expense. {% include end-info.html %} +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the **Expenses** tab. +2. Click the expense you want to add attendees to. +3. Click the attendees field and enter the name or email address of the attendee. + - If the attendee is a member of your workspace, you can select their name from the list. + - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. +4. Click **Save**. + +Once added, you’ll also see the list of attendees in the expense overview on the Expenses tab. To see the cost per employee, hover over the receipt total. These details are also available on any report that you add the expense to. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the **Expenses** tab. +2. Tap the expense you want to add attendees to. +3. Scroll down to the bottom and tap **More Options**. +4. Tap the attendees field and enter the name or email address of the attendee. + - If the attendee is a member of your workspace, you can select their name from the list. + - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. +5. Tap **Save**. + +Attendees will also be listed on any report that you add the expense to. + +{% include end-option.html %} + +{% include end-selector.html %} + +# Add expenses in bulk + +You can upload bulk receipt images or add receipt details in bulk. + +## SmartScan receipt images in bulk + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the **Expenses** tab. +2. Drag and drop up to 10 images or PDF receipts at once from your computer’s files. You can drop them anywhere on the Expense page where you see a green plus icon next to your mouse cursor. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the mobile app and tap the camera icon in the bottom right corner. +2. Tap the camera icon in the right corner to select the Rapid Fire mode. +3. Take a clear photo of each receipt. +4. When all receipts are captured, tap the X in the left corner to close the camera. +{% include end-option.html %} + +{% include end-selector.html %} + +## Manually add receipt details in bulk + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Click the **Expenses** tab. +2. Click **New Expense** and select **Create Multiple**. +3. Enter the expense details for up to 10 expenses and click **Save**. + +## Upload personal expenses via CSV, XLS, etc. + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Under Personal Cards, click **Import Transactions from File**. +4. Click **Upload** and select a .csv, .xls, .ofx, or a .qfx file. + {% include faq-begin.md %} **What’s the difference between a reimbursable and non-reimbursable expense?** -- Reimbursable expenses are things that you pay for with your own money that the company has agreed to pay you back for (like business travel paid for with personal funds). -- Non-reimbursable expenses are things you pay for with company money that need to be documented for accounting purposes (like a lunch paid for with a company card). +- **Reimbursable expenses**: Expenses that the company has agreed to pay you back for. This may include: + - Cash & personal card: Expenses paid for by the employee on behalf of the business. + - Per diem: Expenses for a daily or partial daily rate [configured in your Workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses). + - Time: An hourly rate for your employees or jobs as [set for your workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). This expense type is usually used by contractors or small businesses billing the customer via [Expensify Invoicing](https://help.expensify.com/articles/expensify-classic/workspaces/Set-Up-Invoicing). + - Distance: Expenses related to business travel. +- **Non-reimbursable expenses**: Expenses are things you pay for with company money that need to be documented for accounting purposes (like a lunch paid for with a company card). +- **Billable expenses**: Business or employee expenses that must be billed to a specific client or vendor. This option is for tracking expenses for invoicing to customers, clients, or other departments. Any kind of expense can be billable, in _addition_ to being either reimbursable or non-reimbursable. + +You can also see a breakdown of these expense types on your report and can even organize the report by them. {% include info.html %} If you are an employee under a company workspace, your expenses may automatically be configured as reimbursable or non-reimbursable depending on the details that are entered. If an expense is incorrectly labeled, you must reach out to an admin to have it corrected. {% include end-info.html %} +**Why don't I see the option for one of these types of expenses?** + +If you are an employee under a company workspace, you may not see all of the different expense type options depending on your company’s workspace settings. + +**How do I edit my per diem expenses?** + +Per diem expenses cannot be amended. To make changes, you must delete the expense and recreate it. + {% include faq-end.md %} -
diff --git a/docs/articles/expensify-classic/expenses/Add-expenses-in-bulk.md b/docs/articles/expensify-classic/expenses/Add-expenses-in-bulk.md deleted file mode 100644 index 6ee84e1ead15..000000000000 --- a/docs/articles/expensify-classic/expenses/Add-expenses-in-bulk.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Add expenses in bulk -description: Add multiple expenses at one time ---- -
- -You can upload bulk receipt images or add receipt details in bulk. - -# SmartScan receipt images in bulk - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click the **Expenses** tab. -2. Drag and drop up to 10 images or PDF receipts at once from your computer’s files. You can drop them anywhere on the Expense page where you see a green plus icon next to your mouse cursor. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Open the mobile app and tap the camera icon in the bottom right corner. -2. Tap the camera icon in the right corner to select the Rapid Fire mode. -3. Take a clear photo of each receipt. -4. When all receipts are captured, tap the X in the left corner to close the camera. -{% include end-option.html %} - -{% include end-selector.html %} - -# Manually add receipt details in bulk - -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* - -1. Click the **Expenses** tab. -2. Click **New Expense** and select **Create Multiple**. -3. Enter the expense details for up to 10 expenses and click **Save**. - -# Upload personal expenses via CSV, XLS, etc. - -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* - -1. Hover over Settings, then click **Account**. -2. Click the **Credit Card Import** tab. -3. Under Personal Cards, click **Import Transactions from File**. -4. Click **Upload** and select a .csv, .xls, .ofx, or a .qfx file. - -
diff --git a/docs/articles/expensify-classic/expenses/Track-group-expenses.md b/docs/articles/expensify-classic/expenses/Track-group-expenses.md deleted file mode 100644 index 82921b0e8cd3..000000000000 --- a/docs/articles/expensify-classic/expenses/Track-group-expenses.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Track group expenses -description: Use Attendee Tracking to track group expenses ---- -
- -Capture group and event expenses with Attendee Tracking by documenting who attended and the cost per attendee. The amount is always divided evenly between all attendees—different amounts cannot be allocated to specific attendees. To divide the amounts differently, you’ll first have to split the expense. - -{% include info.html %} -Attendees added to an expense will not be notified that they were added to an expense, nor will they share in the expense or be requested to pay for any portion of the expense. -{% include end-info.html %} - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click the **Expenses** tab. -2. Click the expense you want to add attendees to. -3. Click the attendees field and enter the name or email address of the attendee. - - If the attendee is a member of your workspace, you can select their name from the list. - - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. -4. Click **Save**. - -Once added, you’ll also see the list of attendees in the expense overview on the Expenses tab. To see the cost per employee, hover over the receipt total. These details are also available on any report that you add the expense to. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap the **Expenses** tab. -2. Tap the expense you want to add attendees to. -3. Scroll down to the bottom and tap **More Options**. -4. Tap the attendees field and enter the name or email address of the attendee. - - If the attendee is a member of your workspace, you can select their name from the list. - - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. -5. Tap **Save**. - -Attendees will also be listed on any report that you add the expense to. - -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md b/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md deleted file mode 100644 index e8b9ab0eac75..000000000000 --- a/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Track mileage expenses -description: Add mileage-related expenses ---- - -
- -You can track your mileage-related expenses by logging your trips in Expensify. You have a couple of different options for logging distance: - -- Web app: - - **Manually create**: Manually enter the number of miles for the trip - - **Create from map**: Automatically determine the trip distance based on the start and end location. -- Mobile app: - - **Manually create**: Manually enter the miles for the trip and your mileage rate - - **Odometer**: Enter your odometer reading before and after the trip - - **Start GPS**: Currently under development and unavailable for use. - -{% include info.html %} -When adding a distance expense, the rates available are determined by the rates set in your workspace rate settings. To update these rates or add a new rate, you must be a Workspace Admin. -{% include end-info.html %} - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} - -1. Click the **Expenses** tab. -2. Click **New Expense**. -3. Select the expense type. - - **Manually create**: - - Enter the number of miles for the trip. - - Select your rate. - - If desired, select the category, add a description, or select a report to add the expense to. - - Click **Save**. - - **Create from map**: - - Add your start location as point A. - - Add your end location as point B. - - If applicable, click **Add Destination** to add additional stops. - - To generate a map receipt, leave the Create Receipt checkbox selected. - - Click **Save**. - - Select your rate. - - If desired, select the category, add a description, or select a report to add the expense to. - - Click **Save**. - -{% include end-option.html %} - -{% include option.html value="mobile" %} - -1. Click the + icon in the top right corner. -2. Under the Distance section, select the expense type. - - **Manually create**: - - Enter your mileage. - - Select your rate. - - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. - - Click **Save**. - - **Odometer**: - - Enter your vehicle’s odometer reading before the trip. - - Enter your vehicle’s odometer reading after the trip. - - Select your rate. - - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. - - Click **Save**. -{% include end-option.html %} - -{% include end-selector.html %} - -
- diff --git a/docs/articles/expensify-classic/expenses/Track-per-diem-expenses.md b/docs/articles/expensify-classic/expenses/Track-per-diem-expenses.md deleted file mode 100644 index 88dd91997592..000000000000 --- a/docs/articles/expensify-classic/expenses/Track-per-diem-expenses.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Track per diem expenses -description: Add daily allowance expenses for business travel ---- -
- -A per diem (also called “per diem allowance” or “daily allowance”) is a fixed daily payment provided by an employer to cover expenses during business or work-related travel. These allowances simplify travel expense tracking and reimbursement for meals, lodging, and incidental expenses. - -{% include info.html %} -Before you can add a per diem expense, a Workspace Admin must enable per diem expenses for the workspace and add the per diem rates. If you do not see an option for per diem rates, it is currently unavailable for your workspace, and you’ll need to reach out to one of your Workspace Admins for guidance. -{% include end-info.html %} - -To add a per diem expense, - -1. Click the **Expenses** tab. -2. Click **New Expense** and choose **Per Diem**. -3. Select your travel destination. - - If your trip involves multiple stops, create a separate per diem expense for each destination. -4. Select the start date, end date, start time, and end time for the trip. -5. Select a sub-rate. The available sub-rates are dependent on the trip duration. - - You can include meal deductions or overnight lodging costs if allowed by your workspace. -6. Enter any other required coding information, such as the category, description, or report, and click **Save**. - -# FAQs - -**How do I edit my per diem expenses?** - -Per diem expenses cannot be amended. To make changes, you must delete the expense and recreate it. - -**What if my admin requires daily per diem submissions?** - -No problem! Create a separate per diem expense for each day of your trip. - -
diff --git a/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md b/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md index 6c7457641ce6..8915778962a0 100644 --- a/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md +++ b/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md @@ -4,13 +4,13 @@ description: Use your physical or virtual Expensify Card ---
-As soon as you receive your physical Expensify Visa® Commercial Card, you can start using it right away by swiping it like you would with any other card, or you can link your card to your Apple or Google Pay mobile wallet to make in-person, contactless payments. You can also use your virtual Expensify Card for online and in-app purchases. +As soon as you receive your physical Expensify Visa® Commercial Card, you can start using it right away by swiping it like you would with any other card. You can also link your card to your Apple or Google Pay mobile wallet to make in-person, contactless payments. You can also use your virtual Expensify Card for online and in-app purchases. A virtual card is a digital card that can be used for online transactions. Virtual cards have the same details as physical cards, but they offer several additional benefits: -- **Flexibility**: Virtual cards can be created or deleted instantly. You can use them for individual transactions with predetermined amounts or recurring payments and subscriptions. +- **Flexibility:** Virtual cards can be created or deleted instantly. They can be used for individual transactions with predetermined amounts or recurring payments and subscriptions. - **Customizable limits**: You can set spending limits for each virtual card. -- **Security**: Admins have the option to issue virtual cards for a single-use (e.g. for one of expenses) or fixed-use (e.g. for recurring expenses). Since you have placed a limit on their usage, it makes them less susceptible to unauthorized transactions. -- **Insights**: You can easily track recurring spend for specific vendors when assigning a virtual card to a team, department, or vendor. +- **Security**: Admins have the option to issue virtual cards for a single-use (e.g., for one of the expenses) or fixed-use (e.g., for recurring expenses). Since you have placed a limit on their usage, it makes them less susceptible to unauthorized transactions. +- **Insights**: When assigning a virtual card to a team, department, or vendor, you can easily track recurring spending for specific vendors. # View your virtual card details @@ -34,7 +34,7 @@ A virtual card is a digital card that can be used for online transactions. Virtu {% include faq-begin.md %} -**Why did my transaction get declined?** +## Why did my transaction get declined? Here are some reasons why an Expensify Card transaction might be declined: @@ -43,7 +43,13 @@ Here are some reasons why an Expensify Card transaction might be declined: - **Incorrect card details**: Your card information was entered incorrectly with the merchant. Entering incorrect card information, such as the CVC, ZIP, or expiration date, will also lead to declines. There was suspicious activity - **Fraudulent or risky activity**: If Expensify detects unusual or suspicious activity, we may block transactions as a security measure. This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unusual merchants and try again. If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. The merchant is located in a restricted country -**How do I report my Expensify Card expenses?** +## Where can I use my Expensify Card? + +Generally, the Expensify Card can be used anywhere Visa is accepted. However, the Expensify Card program is based in the US, so we are bound by US sanctions and other international limitations. + +Expensify Card purchases will be declined if a merchant is physically located in, or has its headquarters or billing address, in the following countries -- Belarus, Burundi, Cambodia, Central African Republic, Democratic Republic of the Congo, Cuba, Iran, Iraq, North Korea, Lebanon, Libya, Russia, Somalia, South Sudan, Syrian Arab Republic, Tanzania, Ukraine, Venezuela, Yemen, Zimbabwe + +## How do I report my Expensify Card expenses? You can report and submit Expensify Card expenses just like any other expenses, and you’ll want to submit them regularly to ensure you have a sufficient spending amount available on the card. As your expenses are approved, your Smart Limit updates accordingly. diff --git a/docs/redirects.csv b/docs/redirects.csv index 06fd7c1ef502..bb6729245f83 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -591,3 +591,9 @@ https://help.expensify.com/articles/expensify-classic/articles/expensify-classic https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page +https://help.expensify.com/articles/expensify-classic/expenses/Add-expenses-in-bulk,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/Track-mileage-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/Track-per-diem-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://community.expensify.com/discussion/5116/faq-where-can-i-use-the-expensify-card,https://help.expensify.com/articles/new-expensify/expensify-card/Use-your-Expensify-Card#where-can-i-use-my-expensify-card +https://help.expensify.com/articles/other/Expensify-Lounge,https://help.expensify.com/Hidden/Expensify-Lounge diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 74bcff5bf320..e90fdbe50255 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -426,6 +426,7 @@ platform :ios do api_key_path: "./ios/ios-fastlane-json-key.json", distribute_external: true, notify_external_testers: true, + reject_build_waiting_for_review: true, changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.", groups: ["Applause", "Beta Testers", "Expensify Employees"], demo_account_required: true, diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3351cd2dc056..a3a8a9267ca7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.57.0 + 9.0.57.9 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 60febe801390..d5615a0d382b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.57.0 + 9.0.57.9 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 43d0fe76dbdb..231fc4f51089 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.57 CFBundleVersion - 9.0.57.0 + 9.0.57.9 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d1851cbce1af..6494782a6ec0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2503,7 +2503,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.15.1): + - RNReanimated (3.15.3): - DoubleConversion - glog - hermes-engine @@ -2523,10 +2523,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.15.1) - - RNReanimated/worklets (= 3.15.1) + - RNReanimated/reanimated (= 3.15.3) + - RNReanimated/worklets (= 3.15.3) - Yoga - - RNReanimated/reanimated (3.15.1): + - RNReanimated/reanimated (3.15.3): - DoubleConversion - glog - hermes-engine @@ -2547,7 +2547,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.15.1): + - RNReanimated/worklets (3.15.3): - DoubleConversion - glog - hermes-engine @@ -3269,7 +3269,7 @@ SPEC CHECKSUMS: rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 - RNReanimated: 76901886830e1032f16bbf820153f7dc3f02d51d + RNReanimated: f46df3b08d5d59cd83c47bb6697ce88e565e0dc7 RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 diff --git a/package-lock.json b/package-lock.json index d2f927adf13f..832c46ac06f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.57-0", + "version": "9.0.57-9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.57-0", + "version": "9.0.57-9", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -104,7 +104,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.15.1", + "react-native-reanimated": "3.15.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -35626,9 +35626,10 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.1.tgz", - "integrity": "sha512-DbBeUUExtJ1x1nfE94I8qgDgWjq5ztM3IO/+XFO+agOkPeVpBs5cRnxHfJKrjqJ2MgwhJOUDmtHxo+tDsoeitg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.3.tgz", + "integrity": "sha512-5QBk/7PZvZ98Adxm4MRyglwzsRzReTQIe4Hd2wbBBAZ68IC4OYKvsc8cPEjgx3/1mG8HgHFYhbcDe5U2RjeFqw==", + "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", diff --git a/package.json b/package.json index 9620386b5560..0778eba5fd9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.57-0", + "version": "9.0.57-9", "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.", @@ -161,7 +161,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.15.1", + "react-native-reanimated": "3.15.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", diff --git a/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch b/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch deleted file mode 100644 index 6c511d8cbec1..000000000000 --- a/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch +++ /dev/null @@ -1,299 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 770dfee..73e439b 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -@@ -329,6 +329,12 @@ export type NativeProps = $ReadOnly<{| - */ - returnKeyType?: WithDefault, - -+ /** -+ * Restricts the text value to match the specified regular expression. Use this -+ * instead of implementing the logic in JS to avoid flicker. -+ */ -+ regex?: ?string, -+ - /** - * Limits the maximum number of characters that can be entered. Use this - * instead of implementing the logic in JS to avoid flicker. -@@ -699,6 +705,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { - process: require('../../StyleSheet/processColor').default, - }, - maxLength: true, -+ regex: true, - selectTextOnFocus: true, - textShadowRadius: true, - underlineColorAndroid: { -diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index dbfe5d5..1f359ba 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -@@ -151,6 +151,7 @@ const RCTTextInputViewConfig = { - autoFocus: true, - lineBreakStrategyIOS: true, - smartInsertDelete: true, -+ regex: true, - ...ConditionallyIgnoredEventHandlers({ - onClear: true, - onChange: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -index 20501f7..76f30b9 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -@@ -701,6 +701,12 @@ export interface TextInputProps - */ - inputMode?: InputModeOptions | undefined; - -+ /** -+ * Restricts the text value to match the specified regular expression. Use this -+ * instead of implementing the logic in JS to avoid flicker. -+ */ -+ regex?: string | undefined; -+ - /** - * Limits the maximum number of characters that can be entered. - * Use this instead of implementing the logic in JS to avoid flicker. -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -index 2f35731..5bb94bc 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -@@ -697,6 +697,12 @@ export type Props = $ReadOnly<{| - */ - maxFontSizeMultiplier?: ?number, - -+ /** -+ * Restricts the text value to match the specified regular expression. Use this -+ * instead of implementing the logic in JS to avoid flicker. -+ */ -+ regex?: ?string, -+ - /** - * Limits the maximum number of characters that can be entered. Use this - * instead of implementing the logic in JS to avoid flicker. -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 8cfde15..4f3345c 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -731,6 +731,12 @@ export type Props = $ReadOnly<{| - */ - maxFontSizeMultiplier?: ?number, - -+ /** -+ * Restricts the text value to match the specified regular expression. Use this -+ * instead of implementing the logic in JS to avoid flicker. -+ */ -+ regex?: ?string, -+ - /** - * Limits the maximum number of characters that can be entered. Use this - * instead of implementing the logic in JS to avoid flicker. -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm -index e367394..95f21f2 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm -@@ -59,6 +59,7 @@ @implementation RCTBaseTextInputViewManager { - RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString) - RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString) - RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString) -+RCT_EXPORT_VIEW_PROPERTY(regex, NSString) - - RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) - RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock) -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -index db7cba4..f85f95a 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -@@ -34,6 +34,7 @@ @implementation RCTTextInputComponentView { - UIView *_backedTextInputView; - NSUInteger _mostRecentEventCount; - NSAttributedString *_lastStringStateWasUpdatedWith; -+ NSRegularExpression *_regex; - - /* - * UIKit uses either UITextField or UITextView as its UIKit element for . UITextField is for single line -@@ -224,6 +225,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) { - _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID); - } -+ -+ if (newTextInputProps.regex != oldTextInputProps.regex) { -+ _regex = [NSRegularExpression regularExpressionWithPattern:RCTNSStringFromString(newTextInputProps.regex) -+ options:0 -+ error:nil]; -+ } -+ - [super updateProps:props oldProps:oldProps]; - - [self setDefaultInputAccessoryView]; -@@ -359,6 +367,14 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range - } - } - -+ if (_regex) { -+ NSMutableString *newString = [_backedTextInputView.attributedText.string mutableCopy]; -+ [newString replaceCharactersInRange:range withString:text]; -+ if ([_regex numberOfMatchesInString:newString options:0 range:NSMakeRange(0, newString.length)] == 0) { -+ return nil; -+ } -+ } -+ - if (props.maxLength) { - NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length; - -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 2cceb14..8fdc0c1 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -824,6 +824,47 @@ public class ReactTextInputManager extends BaseViewManager 0) { -+ LinkedList list = new LinkedList<>(); -+ for (InputFilter currentFilter : currentFilters) { -+ if (!(currentFilter instanceof RegexFilter)) { -+ list.add(currentFilter); -+ } -+ } -+ if (!list.isEmpty()) { -+ newFilters = (InputFilter[]) list.toArray(new InputFilter[list.size()]); -+ } -+ } -+ } else { -+ if (currentFilters.length > 0) { -+ newFilters = currentFilters; -+ boolean replaced = false; -+ for (int i = 0; i < currentFilters.length; i++) { -+ if (currentFilters[i] instanceof RegexFilter) { -+ currentFilters[i] = new RegexFilter(regex); -+ replaced = true; -+ } -+ } -+ if (!replaced) { -+ newFilters = new InputFilter[currentFilters.length + 1]; -+ System.arraycopy(currentFilters, 0, newFilters, 0, currentFilters.length); -+ newFilters[currentFilters.length] = new RegexFilter(regex); -+ } -+ } else { -+ newFilters = new InputFilter[1]; -+ newFilters[0] = new RegexFilter(regex); -+ } -+ } -+ -+ view.setFilters(newFilters); -+ } -+ - @ReactProp(name = "maxLength") - public void setMaxLength(ReactEditText view, @Nullable Integer maxLength) { - InputFilter[] currentFilters = view.getFilters(); -@@ -854,7 +895,7 @@ public class ReactTextInputManager extends BaseViewManager `[Take a self-guided product tour](${navatticURL}) and learn about everything Expensify has to offer.`, +}; + const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { @@ -99,6 +106,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { height: 960, }, tasks: [ + selfGuidedTourTask, { type: 'submitExpense', autoCompleted: false, @@ -264,6 +272,7 @@ type OnboardingTaskType = { workspaceMembersLink: string; integrationName: string; workspaceAccountingLink: string; + navatticURL: string; }>, ) => string); }; @@ -305,6 +314,9 @@ const CONST = { ANIMATED_HIGHLIGHT_END_DURATION: 2000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, + ANIMATED_PROGRESS_BAR_DELAY: 300, + ANIMATED_PROGRESS_BAR_OPACITY_DURATION: 300, + ANIMATED_PROGRESS_BAR_DURATION: 750, ANIMATION_IN_TIMING: 100, ANIMATION_DIRECTION: { IN: 'in', @@ -620,7 +632,6 @@ const CONST = { COMPANY_CARD_FEEDS: 'companyCardFeeds', DIRECT_FEEDS: 'directFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', - NEW_DOT_COPILOT: 'newDotCopilot', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', PER_DIEM: 'newDotPerDiem', @@ -879,8 +890,10 @@ const CONST = { // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', NAVATTIC: { - ADMIN_TOUR: 'https://expensify.navattic.com/kh204a7', - EMPLOYEE_TOUR: 'https://expensify.navattic.com/35609gb', + ADMIN_TOUR_PRODUCTION: 'https://expensify.navattic.com/kh204a7', + ADMIN_TOUR_STAGING: 'https://expensify.navattic.com/3i300k18', + EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', + EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, OLDDOT_URLS: { @@ -1599,6 +1612,7 @@ const CONST = { CONTRIBUTORS: 'contributors@expensify.com', FIRST_RESPONDER: 'firstresponders@expensify.com', GUIDES_DOMAIN: 'team.expensify.com', + QA_DOMAIN: 'applause.expensifail.com', HELP: 'help@expensify.com', INTEGRATION_TESTING_CREDS: 'integrationtestingcreds@expensify.com', NOTIFICATIONS: 'notifications@expensify.com', @@ -2736,7 +2750,6 @@ const CONST = { DAILY: 'daily', MONTHLY: 'monthly', }, - CARD_TITLE_INPUT_LIMIT: 255, MANAGE_EXPENSIFY_CARDS_ARTICLE_LINK: 'https://help.expensify.com/articles/new-expensify/expensify-card/Manage-Expensify-Cards', }, COMPANY_CARDS: { @@ -4882,6 +4895,7 @@ const CONST = { '\n' + '*Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.*', }, + selfGuidedTourTask, { type: 'meetGuide', autoCompleted: false, @@ -4986,7 +5000,10 @@ const CONST = { }, ], }, - [onboardingChoices.PERSONAL_SPEND]: onboardingPersonalSpendMessage, + [onboardingChoices.PERSONAL_SPEND]: { + ...onboardingPersonalSpendMessage, + tasks: [selfGuidedTourTask, ...onboardingPersonalSpendMessage.tasks], + }, [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', video: { @@ -4997,6 +5014,7 @@ const CONST = { height: 960, }, tasks: [ + selfGuidedTourTask, { type: 'startChat', autoCompleted: false, diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index d723e5ad2912..9a90de17595d 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -5,7 +5,6 @@ import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -34,7 +33,6 @@ function AccountSwitcher() { const theme = useTheme(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {canUseNewDotCopilot} = usePermissions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); @@ -47,7 +45,7 @@ function AccountSwitcher() { const delegators = account?.delegatedAccess?.delegators ?? []; const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; - const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate); + const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate; const createBaseMenuItem = ( personalDetails: PersonalDetails | undefined, diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index d6b7b8601b13..a230dfa1af8d 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -238,7 +238,6 @@ function AmountForm( forwardDeletePressedRef.current = key === 'delete' || (allowedOS.includes(operatingSystem ?? '') && event.nativeEvent.ctrlKey && key === 'd'); }; - const regex = useMemo(() => MoneyRequestUtils.amountRegex(decimals, amountMaxLength), [decimals, amountMaxLength]); const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -262,7 +261,6 @@ function AmountForm( keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} inputMode={CONST.INPUT_MODE.DECIMAL} errorText={errorText} - regex={regex} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> @@ -302,7 +300,6 @@ function AmountForm( isCurrencyPressable={isCurrencyPressable} style={[styles.iouAmountTextInput]} containerStyle={[styles.iouAmountTextInputContainer]} - regex={regex} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 2e0d3e62afa0..52c32ce1f584 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -39,7 +39,7 @@ type AmountTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; -} & Pick; +} & Pick; function AmountTextInput( { diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx index 6a9fc22f68f8..78b7c84ecb54 100644 --- a/src/components/AmountWithoutCurrencyForm.tsx +++ b/src/components/AmountWithoutCurrencyForm.tsx @@ -1,8 +1,7 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useMemo} from 'react'; import type {ForwardedRef} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import useLocalize from '@hooks/useLocalize'; -import {addLeadingZero, amountRegex, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils'; +import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils'; import CONST from '@src/CONST'; import TextInput from './TextInput'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; @@ -22,11 +21,6 @@ function AmountWithoutCurrencyForm( const {toLocaleDigit} = useLocalize(); const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]); - const [selection, setSelection] = useState({ - start: currentAmount.length, - end: currentAmount.length, - }); - const decimals = 2; /** * Sets the selection and the amount accordingly to the value passed to the input @@ -39,10 +33,7 @@ function AmountWithoutCurrencyForm( const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); const withLeadingZero = addLeadingZero(replacedCommasAmount); - if (!validateAmount(withLeadingZero, decimals)) { - // Use a shallow copy of selection to trigger setSelection - // More info: https://github.com/Expensify/App/issues/16385 - setSelection((prevSelection) => ({...prevSelection})); + if (!validateAmount(withLeadingZero, 2)) { return; } onInputChange?.(withLeadingZero); @@ -50,17 +41,12 @@ function AmountWithoutCurrencyForm( [onInputChange], ); - const regex = useMemo(() => amountRegex(decimals), [decimals]); const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit); return ( ) => { - setSelection(e.nativeEvent.selection); - }} inputID={inputID} name={name} label={label} @@ -69,7 +55,6 @@ function AmountWithoutCurrencyForm( role={role} ref={ref} keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} - regex={regex} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 90f0e0d8a151..fa531ce34adf 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -23,6 +23,7 @@ import Bed from '@assets/images/bed.svg'; import Bell from '@assets/images/bell.svg'; import BellSlash from '@assets/images/bellSlash.svg'; import Bill from '@assets/images/bill.svg'; +import Binoculars from '@assets/images/binoculars.svg'; import Bolt from '@assets/images/bolt.svg'; import Bookmark from '@assets/images/bookmark.svg'; import Box from '@assets/images/box.svg'; @@ -223,6 +224,7 @@ export { Bill, Bell, BellSlash, + Binoculars, Bolt, Box, Briefcase, diff --git a/src/components/InteractiveStepWrapper.tsx b/src/components/InteractiveStepWrapper.tsx index 290ad628f9cf..ec0d2da02108 100644 --- a/src/components/InteractiveStepWrapper.tsx +++ b/src/components/InteractiveStepWrapper.tsx @@ -20,6 +20,9 @@ type InteractiveStepWrapperProps = { // Title of the back button header headerTitle: string; + // Subtitle of the back button header + headerSubtitle?: string; + // Index of the highlighted step startStepIndex?: number; @@ -48,6 +51,7 @@ function InteractiveStepWrapper( wrapperID, handleBackButtonPress, headerTitle, + headerSubtitle, startStepIndex, stepNames, shouldEnableMaxHeight, @@ -72,6 +76,7 @@ function InteractiveStepWrapper( > diff --git a/src/components/LoadingBar.tsx b/src/components/LoadingBar.tsx new file mode 100644 index 000000000000..163ffe2aa66b --- /dev/null +++ b/src/components/LoadingBar.tsx @@ -0,0 +1,85 @@ +import React, {useEffect} from 'react'; +import Animated, {cancelAnimation, Easing, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming} from 'react-native-reanimated'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; + +type LoadingBarProps = { + // Whether or not to show the loading bar + shouldShow: boolean; +}; + +function LoadingBar({shouldShow}: LoadingBarProps) { + const left = useSharedValue(0); + const width = useSharedValue(0); + const opacity = useSharedValue(0); + const isVisible = useSharedValue(false); + const styles = useThemeStyles(); + + useEffect(() => { + if (shouldShow) { + // eslint-disable-next-line react-compiler/react-compiler + isVisible.value = true; + left.value = 0; + width.value = 0; + opacity.value = withTiming(1, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}); + left.value = withDelay( + CONST.ANIMATED_PROGRESS_BAR_DELAY, + withRepeat( + withSequence( + withTiming(0, {duration: 0}), + withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + ), + -1, + false, + ), + ); + + width.value = withDelay( + CONST.ANIMATED_PROGRESS_BAR_DELAY, + withRepeat( + withSequence( + withTiming(0, {duration: 0}), + withTiming(100, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_DURATION, easing: Easing.bezier(0.65, 0, 0.35, 1)}), + ), + -1, + false, + ), + ); + } else if (isVisible.value) { + opacity.value = withTiming(0, {duration: CONST.ANIMATED_PROGRESS_BAR_OPACITY_DURATION}, () => { + runOnJS(() => { + isVisible.value = false; + cancelAnimation(left); + cancelAnimation(width); + }); + }); + } + // we want to update only when shouldShow changes + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [shouldShow]); + + const animatedIndicatorStyle = useAnimatedStyle(() => { + return { + left: `${left.value}%`, + width: `${width.value}%`, + }; + }); + + const animatedContainerStyle = useAnimatedStyle(() => { + return { + opacity: opacity.value, + }; + }); + + return ( + + {isVisible.value ? : null} + + ); +} + +LoadingBar.displayName = 'ProgressBar'; + +export default LoadingBar; diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 85a2298f63d6..39396795c557 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -193,7 +193,7 @@ function BaseModal( safeAreaPaddingRight, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding: !keyboardStateContextValue?.isKeyboardShown && shouldAddBottomSafeAreaPadding, + shouldAddBottomSafeAreaPadding: (!avoidKeyboard || !keyboardStateContextValue?.isKeyboardShown) && shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaPadding, modalContainerStyleMarginTop: modalContainerStyle.marginTop, modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 7eddd718a9f4..9ef33900bb00 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -1,5 +1,5 @@ import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import {useMouseContext} from '@hooks/useMouseContext'; @@ -274,7 +274,6 @@ function MoneyRequestAmountInput( }); }, [amount, currency, onFormatAmount, formatAmountOnBlur, maxLength]); - const regex = useMemo(() => MoneyRequestUtils.amountRegex(decimals), [decimals]); const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); const {setMouseDown, setMouseUp} = useMouseContext(); @@ -338,7 +337,6 @@ function MoneyRequestAmountInput( onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} contentWidth={contentWidth} - regex={regex} /> ); } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e70a8cec4775..01fd15c52bb4 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -2,8 +2,8 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; @@ -14,7 +14,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import type {MileageRate} from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; @@ -49,33 +48,7 @@ import UserListItem from './SelectionList/UserListItem'; import SettlementButton from './SettlementButton'; import Text from './Text'; -type MoneyRequestConfirmationListOnyxProps = { - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry; - - /** Collection of draft categories attached to a policy */ - policyCategoriesDraft: OnyxEntry; - - /** Collection of tags attached to a policy */ - policyTags: OnyxEntry; - - /** The policy of the report */ - policy: OnyxEntry; - - /** The draft policy of the report */ - policyDraft: OnyxEntry; - - /** Mileage rate default for the policy */ - defaultMileageRate: OnyxEntry; - - /** Last selected distance rates */ - lastSelectedDistanceRates: OnyxEntry>; - - /** List of currencies */ - currencyList: OnyxEntry; -}; - -type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { +type MoneyRequestConfirmationListProps = { /** Callback to inform parent modal of success */ onConfirm?: (selectedParticipants: Participant[]) => void; @@ -178,23 +151,18 @@ function MoneyRequestConfirmationList({ onConfirm, iouType = CONST.IOU.TYPE.SUBMIT, iouAmount, - policyCategories: policyCategoriesReal, - policyCategoriesDraft, isDistanceRequest = false, - policy: policyReal, - policyDraft, isPolicyExpenseChat = false, iouCategory = '', shouldShowSmartScanFields = true, isEditingSplitBill, - policyTags, iouCurrencyCode, iouMerchant, selectedParticipants: selectedParticipantsProp, payeePersonalDetails: payeePersonalDetailsProp, isReadOnly = false, bankAccountRoute = '', - policyID = '', + policyID, reportID = '', receiptPath = '', iouAttendees, @@ -205,14 +173,22 @@ function MoneyRequestConfirmationList({ onToggleBillable, hasSmartScanFailed, reportActionID, - defaultMileageRate, - lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, - currencyList, shouldDisplayReceipt = false, shouldPlaySound = true, isConfirmed, }: MoneyRequestConfirmationListProps) { + const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID ?? '-1'}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID ?? '-1'}`); + const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '-1'}`); + const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`); + const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`, { + selector: (selectedPolicy) => DistanceRequestUtils.getDefaultMileageRate(selectedPolicy), + }); + const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID ?? '-1'}`); + const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES); + const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); + const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; @@ -972,67 +948,36 @@ function MoneyRequestConfirmationList({ MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; -export default withOnyx({ - policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - }, - policyCategoriesDraft: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`, - }, - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - defaultMileageRate: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: DistanceRequestUtils.getDefaultMileageRate, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - policyDraft: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, - }, - lastSelectedDistanceRates: { - key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - }, - currencyList: { - key: ONYXKEYS.CURRENCY_LIST, - }, -})( - memo( - MoneyRequestConfirmationList, - (prevProps, nextProps) => - lodashIsEqual(prevProps.transaction, nextProps.transaction) && - prevProps.onSendMoney === nextProps.onSendMoney && - prevProps.onConfirm === nextProps.onConfirm && - prevProps.iouType === nextProps.iouType && - prevProps.iouAmount === nextProps.iouAmount && - prevProps.isDistanceRequest === nextProps.isDistanceRequest && - prevProps.isPolicyExpenseChat === nextProps.isPolicyExpenseChat && - prevProps.iouCategory === nextProps.iouCategory && - prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields && - prevProps.isEditingSplitBill === nextProps.isEditingSplitBill && - prevProps.iouCurrencyCode === nextProps.iouCurrencyCode && - prevProps.iouMerchant === nextProps.iouMerchant && - lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && - lodashIsEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) && - prevProps.isReadOnly === nextProps.isReadOnly && - prevProps.bankAccountRoute === nextProps.bankAccountRoute && - prevProps.policyID === nextProps.policyID && - prevProps.reportID === nextProps.reportID && - prevProps.receiptPath === nextProps.receiptPath && - prevProps.iouAttendees === nextProps.iouAttendees && - prevProps.iouComment === nextProps.iouComment && - prevProps.receiptFilename === nextProps.receiptFilename && - prevProps.iouCreated === nextProps.iouCreated && - prevProps.iouIsBillable === nextProps.iouIsBillable && - prevProps.onToggleBillable === nextProps.onToggleBillable && - prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed && - prevProps.reportActionID === nextProps.reportActionID && - lodashIsEqual(prevProps.defaultMileageRate, nextProps.defaultMileageRate) && - lodashIsEqual(prevProps.lastSelectedDistanceRates, nextProps.lastSelectedDistanceRates) && - lodashIsEqual(prevProps.action, nextProps.action) && - lodashIsEqual(prevProps.currencyList, nextProps.currencyList) && - prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt, - ), +export default memo( + MoneyRequestConfirmationList, + (prevProps, nextProps) => + lodashIsEqual(prevProps.transaction, nextProps.transaction) && + prevProps.onSendMoney === nextProps.onSendMoney && + prevProps.onConfirm === nextProps.onConfirm && + prevProps.iouType === nextProps.iouType && + prevProps.iouAmount === nextProps.iouAmount && + prevProps.isDistanceRequest === nextProps.isDistanceRequest && + prevProps.isPolicyExpenseChat === nextProps.isPolicyExpenseChat && + prevProps.iouCategory === nextProps.iouCategory && + prevProps.shouldShowSmartScanFields === nextProps.shouldShowSmartScanFields && + prevProps.isEditingSplitBill === nextProps.isEditingSplitBill && + prevProps.iouCurrencyCode === nextProps.iouCurrencyCode && + prevProps.iouMerchant === nextProps.iouMerchant && + lodashIsEqual(prevProps.selectedParticipants, nextProps.selectedParticipants) && + lodashIsEqual(prevProps.payeePersonalDetails, nextProps.payeePersonalDetails) && + prevProps.isReadOnly === nextProps.isReadOnly && + prevProps.bankAccountRoute === nextProps.bankAccountRoute && + prevProps.policyID === nextProps.policyID && + prevProps.reportID === nextProps.reportID && + prevProps.receiptPath === nextProps.receiptPath && + prevProps.iouAttendees === nextProps.iouAttendees && + prevProps.iouComment === nextProps.iouComment && + prevProps.receiptFilename === nextProps.receiptFilename && + prevProps.iouCreated === nextProps.iouCreated && + prevProps.iouIsBillable === nextProps.iouIsBillable && + prevProps.onToggleBillable === nextProps.onToggleBillable && + prevProps.hasSmartScanFailed === nextProps.hasSmartScanFailed && + prevProps.reportActionID === nextProps.reportActionID && + lodashIsEqual(prevProps.action, nextProps.action) && + prevProps.shouldDisplayReceipt === nextProps.shouldDisplayReceipt, ); diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index d5be896c1c50..2fb034131c86 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -77,6 +77,8 @@ type SearchQueryAST = { type SearchQueryJSON = { inputQuery: SearchQueryString; hash: number; + /** Hash used for putting queries in recent searches list. It ignores sortOrder and sortBy, because we want to treat queries differing only in sort params as the same query */ + recentSearchHash: number; flatFilters: QueryFilters; } & SearchQueryAST; diff --git a/src/components/TabSelector/getOpacity/index.native.ts b/src/components/TabSelector/getOpacity/index.native.ts index 0da5455214c9..a59d32c2db6e 100644 --- a/src/components/TabSelector/getOpacity/index.native.ts +++ b/src/components/TabSelector/getOpacity/index.native.ts @@ -1,6 +1,6 @@ import type GetOpacity from './types'; -const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, position, isActive}) => { +const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, position}) => { const activeValue = active ? 1 : 0; const inactiveValue = active ? 0 : 1; @@ -9,7 +9,7 @@ const getOpacity: GetOpacity = ({routesLength, tabIndex, active, affectedTabs, p return position?.interpolate({ inputRange, - outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex && isActive ? activeValue : inactiveValue)), + outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), }); } return activeValue; diff --git a/src/components/TextInputWithCurrencySymbol/types.ts b/src/components/TextInputWithCurrencySymbol/types.ts index b1f4e2fb6470..401af75b16cd 100644 --- a/src/components/TextInputWithCurrencySymbol/types.ts +++ b/src/components/TextInputWithCurrencySymbol/types.ts @@ -77,7 +77,7 @@ type BaseTextInputWithCurrencySymbolProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; -} & Pick; +} & Pick; type TextInputWithCurrencySymbolProps = Omit & { onSelectionChange?: (start: number, end: number) => void; diff --git a/src/languages/en.ts b/src/languages/en.ts index 284676843382..25d36ff55e34 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3106,14 +3106,17 @@ const translations = { processorLabel: 'Processor ID', bankLabel: 'Financial institution (bank) ID', companyLabel: 'Company ID', + helpLabel: 'Where do I find these IDs?', }, gl1025: { title: `What's the Amex delivery file name?`, fileNameLabel: 'Delivery file name', + helpLabel: 'Where do I find the delivery file name?', }, cdf: { title: `What's the Mastercard distribution ID?`, distributionLabel: 'Distribution ID', + helpLabel: 'Where do I find the distribution ID?', }, }, amexCorporate: 'Select this if the front of your cards say “Corporate”', @@ -3320,7 +3323,7 @@ const translations = { cardNumber: 'Card number', cardholder: 'Cardholder', cardName: 'Card name', - integrationExport: ({integration, type}: IntegrationExportParams) => `${integration} ${type} export`, + integrationExport: ({integration, type}: IntegrationExportParams) => `${integration} ${type?.toLowerCase()} export`, integrationExportTitleFirstPart: ({integration}: IntegrationExportParams) => `Choose the ${integration} account where transactions should be exported. Select a different`, integrationExportTitleLinkPart: 'export option', integrationExportTitleSecondPart: 'to change the available accounts.', @@ -4753,12 +4756,12 @@ const translations = { }, modifiedDate: 'Date differs from scanned receipt', nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Expense exceeds auto approval limit of ${formattedLimit}`, + overAutoApprovalLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Expense exceeds auto-approval limit of ${formattedLimit}`, overCategoryLimit: ({formattedLimit}: ViolationsOverCategoryLimitParams) => `Amount over ${formattedLimit}/person category limit`, overLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Amount over daily ${formattedLimit}/person category limit`, - receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', + receiptNotSmartScanned: 'Receipt scan incomplete. Please verify details manually.', receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => { let message = 'Receipt required'; if (formattedLimit ?? category) { @@ -5147,7 +5150,7 @@ const translations = { RBR: 'RBR', true: 'true', false: 'false', - viewReport: 'View report', + viewReport: 'View Report', viewTransaction: 'View transaction', createTransactionViolation: 'Create transaction violation', reasonVisibleInLHN: { @@ -5194,6 +5197,10 @@ const translations = { emptySearchView: { takeATour: 'Take a tour', }, + tour: { + takeATwoMinuteTour: 'Take a 2-minute tour', + exploreExpensify: 'Explore everything Expensify has to offer', + }, }; export default translations satisfies TranslationDeepObject; diff --git a/src/languages/es.ts b/src/languages/es.ts index 99d7333c2728..5812312d0b3d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3145,14 +3145,17 @@ const translations = { processorLabel: 'ID del procesador', bankLabel: 'Identificación de la institución financiera (banco)', companyLabel: 'Empresa ID', + helpLabel: '¿Dónde encuentro estos IDs?', }, gl1025: { title: `¿Cuál es el nombre del archivo de entrega de Amex?`, fileNameLabel: 'Nombre del archivo de entrega', + helpLabel: '¿Dónde encuentro el nombre del archivo de entrega?', }, cdf: { title: `¿Cuál es el identificador de distribución de Mastercard?`, distributionLabel: 'ID de distribución', + helpLabel: '¿Dónde encuentro el ID de distribución?', }, }, amexCorporate: 'Seleccione esto si el frente de sus tarjetas dice “Corporativa”', @@ -3361,7 +3364,7 @@ const translations = { cardNumber: 'Número de la tarjeta', cardholder: 'Titular de la tarjeta', cardName: 'Nombre de la tarjeta', - integrationExport: ({integration, type}: IntegrationExportParams) => `Exportación a ${integration} ${type}`, + integrationExport: ({integration, type}: IntegrationExportParams) => `Exportación a ${integration} ${type?.toLowerCase()}`, integrationExportTitleFirstPart: ({integration}: IntegrationExportParams) => `Seleccione la cuenta ${integration} donde se deben exportar las transacciones. Seleccione una cuenta diferente`, integrationExportTitleLinkPart: 'opción de exportación', @@ -5268,7 +5271,7 @@ const translations = { overLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Importe supera el límite${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Importe supera el límite${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, - receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma la exactitud', + receiptNotSmartScanned: 'Escaneo de recibo incompleto. Por favor, verifica los detalles manualmente.', receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => { let message = 'Recibo obligatorio'; if (formattedLimit ?? category) { @@ -5662,7 +5665,7 @@ const translations = { RBR: 'RBR', true: 'verdadero', false: 'falso', - viewReport: 'Ver informe', + viewReport: 'Ver Informe', viewTransaction: 'Ver transacción', createTransactionViolation: 'Crear infracción de transacción', reasonVisibleInLHN: { @@ -5709,6 +5712,10 @@ const translations = { emptySearchView: { takeATour: 'Haz un tour', }, + tour: { + takeATwoMinuteTour: 'Haz un tour de 2 minutos', + exploreExpensify: 'Explora todo lo que Expensify tiene para ofrecer', + }, }; export default translations satisfies TranslationDeepObject; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 0350d59685ce..bf3f749f5bac 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -434,6 +434,7 @@ const WRITE_COMMANDS = { SET_CARD_EXPORT_ACCOUNT: 'SetCardExportAccount', SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS: 'SetPersonalDetailsAndShipExpensifyCards', SET_INVOICING_TRANSFER_BANK_ACCOUNT: 'SetInvoicingTransferBankAccount', + SELF_TOUR_VIEWED: 'SelfTourViewed', UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName', UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite', } as const; @@ -861,6 +862,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY]: Parameters.UpdateCardSettlementFrequencyParams; [WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT]: Parameters.UpdateCardSettlementAccountParams; [WRITE_COMMANDS.SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS]: Parameters.SetPersonalDetailsAndShipExpensifyCardsParams; + [WRITE_COMMANDS.SELF_TOUR_VIEWED]: null; // Xero API [WRITE_COMMANDS.UPDATE_XERO_TENANT_ID]: Parameters.UpdateXeroGenericTypeParams; diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts index 8d97b8d4307e..30a5a77ae9f3 100644 --- a/src/libs/Fullstory/index.native.ts +++ b/src/libs/Fullstory/index.native.ts @@ -40,7 +40,8 @@ const FS = { // after the init function since this function is also called on updates for // UserMetadata onyx key. Environment.getEnvironment().then((envName: string) => { - if (envName !== CONST.ENVIRONMENT.PRODUCTION) { + const isTestEmail = value.email !== undefined && value.email.startsWith('fullstory') && value.email.endsWith(CONST.EMAIL.QA_DOMAIN); + if (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) { return; } FullStory.restart(); diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts index df65af358a55..0aa0b2094591 100644 --- a/src/libs/Fullstory/index.ts +++ b/src/libs/Fullstory/index.ts @@ -57,7 +57,8 @@ const FS = { } try { Environment.getEnvironment().then((envName: string) => { - if (CONST.ENVIRONMENT.PRODUCTION !== envName) { + const isTestEmail = value.email !== undefined && value.email.startsWith('fullstory') && value.email.endsWith(CONST.EMAIL.QA_DOMAIN); + if (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) { return; } FS.onReady().then(() => { diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index 25c8509efaad..206bb8509af6 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -38,21 +38,15 @@ function addLeadingZero(amount: string): string { } /** - * Get amount regex string - */ -function amountRegex(decimals: number, amountMaxLength: number = CONST.IOU.AMOUNT_MAX_LENGTH): string { - return decimals === 0 - ? `^\\d{0,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0 - : `^\\d{0,${amountMaxLength}}(?:(?:\\.|\\,)\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point -} - -/** - * Check if string is a valid amount + * Check if amount is a decimal up to 3 digits */ function validateAmount(amount: string, decimals: number, amountMaxLength: number = CONST.IOU.AMOUNT_MAX_LENGTH): boolean { - const regexString = amountRegex(decimals, amountMaxLength); - const decimalNumberRegex = new RegExp(regexString); - return decimalNumberRegex.test(amount); + const regexString = + decimals === 0 + ? `^\\d{1,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0 + : `^\\d{1,${amountMaxLength}}(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point + const decimalNumberRegex = new RegExp(regexString, 'i'); + return amount === '' || decimalNumberRegex.test(amount); } /** @@ -104,7 +98,6 @@ export { stripDecimalsFromAmount, stripSpacesFromAmount, replaceCommasWithPeriod, - amountRegex, validateAmount, validatePercentage, }; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx index 5336954486e6..054ced8bc9bb 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx @@ -134,7 +134,7 @@ function DebugTabView({selectedTab = '', chatTabBrickRoad, activeWorkspaceID}: D const report = getChatTabBrickRoadReport(activeWorkspaceID); if (report) { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); + Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(report.reportID)); } } if (selectedTab === SCREENS.SETTINGS.ROOT) { diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index a02a456c48fe..009724e73e93 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -36,10 +36,6 @@ function canUseNetSuiteUSATax(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas); } -function canUseNewDotCopilot(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.NEW_DOT_COPILOT) || canUseAllBetas(betas); -} - function canUseCategoryAndTagApprovers(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.CATEGORY_AND_TAG_APPROVERS) || canUseAllBetas(betas); } @@ -69,7 +65,6 @@ export default { canUseCompanyCardFeeds, canUseDirectFeeds, canUseNetSuiteUSATax, - canUseNewDotCopilot, canUseCombinedTrackSubmit, canUseCategoryAndTagApprovers, canUsePerDiem, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e1f036e2c698..d8133991d62b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -53,7 +53,7 @@ import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/ import type {Status} from '@src/types/onyx/PersonalDetails'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report'; -import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction'; import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -258,6 +258,11 @@ type OptimisticCancelPaymentReportAction = Pick< 'actionName' | 'actorAccountID' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' >; +type OptimisticChangeFieldAction = Pick< + OldDotReportAction & ReportAction, + 'actionName' | 'actorAccountID' | 'originalMessage' | 'person' | 'reportActionID' | 'created' | 'pendingAction' | 'message' +>; + type OptimisticEditedTaskReportAction = Pick< ReportAction, 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' | 'delegateAccountID' @@ -2571,6 +2576,56 @@ function getReimbursementDeQueuedActionMessage( return Localize.translateLocal('iou.canceledRequest', {submitterDisplayName, amount: formattedAmount}); } +/** + * Builds an optimistic REIMBURSEMENT_DEQUEUED report action with a randomly generated reportActionID. + * + */ +function buildOptimisticChangeFieldAction(reportField: PolicyReportField, previousReportField: PolicyReportField): OptimisticChangeFieldAction { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD, + actorAccountID: currentUserAccountID, + message: [ + { + type: 'TEXT', + style: 'strong', + text: 'You', + }, + { + type: 'TEXT', + style: 'normal', + text: ` modified field '${reportField.name}'.`, + }, + { + type: 'TEXT', + style: 'normal', + text: ` New value is '${reportField.value}'`, + }, + { + type: 'TEXT', + style: 'normal', + text: ` (previously '${previousReportField.value}').`, + }, + ], + originalMessage: { + fieldName: reportField.name, + newType: reportField.type, + newValue: reportField.value, + oldType: previousReportField.type, + oldValue: previousReportField.value, + }, + person: [ + { + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + type: 'TEXT', + }, + ], + reportActionID: NumberUtils.rand64(), + created: DateUtils.getDBTime(), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + /** * Builds an optimistic REIMBURSEMENT_DEQUEUED report action with a randomly generated reportActionID. * @@ -8671,6 +8726,7 @@ export { hasMissingInvoiceBankAccount, reasonForReportToBeInOptionList, getReasonAndReportActionThatRequiresAttention, + buildOptimisticChangeFieldAction, isPolicyRelatedReport, hasReportErrorsOtherThanFailedReceipt, shouldShowViolations, diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index c84e42704fb9..62d00f8091ed 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -216,15 +216,10 @@ function findIDFromDisplayValue(filterName: ValueOf { filter.filters.sort((a, b) => localeCompare(a.value.toString(), b.value.toString())); @@ -235,7 +230,16 @@ function getQueryHash(query: SearchQueryJSON): number { .sort() .forEach((filterString) => (orderedQuery += ` ${filterString}`)); - return UserUtils.hashText(orderedQuery, 2 ** 32); + const recentSearchHash = UserUtils.hashText(orderedQuery, 2 ** 32); + + orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_BY}:${query.sortBy}`; + orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.SORT_ORDER}:${query.sortOrder}`; + if (query.policyID) { + orderedQuery += ` ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${query.policyID} `; + } + const primaryHash = UserUtils.hashText(orderedQuery, 2 ** 32); + + return {primaryHash, recentSearchHash}; } /** @@ -252,7 +256,9 @@ function buildSearchQueryJSON(query: SearchQueryString) { // Add the full input and hash to the results result.inputQuery = query; result.flatFilters = flatFilters; - result.hash = getQueryHash(result); + const {primaryHash, recentSearchHash} = getQueryHashes(result); + result.hash = primaryHash; + result.recentSearchHash = recentSearchHash; return result; } catch (e) { diff --git a/src/libs/TourUtils.ts b/src/libs/TourUtils.ts new file mode 100644 index 000000000000..a88ee47cc563 --- /dev/null +++ b/src/libs/TourUtils.ts @@ -0,0 +1,14 @@ +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import type {OnboardingPurposeType} from '@src/CONST'; + +function getNavatticURL(environment: ValueOf, introSelected?: OnboardingPurposeType) { + const adminTourURL = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.NAVATTIC.ADMIN_TOUR_PRODUCTION : CONST.NAVATTIC.ADMIN_TOUR_STAGING; + const employeeTourURL = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.NAVATTIC.EMPLOYEE_TOUR_PRODUCTION : CONST.NAVATTIC.EMPLOYEE_TOUR_STAGING; + return introSelected === CONST.SELECTABLE_ONBOARDING_CHOICES.MANAGE_TEAM ? adminTourURL : employeeTourURL; +} + +export { + // eslint-disable-next-line import/prefer-default-export + getNavatticURL, +}; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index a106fbeff510..2955a62f28c7 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -51,13 +51,37 @@ function clearAddNewCardFlow() { }); } -function addNewCompanyCardsFeed(policyID: string, feedType: string, feedDetails: string) { +function addNewCompanyCardsFeed(policyID: string, feedType: CompanyCardFeed, feedDetails: string, lastSelectedFeed?: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); if (!authToken) { return; } + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, + value: feedType, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, + value: lastSelectedFeed ?? null, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, + value: feedType, + }, + ]; + const parameters: RequestFeedSetupParams = { policyID, authToken, @@ -65,7 +89,7 @@ function addNewCompanyCardsFeed(policyID: string, feedType: string, feedDetails: feedDetails, }; - API.write(WRITE_COMMANDS.REQUEST_FEED_SETUP, parameters); + API.write(WRITE_COMMANDS.REQUEST_FEED_SETUP, parameters, {optimisticData, failureData, successData}); } function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: number, bankName: string, userDefinedName: string) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3eac21cd1b18..9ea499283d70 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -58,6 +58,8 @@ import DateUtils from '@libs/DateUtils'; import {prepareDraftComment} from '@libs/DraftCommentUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as Environment from '@libs/Environment/Environment'; +import getEnvironment from '@libs/Environment/getEnvironment'; +import type EnvironmentType from '@libs/Environment/getEnvironment/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; import HttpUtils from '@libs/HttpUtils'; @@ -84,6 +86,7 @@ import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; +import {getNavatticURL} from '@libs/TourUtils'; import Visibility from '@libs/Visibility'; import CONFIG from '@src/CONFIG'; import type {OnboardingAccountingType, OnboardingCompanySizeType, OnboardingPurposeType} from '@src/CONST'; @@ -281,6 +284,11 @@ Onyx.connect({ let environmentURL: string; Environment.getEnvironmentURL().then((url: string) => (environmentURL = url)); +let environment: EnvironmentType; +getEnvironment().then((env) => { + environment = env; +}); + registerPaginationConfig({ initialCommand: WRITE_COMMANDS.OPEN_REPORT, previousCommand: READ_COMMANDS.GET_OLDER_ACTIONS, @@ -1935,6 +1943,8 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre const fieldViolation = ReportUtils.getFieldViolation(reportViolations, reportField); const recentlyUsedValues = allRecentlyUsedReportFields?.[fieldKey] ?? []; + const optimisticChangeFieldAction = ReportUtils.buildOptimisticChangeFieldAction(reportField, previousReportField); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1948,6 +1958,13 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticChangeFieldAction.reportActionID]: optimisticChangeFieldAction, + }, + }, ]; if (fieldViolation) { @@ -1988,6 +2005,15 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticChangeFieldAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericUpdateReportFieldFailureMessage'), + }, + }, + }, ]; if (reportField.type === 'dropdown') { @@ -2013,11 +2039,21 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticChangeFieldAction.reportActionID]: { + pendingAction: null, + }, + }, + }, ]; const parameters = { reportID, reportFields: JSON.stringify({[fieldKey]: reportField}), + reportFieldsActionIDs: JSON.stringify({[fieldKey]: optimisticChangeFieldAction.reportActionID}), }; API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); @@ -3436,7 +3472,6 @@ function completeOnboarding( reportComment: videoComment.commentText, }; } - const tasksData = data.tasks .filter((task) => { if (task.type === 'addAccountingIntegration' && !userReportedIntegration) { @@ -3452,6 +3487,7 @@ function completeOnboarding( workspaceCategoriesLink: `${environmentURL}/${ROUTES.WORKSPACE_CATEGORIES.getRoute(onboardingPolicyID ?? '-1')}`, workspaceMembersLink: `${environmentURL}/${ROUTES.WORKSPACE_MEMBERS.getRoute(onboardingPolicyID ?? '-1')}`, workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID ?? '-1')}`, + navatticURL: getNavatticURL(environment, engagementChoice), integrationName, workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID ?? '-1')}`, }) @@ -3505,7 +3541,7 @@ function completeOnboarding( parentReportActionID: taskReportAction.reportAction.reportActionID, assigneeChatReportID: '', createdTaskReportActionID: taskCreatedAction.reportActionID, - completedTaskReportActionID: completedTaskReportAction?.reportActionID ?? '-1', + completedTaskReportActionID: completedTaskReportAction?.reportActionID ?? undefined, title: currentTask.reportName ?? '', description: taskDescription ?? '', })); diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts index d504c5550331..19a570ab610f 100644 --- a/src/libs/actions/Welcome/index.ts +++ b/src/libs/actions/Welcome/index.ts @@ -2,7 +2,7 @@ import {NativeModules} from 'react-native'; import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; +import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -183,6 +183,20 @@ function resetAllChecks() { OnboardingFlow.clearInitialPath(); } +function setSelfTourViewed() { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_ONBOARDING, + value: { + selfTourViewed: true, + }, + }, + ]; + + API.write(WRITE_COMMANDS.SELF_TOUR_VIEWED, null, {optimisticData}); +} + export { onServerDataReady, isOnboardingFlowCompleted, @@ -195,4 +209,5 @@ export { completeHybridAppOnboarding, setOnboardingErrorMessage, setOnboardingCompanySize, + setSelfTourViewed, }; diff --git a/src/libs/onboardingSelectors.ts b/src/libs/onboardingSelectors.ts index efa67d2aed48..c1e7d0ed0778 100644 --- a/src/libs/onboardingSelectors.ts +++ b/src/libs/onboardingSelectors.ts @@ -35,4 +35,19 @@ function hasCompletedHybridAppOnboardingFlowSelector(tryNewDotData: OnyxValue): boolean | undefined { + if (Array.isArray(onboarding)) { + return false; + } + + return onboarding?.selfTourViewed; +} + +export {hasCompletedGuidedSetupFlowSelector, hasCompletedHybridAppOnboardingFlowSelector, hasSeenTourSelector}; diff --git a/src/pages/Debug/DebugDetails.tsx b/src/pages/Debug/DebugDetails.tsx index 247915f4e431..b3f0e04548aa 100644 --- a/src/pages/Debug/DebugDetails.tsx +++ b/src/pages/Debug/DebugDetails.tsx @@ -15,7 +15,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {ObjectType, OnyxDataType} from '@libs/DebugUtils'; import DebugUtils from '@libs/DebugUtils'; -import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import Debug from '@userActions/Debug'; @@ -251,7 +250,6 @@ function DebugDetails({formType, data, children, onSave, onDelete, validate}: De text={translate('common.delete')} onPress={() => { onDelete(); - Navigation.goBack(); }} /> diff --git a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx index 205bfc7bb48c..4c288beded37 100644 --- a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx +++ b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx @@ -5,7 +5,6 @@ import CategoryPicker from '@components/CategoryPicker'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import type {ListItem} from '@components/SelectionList/types'; -import TagPicker from '@components/TagPicker'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {DebugParamList} from '@libs/Navigation/types'; diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 0e250bc7f14a..285ad0819bf5 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import Text from '@components/Text'; @@ -11,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {navigateToConciergeChatAndDeleteReport} from '@libs/actions/Report'; import DebugUtils from '@libs/DebugUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; @@ -19,6 +21,7 @@ import type {DebugParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import Debug from '@userActions/Debug'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -113,6 +116,10 @@ function DebugReportPage({ ]; }, [parentReportAction, report, reportActions, reportID, transactionViolations, translate]); + if (!report) { + return ; + } + return ( { Debug.mergeDebugData(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); + navigateToConciergeChatAndDeleteReport(reportID, true, true); }} validate={DebugUtils.validateReportDraftProperty} > @@ -159,6 +167,13 @@ function DebugReportPage({ )} ))} +