diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 85fb866b05c4..9887943c77e0 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -69,9 +69,6 @@ jobs: uses: ./.github/actions/javascript/getGraphiteString - name: Send graphite data - env: - GRAPHITE_SERVER: ${{ vars.GRAPHITE_SERVER }} - GRAPHITE_PORT: ${{ vars.GRAPHITE_PORT }} # run only when merged to main if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' - run: echo -e "${{ steps.saveGraphiteString.outputs.GRAPHITE_STRING }}" | nc -q0 "$GRAPHITE_SERVER" "$GRAPHITE_PORT" + run: echo -e "${{ steps.saveGraphiteString.outputs.GRAPHITE_STRING }}" | nc -q0 stats.expensify.com 3003 diff --git a/README.md b/README.md index 72736b3fedb7..400260393bc1 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,79 @@ Our React Native Android app now uses the `Hermes` JS engine which requires your To make it easier to test things in web, we expose the Onyx object to the window, so you can easily do `Onyx.set('bla', 1)`. +---- + +# Release Profiler +Often, performance issue debugging occurs in debug builds, which can introduce errors from elements such as JS Garbage Collection, Hermes debug markers, or LLDB pauses. + +`react-native-release-profiler` facilitates profiling within release builds for accurate local problem-solving and broad performance analysis in production to spot regressions or collect extensive device data. Therefore, we will utilize the production build version + +### Getting Started with Source Maps +To accurately profile your application, generating source maps for Android and iOS is crucial. Here's how to enable them: +1. Enable source maps on Android +Ensure the following is set in your app's `android/app/build.gradle` file. + + ```jsx + project.ext.react = [ + enableHermes: true, + hermesFlagsRelease: ["-O", "-output-source-map"], // <-- here, plus whichever flag was required to set this away from default + ] + ``` + +2. Enable source maps on IOS +Within Xcode head to the build phase - `Bundle React Native code and images`. + + ```jsx + export SOURCEMAP_FILE="$(pwd)/../main.jsbundle.map" // <-- here; + + export NODE_BINARY=node + ../node_modules/react-native/scripts/react-native-xcode.sh + ``` +3. Install the necessary packages and CocoaPods dependencies: + ```jsx + npm i && npm run pod-install + ``` +7. Depending on the platform you are targeting, run your Android/iOS app in production mode. +8. Upon completion, the generated source map can be found at: + Android: `android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map` + IOS: `main.jsbundle.map` + +### Recording a Trace: +1. Ensure you have generated the source map as outlined above. +2. Launch the app in production mode. +2. Navigate to the feature you wish to profile. +3. Initiate the profiling session by tapping with four fingers to open the menu and selecting **`Use Profiling`**. +4. Close the menu and interact with the app. +5. After completing your interactions, tap with four fingers again and select to stop profiling. +6. You will be presented with a **`Share`** option to export the trace, which includes a trace file (`Profile.cpuprofile`) and build info (`AppInfo.json`). + +Build info: +```jsx +{ + appVersion: "1.0.0", + environment: "production", + platform: "IOS", + totalMemory: "3GB", + usedMemory: "300MB" +} +``` + +### How to symbolicate trace record: +1. You have two files: `AppInfo.json` and `Profile.cpuprofile` +2. Place the `Profile.cpuprofile` file at the root of your project. +3. If you have already generated a source map from the steps above for this branch, you can skip to the next step. Otherwise, obtain the app version from `AppInfo.json` switch to that branch and generate the source map as described. + +`IMPORTANT:` You should generate the source map from the same branch as the trace was recorded. + +4. Use the following commands to symbolicate the trace for Android and iOS, respectively: +Android: `npm run symbolicate-release:android` +IOS: `npm run symbolicate-release:ios` +5. A new file named `Profile_trace_for_-converted.json` will appear in your project's root folder. +6. Open this file in your tool of choice: + - SpeedScope ([https://www.speedscope.app](https://www.speedscope.app/)) + - Perfetto UI (https://ui.perfetto.dev/) + - Google Chrome's Tracing UI (chrome://tracing) + --- # App Structure and Conventions diff --git a/android/app/build.gradle b/android/app/build.gradle index 73e8e61111dc..ee8aa986ddf2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044800 - versionName "1.4.48-0" + versionCode 1001045005 + versionName "1.4.50-5" } flavorDimensions "default" @@ -181,7 +181,6 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("com.facebook.react:flipper-integration") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") diff --git a/android/build.gradle b/android/build.gradle index 7b5dd81e5bf1..10600480d8bb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,6 +44,9 @@ allprojects { force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION force "com.facebook.react:hermes-engine:" + REACT_NATIVE_VERSION + //Fix Investigate App Crash MainActivity.onCreate #35655 + force "com.facebook.soloader:soloader:0.10.4+" + eachDependency { dependency -> if (dependency.requested.group == 'org.bouncycastle') { println dependency.requested.module diff --git a/assets/images/document-slash.svg b/assets/images/document-slash.svg new file mode 100644 index 000000000000..25a4c96038b4 --- /dev/null +++ b/assets/images/document-slash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__accounting.svg b/assets/images/simple-illustrations/simple-illustration__accounting.svg new file mode 100644 index 000000000000..f7634141e966 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__accounting.svg @@ -0,0 +1,32 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__car-ice.svg b/assets/images/simple-illustrations/simple-illustration__car-ice.svg new file mode 100644 index 000000000000..ba2b79bca6aa --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__car-ice.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__car.svg b/assets/images/simple-illustrations/simple-illustration__car.svg new file mode 100644 index 000000000000..2d420be6c3a9 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__car.svg @@ -0,0 +1,25 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__coins.svg b/assets/images/simple-illustrations/simple-illustration__coins.svg new file mode 100644 index 000000000000..5350886402c6 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__coins.svg @@ -0,0 +1,26 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__pencil.svg b/assets/images/simple-illustrations/simple-illustration__pencil.svg new file mode 100644 index 000000000000..8d9f06991612 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__pencil.svg @@ -0,0 +1,20 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg index 47d30d54310f..b684c58126f7 100644 --- a/assets/images/simple-illustrations/simple-illustration__workflows.svg +++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg @@ -1 +1,153 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/babel.config.js b/babel.config.js index 2a09d086dc5c..7e90fca1c9be 100644 --- a/babel.config.js +++ b/babel.config.js @@ -82,7 +82,7 @@ const metro = { }; /* - * We use Flipper, and react-native-performance to capture/monitor stats + * We use and react-native-performance to capture/monitor stats * By default is disabled in production as it adds small overhead * When CAPTURE_METRICS is set we're explicitly saying that we want to capture metrics * To enable the for release builds we add these aliases */ diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 170198987793..2fed8a477aab 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -18,7 +18,6 @@ const includeModules = [ '@react-native-picker', 'react-native-modal', 'react-native-gesture-handler', - 'react-native-flipper', 'react-native-google-places-autocomplete', 'react-native-qrcode-svg', 'react-native-view-shot', diff --git a/desktop/package-lock.json b/desktop/package-lock.json index f6f96b647ae1..b8ae9d0a2be5 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,7 +10,7 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", "electron-serve": "^1.3.0", - "electron-updater": "^6.1.8", + "electron-updater": "^6.1.9", "node-machine-id": "^1.1.12" } }, @@ -50,9 +50,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -156,11 +156,11 @@ } }, "node_modules/electron-updater": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", - "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.9.tgz", + "integrity": "sha512-omoTwGSG+/H8G62cEZS/dc5Lmc4HohAd4198AP+JNv8H7bfxXUCKekaR6WpsN1n2DiWzvcqOusfGSogZv/uj9w==", "dependencies": { - "builder-util-runtime": "9.2.3", + "builder-util-runtime": "9.2.4", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -467,9 +467,9 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, "builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "requires": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -541,11 +541,11 @@ "integrity": "sha512-OEC/48ZBJxR6XNSZtCl4cKPyQ1lvsu8yp8GdCplMWwGS1eEyMcEmzML5BRs/io/RLDnpgyf+7rSL+X6ICifRIg==" }, "electron-updater": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.8.tgz", - "integrity": "sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==", + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.9.tgz", + "integrity": "sha512-omoTwGSG+/H8G62cEZS/dc5Lmc4HohAd4198AP+JNv8H7bfxXUCKekaR6WpsN1n2DiWzvcqOusfGSogZv/uj9w==", "requires": { - "builder-util-runtime": "9.2.3", + "builder-util-runtime": "9.2.4", "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 de5834b9ce7a..606fcac92500 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -7,7 +7,7 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", "electron-serve": "^1.3.0", - "electron-updater": "^6.1.8", + "electron-updater": "^6.1.9", "node-machine-id": "^1.1.12" }, "author": "Expensify, Inc.", diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index c01243e777d5..3d0d16b00587 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -29,15 +29,15 @@ platforms: icon: /assets/images/bank-card.svg description: Find out how to connect Expensify to your financial institutions, track credit card transactions, and best practices for reconciling company cards. - - href: billing-and-subscriptions - title: Billing & Subscriptions + - href: expensify-billing + title: Expensify Billing icon: /assets/images/subscription-annual.svg - description: Here is where you can review Expensify's billing and subscription options, plan types, and payment methods. + description: Review Expensify's subscription options, plan types, and payment methods. - - href: expense-and-report-features - title: Expense & Report Features + - href: reports + title: Reports icon: /assets/images/money-receipt.svg - description: From enabling automatic expense auditing to tracking attendees, here is where you can review tips and tutorials to streamline expense management. + description: Set approval workflows and use Expensify’s automated report features. - href: expensify-card title: Expensify Card @@ -49,10 +49,10 @@ platforms: icon: /assets/images/handshake.svg description: Discover how to get the most out of Expensify as an ExpensifyApproved! accountant partner. Learn how to set up your clients, receive CPE credits, and take advantage of your partner discount. - - href: get-paid-back - title: Get Paid Back + - href: expenses + title: Expenses icon: /assets/images/money-into-wallet.svg - description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. + description: Learn more about expense tracking and submission. - href: insights-and-custom-reporting title: Insights & Custom Reporting @@ -64,20 +64,25 @@ platforms: icon: /assets/images/workflow.svg description: Enhance Expensify’s capabilities by integrating it with your accounting or HR software. Here is where you can learn more about creating a synchronized financial management ecosystem. - - href: manage-employees-and-report-approvals - title: Manage Employees & Report Approvals + - href: copilots-and-delegates + title: Copilots & Delegates icon: /assets/images/envelope-receipt.svg - description: Master the art of overseeing employees and reports by utilizing Expensify’s automation features and approval workflows. + description: Assign Copilots and delegate report approvals. - href: send-payments title: Send Payments icon: /assets/images/send-money.svg description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. - - href: workspace-and-domain-settings - title: Workspace & Domain Settings + - href: workspaces + title: Workspaces icon: /assets/images/shield.svg - description: Discover how to set up and manage workspace, define user permissions, and implement compliance rules to maintain a secure and compliant financial management landscape. + description: Configure rules, settings, and limits for your company’s spending. + + - href: domains + title: Domains + icon: /assets/images/domains.svg + description: Claim and verify your company’s domain to access additional management and security features. - href: new-expensify title: New Expensify diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md b/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows.md rename to docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md b/docs/articles/expensify-classic/copilots-and-delegates/Approving-Reports.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md rename to docs/articles/expensify-classic/copilots-and-delegates/Approving-Reports.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members.md rename to docs/articles/expensify-classic/copilots-and-delegates/Invite-Members.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md b/docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md rename to docs/articles/expensify-classic/copilots-and-delegates/Removing-Members.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md b/docs/articles/expensify-classic/copilots-and-delegates/User-Roles.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md rename to docs/articles/expensify-classic/copilots-and-delegates/User-Roles.md diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md b/docs/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate.md similarity index 100% rename from docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md rename to docs/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate.md diff --git a/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md new file mode 100644 index 000000000000..bb4b21547892 --- /dev/null +++ b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md @@ -0,0 +1,50 @@ +--- +title: Claim and verify a domain +description: Grant specific employee permissions by claiming a private domain and verifying it in Expensify +--- +
+ +If you have a private domain (like yourcompany.com), you can add the domain to your Expensify account. Claiming a domain allows you to grant specific permissions to accounts that include the domain in their email address (for example, if your domain is yourcompany.com, anyone who signs up under this domain—like yourname@yourcompany.com—will have these domain rules applied to their account). + +Claiming a domain also allows you to: +* Import and reconcile company credit cards and Expensify Cards +* Add company credit card and Expensify Card rules and restrictions + +Once you verify your domain, you’ll be able to: +* Assign delegates for employees who are on vacation +* Delete employee Expensify accounts +* Enable SAML / SSO settings for secure log in + +{% include info.html %} +You can claim and verify private domains only. Public domains (like gmail.com) cannot be used to create a domain. +{% include end-info.html %} + +# Step 1: Claim domain + +
    +
  1. Hover over Settings, then click Domains.
  2. +
  3. Click New Domain.
  4. +
  5. Enter your domain name (e.g., yourcompany.com).
  6. +
  7. Click Submit.
  8. +
+ +# Step 2: Verify domain ownership + +{% include info.html %} +To complete this step, you must have a Control workspace, and you’ll need access to your domain provider account (GoDaddy, Wix, GSuite, etc.). If you don’t verify the domain, you will still have access to the domain to add and manage credit card expenses and domain admins, but you will not be able to invite members, add groups, use domain reporting tools, set delegates for employees on vacation, or enable SAML SSO. For more guidance on how to complete this process for a specific provider, check the provider’s website.{% include end-info.html %} + +
    +
  1. Log in to your DNS service provider (which may be the website you purchased the domain from or that currently hosts the domain, like NameCheap, GoDaddy, DNSMadeEasy, or Amazon Route53. You may need to contact your company’s IT department if your domain is managed internally).
  2. +
  3. Find the page for DNS records, which might be labeled as DNS Management or Zone File Editor.
  4. +
  5. Add a new TXT record and set the value as 532F6180D8.
  6. +
  7. Save your changes.
  8. +
  9. In Expensify, click the Domain Members tab and click Verify.
  10. +
+ +After successful verification, an email will be sent to all members of the Expensify domain to inform them that their accounts will be under domain control (i.e. the rules set for the domain will affect their account). + +# Add another domain + +To add an additional domain, you’ll have to first add your email address that is connected with your domain as your [primary or secondary email] (https://help.expensify.com/articles/expensify-classic/settings/account-settings/Change-or-add-email-address) (for example, if your domain is yourcompany.com, then you want to add and verify your email address @yourcompany.com as your primary or secondary email address). Then you can complete the steps above to add the domain. + +
\ No newline at end of file diff --git a/docs/articles/expensify-classic/domains/Create-A-Group.md b/docs/articles/expensify-classic/domains/Create-A-Group.md new file mode 100644 index 000000000000..fb70faffa27e --- /dev/null +++ b/docs/articles/expensify-classic/domains/Create-A-Group.md @@ -0,0 +1,26 @@ +--- +title: Create a group +description: How to set different rules for different members of your domain +--- +
+ +To set different domain rules for different members, you can place them into groups. For example, many organizations create different groups for employees and managers since they generally need different domain permissions. + +To create a group, + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Click the **Groups** tab on the left. +4. Click **Create Group**. +5. Select all of the group settings and permissions. + - **Permission Group Name**: Enter a name for the group + - **Default Group**: Determine if new domain members will be automatically added to this group. + - **Strictly enforce expense workspace rules**: Determine if all expense rules must be met before people in this group can submit a report. + - **Restrict primary login selection**: Determine if members of this group will be restricted from using a personal email address to access their Expensify account. + - **Restrict expense workspace creation/removal**: Determine if members of this group will be allowed to create new workspaces. + - **Preferred workspace**: Determine if this group will automatically have their expenses and reports posted to a specific workspace. + - **Set preferred workspace to**: If preferred workspace is enabled, select which workspace members of this group will have set as their preferred workspace. + - **Expensify Card Preferred Workspace**: If preferred workspace is enabled, determine if Expensify Card transactions for this group will be posted to the preferred workspace listed for the Expensify Card instead of the preferred workspace listed in the above settings. +6. Click **Save**. + +
\ No newline at end of file diff --git a/docs/articles/expensify-classic/get-paid-back/Distance-Tracking.md b/docs/articles/expensify-classic/expenses/Distance-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/Distance-Tracking.md rename to docs/articles/expensify-classic/expenses/Distance-Tracking.md diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md b/docs/articles/expensify-classic/expenses/Per-Diem-Expenses.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md rename to docs/articles/expensify-classic/expenses/Per-Diem-Expenses.md diff --git a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md b/docs/articles/expensify-classic/expenses/Referral-Program.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/Referral-Program.md rename to docs/articles/expensify-classic/expenses/Referral-Program.md diff --git a/docs/articles/expensify-classic/get-paid-back/Trips.md b/docs/articles/expensify-classic/expenses/Trips.md similarity index 96% rename from docs/articles/expensify-classic/get-paid-back/Trips.md rename to docs/articles/expensify-classic/expenses/Trips.md index ccfbe1592291..04f95c96eb44 100644 --- a/docs/articles/expensify-classic/get-paid-back/Trips.md +++ b/docs/articles/expensify-classic/expenses/Trips.md @@ -34,6 +34,6 @@ To view details about your past or upcoming trips, follow these steps within the If you received your receipt in an email that is not associated with your Expensify account, you can add this email as a [secondary login](https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#how-to-add-a-secondary-login) to directly forward the receipt into your account. ## How do I upload Trip receipts that were not sent to me by email? -If your trip receipt was not sent to you by email, you can manually upload the receipt to your account. Check out this resource for more information on [manually uploading receipts](https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts#manually-upload). +If your trip receipt was not sent to you by email, you can manually upload the receipt to your account. Check out this resource for more information on [manually uploading receipts](https://help.expensify.com/articles/expensify-classic/expenses/expenses/Upload-Receipts#manually-upload). {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/expenses/Apply-Tax.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md rename to docs/articles/expensify-classic/expenses/expenses/Apply-Tax.md diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md b/docs/articles/expensify-classic/expenses/expenses/Create-Expenses.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md rename to docs/articles/expensify-classic/expenses/expenses/Create-Expenses.md diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md b/docs/articles/expensify-classic/expenses/expenses/Merge-Expenses.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md rename to docs/articles/expensify-classic/expenses/expenses/Merge-Expenses.md diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md b/docs/articles/expensify-classic/expenses/expenses/Upload-Receipts.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md rename to docs/articles/expensify-classic/expenses/expenses/Upload-Receipts.md diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md b/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md rename to docs/articles/expensify-classic/expenses/reports/Create-A-Report.md diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md b/docs/articles/expensify-classic/expenses/reports/Reimbursements.md similarity index 100% rename from docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md rename to docs/articles/expensify-classic/expenses/reports/Reimbursements.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Annual-Subscription.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Annual-Subscription.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md rename to docs/articles/expensify-classic/expensify-billing/Billing-Overview.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md b/docs/articles/expensify-classic/expensify-billing/Billing-Owner.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md rename to docs/articles/expensify-classic/expensify-billing/Billing-Owner.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md rename to docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Individual-Subscription.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Individual-Subscription.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription.md similarity index 93% rename from docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md rename to docs/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription.md index 326ce7fe33ab..fac605ada1bd 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md +++ b/docs/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription.md @@ -5,7 +5,7 @@ description: Learn more about your pay-per-use subscription. # Overview Pay-per-use is a billing option for people who prefer to use Expensify month to month or on an as-needed basis. On a pay-per-use subscription, you will only pay for active users in that given month. -**We recommend this billing setup for companies that use Expensify a few months out of the year**. If you have expenses to manage for more than 6 out of 12 months, an [**Annual Subscription**](https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription) may better suit your needs. +**We recommend this billing setup for companies that use Expensify a few months out of the year**. If you have expenses to manage for more than 6 out of 12 months, an [**Annual Subscription**](https://help.expensify.com/articles/expensify-classic/expensify-billing/Annual-Subscription) may better suit your needs. # How to start a pay-per-use subscription 1. Create a Group Workspace if you haven’t already by going to **Settings > Workspaces > Group > New Workspace** diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md rename to docs/articles/expensify-classic/expensify-billing/Receipt-Breakdown.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md b/docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md similarity index 100% rename from docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md rename to docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md index aa63aa3c38bd..42d06d45fa87 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-Small-To-Medium-Sized-Businesses.md @@ -114,7 +114,7 @@ For an efficient company, we recommend setting up [Scheduled Submit](https://hel - You’ll notice *Scheduled Submit* is located directly under *Report Basics* - Choose *Daily* -Between Expensify's SmartScan technology, automatic categorization, and [DoubleCheck](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports) features, your employees shouldn't need to do anything more than swipe their Expensify Card or take a photo of their receipt. +Between Expensify's SmartScan technology, automatic categorization, and [DoubleCheck](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approving-Reports) features, your employees shouldn't need to do anything more than swipe their Expensify Card or take a photo of their receipt. Expenses with violations will stay behind for the employee to fix, while expenses that are “in-workspace” will move into an approver’s queue to mitigate any potential for delays. Scheduled Submit will ensure all expenses are submitted automatically for approval. @@ -147,10 +147,10 @@ You only need to do this once: you are fully set up for not only reimbursing exp ## Step 9: Invite employees and set an approval workflow *Select an Approval Mode* -We recommend you select *Advanced Approval* as your Approval Mode to set up a middle-management layer of approval. If you have a single layer of approval, we recommend selecting [Submit & Approve](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows). But if *Advanced Approval* is your jam, keep reading! +We recommend you select *Advanced Approval* as your Approval Mode to set up a middle-management layer of approval. If you have a single layer of approval, we recommend selecting [Submit & Approve](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows). But if *Advanced Approval* is your jam, keep reading! *Import your employees in bulk via CSV* -Given the amount of employees you have, it’s best you import employees in bulk via CSV. You can learn more about using a CSV file to bulk upload employees with *Advanced Approval [here](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows)* +Given the amount of employees you have, it’s best you import employees in bulk via CSV. You can learn more about using a CSV file to bulk upload employees with *Advanced Approval [here](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows)* ![Bulk import your employees](https://help.expensify.com/assets/images/playbook-impoort-employees.png){:width="100%"} diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md index b9e06db13bfc..30d3b3e7732c 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md @@ -22,7 +22,7 @@ If you don't already have one, go to *[new.expensify.com](https://new.expensify. There are three paid policies; Individual, Collect, and Control, but for your needs we recommend the Control Policy for the following reasons: - You can cap spend on certain expense types, and set compliance controls so Expensify’s built-in Concierge Audit Tracking can detect violations on your behalf -- As a growing business with VC-funding, the Control plan will scale with you as your team grows and you start to introduce more sophisticated [approval workflows](https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Approval-Workflows) (see Step 8 below). +- As a growing business with VC-funding, the Control plan will scale with you as your team grows and you start to introduce more sophisticated [approval workflows](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows) (see Step 8 below). To create your Control Policy: diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md index 8f8d3cfc3dea..4db5ad1f27b9 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md @@ -12,7 +12,7 @@ First, you’ll need to add these two tags to your Workspace: 1) Number of Internal Attendees 2) Number of External Attendees -These tags must be named exactly as written above, ensuring there are no extra spaces at the beginning or at the end. You’ll need to set the tags to be numbers 00 - 10 or whatever number you wish to go up to (up to the maximum number of attendees you would expect at any one time), one tag per number i.e. “01”, “02”, “03” etc. These tags can be added in addition to those that are pulled in from your accounting solution. Follow these [instructions](https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags) to add tags. +These tags must be named exactly as written above, ensuring there are no extra spaces at the beginning or at the end. You’ll need to set the tags to be numbers 00 - 10 or whatever number you wish to go up to (up to the maximum number of attendees you would expect at any one time), one tag per number i.e. “01”, “02”, “03” etc. These tags can be added in addition to those that are pulled in from your accounting solution. Follow these [instructions](https://help.expensify.com/articles/expensify-classic/workspaces/Tags) to add tags. ## Add Payroll Code Go to **Settings > Workspaces > Group > _Workspace Name_ > Categories** and within the categories you wish to track FBT against, select **Edit Category** and add the code “TAG”: diff --git a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md b/docs/articles/expensify-classic/reports/Attendee-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md rename to docs/articles/expensify-classic/reports/Attendee-Tracking.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Currency.md b/docs/articles/expensify-classic/reports/Currency.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Currency.md rename to docs/articles/expensify-classic/reports/Currency.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md b/docs/articles/expensify-classic/reports/Expense-Rules.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md rename to docs/articles/expensify-classic/reports/Expense-Rules.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md b/docs/articles/expensify-classic/reports/Expense-Types.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md rename to docs/articles/expensify-classic/reports/Expense-Types.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md b/docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md rename to docs/articles/expensify-classic/reports/Report-Audit-Log-and-Comments.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md b/docs/articles/expensify-classic/reports/The-Expenses-Page.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md rename to docs/articles/expensify-classic/reports/The-Expenses-Page.md diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md b/docs/articles/expensify-classic/reports/The-Reports-Page.md similarity index 100% rename from docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md rename to docs/articles/expensify-classic/reports/The-Reports-Page.md diff --git a/docs/articles/expensify-classic/settings/Preferences.md b/docs/articles/expensify-classic/settings/Preferences.md deleted file mode 100644 index 8131cd0e80c5..000000000000 --- a/docs/articles/expensify-classic/settings/Preferences.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Account Preferences -description: Expensify Account Preferences ---- - -# Overview -Customize your Expensify experience by updating your account details and preferences. Here you can update your profile picture, adjust contact preferences, and perform other actions to personalize your account. - -# How to manage contact preferences -To edit your notification preferences or unsubscribe from Expensify updates: -- On the web, navigate to **Settings > Account > Preferences** -Scroll down to find the ‘Contact Preferences’ section. To stop receiving a specific type of email, uncheck the corresponding box. - -# How to set your time zone -Wherever you are, we'll time-stamp your report actions according to your local time. This helps you keep track of when submissions or approvals occurred. Setting your time zone in Expensify is simple and ensures accurate time-stamping for your report actions, especially in the comments section of the expense report you're reviewing. - -To set your time zone: -Navigate to **Settings > Account > Preferences > Scroll down to Time Zone** - -![ExpensifyHelp_Timezone]({{site.url}}/assets/images/ExpensifyHelp_Timezone.png){:width="100%"} - - **Note:** To set your time zone automatically based on your location, tick the box that says **Set my time zone automatically**. - -If you prefer to set your time zone manually, leave the box unticked and select your time zone from the searchable list of locations. - -When you add a comment to a report, all the report actions will be time-stamped in your local time. Adjusting your time zone to the appropriate location makes tracking and understanding submission and approval times much easier. - -![ExpensifyHelp_Time]({{site.url}}/assets/images/ExpensifyHelp_Time.png){:width="100%"} diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-time-zone.md b/docs/articles/expensify-classic/settings/account-settings/Set-time-zone.md new file mode 100644 index 000000000000..7d4842f936b9 --- /dev/null +++ b/docs/articles/expensify-classic/settings/account-settings/Set-time-zone.md @@ -0,0 +1,21 @@ +--- +title: Set time zone +description: Set your time zone in Expensify +--- +
+ +You can manually set your time zone or allow Expensify to automatically set your time zone based on your location. + +{% include info.html %} +Some actions you take in Expensify are timestamped. To ensure the most accurate time is captured, you’ll want to make sure your time zone matches your current location. If you select the automatic time zone option, your time zone will automatically reflect your current location. If you manually set your time zone, you must manually update the time zone when traveling. +{% include end-info.html %} + +*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 **Preferences** tab. +3. Scroll down to the Time Zone section and select your time zone preferences. + - Automatic time zone: To allow Expensify to automatically set your time zone based on your location, select the Set my time zone automatically checkbox. + - Manual time zone: To manually select your time zone, select your time zone from the list. + +
diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspaces/Budgets.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md rename to docs/articles/expensify-classic/workspaces/Budgets.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md b/docs/articles/expensify-classic/workspaces/Categories.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md rename to docs/articles/expensify-classic/workspaces/Categories.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspaces/Domains-Overview.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md rename to docs/articles/expensify-classic/workspaces/Domains-Overview.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/workspaces/Expenses.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md rename to docs/articles/expensify-classic/workspaces/Expenses.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Invoicing.md b/docs/articles/expensify-classic/workspaces/Invoicing.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Invoicing.md rename to docs/articles/expensify-classic/workspaces/Invoicing.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md b/docs/articles/expensify-classic/workspaces/Per-Diem.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md rename to docs/articles/expensify-classic/workspaces/Per-Diem.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md b/docs/articles/expensify-classic/workspaces/Reimbursement.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md rename to docs/articles/expensify-classic/workspaces/Reimbursement.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md b/docs/articles/expensify-classic/workspaces/SAML-SSO.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md rename to docs/articles/expensify-classic/workspaces/SAML-SSO.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md b/docs/articles/expensify-classic/workspaces/Tags.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md rename to docs/articles/expensify-classic/workspaces/Tags.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Currency.md b/docs/articles/expensify-classic/workspaces/reports/Currency.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/reports/Currency.md rename to docs/articles/expensify-classic/workspaces/reports/Currency.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles.md rename to docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit.md rename to docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/tax-tracking.md b/docs/articles/expensify-classic/workspaces/tax-tracking.md similarity index 100% rename from docs/articles/expensify-classic/workspace-and-domain-settings/tax-tracking.md rename to docs/articles/expensify-classic/workspaces/tax-tracking.md diff --git a/docs/assets/images/domains.svg b/docs/assets/images/domains.svg new file mode 100644 index 000000000000..3a3c95604b79 --- /dev/null +++ b/docs/assets/images/domains.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/redirects.csv b/docs/redirects.csv index b9a7826f98b9..3c89e920e3f7 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -1,5 +1,5 @@ sourceURL,targetURL -https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Reimbursements +https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/expenses/reports/Reimbursements https://community.expensify.com/discussion/4925/how-to-dispute-an-expensify-card-transaction,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction https://community.expensify.com/discussion/5184/faq-how-am-i-protected-from-fraud-using-the-expensify-card,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction https://community.expensify.com/discussion/4887/deep-dive-understanding-your-expensify-card-statement,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements @@ -17,14 +17,14 @@ https://community.expensify.com/discussion/5802/deep-dive-understanding-math-and https://community.expensify.com/discussion/5796/deep-dive-user-level-formula,https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates https://community.expensify.com/discussion/4750/how-to-create-a-custom-export,https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates https://community.expensify.com/discussion/4642/how-to-export-reports-to-a-custom-template,https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates -https://community.expensify.com/discussion/5648/deep-dive-policy-users-and-roles,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles -https://community.expensify.com/discussion/5740/deep-dive-what-expense-information-is-available-based-on-role,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles -https://community.expensify.com/discussion/4472/how-to-set-or-edit-a-user-role,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles -https://community.expensify.com/discussion/5655/deep-dive-what-is-a-vacation-delegate,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate -https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegate-for-an-employee-through-domains,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate -https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate +https://community.expensify.com/discussion/5648/deep-dive-policy-users-and-roles,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles +https://community.expensify.com/discussion/5740/deep-dive-what-expense-information-is-available-based-on-role,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles +https://community.expensify.com/discussion/4472/how-to-set-or-edit-a-user-role,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles +https://community.expensify.com/discussion/5655/deep-dive-what-is-a-vacation-delegate,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate +https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegate-for-an-employee-through-domains,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate +https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vacation-Delegate https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP -https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking +https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/expenses/Distance-Tracking https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings @@ -54,13 +54,14 @@ https://help.expensify.com/articles/expensify-classic/getting-started/Employees, https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace https://help.expensify.com/articles/expensify-classic/getting-started/support/Expensify-Support,https://use.expensify.com/support https://help.expensify.com/articles/expensify-classic/getting-started/Plan-Types,https://use.expensify.com/ -https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program +https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/expenses/Referral-Program https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://help.expensify.com/articles/expensify-classic/account-settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://help.expensify.com/articles/expensify-classic/getting-started/Individual-Users,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself -https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members -https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription,https://use.expensify.com/ -https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription,https://use.expensify.com/ -https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members +https://help.expensify.com/articles/expensify-classic/expensify-billing/Annual-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/expensify-billing/Pay-Per-Use-Subscription,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/expensify-billing/Individual-Subscription,https://use.expensify.com/ https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts +https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 643c81bd0b9c..c4412cf650ee 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index 8a5170cfe697..999442b550da 100644 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index acfc4d933954..e39542ef0303 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -543,13 +543,10 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); @@ -557,13 +554,10 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); @@ -613,13 +607,10 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", ); @@ -627,13 +618,10 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", ); diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 935fb8a0083f..127f98edc070 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.48 + 1.4.50 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.48.0 + 1.4.50.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9d42e39387b4..8a2fef8c99c6 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.48 + 1.4.50 CFBundleSignature ???? CFBundleVersion - 1.4.48.0 + 1.4.50.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index dd33f988f7c4..34ef35759e15 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.48 + 1.4.50 CFBundleVersion - 1.4.48.0 + 1.4.50.5 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index aa87c3e295f3..83c21797bd0a 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -26,17 +26,6 @@ setup_permissions([ 'LocationWhenInUse' ]) -# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. -# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded -# -# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` -# ```js -# module.exports = { -# dependencies: { -# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), -# ``` -flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled(['DebugProduction', 'DebugDevelopment', 'DebugAdHoc']) - linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green @@ -86,11 +75,6 @@ target 'NewExpensify' do use_react_native!( :path => config[:reactNativePath], - # Enables Flipper. - # - # Note that if you have use_frameworks! enabled, Flipper will not work and - # you should disable the next line. - :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 491ec28b59e5..d0007ec51668 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -30,14 +30,10 @@ PODS: - boost (1.83.0) - BVLinearGradient (2.8.1): - React-Core - - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - EXAV (13.10.4): - ExpoModulesCore - ReactCommon/turbomodule/core - - EXImageLoader (4.6.0): - - ExpoModulesCore - - React-Core - Expo (50.0.4): - ExpoModulesCore - ExpoImage (1.10.1): @@ -46,9 +42,6 @@ PODS: - SDWebImageAVIFCoder (~> 0.10.1) - SDWebImageSVGCoder (~> 1.7.0) - SDWebImageWebPCoder (~> 0.13.0) - - ExpoImageManipulator (11.8.0): - - EXImageLoader - - ExpoModulesCore - ExpoModulesCore (1.11.8): - glog - RCT-Folly (= 2022.05.16.00) @@ -133,62 +126,6 @@ PODS: - FirebaseInstallations (~> 8.0) - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - Flipper (0.201.0): - - Flipper-Folly (~> 2.6) - - Flipper-Boost-iOSX (1.76.0.1.11) - - Flipper-DoubleConversion (3.2.0.1) - - Flipper-Fmt (7.1.7) - - Flipper-Folly (2.6.10): - - Flipper-Boost-iOSX - - Flipper-DoubleConversion - - Flipper-Fmt (= 7.1.7) - - Flipper-Glog - - libevent (~> 2.1.12) - - OpenSSL-Universal (= 1.1.1100) - - Flipper-Glog (0.5.0.5) - - Flipper-PeerTalk (0.0.4) - - FlipperKit (0.201.0): - - FlipperKit/Core (= 0.201.0) - - FlipperKit/Core (0.201.0): - - Flipper (~> 0.201.0) - - FlipperKit/CppBridge - - FlipperKit/FBCxxFollyDynamicConvert - - FlipperKit/FBDefines - - FlipperKit/FKPortForwarding - - SocketRocket (~> 0.6.0) - - FlipperKit/CppBridge (0.201.0): - - Flipper (~> 0.201.0) - - FlipperKit/FBCxxFollyDynamicConvert (0.201.0): - - Flipper-Folly (~> 2.6) - - FlipperKit/FBDefines (0.201.0) - - FlipperKit/FKPortForwarding (0.201.0): - - CocoaAsyncSocket (~> 7.6) - - Flipper-PeerTalk (~> 0.0.4) - - FlipperKit/FlipperKitHighlightOverlay (0.201.0) - - FlipperKit/FlipperKitLayoutHelpers (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutTextSearchable - - FlipperKit/FlipperKitLayoutIOSDescriptors (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutHelpers - - FlipperKit/FlipperKitLayoutPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitHighlightOverlay - - FlipperKit/FlipperKitLayoutHelpers - - FlipperKit/FlipperKitLayoutIOSDescriptors - - FlipperKit/FlipperKitLayoutTextSearchable - - FlipperKit/FlipperKitLayoutTextSearchable (0.201.0) - - FlipperKit/FlipperKitNetworkPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitReactPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitUserDefaultsPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/SKIOSNetworkPlugin (0.201.0): - - FlipperKit/Core - - FlipperKit/FlipperKitNetworkPlugin - fmt (6.2.1) - glog (0.3.5) - GoogleAppMeasurement (8.8.0): @@ -290,7 +227,6 @@ PODS: - onfido-react-native-sdk (10.6.0): - Onfido (~> 29.6.0) - React - - OpenSSL-Universal (1.1.1100) - Plaid (4.7.0) - PromisesObjC (2.3.1) - RCT-Folly (2022.05.16.00): @@ -1181,6 +1117,8 @@ PODS: - React-Core - react-native-geolocation (3.0.6): - React-Core + - react-native-image-manipulator (1.0.5): + - React - react-native-image-picker (7.0.3): - React-Core - react-native-key-command (1.0.6): @@ -1202,6 +1140,10 @@ PODS: - React - React-callinvoker - React-Core + - react-native-release-profiler (0.1.6): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - react-native-render-html (6.3.1): - React-Core - react-native-safe-area-context (4.8.2): @@ -1447,6 +1389,8 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - RNShare (10.0.2): + - React-Core - RNSound (0.11.2): - React-Core - RNSound/Core (= 0.11.2) @@ -1479,39 +1423,16 @@ DEPENDENCIES: - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXAV (from `../node_modules/expo-av/ios`) - - EXImageLoader (from `../node_modules/expo-image-loader/ios`) - Expo (from `../node_modules/expo`) - ExpoImage (from `../node_modules/expo-image/ios`) - - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - - Flipper (= 0.201.0) - - Flipper-Boost-iOSX (= 1.76.0.1.11) - - Flipper-DoubleConversion (= 3.2.0.1) - - Flipper-Fmt (= 7.1.7) - - Flipper-Folly (= 2.6.10) - - Flipper-Glog (= 0.5.0.5) - - Flipper-PeerTalk (= 0.0.4) - - FlipperKit (= 0.201.0) - - FlipperKit/Core (= 0.201.0) - - FlipperKit/CppBridge (= 0.201.0) - - FlipperKit/FBCxxFollyDynamicConvert (= 0.201.0) - - FlipperKit/FBDefines (= 0.201.0) - - FlipperKit/FKPortForwarding (= 0.201.0) - - FlipperKit/FlipperKitHighlightOverlay (= 0.201.0) - - FlipperKit/FlipperKitLayoutPlugin (= 0.201.0) - - FlipperKit/FlipperKitLayoutTextSearchable (= 0.201.0) - - FlipperKit/FlipperKitNetworkPlugin (= 0.201.0) - - FlipperKit/FlipperKitReactPlugin (= 0.201.0) - - FlipperKit/FlipperKitUserDefaultsPlugin (= 0.201.0) - - FlipperKit/SKIOSNetworkPlugin (= 0.201.0) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) - lottie-react-native (from `../node_modules/lottie-react-native`) - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)" - - OpenSSL-Universal (= 1.1.1100) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) @@ -1520,7 +1441,6 @@ DEPENDENCIES: - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - React-Codegen (from `build/generated/ios`) - React-Core (from `../node_modules/react-native/`) - - React-Core/DevSupport (from `../node_modules/react-native/`) - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) @@ -1542,6 +1462,7 @@ DEPENDENCIES: - react-native-config (from `../node_modules/react-native-config`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" + - "react-native-image-manipulator (from `../node_modules/@oguzhnatly/react-native-image-manipulator`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-key-command (from `../node_modules/react-native-key-command`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) @@ -1551,6 +1472,7 @@ DEPENDENCIES: - react-native-performance (from `../node_modules/react-native-performance`) - react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`) - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) + - react-native-release-profiler (from `../node_modules/react-native-release-profiler`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) @@ -1596,6 +1518,7 @@ DEPENDENCIES: - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) + - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) - VisionCamera (from `../node_modules/react-native-vision-camera`) @@ -1607,7 +1530,6 @@ SPEC REPOS: - AirshipFrameworkProxy - AirshipServiceExtension - AppAuth - - CocoaAsyncSocket - Firebase - FirebaseABTesting - FirebaseAnalytics @@ -1617,14 +1539,6 @@ SPEC REPOS: - FirebaseInstallations - FirebasePerformance - FirebaseRemoteConfig - - Flipper - - Flipper-Boost-iOSX - - Flipper-DoubleConversion - - Flipper-Fmt - - Flipper-Folly - - Flipper-Glog - - Flipper-PeerTalk - - FlipperKit - fmt - GoogleAppMeasurement - GoogleDataTransport @@ -1644,7 +1558,6 @@ SPEC REPOS: - MapboxMobileEvents - nanopb - Onfido - - OpenSSL-Universal - Plaid - PromisesObjC - SDWebImage @@ -1663,14 +1576,10 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXAV: :path: "../node_modules/expo-av/ios" - EXImageLoader: - :path: "../node_modules/expo-image-loader/ios" Expo: :path: "../node_modules/expo" ExpoImage: :path: "../node_modules/expo-image/ios" - ExpoImageManipulator: - :path: "../node_modules/expo-image-manipulator/ios" ExpoModulesCore: :path: "../node_modules/expo-modules-core" FBLazyVector: @@ -1740,6 +1649,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-document-picker" react-native-geolocation: :path: "../node_modules/@react-native-community/geolocation" + react-native-image-manipulator: + :path: "../node_modules/@oguzhnatly/react-native-image-manipulator" react-native-image-picker: :path: "../node_modules/react-native-image-picker" react-native-key-command: @@ -1758,6 +1669,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-plaid-link-sdk" react-native-quick-sqlite: :path: "../node_modules/react-native-quick-sqlite" + react-native-release-profiler: + :path: "../node_modules/react-native-release-profiler" react-native-render-html: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: @@ -1848,6 +1761,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" + RNShare: + :path: "../node_modules/react-native-share" RNSound: :path: "../node_modules/react-native-sound" RNSVG: @@ -1864,13 +1779,10 @@ SPEC CHECKSUMS: AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 boost: d3f49c53809116a5d38da093a8aa78bf551aed09 BVLinearGradient: 421743791a59d259aec53f4c58793aad031da2ca - CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 EXAV: 09a4d87fa6b113fbb0ada3aade6799f78271cb44 - EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b Expo: 1e3bcf9dd99de57a636127057f6b488f0609681a ExpoImage: 1cdaa65a6a70bb01067e21ad1347ff2d973885f5 - ExpoImageManipulator: c1d7cb865eacd620a35659f3da34c70531f10b59 ExpoModulesCore: 96d1751929ad10622773bb729ab28a8423f0dd0c FBLazyVector: fbc4957d9aa695250b55d879c1d86f79d7e69ab4 FBReactNativeSpec: 86de768f89901ef6ed3207cd686362189d64ac88 @@ -1883,14 +1795,6 @@ SPEC CHECKSUMS: FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b - Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44 - Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c - Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 - Flipper-Fmt: 60cbdd92fc254826e61d669a5d87ef7015396a9b - Flipper-Folly: 584845625005ff068a6ebf41f857f468decd26b3 - Flipper-Glog: 70c50ce58ddaf67dc35180db05f191692570f446 - Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 - FlipperKit: 37525a5d056ef9b93d1578e04bc3ea1de940094f fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 @@ -1914,7 +1818,6 @@ SPEC CHECKSUMS: nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: c52e797b10cc9e6d29ba91996cb62e501000bfdd onfido-react-native-sdk: 4e7f0a7a986ed93cb906d2e0b67a6aab9202de0b - OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c Plaid: 431ef9be5314a1345efb451bc5e6b067bfb3b4c6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 @@ -1944,6 +1847,7 @@ SPEC CHECKSUMS: react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e react-native-document-picker: 3599b238843369026201d2ef466df53f77ae0452 react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903 + react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56 react-native-image-picker: 2381c008bbb09e72395a2d043c147b11bd1523d9 react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d @@ -1953,6 +1857,7 @@ SPEC CHECKSUMS: react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1 react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 + react-native-release-profiler: 86f2004d5f8c4fff17d90a5580513519a685d7ae react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -1998,6 +1903,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: 3850671fd0c67051ea8e1e648e8c3e86bf3a28eb RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 + RNShare: 859ff710211285676b0bcedd156c12437ea1d564 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 @@ -2009,6 +1915,6 @@ SPEC CHECKSUMS: VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a Yoga: 13c8ef87792450193e117976337b8527b49e8c03 -PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 +PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530 COCOAPODS: 1.13.0 diff --git a/jest/setup.ts b/jest/setup.ts index 11b0d77ed7ac..488e3e36a1d3 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -47,3 +47,7 @@ jest.mock('react-native-sound', () => { return SoundMock; }); + +jest.mock('react-native-share', () => ({ + default: jest.fn(), +})); diff --git a/package-lock.json b/package-lock.json index 8f3a17edec48..bc373abcd9b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.48-0", + "version": "1.4.50-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.48-0", + "version": "1.4.50-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -23,6 +23,7 @@ "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "10.6.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", @@ -55,7 +56,6 @@ "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", - "expo-image-manipulator": "11.8.0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -64,7 +64,7 @@ "lodash": "4.17.21", "lottie-react-native": "6.4.1", "mapbox-gl": "^2.15.0", - "onfido-sdk-ui": "13.6.1", + "onfido-sdk-ui": "14.15.0", "patch-package": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", @@ -107,9 +107,11 @@ "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", "react-native-reanimated": "^3.7.2", + "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", "react-native-tab-view": "^3.5.2", @@ -227,7 +229,6 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", - "react-native-performance-flipper-reporter": "^2.0.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", "setimmediate": "^1.0.5", @@ -7713,16 +7714,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@mediapipe/face_detection": { - "version": "0.4.1646425229", - "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", - "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==" - }, - "node_modules/@mediapipe/face_mesh": { - "version": "0.4.1633559619", - "resolved": "https://registry.npmjs.org/@mediapipe/face_mesh/-/face_mesh-0.4.1633559619.tgz", - "integrity": "sha512-Vc8cdjxS5+O2gnjWH9KncYpUCVXT0h714KlWAsyqJvJbIgUJBqpppbIx8yWcAzBDxm/5cYSuBI5p5ySIPxzcEg==" - }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -8048,6 +8039,12 @@ "@octokit/openapi-types": "^12.11.0" } }, + "node_modules/@oguzhnatly/react-native-image-manipulator": { + "version": "1.0.5", + "resolved": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", + "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", + "license": "MIT" + }, "node_modules/@onfido/active-video-capture": { "version": "0.28.6", "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", @@ -10481,78 +10478,6 @@ "join-component": "^1.1.0" } }, - "node_modules/@sentry/browser": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/core": "7.11.1", - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/core": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/hub": "7.11.1", - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/hub": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "7.11.1", - "@sentry/utils": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/hub/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/types": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.11.1", - "license": "BSD-3-Clause", - "dependencies": { - "@sentry/types": "7.11.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, "node_modules/@shopify/flash-list": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.6.3.tgz", @@ -10631,11 +10556,6 @@ "@sinonjs/commons": "^2.0.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, "node_modules/@storybook/addon-a11y": { "version": "6.5.10", "dev": true, @@ -19999,88 +19919,6 @@ "node": ">=10" } }, - "node_modules/@tensorflow-models/face-detection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-detection/-/face-detection-1.0.2.tgz", - "integrity": "sha512-anjSxy3MnZdTiVluOEQZeaFWM30IPswFM+SltX6wseXKja/AbrHYqamGNZKUylAs2JAyudq+xqTRPS+nA2ourg==", - "dependencies": { - "rimraf": "^3.0.2", - "tslib": "2.4.0" - }, - "peerDependencies": { - "@mediapipe/face_detection": "~0.4.0", - "@tensorflow/tfjs-backend-webgl": "^4.4.0", - "@tensorflow/tfjs-converter": "^4.4.0", - "@tensorflow/tfjs-core": "^4.4.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.16.0.tgz", - "integrity": "sha512-bQFu7FTUgqgss1AwnqSwQ1f02IPrfLLc2lLn5pyyVrS6Ex7zA6Y4YkfktqoJSRE6LlRZv3vxSriUGE1avRe4qQ==", - "peer": true, - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.16.0.tgz", - "integrity": "sha512-cIGZWuY892iwTRokbDj3qsLi0AlpQn+U7rzB1mddhHrWr9kBXrrnAvIq0h2aiFzRFNePWUcsbgK+HmYG32kosg==", - "peer": true, - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "4.16.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-converter": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.16.0.tgz", - "integrity": "sha512-gd8dHl9tqEPQOHZLAUza713nKr42rpvUXrtm7yUhk10THvJT6TXe9Q2AJKmni8J3vfR+ghsCh77F8D4RbShx1Q==", - "peer": true, - "peerDependencies": { - "@tensorflow/tfjs-core": "4.16.0" - } - }, - "node_modules/@tensorflow/tfjs-core": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.16.0.tgz", - "integrity": "sha512-MarAtO+Up6wA8pI9QDpQOwwJgb/imYMN++tsoaalyOEE9+B5HS4lQldxDJKXO8Frf4DyXf4FItJktEXaiPfRHw==", - "peer": true, - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.7.0", - "@types/seedrandom": "^2.4.28", - "@webgpu/types": "0.1.38", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "peer": true - }, "node_modules/@testing-library/jest-native": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-native/-/jest-native-5.4.1.tgz", @@ -20570,11 +20408,6 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, - "node_modules/@types/emscripten": { - "version": "0.0.34", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-0.0.34.tgz", - "integrity": "sha512-QSb9ojDincskc+uKMI0KXp8e1NALFINCrMlp8VGKGcTSxeEyRTTKyjWw75NYrCZHUsVEEEpr1tYHpbtaC++/sQ==" - }, "node_modules/@types/eslint": { "version": "8.4.6", "license": "MIT", @@ -20819,11 +20652,6 @@ "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", "dev": true }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "node_modules/@types/mapbox-gl": { "version": "2.7.13", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.13.tgz", @@ -20896,11 +20724,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/offscreencanvas": { - "version": "2019.3.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", - "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" - }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -21058,11 +20881,6 @@ "version": "0.16.2", "license": "MIT" }, - "node_modules/@types/seedrandom": { - "version": "2.4.34", - "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", - "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==" - }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -21162,16 +20980,6 @@ "dev": true, "optional": true }, - "node_modules/@types/webgl-ext": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", - "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" - }, - "node_modules/@types/webgl2": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" - }, "node_modules/@types/webpack": { "version": "4.41.32", "dev": true, @@ -22189,12 +21997,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webgpu/types": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz", - "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA==", - "peer": true - }, "node_modules/@webpack-cli/configtest": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", @@ -24623,12 +24425,6 @@ "bluebird": "^3.5.5" } }, - "node_modules/blueimp-load-image": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/blueimp-load-image/-/blueimp-load-image-2.29.0.tgz", - "integrity": "sha512-psm81GlZ0ffKxVT0QN9dvhpzXMv1KxgXSg8ars0XGAcEGsTwFT2IPo59HDXlw4Lo2oImdPzwrwkliZSiLLUpIw==", - "license": "MIT" - }, "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -28731,10 +28527,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dompurify": { - "version": "2.3.10", - "license": "(MPL-2.0 OR Apache-2.0)" - }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -29253,46 +29045,6 @@ "objectorarray": "^1.0.5" } }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -29333,12 +29085,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/enumerate-devices": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/enumerate-devices/-/enumerate-devices-1.1.1.tgz", - "integrity": "sha512-8zDbrc7ocusTL1ZGmvgy0cTwdyCaM7sGZoYLRmnWJalLQzmftDtce+uDU91gafOTo9MCtgjSIxyMv/F4+Hcchw==", - "license": "MIT" - }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -30735,12 +30481,6 @@ "node": ">=6" } }, - "node_modules/eventemitter2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-2.2.2.tgz", - "integrity": "sha512-AmQ734LWUB9Iyk+2WIU3Z8iRhdL1XQihEE0iF/QC5Xp11zST0Z5tn5jRHa/PgIld2QIPSCys3CREqOQLUhNvkw==", - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -31144,25 +30884,6 @@ "expo": "*" } }, - "node_modules/expo-image-loader": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.6.0.tgz", - "integrity": "sha512-RHQTDak7/KyhWUxikn2yNzXL7i2cs16cMp6gEAgkHOjVhoCJQoOJ0Ljrt4cKQ3IowxgCuOrAgSUzGkqs7omj8Q==", - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-image-manipulator": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-11.8.0.tgz", - "integrity": "sha512-ZWVrHnYmwJq6h7auk+ropsxcNi+LyZcPFKQc8oy+JA0SaJosfShvkCm7RADWAunHmfPCmjHrhwPGEu/rs7WG/A==", - "dependencies": { - "expo-image-loader": "~4.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-keep-awake": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-12.8.2.tgz", @@ -31836,14 +31557,6 @@ "url": "https://opencollective.com/ramda" } }, - "node_modules/file-type": { - "version": "12.4.2", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", - "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==", - "engines": { - "node": ">=8" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -33419,19 +33132,6 @@ "node": ">= 8" } }, - "node_modules/history": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.5.1.tgz", - "integrity": "sha512-gfHeJeYeMzFtos61gdA1AloO0hGXPF2Yum+2FRdJvlylYQOz51OnT1zuwg9UYst1BRrONhcAh3Nmsg9iblgl6g==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.0.0", - "value-equal": "^0.2.0", - "warning": "^3.0.0" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -38500,13 +38200,6 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" }, - "node_modules/js-cookie": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -39514,11 +39207,6 @@ "node": ">=6" } }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "node_modules/longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -41133,22 +40821,6 @@ "node": ">= 8" } }, - "node_modules/mirada": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/mirada/-/mirada-0.0.15.tgz", - "integrity": "sha512-mbm4c+wjBVcmUzHRLv/TfOAq+iy03D24KwGxx8H+NSXkD5EOZV9zFWbVxTvZCc9XwR0FIUhryU/kQm12SMSQ3g==", - "dependencies": { - "buffer": "^5.4.3", - "cross-fetch": "^3.0.4", - "file-type": "^12.3.0", - "misc-utils-of-mine-generic": "^0.2.31" - } - }, - "node_modules/misc-utils-of-mine-generic": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/misc-utils-of-mine-generic/-/misc-utils-of-mine-generic-0.2.45.tgz", - "integrity": "sha512-WsG2zYiui2cdEbHF2pXmJfnjHb4zL+cy+PaYcLgIpMju98hwX89VbjlvGIfamCfEodbQ0qjCEvD3ocgkCXfMOQ==" - }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -42160,41 +41832,9 @@ } }, "node_modules/onfido-sdk-ui": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-13.6.1.tgz", - "integrity": "sha512-EcFqTN9uaVINRUttSdt6ySUBlfg25dE9f2yxxXVUmrM9a4M1luv+aICej1zE3vRZPFEuFJ9mqJZQUTYo0YMFyg==", - "dependencies": { - "@onfido/active-video-capture": "^0.28.2", - "@onfido/opencv": "^2.0.0", - "@sentry/browser": "^7.2.0", - "blueimp-load-image": "~2.29.0", - "classnames": "~2.2.5", - "core-js": "^3.21.1", - "deepmerge": "^4.2.2", - "dompurify": "^2.2.6", - "enumerate-devices": "^1.1.1", - "eventemitter2": "~2.2.2", - "history": "~4.5.1", - "hoist-non-react-statics": "^3.3.2", - "js-cookie": "^3.0.1", - "pdfobject": "^2.2.7", - "preact": "10.11.3", - "redux": "^4.0.5", - "socket.io-client": "^4.2.0", - "supports-webp": "~1.0.3", - "uuid": "^8.3.2", - "visibilityjs": "~1.2.4", - "xstate": "^4.33.6" - }, - "bin": { - "migrate_locales": "scripts/migrate_locales.js" - } - }, - "node_modules/onfido-sdk-ui/node_modules/classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "license": "MIT" + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-14.15.0.tgz", + "integrity": "sha512-4Z+tnH6pQjK4SyazlzJq17NXO8AnhGcwEACbA3PVbAo90LBpGu1WAZ1r6VidlxFr/oPbu6sg/hisYvfXiqOtTg==" }, "node_modules/open": { "version": "8.4.2", @@ -43044,10 +42684,6 @@ "canvas": "^2.11.2" } }, - "node_modules/pdfobject": { - "version": "2.2.8", - "license": "MIT" - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -43434,15 +43070,6 @@ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, - "node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -44608,16 +44235,6 @@ "react-native-reanimated": ">=2.8.0" } }, - "node_modules/react-native-flipper": { - "version": "0.159.0", - "dev": true, - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-native": ">0.62.0" - } - }, "node_modules/react-native-fs": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", @@ -44850,17 +44467,6 @@ "react-native": "*" } }, - "node_modules/react-native-performance-flipper-reporter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-native-performance-flipper-reporter/-/react-native-performance-flipper-reporter-2.0.0.tgz", - "integrity": "sha512-ccOgq99eK3OvrNNhpJDC4ydNk/1JGgWZPo2FLrPDLUHXAR4EcE9cUAtb46oGOpvHk5ZOb5aEDofc/CS9OEGcag==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react-native-flipper": "*", - "react-native-performance": "*" - } - }, "node_modules/react-native-permissions": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.9.3.tgz", @@ -44950,6 +44556,33 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/react-native-release-profiler": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.1.6.tgz", + "integrity": "sha512-kSAPYjO3PDzV4xbjgj2NoiHtL7EaXmBira/WOcyz6S7mz1MVBoF0Bj74z5jAZo6BoBJRKqmQWI4ep+m0xvoF+g==", + "dependencies": { + "@react-native-community/cli": "^12.2.1", + "commander": "^11.1.0" + }, + "bin": { + "react-native-release-profiler": "lib/commonjs/cli.js" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-release-profiler/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-render-html": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/react-native-render-html/-/react-native-render-html-6.3.1.tgz", @@ -45019,6 +44652,14 @@ "react-native": "*" } }, + "node_modules/react-native-share": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-10.0.2.tgz", + "integrity": "sha512-EZs4MtsyauAI1zP8xXT1hIFB/pXOZJNDCKcgCpEfTZFXgCUzz8MDVbI1ocP2hA59XHRSkqAQdbJ0BFTpjxOBlg==", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-sound": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.11.2.tgz", @@ -46927,12 +46568,6 @@ "node": ">=8" } }, - "node_modules/resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==", - "license": "MIT" - }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", @@ -47301,6 +46936,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true, "license": "MIT" }, "node_modules/select": { @@ -48199,32 +47835,6 @@ "node": ">=0.10.0" } }, - "node_modules/socket.io-client": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", - "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -49285,12 +48895,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/supports-webp": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/supports-webp/-/supports-webp-1.0.7.tgz", - "integrity": "sha512-ZlqT+sCgZKcykOLrk8DYR4t3Em+nyVSHpiV3q7uzOutLwKIYU23n88KibCLw3FzM4NCQeRorvZ55AV/77lQyOQ==", - "license": "MIT" - }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -51427,12 +51031,6 @@ "builtins": "^1.0.3" } }, - "node_modules/value-equal": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.2.1.tgz", - "integrity": "sha512-yRL36Xb2K/HmFT5Fe3M86S7mu4+a12/3l7uytUh6eNPPjP77ldPBvsAvmnWff39sXn55naRMZN8LZWRO8PWaeQ==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -51500,12 +51098,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/visibilityjs": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/visibilityjs/-/visibilityjs-1.2.8.tgz", - "integrity": "sha512-Y+aL3OUX88b+/VSmkmC2ApuLbf0grzbNLpCfIDSw3BzTU6PqcPsdgIOaw8b+eZoy+DdQqnVN3y/Evow9vQq9Ig==", - "license": "MIT" - }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", @@ -51567,15 +51159,6 @@ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", "license": "MIT" }, - "node_modules/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==", - "license": "BSD-3-Clause", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -53009,22 +52592,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xstate": { - "version": "4.37.2", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/xstate" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 46ee7afd6548..1c4f23700bc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.48-0", + "version": "1.4.50-5", "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.", @@ -50,6 +50,8 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", + "symbolicate-release:ios": "scripts/release-profile.js --platform=ios", + "symbolicate-release:android": "scripts/release-profile.js --platform=android", "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts", "test:e2e:dev": "ts-node tests/e2e/testRunner.js --config ./config.dev.js", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", @@ -72,6 +74,7 @@ "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "10.6.0", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", @@ -104,7 +107,6 @@ "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", - "expo-image-manipulator": "11.8.0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -113,7 +115,7 @@ "lodash": "4.17.21", "lottie-react-native": "6.4.1", "mapbox-gl": "^2.15.0", - "onfido-sdk-ui": "13.6.1", + "onfido-sdk-ui": "14.15.0", "patch-package": "^8.0.0", "process": "^0.11.10", "prop-types": "^15.7.2", @@ -155,10 +157,12 @@ "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-release-profiler": "^0.1.6", "react-native-reanimated": "^3.7.2", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", "react-native-tab-view": "^3.5.2", @@ -276,7 +280,6 @@ "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", - "react-native-performance-flipper-reporter": "^2.0.0", "react-test-renderer": "18.2.0", "reassure": "^0.10.1", "setimmediate": "^1.0.5", diff --git a/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch b/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch new file mode 100644 index 000000000000..d5a390daf201 --- /dev/null +++ b/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch @@ -0,0 +1,19 @@ +diff --git a/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle b/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle +index 3a1a548..fe030bb 100644 +--- a/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle ++++ b/node_modules/@oguzhnatly/react-native-image-manipulator/android/build.gradle +@@ -13,12 +13,12 @@ buildscript { + apply plugin: 'com.android.library' + + android { +- compileSdkVersion 28 ++ compileSdkVersion 34 + buildToolsVersion "28.0.3" + + defaultConfig { + minSdkVersion 16 +- targetSdkVersion 28 ++ targetSdkVersion 34 + versionCode 1 + versionName "1.0" + } diff --git a/patches/@react-native-community+cli-platform-ios+12.3.0.patch b/patches/@react-native-community+cli-platform-ios+12.3.0.patch index cfae504e44fa..e54ab17c43dd 100644 --- a/patches/@react-native-community+cli-platform-ios+12.3.0.patch +++ b/patches/@react-native-community+cli-platform-ios+12.3.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -index 82f537c..f5e2cda 100644 +index 82f537c..df441e2 100644 --- a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb +++ b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb @@ -12,7 +12,7 @@ @@ -19,7 +19,7 @@ index 82f537c..f5e2cda 100644 if (!config) json = [] -@@ -36,10 +35,30 @@ def use_native_modules!(config = nil) +@@ -36,9 +35,24 @@ def use_native_modules!(config = nil) config = JSON.parse(json.join("\n")) end @@ -42,11 +42,5 @@ index 82f537c..f5e2cda 100644 + end + packages = config["dependencies"] -+ -+ if (ENV["NO_FLIPPER"]) -+ packages = {**packages, "react-native-flipper" => {"platforms" => {"ios" => nil}}} -+ end -+ found_pods = [] - packages.each do |package_name, package| diff --git a/scripts/release-profile.js b/scripts/release-profile.js new file mode 100755 index 000000000000..0f96232bcdca --- /dev/null +++ b/scripts/release-profile.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +const fs = require('fs'); +const {execSync} = require('child_process'); + +// Function to parse command-line arguments into a key-value object +function parseCommandLineArguments() { + const args = process.argv.slice(2); // Skip node and script paths + const argsMap = {}; + args.forEach((arg) => { + const [key, value] = arg.split('='); + if (key.startsWith('--')) { + argsMap[key.substring(2)] = value; + } + }); + return argsMap; +} + +// Function to find .cpuprofile files in the current directory +function findCpuProfileFiles() { + const files = fs.readdirSync(process.cwd()); + // eslint-disable-next-line rulesdir/prefer-underscore-method + return files.filter((file) => file.endsWith('.cpuprofile')); +} + +const argsMap = parseCommandLineArguments(); + +// Determine sourcemapPath based on the platform flag passed +let sourcemapPath; +if (argsMap.platform === 'ios') { + sourcemapPath = 'main.jsbundle.map'; +} else if (argsMap.platform === 'android') { + sourcemapPath = 'android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map'; +} else { + console.error('Please specify the platform using --platform=ios or --platform=android'); + process.exit(1); +} + +// Attempt to find .cpuprofile files +const cpuProfiles = findCpuProfileFiles(); +if (cpuProfiles.length === 0) { + console.error('No .cpuprofile files found in the root directory.'); + process.exit(1); +} else if (cpuProfiles.length > 1) { + console.error('Multiple .cpuprofile files found. Please specify which one to use by placing only one .cpuprofile in the root or specifying the filename as an argument.'); + process.exit(1); +} else { + // Construct the command + const cpuprofileName = cpuProfiles[0]; + const command = `npx react-native-release-profiler --local ${cpuprofileName} --sourcemap-path ${sourcemapPath}`; + + console.log(`Executing: ${command}`); + + // Execute the command + try { + const output = execSync(command, {stdio: 'inherit'}); + console.log(output.toString()); + } catch (error) { + console.error(`Error executing command: ${error}`); + process.exit(1); + } +} diff --git a/src/CONST.ts b/src/CONST.ts index 6861fe174ffe..a163c63404a7 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -98,6 +98,8 @@ const CONST = { AVATAR_MAX_WIDTH_PX: 4096, AVATAR_MAX_HEIGHT_PX: 4096, + LOGO_MAX_SCALE: 1.5, + BREADCRUMB_TYPE: { ROOT: 'root', STRONG: 'strong', @@ -575,6 +577,7 @@ const CONST = { LIMIT: 50, TYPE: { ADDCOMMENT: 'ADDCOMMENT', + ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST', APPROVED: 'APPROVED', CHRONOSOOOLIST: 'CHRONOSOOOLIST', CLOSED: 'CLOSED', @@ -678,6 +681,10 @@ const CONST = { INVITE: 'invited', NOTHING: 'nothing', }, + ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION: { + ACCEPT: 'accept', + DECLINE: 'decline', + }, ARCHIVE_REASON: { DEFAULT: 'default', ACCOUNT_CLOSED: 'accountClosed', @@ -1030,12 +1037,6 @@ const CONST = { VIDEO: 'video', }, - IMAGE_FILE_FORMAT: { - PNG: 'image/png', - WEBP: 'image/webp', - JPEG: 'image/jpeg', - }, - FILE_TYPE_REGEX: { // Image MimeTypes allowed by iOS photos app. IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/, @@ -1170,6 +1171,7 @@ const CONST = { MISSING_FIELD: 'Missing required additional details fields', WRONG_ANSWERS: 'Wrong answers', ONFIDO_FIXABLE_ERROR: 'Onfido returned a fixable error', + ONFIDO_USER_CONSENT_DENIED: 'user_consent_denied', // KBA stands for Knowledge Based Answers (requiring us to show Idology questions) KBA_NEEDED: 'KBA needed', @@ -1412,6 +1414,11 @@ const CONST = { MAKE_MEMBER: 'makeMember', MAKE_ADMIN: 'makeAdmin', }, + DISTANCE_RATES_BULK_ACTION_TYPES: { + DELETE: 'delete', + DISABLE: 'disable', + ENABLE: 'enable', + }, }, CUSTOM_UNITS: { @@ -3157,7 +3164,7 @@ const CONST = { SHARE_CODE: 'shareCode', }, REVENUE: 250, - LEARN_MORE_LINK: 'https://help.expensify.com/articles/new-expensify/get-paid-back/Referral-Program', + LEARN_MORE_LINK: 'https://help.expensify.com/articles/new-expensify/expenses/Referral-Program', LINK: 'https://join.my.expensify.com', }, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 188ab5646d30..b9e7c4d5d274 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; @@ -216,6 +216,9 @@ const ONYXKEYS = { /** Is the test tools modal open? */ IS_TEST_TOOLS_MODAL_OPEN: 'isTestToolsModalOpen', + /** Is app in profiling mode */ + APP_PROFILING_IN_PROGRESS: 'isProfilingInProgress', + /** Stores information about active wallet transfer amount, selectedAccountID, status, etc */ WALLET_TRANSFER: 'walletTransfer', @@ -241,9 +244,6 @@ const ONYXKEYS = { // This can be either "light", "dark" or "system" PREFERRED_THEME: 'preferredTheme', - // Experimental memory only Onyx mode flag - IS_USING_MEMORY_ONLY_KEYS: 'isUsingMemoryOnlyKeys', - // Information about the onyx updates IDs that were received from the server ONYX_UPDATES_FROM_SERVER: 'onyxUpdatesFromServer', @@ -287,6 +287,7 @@ const ONYXKEYS = { POLICY_MEMBERS: 'policyMembers_', POLICY_DRAFTS: 'policyDrafts_', POLICY_MEMBERS_DRAFTS: 'policyMembersDrafts_', + POLICY_JOIN_MEMBER: 'policyJoinMember_', POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', @@ -404,6 +405,8 @@ const ONYXKEYS = { EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft', + POLICY_TAG_NAME_FORM: 'policyTagNameForm', + POLICY_TAG_NAME_FORM_DRAFT: 'policyTagNameFormDraft', }, } as const; @@ -449,6 +452,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; + [ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm; }; type OnyxFormDraftValuesMapping = { @@ -487,6 +491,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; + [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; }; type OnyxValuesMapping = { @@ -554,6 +559,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_LOADING_PAYMENT_METHODS]: boolean; [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; + [ONYXKEYS.APP_PROFILING_IN_PROGRESS]: boolean; [ONYXKEYS.IS_LOADING_APP]: boolean; [ONYXKEYS.IS_SWITCHING_TO_OLD_DOT]: boolean; [ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer; @@ -564,7 +570,6 @@ type OnyxValuesMapping = { [ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS]: Record; [ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID]: string; [ONYXKEYS.PREFERRED_THEME]: ValueOf; - [ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS]: boolean; [ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken; [ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer; [ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number; @@ -589,7 +594,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping; type OnyxValueKey = keyof OnyxValuesMapping; type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey; -type OnyxValue = OnyxEntry; +type OnyxValue = TOnyxKey extends keyof OnyxCollectionValuesMapping ? OnyxCollection : OnyxEntry; type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bc8af4df5686..d9f0c6658a2b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -385,9 +385,16 @@ const ROUTES = { getUrlWithBackToParam(`${action}/${iouType}/scan/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_TAG: { - route: ':action/:iouType/tag/:tagIndex/:transactionID/:reportID', - getRoute: (action: ValueOf, iouType: ValueOf, tagIndex: number, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action}/${iouType}/tag/${tagIndex}/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/tag/:tagIndex/:transactionID/:reportID/:reportActionID?', + getRoute: ( + action: ValueOf, + iouType: ValueOf, + tagIndex: number, + transactionID: string, + reportID: string, + backTo = '', + reportActionID?: string, + ) => getUrlWithBackToParam(`${action}/${iouType}/tag/${tagIndex}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', @@ -478,6 +485,10 @@ const ROUTES = { route: 'workspace/:policyID/avatar', getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const, }, + WORKSPACE_JOIN_USER: { + route: 'workspace/:policyID/join', + getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const, + }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, @@ -546,6 +557,10 @@ const ROUTES = { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, }, + WORKSPACE_MORE_FEATURES: { + route: 'workspace/:policyID/more-features', + getRoute: (policyID: string) => `workspace/${policyID}/more-features` as const, + }, WORKSPACE_CATEGORY_CREATE: { route: 'workspace/:policyID/categories/new', getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const, @@ -554,12 +569,37 @@ const ROUTES = { route: 'workspace/:policyID/tags', getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, }, + WORKSPACE_TAGS_SETTINGS: { + route: 'workspace/:policyID/tags/settings', + getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const, + }, + WORKSPACE_EDIT_TAGS: { + route: 'workspace/:policyID/tags/edit', + getRoute: (policyID: string) => `workspace/${policyID}/tags/edit` as const, + }, + WORKSPACE_MEMBER_DETAILS: { + route: 'workspace/:policyID/members/:accountID', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo), + }, + WORKSPACE_MEMBER_ROLE_SELECTION: { + route: 'workspace/:policyID/members/:accountID/role-selection', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo), + }, + WORKSPACE_DISTANCE_RATES: { + route: 'workspace/:policyID/distance-rates', + getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, + }, + // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', + TRANSACTION_RECEIPT: { + route: 'r/:reportID/transaction/:transactionID/receipt', + getRoute: (reportID: string, transactionID: string) => `r/${reportID}/transaction/${transactionID}/receipt` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 17073d289f8f..a0e06b98da2b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -128,6 +128,7 @@ const SCREENS = { SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', + WORKSPACE_JOIN_USER: 'WorkspaceJoinUser', MONEY_REQUEST: { MANUAL_TAB: 'manual', @@ -215,6 +216,8 @@ const SCREENS = { INVITE_MESSAGE: 'Workspace_Invite_Message', CATEGORIES: 'Workspace_Categories', TAGS: 'Workspace_Tags', + TAGS_SETTINGS: 'Tags_Settings', + TAGS_EDIT: 'Tags_Edit', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', @@ -226,6 +229,10 @@ const SCREENS = { CATEGORY_CREATE: 'Category_Create', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', + MORE_FEATURES: 'Workspace_More_Features', + MEMBER_DETAILS: 'Workspace_Member_Details', + MEMBER_DETAILS_ROLE_SELECTION: 'Workspace_Member_Details_Role_Selection', + DISTANCE_RATES: 'Distance_Rates', }, EDIT_REQUEST: { @@ -273,6 +280,7 @@ const SCREENS = { GET_ASSISTANCE: 'GetAssistance', REFERRAL_DETAILS: 'Referral_Details', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', + TRANSACTION_RECEIPT: 'TransactionReceipt', } as const; type Screen = DeepValueOf; diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 083c8340baa6..9713e40136a2 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getCurrentUserAccountID} from '@libs/actions/Report'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -31,7 +32,8 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]); + const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? 0]; + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(actorPersonalDetails); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { @@ -56,6 +58,7 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} displayName: `${displayName}`, oldDisplayName: `${oldDisplayName}`, policyName: `${policyName}`, + shouldUseYou: actorPersonalDetails?.accountID === getCurrentUserAccountID(), }) : translate(`reportArchiveReasons.${archiveReason}`); diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index eed40d75387e..1ed7b6d188a0 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -288,7 +288,7 @@ function AttachmentModal({ const deleteAndCloseModal = useCallback(() => { IOU.detachReceipt(transaction?.transactionID ?? ''); setIsDeleteReceiptConfirmModalVisible(false); - Navigation.dismissModal(report?.reportID); + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '')); }, [transaction, report]); const isValidFile = useCallback((fileObject: FileObject) => { @@ -446,7 +446,7 @@ function AttachmentModal({ onSelected: () => downloadAttachment(), }); } - if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEditReceipt) { + if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEditReceipt && !TransactionUtils.hasMissingSmartscanFields(transaction)) { menuItems.push({ icon: Expensicons.Trashcan, text: translate('receipt.deleteReceipt'), diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 5be33e6ff2ec..635645b0035b 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -2,8 +2,12 @@ import React, {useCallback} from 'react'; import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; +import Icon from './Icon'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Text from './Text'; @@ -31,11 +35,29 @@ type BadgeProps = { /** Callback to be called on onPress */ onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; + + /** The icon asset to display to the left of the text */ + icon?: IconAsset | null; + + /** Any additional styles to pass to the left icon container. */ + iconStyles?: StyleProp; }; -function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) { +function Badge({ + success = false, + error = false, + pressable = false, + text, + environment = CONST.ENVIRONMENT.DEV, + badgeStyles, + textStyles, + onPress = () => {}, + icon, + iconStyles = [], +}: BadgeProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const theme = useTheme(); const textColorStyles = success || error ? styles.textWhite : undefined; const Wrapper = pressable ? PressableWithoutFeedback : View; @@ -53,6 +75,16 @@ function Badge({success = false, error = false, pressable = false, text, environ aria-label={!pressable ? text : undefined} accessible={false} > + {icon && ( + + + + )} ) { +function BaseMiniContextMenuItem( + {tooltipText, onPress, children, isDelayButtonStateComplete = true, shouldPreventDefaultFocusOnPress = true}: BaseMiniContextMenuItemProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); return ( @@ -64,7 +71,9 @@ function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonS } // Prevent text input blur on left click - event.preventDefault(); + if (shouldPreventDefaultFocusOnPress) { + event.preventDefault(); + } }} accessibilityLabel={tooltipText} role={CONST.ROLE.BUTTON} diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 34bc3f7e30c8..e5eb09691eba 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; +import {PixelRatio, View} from 'react-native'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -36,7 +36,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { const theme = useTheme(); const styles = useThemeStyles(); const [primaryBreadcrumb, secondaryBreadcrumb] = breadcrumbs; - + const fontScale = PixelRatio.getFontScale() > CONST.LOGO_MAX_SCALE ? CONST.LOGO_MAX_SCALE : PixelRatio.getFontScale(); return ( {primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT ? ( @@ -47,8 +47,8 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { contentFit="contain" src={LogoComponent} fill={theme.text} - width={variables.lhnLogoWidth} - height={variables.lhnLogoHeight} + width={variables.lhnLogoWidth * fontScale} + height={variables.lhnLogoHeight * fontScale} /> } shouldShowEnvironmentBadge diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index a25c7ff7129c..ca1bc391e800 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -198,7 +198,7 @@ function Button( accessibilityLabel = '', ...rest }: ButtonProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 61d3409c65ab..5f426f77b731 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -32,6 +32,7 @@ function ButtonWithDropdownMenu({ options, onOptionSelected, enterKeyEventListenerPriority = 0, + wrapperStyle, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -66,7 +67,7 @@ function ButtonWithDropdownMenu({ }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); return ( - + {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( )} @@ -508,49 +554,46 @@ function ReportActionItem(props) { for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} + {actionableItemButtons.length > 0 && } ) : ( )} ); } - const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); + const numberOfThreadReplies = action.childVisibleActionCount ?? 0; - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); - const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); - const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); + const oldestFourAccountIDs = + action.childOldestFourAccountIDs + ?.split(',') + .map((accountID) => Number(accountID)) + .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; + const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( - - !_.isEmpty(item))} /> + {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( + + !isEmptyObject(item))} /> )} - {!ReportActionsUtils.isMessageDeleted(props.action) && ( + {!ReportActionsUtils.isMessageDeleted(action) && ( { if (Session.isAnonymousUser()) { @@ -571,9 +614,9 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( { + const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => { const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors); - if (!_.isUndefined(props.draftMessage)) { + if (draftMessage !== undefined) { return {content}; } - if (!props.displayAsGroup) { + if (!displayAsGroup) { return ( item === moderationDecision) && + !ReportActionsUtils.isPendingRemove(action) } > {content} @@ -621,23 +664,22 @@ function ReportActionItem(props) { return {content}; }; - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { if (ReportActionsUtils.isTransactionThread(parentReportAction)) { const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction); if (ReportActionsUtils.isDeletedParentAction(parentReportAction) || isReversedTransaction) { return ( - + - - + + ${props.translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} + html={`${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} /> @@ -649,25 +691,25 @@ function ReportActionItem(props) { return ( ); } - if (ReportUtils.isTaskReport(props.report)) { - if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { + if (ReportUtils.isTaskReport(report)) { + if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { return ( - + - - + + - ${props.translate('parentReportAction.deletedTask')}`} /> + ${translate('parentReportAction.deletedTask')}`} /> @@ -676,25 +718,25 @@ function ReportActionItem(props) { ); } return ( - + - + ); } - if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { return ( - + ); @@ -702,96 +744,94 @@ function ReportActionItem(props) { return ( ); } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ( ); } // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet - if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(props.report, 'isWaitingOnBankAccount', false) && - originalMessage && - originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && - !isSendingMoney - ) { + if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { return null; } // if action is actionable mention whisper and resolved by user, then we don't want to render anything - if (ReportActionsUtils.isActionableMentionWhisper(props.action) && lodashGet(props.action, 'originalMessage.resolution', null)) { + if (ReportActionsUtils.isActionableMentionWhisper(action) && (action.originalMessage.resolution ?? null)) { return null; } // We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them. // This is a temporary solution needed for comment-linking. // The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt. - if (ReportActionsUtils.isWhisperActionTargetedToOthers(props.action)) { + if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) { return null; } - const hasErrors = !_.isEmpty(props.action.errors); - const whisperedToAccountIDs = props.action.whisperedToAccountIDs || []; + const hasErrors = !isEmptyObject(action.errors); + const whisperedToAccountIDs = action.whisperedToAccountIDs ?? []; const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : []; + const whisperedToPersonalDetails = isWhisper + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPress={onPress} + style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]} + onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} + preventDefaultContextMenu={draftMessage === undefined && !hasErrors} withoutFocusOnSecondaryInteraction - accessibilityLabel={props.translate('accessibilityHints.chatMessage')} + accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessible > {(hovered) => ( - {props.shouldDisplayNewMarker && } + {shouldDisplayNewMarker && } - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} + onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') + draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} - errors={ErrorUtils.getLatestErrorMessageField(props.action)} + shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} + errors={ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} shouldDisableStrikeThrough > {isWhisper && ( @@ -804,11 +844,11 @@ function ReportActionItem(props) { /> - {props.translate('reportActionContextMenu.onlyVisible')} + {translate('reportActionContextMenu.onlyVisible')}   )} - {renderReportActionItem(hovered || isReportActionLinked, isWhisper, hasErrors)} + {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} )} - + ); } -ReportActionItem.propTypes = propTypes; -ReportActionItem.defaultProps = defaultProps; - -export default compose( - withWindowDimensions, - withLocalize, - withNetwork(), - withBlockedFromConcierge({propName: 'blockedFromConcierge'}), - withReportActionsDrafts({ - propName: 'draftMessage', - transformValue: (drafts, props) => { - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); - }, - }), - withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, - }, - iouReport: { - key: ({action}) => { - const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); - return iouReportID ? `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}` : undefined; - }, - initialValue: {}, - }, - policyReportFields: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), - initialValue: [], +export default withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, + }, + iouReport: { + key: ({action}) => { + const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); + return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID ?? 0}`; }, - policy: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined), - initialValue: {}, - }, - emojiReactions: { - key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, - initialValue: {}, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`, - canEvict: false, - }, - }), -)( + initialValue: {} as OnyxTypes.Report, + }, + policyReportFields: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID ?? 0}`, + initialValue: {}, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID ?? 0}`, + initialValue: {} as OnyxTypes.Policy, + }, + emojiReactions: { + key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, + initialValue: {}, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, +})( memo(ReportActionItem, (prevProps, nextProps) => { - const prevParentReportAction = prevProps.parentReportActions[prevProps.report.parentReportActionID]; - const nextParentReportAction = nextProps.parentReportActions[nextProps.report.parentReportActionID]; + const prevParentReportAction = prevProps.parentReportAction; + const nextParentReportAction = nextProps.parentReportAction; return ( prevProps.displayAsGroup === nextProps.displayAsGroup && - prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && - _.isEqual(prevProps.action, nextProps.action) && - _.isEqual(prevProps.iouReport, nextProps.iouReport) && - _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && - _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && - _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && - lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && - lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && - lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') && - lodashGet(prevProps.report, 'parentReportActionID') === lodashGet(nextProps.report, 'parentReportActionID') && - prevProps.translate === nextProps.translate && + lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && + lodashIsEqual(prevProps.action, nextProps.action) && + lodashIsEqual(prevProps.iouReport, nextProps.iouReport) && + lodashIsEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + lodashIsEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && + lodashIsEqual(prevProps.report.errorFields, nextProps.report.errorFields) && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum && + prevProps.report?.parentReportID === nextProps.report?.parentReportID && + prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID && // TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) && prevProps.action.actionName === nextProps.action.actionName && @@ -906,13 +923,13 @@ export default compose( ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) && prevProps.report.managerID === nextProps.report.managerID && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && - lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && - lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && + prevProps.report?.total === nextProps.report?.total && + prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && - _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && - _.isEqual(prevProps.policy, nextProps.policy) && - _.isEqual(prevParentReportAction, nextParentReportAction) + lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) && + lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) && + lodashIsEqual(prevProps.policy, nextProps.policy) && + lodashIsEqual(prevParentReportAction, nextParentReportAction) ); }), ); diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx index 35141a42b726..a28f2af24448 100644 --- a/src/pages/home/report/ReportActionItemBasicMessage.tsx +++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx @@ -5,7 +5,7 @@ import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type ReportActionItemBasicMessageProps = ChildrenProps & { +type ReportActionItemBasicMessageProps = Partial & { message: string; }; diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 95578c10e816..4fe52f6adf41 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -35,7 +35,7 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { /** The id of the policy */ // eslint-disable-next-line react/no-unused-prop-types - policyID: string; + policyID: string | undefined; }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index e16d94eb7db7..04391bb19cd5 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -70,6 +70,7 @@ const MUTED_ACTIONS = [ CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.MOVED, + CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST, ] as ActionName[]; function ReportActionItemFragment({ diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 2c9a4cbd21e8..fbf2da69aa31 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -5,6 +5,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -58,7 +59,7 @@ type ReportActionItemMessageEditProps = { shouldDisableEmojiPicker?: boolean; /** Stores user's preferred skin tone */ - preferredSkinTone?: number; + preferredSkinTone?: OnyxEntry; }; // native ids @@ -69,7 +70,7 @@ const isMobileSafari = Browser.isMobileSafari(); function ReportActionItemMessageEdit( {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, - forwardedRef: ForwardedRef, + forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index af1c4e85104e..7185ab728ccd 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -1,6 +1,5 @@ import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -16,12 +15,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import ReportActionItem from './ReportActionItem'; -type ReportActionItemParentActionOnyxProps = { - /** The current report is displayed */ - report: OnyxEntry; -}; - -type ReportActionItemParentActionProps = ReportActionItemParentActionOnyxProps & { +type ReportActionItemParentActionProps = { /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine?: boolean; @@ -31,9 +25,15 @@ type ReportActionItemParentActionProps = ReportActionItemParentActionOnyxProps & /** The id of the report */ // eslint-disable-next-line react/no-unused-prop-types reportID: string; + + /** The current report is displayed */ + report: OnyxEntry; + + /** Report actions belonging to the report's parent */ + parentReportAction: OnyxEntry; }; -function ReportActionItemParentAction({report, index = 0, shouldHideThreadDividerLine = false}: ReportActionItemParentActionProps) { +function ReportActionItemParentAction({report, parentReportAction, index = 0, shouldHideThreadDividerLine = false}: ReportActionItemParentActionProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -83,8 +83,8 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))} + parentReportAction={parentReportAction} report={ancestor.report} action={ancestor.reportAction} displayAsGroup={false} @@ -101,8 +101,4 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide ReportActionItemParentAction.displayName = 'ReportActionItemParentAction'; -export default withOnyx({ - report: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - }, -})(ReportActionItemParentAction); +export default ReportActionItemParentAction; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 741422cc7e82..696cd7a7d850 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -29,7 +30,7 @@ import ReportActionItemFragment from './ReportActionItemFragment'; type ReportActionItemSingleProps = Partial & { /** All the data of the action */ - action: ReportAction; + action: OnyxEntry; /** Styles for the outermost View */ wrapperStyle?: StyleProp; @@ -38,7 +39,7 @@ type ReportActionItemSingleProps = Partial & { report: Report; /** IOU Report for this action, if any */ - iouReport?: Report; + iouReport?: OnyxEntry; /** Show header for action */ showHeader?: boolean; @@ -77,12 +78,12 @@ function ReportActionItemSingle({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; + const actorAccountID = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action?.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]); const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); @@ -90,7 +91,7 @@ function ReportActionItemSingle({ displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); - } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) { + } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. const delegateDetails = personalDetails[action.delegateAccountID]; @@ -141,7 +142,7 @@ function ReportActionItemSingle({ text: displayName, }, ] - : action.person; + : action?.person; const reportID = report?.reportID; const iouReportID = iouReport?.reportID; @@ -155,14 +156,14 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); + showUserDetails(action?.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); } - }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); + }, [isWorkspaceActor, reportID, actorAccountID, action?.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( () => CONST.RESTRICTED_ACCOUNT_IDS.includes(actorAccountID ?? 0) || - (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), + (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action?.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), [action, isWorkspaceActor, actorAccountID], ); @@ -189,7 +190,7 @@ function ReportActionItemSingle({ return ( @@ -237,13 +238,13 @@ function ReportActionItemSingle({ {personArray?.map((fragment, index) => ( ))} @@ -255,7 +256,7 @@ function ReportActionItemSingle({ >{`${status?.emojiCode}`} )} - + ) : null} {children} diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index f7c7e5fcf91d..c0dbe2a3825d 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; @@ -26,7 +27,7 @@ type ReportActionItemThreadProps = { isHovered: boolean; /** The function that should be called when the thread is LongPressed or right-clicked */ - onSecondaryInteraction: () => void; + onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void; }; function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) { diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.tsx similarity index 83% rename from src/pages/home/report/ReportActionsList.js rename to src/pages/home/report/ReportActionsList.tsx index dabf7a9f8d36..eba17a234112 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.tsx @@ -1,88 +1,74 @@ +import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; import {useIsFocused, useRoute} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {RouteProp} from '@react-navigation/native'; +import type {DebouncedFunc} from 'lodash'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; +import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; -import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; import {usePersonalDetails} from '@components/OnyxProvider'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import FloatingMessageCounter from './FloatingMessageCounter'; -import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; -import reportActionPropTypes from './reportActionPropTypes'; +import ListBoundaryLoader from './ListBoundaryLoader'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; -const propTypes = { +type LoadNewerChats = DebouncedFunc<(params: {distanceFromStart: number}) => void>; + +type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { /** The report currently being looked at */ - report: reportPropTypes.isRequired, + report: OnyxTypes.Report; /** The report's parentReportAction */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + parentReportAction: OnyxEntry; /** Sorted actions prepared for display */ - sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)).isRequired, + sortedReportActions: OnyxTypes.ReportAction[]; /** The ID of the most recent IOU report action connected with the shown report */ - mostRecentIOUReportActionID: PropTypes.string, + mostRecentIOUReportActionID?: string | null; /** The report metadata loading states */ - isLoadingInitialReportActions: PropTypes.bool, + isLoadingInitialReportActions?: boolean; /** Are we loading more report actions? */ - isLoadingOlderReportActions: PropTypes.bool, + isLoadingOlderReportActions?: boolean; /** Are we loading newer report actions? */ - isLoadingNewerReportActions: PropTypes.bool, + isLoadingNewerReportActions?: boolean; /** Callback executed on list layout */ - onLayout: PropTypes.func.isRequired, + onLayout: (event: LayoutChangeEvent) => void; /** Callback executed on scroll */ - onScroll: PropTypes.func, + onScroll?: (event: NativeSyntheticEvent) => void; /** Function to load more chats */ - loadOlderChats: PropTypes.func.isRequired, + loadOlderChats: () => void; /** Function to load newer chats */ - loadNewerChats: PropTypes.func.isRequired, - - /** The policy object for the current route */ - policy: PropTypes.shape({ - /** The name of the policy */ - name: PropTypes.string, + loadNewerChats: LoadNewerChats; - /** The URL for the policy avatar */ - avatar: PropTypes.string, - }), - - ...windowDimensionsPropTypes, - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - onScroll: () => {}, - mostRecentIOUReportActionID: '', - isLoadingInitialReportActions: false, - isLoadingOlderReportActions: false, - isLoadingNewerReportActions: false, - ...withCurrentUserPersonalDetailsDefaultProps, - policy: {}, - parentReportAction: {}, + /** Whether the composer is in full size */ + isComposerFullSize?: boolean; }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -92,64 +78,60 @@ const MSG_VISIBLE_THRESHOLD = 250; // As there is the possibility that there are multiple instances of a ReportScreen // for the same report, we only ever want one subscription to be active, as // the subscriptions could otherwise be conflicting. -const newActionUnsubscribeMap = {}; +const newActionUnsubscribeMap: Record void> = {}; // Caching the reportID and reportActionID for unread markers ensures persistent tracking // across multiple reports, preserving the green line placement and allowing retrieval // of the relevant reportActionID for displaying the green line. // We need to persist it across reports because there are at least 3 ReportScreen components created so the // internal states are resetted or recreated. -const cacheUnreadMarkers = new Map(); +const cacheUnreadMarkers = new Map(); // Seems that there is an architecture issue that prevents us from using the reportID with useRef // the useRef value gets reset when the reportID changes, so we use a global variable to keep track -let prevReportID = null; +let prevReportID: string | null = null; /** * Create a unique key for each action in the FlatList. * We use the reportActionID that is a string representation of a random 64-bit int, which should be * random enough to avoid collisions - * @param {Object} item - * @param {Object} item.action - * @return {String} */ -function keyExtractor(item) { +function keyExtractor(item: OnyxTypes.ReportAction): string { return item.reportActionID; } -function isMessageUnread(message, lastReadTime) { +function isMessageUnread(message: OnyxTypes.ReportAction, lastReadTime?: string): boolean { if (!lastReadTime) { - return Boolean(!ReportActionsUtils.isCreatedAction(message)); + return !ReportActionsUtils.isCreatedAction(message); } - return Boolean(message && lastReadTime && message.created && lastReadTime < message.created); + return !!(message && lastReadTime && message.created && lastReadTime < message.created); } function ReportActionsList({ report, parentReportAction, - isLoadingInitialReportActions, - isLoadingOlderReportActions, - isLoadingNewerReportActions, + isLoadingInitialReportActions = false, + isLoadingOlderReportActions = false, + isLoadingNewerReportActions = false, sortedReportActions, - windowHeight, onScroll, - mostRecentIOUReportActionID, - isSmallScreenWidth, + mostRecentIOUReportActionID = '', currentUserPersonalDetails, loadNewerChats, loadOlderChats, onLayout, isComposerFullSize, -}) { +}: ReportActionsListProps) { const personalDetailsList = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); const {translate} = useLocalize(); + const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const {isOffline} = useNetwork(); - const route = useRoute(); + const route = useRoute>(); const opacity = useSharedValue(0); - const userActiveSince = useRef(null); - const lastMessageTime = useRef(null); + const userActiveSince = useRef(null); + const lastMessageTime = useRef(null); const [isVisible, setIsVisible] = useState(false); const isFocused = useIsFocused(); @@ -178,15 +160,15 @@ function ReportActionsList({ const lastReadTimeRef = useRef(report.lastReadTime); const sortedVisibleReportActions = useMemo( - () => _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors), + () => sortedReportActions.filter((reportAction) => isOffline || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors), [sortedReportActions, isOffline], ); - const lastActionIndex = lodashGet(sortedVisibleReportActions, [0, 'reportActionID']); + const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); const previousLastIndex = useRef(lastActionIndex); - const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); + const linkedReportActionID = route.params?.reportActionID ?? ''; // This state is used to force a re-render when the user manually marks a message as unread // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before @@ -233,7 +215,7 @@ function ReportActionsList({ } } - if (currentUnreadMarker || lastVisibleActionCreatedRef.current === report.lastVisibleActionCreated) { + if (!!currentUnreadMarker || lastVisibleActionCreatedRef.current === report.lastVisibleActionCreated) { return; } @@ -247,7 +229,7 @@ function ReportActionsList({ if (!userActiveSince.current || report.reportID !== prevReportID) { return; } - if (!messageManuallyMarkedUnread && (lastReadTimeRef.current || '') < report.lastReadTime) { + if (!messageManuallyMarkedUnread && (lastReadTimeRef.current ?? '') < (report.lastReadTime ?? '')) { cacheUnreadMarkers.delete(report.reportID); } lastReadTimeRef.current = report.lastReadTime; @@ -257,7 +239,7 @@ function ReportActionsList({ }, [report.lastReadTime, report.reportID]); useEffect(() => { - const resetUnreadMarker = (newLastReadTime) => { + const resetUnreadMarker = (newLastReadTime: string) => { cacheUnreadMarkers.delete(report.reportID); lastReadTimeRef.current = newLastReadTime; setCurrentUnreadMarker(null); @@ -341,10 +323,10 @@ function ReportActionsList({ } }; - const trackVerticalScrolling = (event) => { + const trackVerticalScrolling = (event: NativeSyntheticEvent) => { scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; handleUnreadFloatingButton(); - onScroll(event); + onScroll?.(event); }; const scrollToBottomAndMarkReportAsRead = () => { @@ -356,9 +338,8 @@ function ReportActionsList({ /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on * the height of the smallest report action possible. - * @return {Number} */ - const initialNumToRender = useMemo(() => { + const initialNumToRender = useMemo((): number | undefined => { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); const itemsToRender = Math.ceil(availableHeight / minimumReportActionHeight); @@ -370,24 +351,22 @@ function ReportActionsList({ * This is so that it will not be conflicting with header's separator line. */ const shouldHideThreadDividerLine = useMemo( - () => ReportActionsUtils.getFirstVisibleReportActionID(sortedReportActions, isOffline) === currentUnreadMarker, + (): boolean => ReportActionsUtils.getFirstVisibleReportActionID(sortedReportActions, isOffline) === currentUnreadMarker, [sortedReportActions, isOffline, currentUnreadMarker], ); /** * Evaluate new unread marker visibility for each of the report actions. - * @returns boolean */ - const shouldDisplayNewMarker = useCallback( - (reportAction, index) => { + (reportAction: OnyxTypes.ReportAction, index: number): boolean => { let shouldDisplay = false; if (!currentUnreadMarker) { const nextMessage = sortedVisibleReportActions[index + 1]; const isCurrentMessageUnread = isMessageUnread(reportAction, lastReadTimeRef.current); shouldDisplay = isCurrentMessageUnread && (!nextMessage || !isMessageUnread(nextMessage, lastReadTimeRef.current)) && !ReportActionsUtils.shouldHideNewMarker(reportAction); if (shouldDisplay && !messageManuallyMarkedUnread) { - const isWithinVisibleThreshold = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; + const isWithinVisibleThreshold = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < (userActiveSince.current ?? '') : true; // Prevent displaying a new marker line when report action is of type "REPORTPREVIEW" and last actor is the current user shouldDisplay = (ReportActionsUtils.isReportPreviewAction(reportAction) ? !reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID() && @@ -410,7 +389,7 @@ function ReportActionsList({ // This is to avoid a warning of: // Cannot update a component (ReportActionsList) while rendering a different component (CellRenderer). let markerFound = false; - _.each(sortedVisibleReportActions, (reportAction, index) => { + sortedVisibleReportActions.forEach((reportAction, index) => { if (!shouldDisplayNewMarker(reportAction, index)) { return; } @@ -436,22 +415,22 @@ function ReportActionsList({ if (!isVisible || !isFocused) { if (!lastMessageTime.current) { - lastMessageTime.current = lodashGet(sortedVisibleReportActions, '[0].created', ''); + lastMessageTime.current = sortedVisibleReportActions[0]?.created ?? ''; } return; } // In case the user read new messages (after being inactive) with other device we should // show marker based on report.lastReadTime - const newMessageTimeReference = lastMessageTime.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime; + const newMessageTimeReference = lastMessageTime.current && report.lastReadTime && lastMessageTime.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime; lastMessageTime.current = null; if ( scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !( sortedVisibleReportActions && - _.some( - sortedVisibleReportActions, + sortedVisibleReportActions.some( (reportAction) => + newMessageTimeReference && newMessageTimeReference < reportAction.created && (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), ) @@ -475,7 +454,7 @@ function ReportActionsList({ }, [isFocused, isVisible]); const renderItem = useCallback( - ({item: reportAction, index}) => ( + ({item: reportAction, index}: ListRenderItemInfo) => ( = useMemo( () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions, styles.chatContentScrollView, styles.chatContentScrollViewWithHeaderLoader], ); - const lastReportAction = useMemo(() => _.last(sortedReportActions) || {}, [sortedReportActions]); + const lastReportAction: OnyxTypes.ReportAction | EmptyObject = useMemo(() => sortedReportActions.at(-1) ?? {}, [sortedReportActions]); const listFooterComponent = useCallback(() => { // Skip this hook on the first render (when online), as we are not sure if more actions are going to be loaded, @@ -524,7 +503,7 @@ function ReportActionsList({ }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction.actionName, isOffline]); const onLayoutInner = useCallback( - (event) => { + (event: LayoutChangeEvent) => { onLayout(event); }, [onLayout], @@ -578,8 +557,8 @@ function ReportActionsList({ ); } -ReportActionsList.propTypes = propTypes; -ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions, withCurrentUserPersonalDetails)(memo(ReportActionsList)); +export default withCurrentUserPersonalDetails(memo(ReportActionsList)); + +export type {LoadNewerChats}; diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.tsx similarity index 55% rename from src/pages/home/report/ReportActionsListItemRenderer.js rename to src/pages/home/report/ReportActionsListItemRenderer.tsx index bc8e6a94359f..fb51753e3eb7 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -1,47 +1,39 @@ -import PropTypes from 'prop-types'; import React, {memo, useMemo} from 'react'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; +import type {Report, ReportAction} from '@src/types/onyx'; import ReportActionItem from './ReportActionItem'; import ReportActionItemParentAction from './ReportActionItemParentAction'; -import reportActionPropTypes from './reportActionPropTypes'; -const propTypes = { +type ReportActionsListItemRendererProps = { /** All the data of the action item */ - reportAction: PropTypes.shape(reportActionPropTypes).isRequired, + reportAction: ReportAction; /** The report's parentReportAction */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + parentReportAction: OnyxEntry; /** Position index of the report action in the overall report FlatList view */ - index: PropTypes.number.isRequired, + index: number; /** Report for this action */ - report: reportPropTypes.isRequired, + report: Report; /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool.isRequired, + displayAsGroup: boolean; /** The ID of the most recent IOU report action connected with the shown report */ - mostRecentIOUReportActionID: PropTypes.string, + mostRecentIOUReportActionID?: string | null; /** If the thread divider line should be hidden */ - shouldHideThreadDividerLine: PropTypes.bool.isRequired, + shouldHideThreadDividerLine: boolean; /** Should we display the new marker on top of the comment? */ - shouldDisplayNewMarker: PropTypes.bool.isRequired, + shouldDisplayNewMarker: boolean; /** Linked report action ID */ - linkedReportActionID: PropTypes.string, -}; - -const defaultProps = { - mostRecentIOUReportActionID: '', - linkedReportActionID: '', - parentReportAction: {}, + linkedReportActionID?: string; }; function ReportActionsListItemRenderer({ @@ -50,11 +42,11 @@ function ReportActionsListItemRenderer({ index, report, displayAsGroup, - mostRecentIOUReportActionID, + mostRecentIOUReportActionID = '', shouldHideThreadDividerLine, shouldDisplayNewMarker, - linkedReportActionID, -}) { + linkedReportActionID = '', +}: ReportActionsListItemRendererProps) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && !ReportActionsUtils.isTransactionThread(parentReportAction); @@ -62,36 +54,37 @@ function ReportActionsListItemRenderer({ * Create a lightweight ReportAction so as to keep the re-rendering as light as possible by * passing in only the required props. */ - const action = useMemo( - () => ({ - reportActionID: reportAction.reportActionID, - message: reportAction.message, - pendingAction: reportAction.pendingAction, - actionName: reportAction.actionName, - errors: reportAction.errors, - originalMessage: reportAction.originalMessage, - childCommenterCount: reportAction.childCommenterCount, - linkMetadata: reportAction.linkMetadata, - childReportID: reportAction.childReportID, - childLastVisibleActionCreated: reportAction.childLastVisibleActionCreated, - whisperedToAccountIDs: reportAction.whisperedToAccountIDs, - error: reportAction.error, - created: reportAction.created, - actorAccountID: reportAction.actorAccountID, - childVisibleActionCount: reportAction.childVisibleActionCount, - childOldestFourAccountIDs: reportAction.childOldestFourAccountIDs, - childType: reportAction.childType, - person: reportAction.person, - isOptimisticAction: reportAction.isOptimisticAction, - delegateAccountID: reportAction.delegateAccountID, - previousMessage: reportAction.previousMessage, - attachmentInfo: reportAction.attachmentInfo, - childStateNum: reportAction.childStateNum, - childStatusNum: reportAction.childStatusNum, - childReportName: reportAction.childReportName, - childManagerAccountID: reportAction.childManagerAccountID, - childMoneyRequestCount: reportAction.childMoneyRequestCount, - }), + const action: ReportAction = useMemo( + () => + ({ + reportActionID: reportAction.reportActionID, + message: reportAction.message, + pendingAction: reportAction.pendingAction, + actionName: reportAction.actionName, + errors: reportAction.errors, + originalMessage: reportAction.originalMessage, + childCommenterCount: reportAction.childCommenterCount, + linkMetadata: reportAction.linkMetadata, + childReportID: reportAction.childReportID, + childLastVisibleActionCreated: reportAction.childLastVisibleActionCreated, + whisperedToAccountIDs: reportAction.whisperedToAccountIDs, + error: reportAction.error, + created: reportAction.created, + actorAccountID: reportAction.actorAccountID, + childVisibleActionCount: reportAction.childVisibleActionCount, + childOldestFourAccountIDs: reportAction.childOldestFourAccountIDs, + childType: reportAction.childType, + person: reportAction.person, + isOptimisticAction: reportAction.isOptimisticAction, + delegateAccountID: reportAction.delegateAccountID, + previousMessage: reportAction.previousMessage, + attachmentInfo: reportAction.attachmentInfo, + childStateNum: reportAction.childStateNum, + childStatusNum: reportAction.childStatusNum, + childReportName: reportAction.childReportName, + childManagerAccountID: reportAction.childManagerAccountID, + childMoneyRequestCount: reportAction.childMoneyRequestCount, + } as ReportAction), [ reportAction.actionName, reportAction.childCommenterCount, @@ -126,12 +119,15 @@ function ReportActionsListItemRenderer({ return shouldDisplayParentAction ? ( ) : ( type === reportAction.actionName, ) } isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID} @@ -150,8 +145,6 @@ function ReportActionsListItemRenderer({ ); } -ReportActionsListItemRenderer.propTypes = propTypes; -ReportActionsListItemRenderer.defaultProps = defaultProps; ReportActionsListItemRenderer.displayName = 'ReportActionsListItemRenderer'; export default memo(ReportActionsListItemRenderer); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.tsx similarity index 59% rename from src/pages/home/report/ReportActionsView.js rename to src/pages/home/report/ReportActionsView.tsx index ca3ee7d2ab6a..ab3bda2fa8ca 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,115 +1,88 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; +import lodashThrottle from 'lodash/throttle'; import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PopoverReactionList from './ReactionList/PopoverReactionList'; -import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsList from './ReportActionsList'; +import type {LoadNewerChats} from './ReportActionsList'; -const propTypes = { +type ReportActionsViewOnyxProps = { + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type ReportActionsViewProps = ReportActionsViewOnyxProps & { /** The report currently being looked at */ - report: reportPropTypes.isRequired, + report: OnyxTypes.Report; /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + reportActions?: OnyxTypes.ReportAction[]; /** The report's parentReportAction */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + parentReportAction: OnyxEntry; /** The report metadata loading states */ - isLoadingInitialReportActions: PropTypes.bool, + isLoadingInitialReportActions?: boolean; /** The report actions are loading more data */ - isLoadingOlderReportActions: PropTypes.bool, + isLoadingOlderReportActions?: boolean; /** The report actions are loading newer data */ - isLoadingNewerReportActions: PropTypes.bool, - - /** Whether the composer is full size */ - /* eslint-disable-next-line react/no-unused-prop-types */ - isComposerFullSize: PropTypes.bool.isRequired, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** The policy object for the current route */ - policy: PropTypes.shape({ - /** The name of the policy */ - name: PropTypes.string, - - /** The URL for the policy avatar */ - avatar: PropTypes.string, - }), - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), - - ...windowDimensionsPropTypes, - ...withLocalizePropTypes, -}; - -const defaultProps = { - reportActions: [], - policy: null, - isLoadingInitialReportActions: false, - isLoadingOlderReportActions: false, - isLoadingNewerReportActions: false, - session: { - authTokenType: '', - }, - parentReportAction: {}, + isLoadingNewerReportActions?: boolean; }; -function ReportActionsView(props) { +function ReportActionsView({ + report, + session, + parentReportAction, + reportActions = [], + isLoadingInitialReportActions = false, + isLoadingOlderReportActions = false, + isLoadingNewerReportActions = false, +}: ReportActionsViewProps) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]); - const prevNetworkRef = useRef(props.network); - const prevAuthTokenType = usePrevious(props.session.authTokenType); + const hasCachedActions = useInitialValue(() => reportActions.length > 0); + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); + const network = useNetwork(); + const {isSmallScreenWidth} = useWindowDimensions(); + const prevNetworkRef = useRef(network); + const prevAuthTokenType = usePrevious(session?.authTokenType); - const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); + const prevIsSmallScreenWidthRef = useRef(isSmallScreenWidth); const isFocused = useIsFocused(); - const reportID = props.report.reportID; - const hasNewestReportAction = lodashGet(props.reportActions[0], 'isNewestReportAction'); + const reportID = report.reportID; + const hasNewestReportAction = reportActions[0]?.isNewestReportAction; - /** - * @returns {Boolean} - */ - const isReportFullyVisible = useMemo(() => getIsReportFullyVisible(isFocused), [isFocused]); + const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); const openReportIfNecessary = () => { - const createChatError = _.get(props.report, ['errorFields', 'createChat']); + const createChatError = report.errorFields?.createChat; // If the report is optimistic (AKA not yet created) we don't need to call openReport again - if (props.report.isOptimisticReport || !_.isEmpty(createChatError)) { + if (!!report.isOptimisticReport || !isEmptyObject(createChatError)) { return; } @@ -126,7 +99,7 @@ function ReportActionsView(props) { // When returning from offline to online state we want to trigger a request to OpenReport which // will fetch the reportActions data and mark the report as read. If the report is not fully visible // then we call ReconnectToReport which only loads the reportActions data without marking the report as read. - const wasNetworkChangeDetected = lodashGet(prevNetwork, 'isOffline') && !lodashGet(props.network, 'isOffline'); + const wasNetworkChangeDetected = prevNetwork.isOffline && !network.isOffline; if (wasNetworkChangeDetected) { if (isReportFullyVisible) { openReportIfNecessary(); @@ -135,13 +108,13 @@ function ReportActionsView(props) { } } // update ref with current network state - prevNetworkRef.current = props.network; + prevNetworkRef.current = network; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.network, isReportFullyVisible]); + }, [network, isReportFullyVisible]); useEffect(() => { - const wasLoginChangedDetected = prevAuthTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && !props.session.authTokenType; - if (wasLoginChangedDetected && didUserLogInDuringSession() && isUserCreatedPolicyRoom(props.report)) { + const wasLoginChangedDetected = prevAuthTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && !session?.authTokenType; + if (wasLoginChangedDetected && didUserLogInDuringSession() && isUserCreatedPolicyRoom(report)) { if (isReportFullyVisible) { openReportIfNecessary(); } else { @@ -149,35 +122,35 @@ function ReportActionsView(props) { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.session, props.report, isReportFullyVisible]); + }, [session, report, isReportFullyVisible]); useEffect(() => { const prevIsSmallScreenWidth = prevIsSmallScreenWidthRef.current; // If the view is expanded from mobile to desktop layout // we update the new marker position, mark the report as read, and fetch new report actions - const didScreenSizeIncrease = prevIsSmallScreenWidth && !props.isSmallScreenWidth; + const didScreenSizeIncrease = prevIsSmallScreenWidth && !isSmallScreenWidth; const didReportBecomeVisible = isReportFullyVisible && didScreenSizeIncrease; if (didReportBecomeVisible) { openReportIfNecessary(); } // update ref with current state - prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth; + prevIsSmallScreenWidthRef.current = isSmallScreenWidth; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isSmallScreenWidth, props.reportActions, isReportFullyVisible]); + }, [isSmallScreenWidth, reportActions, isReportFullyVisible]); useEffect(() => { // Ensures subscription event succeeds when the report/workspace room is created optimistically. // Check if the optimistic `OpenReport` or `AddWorkspaceRoom` has succeeded by confirming // any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null. // Existing reports created will have empty fields for `pendingFields`. - const didCreateReportSuccessfully = !props.report.pendingFields || (!props.report.pendingFields.addWorkspaceRoom && !props.report.pendingFields.createChat); + const didCreateReportSuccessfully = !report.pendingFields || (!report.pendingFields.addWorkspaceRoom && !report.pendingFields.createChat); if (!didSubscribeToReportTypingEvents.current && didCreateReportSuccessfully) { Report.subscribeToReportTypingEvents(reportID); didSubscribeToReportTypingEvents.current = true; } - }, [props.report.pendingFields, didSubscribeToReportTypingEvents, reportID]); + }, [report.pendingFields, didSubscribeToReportTypingEvents, reportID]); - const oldestReportAction = useMemo(() => _.last(props.reportActions), [props.reportActions]); + const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -185,7 +158,7 @@ function ReportActionsView(props) { */ const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. - if (props.network.isOffline || props.isLoadingOlderReportActions) { + if (!!network.isOffline || isLoadingOlderReportActions) { return; } @@ -195,16 +168,16 @@ function ReportActionsView(props) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID); - }, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]); + }, [isLoadingOlderReportActions, network.isOffline, oldestReportAction, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadNewerChats = useMemo( + const loadNewerChats: LoadNewerChats = useMemo( () => - _.throttle(({distanceFromStart}) => { - if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions || hasNewestReportAction) { + lodashThrottle(({distanceFromStart}) => { + if (isLoadingNewerReportActions || isLoadingInitialReportActions || hasNewestReportAction) { return; } @@ -225,7 +198,7 @@ function ReportActionsView(props) { Report.getNewerActions(reportID); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, hasNewestReportAction], + [isLoadingNewerReportActions, isLoadingInitialReportActions, reportID, hasNewestReportAction], ); /** @@ -249,48 +222,42 @@ function ReportActionsView(props) { }, [hasCachedActions]); // Comments have not loaded at all yet do nothing - if (!_.size(props.reportActions)) { + if (!reportActions.length) { return null; } return ( <> ); } -ReportActionsView.propTypes = propTypes; -ReportActionsView.defaultProps = defaultProps; ReportActionsView.displayName = 'ReportActionsView'; +ReportActionsView.initMeasured = false; -function arePropsEqual(oldProps, newProps) { - if (!_.isEqual(oldProps.reportActions, newProps.reportActions)) { +function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean { + if (!lodashIsEqual(oldProps.reportActions, newProps.reportActions)) { return false; } - if (!_.isEqual(oldProps.parentReportAction, newProps.parentReportAction)) { + if (!lodashIsEqual(oldProps.parentReportAction, newProps.parentReportAction)) { return false; } - if (lodashGet(oldProps.network, 'isOffline') !== lodashGet(newProps.network, 'isOffline')) { - return false; - } - - if (lodashGet(oldProps.session, 'authTokenType') !== lodashGet(newProps.session, 'authTokenType')) { + if (oldProps.session?.authTokenType !== newProps.session?.authTokenType) { return false; } @@ -306,35 +273,15 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (newProps.isSmallScreenWidth !== oldProps.isSmallScreenWidth) { - return false; - } - - if (newProps.isComposerFullSize !== oldProps.isComposerFullSize) { - return false; - } - - if (lodashGet(newProps, 'policy.avatar') !== lodashGet(oldProps, 'policy.avatar')) { - return false; - } - - if (lodashGet(newProps, 'policy.name') !== lodashGet(oldProps, 'policy.name')) { - return false; - } - - return _.isEqual(oldProps.report, newProps.report); + return lodashIsEqual(oldProps.report, newProps.report); } const MemoizedReportActionsView = React.memo(ReportActionsView, arePropsEqual); -export default compose( - Performance.withRenderTrace({id: ' rendering'}), - withWindowDimensions, - withLocalize, - withNetwork(), - withOnyx({ +export default Performance.withRenderTrace({id: ' rendering'})( + withOnyx({ session: { key: ONYXKEYS.SESSION, }, - }), -)(MemoizedReportActionsView); + })(MemoizedReportActionsView), +); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.tsx similarity index 51% rename from src/pages/home/report/ReportFooter.js rename to src/pages/home/report/ReportFooter.tsx index ec242116269e..c8f8433bbc46 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.tsx @@ -1,84 +1,83 @@ -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _, {isEqual} from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import AnonymousReportFooter from '@components/AnonymousReportFooter'; import ArchivedReportFooter from '@components/ArchivedReportFooter'; import OfflineIndicator from '@components/OfflineIndicator'; import {usePersonalDetails} from '@components/OnyxProvider'; import SwipeableView from '@components/SwipeableView'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; -import reportActionPropTypes from './reportActionPropTypes'; -const propTypes = { +type ReportFooterOnyxProps = { + /** Whether to show the compose input */ + shouldShowComposeInput: OnyxEntry; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type ReportFooterProps = ReportFooterOnyxProps & { /** Report object for the current report */ - report: reportPropTypes, + report?: OnyxTypes.Report; - lastReportAction: PropTypes.shape(reportActionPropTypes), + /** The last report action */ + lastReportAction?: OnyxEntry; - isEmptyChat: PropTypes.bool, + /** Whether the chat is empty */ + isEmptyChat?: boolean; /** The pending action when we are adding a chat */ - pendingAction: PropTypes.string, + pendingAction?: PendingAction; /** Height of the list which the composer is part of */ - listHeight: PropTypes.number, - - /** Whetjer the report is ready for display */ - isReportReadyForDisplay: PropTypes.bool, + listHeight?: number; - /** Whether to show the compose input */ - shouldShowComposeInput: PropTypes.bool, + /** Whether the report is ready for display */ + isReportReadyForDisplay?: boolean; - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user auth token type */ - authTokenType: PropTypes.string, - }), - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - report: {reportID: '0'}, - pendingAction: null, - listHeight: 0, - isReportReadyForDisplay: true, - lastReportAction: null, - isEmptyChat: true, - shouldShowComposeInput: false, - session: {}, + /** Whether the composer is in full size */ + isComposerFullSize?: boolean; }; -function ReportFooter(props) { +function ReportFooter({ + lastReportAction, + pendingAction, + session, + report = {reportID: '0'}, + shouldShowComposeInput = false, + isEmptyChat = true, + isReportReadyForDisplay = true, + listHeight = 0, + isComposerFullSize = false, +}: ReportFooterProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); + const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const chatFooterStyles = {...styles.chatFooter, minHeight: !isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0}; - const isArchivedRoom = ReportUtils.isArchivedRoom(props.report); - const isAnonymousUser = props.session.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; + const isArchivedRoom = ReportUtils.isArchivedRoom(report); + const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; - const isSmallSizeLayout = props.windowWidth - (props.isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; - const hideComposer = !ReportUtils.canUserPerformWriteAction(props.report); + const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; + const hideComposer = !ReportUtils.canUserPerformWriteAction(report); const allPersonalDetails = usePersonalDetails(); - /** - * @param {String} text - */ const handleCreateTask = useCallback( - (text) => { + (text: string): boolean => { /** * Matching task rule by group * Group 1: Start task rule with [] @@ -96,57 +95,56 @@ function ReportFooter(props) { return false; } const email = match[1] ? match[1].trim() : undefined; - let assignee = {}; + let assignee: OnyxTypes.PersonalDetails | EmptyObject = {}; if (email) { - assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; + assignee = Object.values(allPersonalDetails).find((value) => value?.login === email) ?? {}; } - Task.createTaskAndNavigate(props.report.reportID, title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, props.report.policyID); + Task.createTaskAndNavigate(report.reportID, title, '', assignee?.login ?? '', assignee.accountID, assignee.assigneeChatReport, report.policyID); return true; }, - [allPersonalDetails, props.report.policyID, props.report.reportID], + [allPersonalDetails, report.policyID, report.reportID], ); const onSubmitComment = useCallback( - (text) => { + (text: string) => { const isTaskCreated = handleCreateTask(text); if (isTaskCreated) { return; } - Report.addComment(props.report.reportID, text); + Report.addComment(report.reportID, text); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [props.report.reportID, handleCreateTask], + [report.reportID, handleCreateTask], ); return ( <> {hideComposer && ( - + {isAnonymousUser && !isArchivedRoom && ( )} - {isArchivedRoom && } - {!props.isSmallScreenWidth && ( - {hideComposer && } - )} + {isArchivedRoom && } + {!isSmallScreenWidth && {hideComposer && }} )} - {!hideComposer && (props.shouldShowComposeInput || !props.isSmallScreenWidth) && ( - + {!hideComposer && (!!shouldShowComposeInput || !isSmallScreenWidth) && ( + @@ -156,33 +154,27 @@ function ReportFooter(props) { } ReportFooter.displayName = 'ReportFooter'; -ReportFooter.propTypes = propTypes; -ReportFooter.defaultProps = defaultProps; -export default compose( - withWindowDimensions, - withOnyx({ - shouldShowComposeInput: { - key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, - initialValue: false, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), -)( + +export default withOnyx({ + shouldShowComposeInput: { + key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, + initialValue: false, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})( memo( ReportFooter, (prevProps, nextProps) => - isEqual(prevProps.report, nextProps.report) && + lodashIsEqual(prevProps.report, nextProps.report) && prevProps.pendingAction === nextProps.pendingAction && prevProps.listHeight === nextProps.listHeight && prevProps.isComposerFullSize === nextProps.isComposerFullSize && prevProps.isEmptyChat === nextProps.isEmptyChat && prevProps.lastReportAction === nextProps.lastReportAction && prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput && - prevProps.windowWidth === nextProps.windowWidth && - prevProps.isSmallScreenWidth === nextProps.isSmallScreenWidth && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && - isEqual(prevProps.session, nextProps.session), + lodashIsEqual(prevProps.session, nextProps.session), ), ); diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 0e3df33358c0..0d5c5b8a327b 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -1,5 +1,5 @@ import type {RouteProp} from '@react-navigation/native'; -import React, {useCallback} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -10,10 +10,14 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; +import * as FormActions from '@userActions/FormActions'; import * as IOU from '@userActions/IOU'; -import type ONYXKEYS from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; @@ -39,23 +43,43 @@ function HoldReasonPage({route}: HoldReasonPageProps) { const {transactionID, reportID, backTo} = route.params; + const report = ReportUtils.getReport(reportID); + const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + const navigateBack = () => { Navigation.navigate(backTo); }; const onSubmit = (values: FormOnyxValues) => { + if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + return; + } + IOU.putOnHold(transactionID, values.comment, reportID); navigateBack(); }; - const validate = useCallback((values: FormOnyxValues) => { - const errors: FormInputErrors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMMENT]); + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMMENT]); - if (!values.comment) { - errors.comment = 'common.error.fieldRequired'; - } + if (!values.comment) { + errors.comment = 'common.error.fieldRequired'; + } + if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + const formErrors = {}; + ErrorUtils.addErrorMessage(formErrors, 'reportModified', 'common.error.requestModified'); + FormActions.setErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM, formErrors); + } + + return errors; + }, + [parentReportAction], + ); - return errors; + useEffect(() => { + FormActions.clearErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM); + FormActions.clearErrorFields(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM); }, []); return ( diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index fb3a4d9457d5..01b6b0f4d21c 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -28,9 +28,6 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), - /** An object that holds data about which referral banners have been dismissed */ - dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool), - /** Callback to request parent modal to go to next step, which should be split */ onFinish: PropTypes.func.isRequired, @@ -69,7 +66,6 @@ const defaultProps = { safeAreaPaddingBottomStyle: {}, reports: {}, betas: [], - dismissedReferralBanners: {}, didScreenTransitionEnd: false, }; @@ -82,7 +78,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ safeAreaPaddingBottomStyle, iouType, iouRequestType, - dismissedReferralBanners, didScreenTransitionEnd, }) { const {translate} = useLocalize(); @@ -268,11 +263,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const footerContent = useMemo( () => ( - {!dismissedReferralBanners[referralContentType] && ( - - - - )} + {shouldShowSplitBillErrorMessage && ( ), - [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], ); const itemRightSideComponent = useCallback( @@ -358,10 +352,6 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.defaultProps = defaultProps MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector'; export default withOnyx({ - dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data.dismissedReferralBanners || {}, - }, reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index af1de64f8930..a0d148dab085 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -1,5 +1,7 @@ +import lodashGet from 'lodash/get'; +import lodashIsEmpty from 'lodash/isEmpty'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; import TagPicker from '@components/TagPicker'; @@ -11,7 +13,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import {canEditMoneyRequest} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; @@ -33,6 +37,9 @@ const propTypes = { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: transactionPropTypes, + /** The draft transaction that holds data to be persisted on the current transaction */ + splitDraftTransaction: transactionPropTypes, + /** The report currently being used */ report: reportPropTypes, @@ -46,7 +53,16 @@ const propTypes = { policyTags: tagPropTypes, /** The actions from the parent report */ - parentReportActions: PropTypes.shape(reportActionPropTypes), + reportActions: PropTypes.shape(reportActionPropTypes), + + /** Session info for the currently logged in user. */ + session: PropTypes.shape({ + /** Currently logged in user accountID */ + accountID: PropTypes.number, + + /** Currently logged in user email */ + email: PropTypes.string, + }).isRequired, }; const defaultProps = { @@ -55,7 +71,8 @@ const defaultProps = { policyTags: null, policyCategories: null, transaction: {}, - parentReportActions: {}, + splitDraftTransaction: {}, + reportActions: {}, }; function IOURequestStepTag({ @@ -64,24 +81,33 @@ function IOURequestStepTag({ policyTags, report, route: { - params: {action, tagIndex: rawTagIndex, transactionID, backTo, iouType}, + params: {action, tagIndex: rawTagIndex, transactionID, backTo, iouType, reportActionID}, }, transaction, - parentReportActions, + splitDraftTransaction, + reportActions, + session, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); const tagIndex = Number(rawTagIndex); const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex); - const transactionTag = TransactionUtils.getTag(transaction); - const tag = TransactionUtils.getTag(transaction, tagIndex); + const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; - const parentReportAction = parentReportActions[report.parentReportActionID]; + const isEditingSplitBill = isEditing && isSplitBill; + const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction; + const transactionTag = TransactionUtils.getTag(currentTransaction); + const tag = TransactionUtils.getTag(currentTransaction, tagIndex); + const reportAction = reportActions[report.parentReportActionID || reportActionID]; + const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + + const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = isEditing && !canEditMoneyRequest(parentReportAction); + const shouldShowNotFoundPage = !shouldShowTag || (isEditing && (isSplitBill ? !canEditSplitBill : !canEditMoneyRequest(reportAction))); const navigateBack = () => { Navigation.goBack(backTo); @@ -94,7 +120,7 @@ function IOURequestStepTag({ const updateTag = (selectedTag) => { const isSelectedTag = selectedTag.searchText === tag; const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagIndex); - if (isSplitBill && isEditing) { + if (isEditingSplitBill) { IOU.setDraftSplitTransaction(transactionID, {tag: updatedTag}); navigateBack(); return; @@ -141,6 +167,12 @@ export default compose( withWritableReportOrNotFound, withFullTransactionOrNotFound, withOnyx({ + splitDraftTransaction: { + key: ({route}) => { + const transactionID = lodashGet(route, 'params.transactionID', 0); + return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; + }, + }, policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, @@ -150,9 +182,23 @@ export default compose( policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, + reportActions: { + key: ({ + report, + route: { + params: {action, iouType}, + }, + }) => { + let reportID = '0'; + if (action === CONST.IOU.ACTION.EDIT) { + reportID = iouType === CONST.IOU.TYPE.SPLIT ? report.reportID : report.parentReportID; + } + return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; + }, canEvict: false, }, + session: { + key: ONYXKEYS.SESSION, + }, }), )(IOURequestStepTag); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 1ad6488aeee9..ca52053eca33 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -36,9 +36,6 @@ const propTypes = { /** Callback to add participants in MoneyRequestModal */ onAddParticipants: PropTypes.func.isRequired, - /** An object that holds data about which referral banners have been dismissed */ - dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool), - /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf( PropTypes.shape({ @@ -67,7 +64,6 @@ const propTypes = { }; const defaultProps = { - dismissedReferralBanners: {}, participants: [], safeAreaPaddingBottomStyle: {}, reports: {}, @@ -78,7 +74,6 @@ const defaultProps = { function MoneyRequestParticipantsSelector({ betas, - dismissedReferralBanners, participants, reports, navigateToRequest, @@ -286,11 +281,10 @@ function MoneyRequestParticipantsSelector({ const footerContent = useMemo( () => ( - {!dismissedReferralBanners[referralContentType] && ( - - - - )} + {shouldShowSplitBillErrorMessage && ( ), - [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], ); const itemRightSideComponent = useCallback( @@ -371,10 +365,6 @@ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector MoneyRequestParticipantsSelector.defaultProps = defaultProps; export default withOnyx({ - dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data.dismissedReferralBanners || {}, - }, reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/settings/AppDownloadLinks.tsx b/src/pages/settings/AppDownloadLinks.tsx index e4165178ff2f..00f6aeec04db 100644 --- a/src/pages/settings/AppDownloadLinks.tsx +++ b/src/pages/settings/AppDownloadLinks.tsx @@ -47,7 +47,7 @@ function AppDownloadLinksPage() { { translationKey: 'initialSettingsPage.appDownloadLinks.desktop.label', action: () => { - Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.DESKTOP); + Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.DESKTOP, true); }, link: CONST.APP_DOWNLOAD_LINKS.DESKTOP, icon: Expensicons.Monitor, diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.js index e8a1b5a39cfb..c443a91a7313 100644 --- a/src/pages/settings/Profile/DisplayNamePage.js +++ b/src/pages/settings/Profile/DisplayNamePage.js @@ -59,6 +59,8 @@ function DisplayNamePage(props) { // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); + } else if (values.firstName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'firstName', ['common.error.characterLimitExceedCounter', {length: values.firstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); @@ -67,6 +69,8 @@ function DisplayNamePage(props) { // Then we validate the last name field if (!ValidationUtils.isValidDisplayName(values.lastName)) { ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.hasInvalidCharacter'); + } else if (values.lastName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'lastName', ['common.error.characterLimitExceedCounter', {length: values.lastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } if (ValidationUtils.doesContainReservedWord(values.lastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.containsReservedWord'); @@ -107,7 +111,6 @@ function DisplayNamePage(props) { aria-label={props.translate('common.firstName')} role={CONST.ROLE.PRESENTATION} defaultValue={lodashGet(currentUserDetails, 'firstName', '')} - maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> @@ -120,7 +123,6 @@ function DisplayNamePage(props) { aria-label={props.translate('common.lastName')} role={CONST.ROLE.PRESENTATION} defaultValue={lodashGet(currentUserDetails, 'lastName', '')} - maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx index 85b7bef0550c..b50e5520a999 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.tsx +++ b/src/pages/settings/Wallet/TransferBalancePage.tsx @@ -156,12 +156,10 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans titleKey="notFound.pageNotFound" subtitleKey="transferAmountPage.notHereSubTitle" linkKey="transferAmountPage.goToWallet" - onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)} > Navigation.goBack(ROUTES.SETTINGS_WALLET)} /> diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index cd8d5dc18d6b..88236e06f9a9 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -75,7 +75,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi }); const addPaymentMethodAnchorRef = useRef(null); - const paymentMethodButtonRef = useRef(null); + const paymentMethodButtonRef = useRef(null); const [anchorPosition, setAnchorPosition] = useState({ anchorPositionHorizontal: 0, anchorPositionVertical: 0, @@ -164,7 +164,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi setShouldShowDefaultDeleteMenu(false); return; } - paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLElement; + paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement; // The delete/default menu if (accountType) { diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index bca0fbd2f8ef..4286a2603341 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -22,15 +22,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as ErrorUtils from '@libs/ErrorUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; -import Log from '@libs/Log'; import * as LoginUtils from '@libs/LoginUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import * as CloseAccount from '@userActions/CloseAccount'; -import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; @@ -151,12 +148,6 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false const loginTrim = login.trim(); - // If the user has entered a guide email, then we are going to enable an experimental Onyx mode to help with performance - if (PolicyUtils.isExpensifyGuideTeam(loginTrim)) { - Log.info('Detected guide email in login field, setting memory only keys.'); - MemoryOnlyKeys.enable(); - } - const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(loginTrim)); const parsedPhoneNumber = parsePhoneNumber(phoneLogin); diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 428df32bf032..8111e8d39afa 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -17,6 +17,7 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import AccountUtils from '@libs/AccountUtils'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -76,8 +77,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs const hasError = !!account && !isEmptyObject(account?.errors) && !needToClearError; const isLoadingResendValidationForm = account?.loadingForm === CONST.FORMS.RESEND_VALIDATE_CODE_FORM; const shouldDisableResendValidateCode = isOffline ?? account?.isLoading; - const isValidateCodeFormSubmitting = - account?.isLoading && account?.loadingForm === (account?.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM); + const isValidateCodeFormSubmitting = AccountUtils.isValidateCodeFormSubmitting(account); useEffect(() => { if (!(inputValidateCodeRef.current && hasError && (session?.autoAuthState === CONST.AUTO_AUTH_STATE.FAILED || account?.isLoading))) { diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.tsx similarity index 63% rename from src/pages/tasks/TaskAssigneeSelectorModal.js rename to src/pages/tasks/TaskAssigneeSelectorModal.tsx index d6b5d2e62a35..0ffb33b7590b 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -1,58 +1,49 @@ /* eslint-disable es/no-optional-chaining */ +import type {RouteProp} from '@react-navigation/native'; import {useRoute} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import lodashPick from 'lodash/pick'; -import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useBetas, usePersonalDetails, useSession} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as Task from '@userActions/Task'; +import type {TaskDetailsNavigatorParamList} from '@navigation/types'; +import * as TaskActions from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Report, Task} from '@src/types/onyx'; -const propTypes = { +type TaskAssigneeSelectorModalOnyxProps = { /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), + reports: OnyxCollection; /** Grab the Share destination of the Task */ - task: PropTypes.shape({ - /** Share destination of the Task */ - shareDestination: PropTypes.string, - - /** The task report if it's currently being edited */ - report: reportPropTypes, - }), - - /** The policy of root parent report */ - rootParentReportPolicy: PropTypes.shape({ - /** The role of current user */ - role: PropTypes.string, - }), + task: OnyxEntry; }; -const defaultProps = { - reports: {}, - task: {}, - rootParentReportPolicy: {}, +type UseOptions = { + reports: OnyxCollection; }; -function useOptions({reports}) { +type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; + +function useOptions({reports}: UseOptions) { const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); @@ -78,7 +69,7 @@ function useOptions({reports}) { ); const headerMessage = OptionsListUtils.getHeaderMessage( - (recentReports.length || 0 + personalDetails.length || 0) !== 0 || currentUserOption, + (recentReports?.length || 0) + (personalDetails?.length || 0) !== 0 || Boolean(currentUserOption), Boolean(userToInvite), debouncedSearchValue, ); @@ -99,20 +90,20 @@ function useOptions({reports}) { return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; } -function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { +function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); - const route = useRoute(); + const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task}); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); }; - const report = useMemo(() => { - if (!route.params || !route.params.reportID) { + const report: OnyxEntry = useMemo(() => { + if (!route.params?.reportID) { return null; } if (report && !ReportUtils.isTaskReport(report)) { @@ -120,7 +111,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { Navigation.dismissModal(report.reportID); }); } - return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`]; + return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`] ?? null; }, [reports, route]); const sections = useMemo(() => { @@ -155,17 +146,29 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { if (userToInvite) { sectionsList.push({ + title: '', data: [userToInvite], shouldShow: true, indexOffset, }); } - return sectionsList; - }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]); + return sectionsList.map((section) => ({ + ...section, + data: section.data.map((option) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + })); + }, [currentUserOption, personalDetails, recentReports, translate, userToInvite]); const selectReport = useCallback( - (option) => { + (option: ListItem) => { if (!option) { return; } @@ -173,25 +176,35 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { // Check to see if we're editing a task and if so, update the assignee if (report) { if (option.accountID !== report.managerID) { - const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option)); + const assigneeChatReport = TaskActions.setAssigneeValue( + option?.login ?? '', + option?.accountID ?? -1, + report.reportID, + OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? ''}), + ); // Pass through the selected assignee - Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport); + TaskActions.editTaskAssignee(report, session?.accountID ?? 0, option?.login ?? '', option?.accountID, assigneeChatReport); } Navigation.dismissModal(report.reportID); // If there's no report, we're creating a new task } else if (option.accountID) { - Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option)); + TaskActions.setAssigneeValue( + option?.login ?? '', + option.accountID, + task?.shareDestination ?? '', + OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? undefined}), + ); Navigation.goBack(ROUTES.NEW_TASK); } }, - [session.accountID, task.shareDestination, report], + [session?.accountID, task?.shareDestination, report], ); - const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); + const handleBackButtonPress = useCallback(() => (route.params?.reportID ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', '')); + const canModifyTask = TaskActions.canModifyTask(report, currentUserPersonalDetails.accountID); const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( @@ -199,7 +212,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { includeSafeAreaPaddingBottom={false} testID={TaskAssigneeSelectorModal.displayName} > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( @@ -225,26 +237,14 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { } TaskAssigneeSelectorModal.displayName = 'TaskAssigneeSelectorModal'; -TaskAssigneeSelectorModal.propTypes = propTypes; -TaskAssigneeSelectorModal.defaultProps = defaultProps; -export default compose( - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - task: { - key: ONYXKEYS.TASK, - }, - }), - withOnyx({ - rootParentReportPolicy: { - key: ({reports, route}) => { - const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`]; - const rootParentReport = ReportUtils.getRootParentReport(report); - return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`; - }, - selector: (policy) => lodashPick(policy, ['role']), - }, - }), -)(TaskAssigneeSelectorModal); +const TaskAssigneeSelectorModalWithOnyx = withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + task: { + key: ONYXKEYS.TASK, + }, +})(TaskAssigneeSelectorModal); + +export default withCurrentUserPersonalDetails(TaskAssigneeSelectorModalWithOnyx); diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.tsx similarity index 61% rename from src/pages/tasks/TaskDescriptionPage.js rename to src/pages/tasks/TaskDescriptionPage.tsx index b8b48abd09ff..e08d6380bb18 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -2,53 +2,43 @@ import {useFocusEffect} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/EditTaskForm'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - - /* Onyx Props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, -}; +type TaskDescriptionPageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps; const parser = new ExpensiMark(); -function TaskDescriptionPage(props) { + +function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescriptionPageProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); - /** - * @param {Object} values - form input values passed by the Form component - * @returns {Boolean} - */ - const validate = useCallback((values) => { + const validate = useCallback((values: FormOnyxValues): FormInputErrors => { const errors = {}; - if (values.description.length > CONST.DESCRIPTION_LIMIT) { + if (values?.description && values.description?.length > CONST.DESCRIPTION_LIMIT) { ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]); } @@ -56,30 +46,30 @@ function TaskDescriptionPage(props) { }, []); const submit = useCallback( - (values) => { - // props.report.description might contain CRLF from the server - if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(props.report.description)) { + (values: FormOnyxValues) => { + // report.description might contain CRLF from the server + if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(report?.description) && !isEmptyObject(report)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server - Task.editTask(props.report, {description: values.description}); + Task.editTask(report, {description: values.description}); } - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }, - [props], + [report], ); - if (!ReportUtils.isTaskReport(props.report)) { + if (!ReportUtils.isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }); } - const inputRef = useRef(null); - const focusTimeoutRef = useRef(null); + const inputRef = useRef(null); + const focusTimeoutRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(props.report); - const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const isOpen = ReportUtils.isOpenTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); useFocusEffect( useCallback(() => { @@ -104,13 +94,13 @@ function TaskDescriptionPage(props) { testID={TaskDescriptionPage.displayName} > - + @@ -119,14 +109,14 @@ function TaskDescriptionPage(props) { role={CONST.ROLE.PRESENTATION} inputID={INPUT_IDS.DESCRIPTION} name={INPUT_IDS.DESCRIPTION} - label={props.translate('newTaskPage.descriptionOptional')} - accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} - defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')} - ref={(el) => { - if (!el) { + label={translate('newTaskPage.descriptionOptional')} + accessibilityLabel={translate('newTaskPage.descriptionOptional')} + defaultValue={parser.htmlToMarkdown((report && parser.replace(report?.description ?? '')) || '')} + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } - inputRef.current = el; + inputRef.current = element; updateMultilineInputRange(inputRef.current); }} autoGrowHeight @@ -140,17 +130,8 @@ function TaskDescriptionPage(props) { ); } -TaskDescriptionPage.propTypes = propTypes; -TaskDescriptionPage.defaultProps = defaultProps; TaskDescriptionPage.displayName = 'TaskDescriptionPage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - }), -)(TaskDescriptionPage); +const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskDescriptionPage); + +export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails); diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx similarity index 62% rename from src/pages/tasks/TaskShareDestinationSelectorModal.js rename to src/pages/tasks/TaskShareDestinationSelectorModal.tsx index b62440b22967..5b56e58752ac 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,7 @@ -import keys from 'lodash/keys'; -import reduce from 'lodash/reduce'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,51 +11,45 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Report from '@libs/actions/Report'; +import * as ReportActions from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; -const propTypes = { - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Whether or not we are searching for reports on the server */ - isSearchingForReports: PropTypes.bool, -}; +type TaskShareDestinationSelectorModalOnyxProps = { + reports: OnyxCollection; -const defaultProps = { - reports: {}, - isSearchingForReports: false, + isSearchingForReports: OnyxEntry; }; -const selectReportHandler = (option) => { - if (!option || !option.reportID) { +type TaskShareDestinationSelectorModalProps = TaskShareDestinationSelectorModalOnyxProps; + +const selectReportHandler = (option: unknown) => { + const optionItem = option as ReportUtils.OptionData; + + if (!optionItem || !optionItem?.reportID) { return; } - Task.setShareDestinationValue(option.reportID); + Task.setShareDestinationValue(optionItem?.reportID); Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reports) => - reduce( - keys(reports), - (filtered, reportKey) => { - const report = reports[reportKey]; - if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - return {...filtered, [reportKey]: report}; - } - return filtered; - }, - {}, - ); +const reportFilter = (reports: OnyxCollection) => + Object.keys(reports ?? {}).reduce((filtered, reportKey) => { + const report: OnyxEntry = reports?.[reportKey] ?? null; + if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { + return {...filtered, [reportKey]: report}; + } + return filtered; + }, {}); -function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { +function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); @@ -73,13 +65,29 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); - const sections = recentReports && recentReports.length > 0 ? [{data: recentReports, shouldShow: true}] : []; + const sections = + recentReports && recentReports.length > 0 + ? [ + { + data: recentReports.map((option) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + shouldShow: true, + }, + ] + : []; return {sections, headerMessage}; }, [personalDetails, reports, debouncedSearchValue]); useEffect(() => { - Report.searchInServer(debouncedSearchValue); + ReportActions.searchInServer(debouncedSearchValue); }, [debouncedSearchValue]); return ( @@ -87,7 +95,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { includeSafeAreaPaddingBottom={false} testID="TaskShareDestinationSelectorModal" > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( <> @@ -115,10 +122,8 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { } TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; -TaskShareDestinationSelectorModal.propTypes = propTypes; -TaskShareDestinationSelectorModal.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.tsx similarity index 50% rename from src/pages/tasks/TaskTitlePage.js rename to src/pages/tasks/TaskTitlePage.tsx index 370baab7cd89..009983beac3e 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.tsx @@ -1,98 +1,85 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/EditTaskForm'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, +type TaskTitlePageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps; - /* Onyx Props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, -}; - -function TaskTitlePage(props) { +function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) { const styles = useThemeStyles(); - /** - * @param {Object} values - * @param {String} values.title - * @returns {Object} - An object containing the errors for each inputID - */ - const validate = useCallback((values) => { - const errors = {}; + const {translate} = useLocalize(); + + const validate = useCallback(({title}: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; - if (_.isEmpty(values.title)) { + if (!title) { errors.title = 'newTaskPage.pleaseEnterTaskName'; - } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; }, []); const submit = useCallback( - (values) => { - if (values.title !== props.report.reportName) { + (values: FormOnyxValues) => { + if (values.title !== report?.reportName && !isEmptyObject(report)) { // Set the title of the report in the store and then call EditTask API // to update the title of the report on the server - Task.editTask(props.report, {title: values.title}); + Task.editTask(report, {title: values.title}); } - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }, - [props], + [report], ); - if (!ReportUtils.isTaskReport(props.report)) { + if (!ReportUtils.isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }); } - const inputRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(props.report); - const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const inputRef = useRef(null); + const isOpen = ReportUtils.isOpenTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( inputRef.current && inputRef.current.focus()} + onEntryTransitionEnd={() => { + inputRef?.current?.focus(); + }} shouldEnableMaxHeight testID={TaskTitlePage.displayName} > {({didScreenTransitionEnd}) => ( - + @@ -101,17 +88,17 @@ function TaskTitlePage(props) { role={CONST.ROLE.PRESENTATION} inputID={INPUT_IDS.TITLE} name={INPUT_IDS.TITLE} - label={props.translate('task.title')} - accessibilityLabel={props.translate('task.title')} - defaultValue={(props.report && props.report.reportName) || ''} - ref={(el) => { - if (!el) { + label={translate('task.title')} + accessibilityLabel={translate('task.title')} + defaultValue={report?.reportName ?? ''} + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } if (!inputRef.current && didScreenTransitionEnd) { - el.focus(); + element.focus(); } - inputRef.current = el; + inputRef.current = element; }} /> @@ -122,17 +109,8 @@ function TaskTitlePage(props) { ); } -TaskTitlePage.propTypes = propTypes; -TaskTitlePage.defaultProps = defaultProps; TaskTitlePage.displayName = 'TaskTitlePage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - }), -)(TaskTitlePage); +const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskTitlePage); + +export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index c4f4d6399dbd..240a148110f7 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -143,26 +143,50 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r }, ]; - const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = [ - { + const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = []; + + if (policy?.areDistanceRatesEnabled) { + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.distanceRates', + icon: Expensicons.Car, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.DISTANCE_RATES, + }); + } + + if (policy?.areWorkflowsEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.workflows', icon: Expensicons.Workflows, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.WORKFLOWS, - }, - { + }); + } + + if (policy?.areCategoriesEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.categories', icon: Expensicons.Folder, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.CATEGORIES, - }, - { + }); + } + + if (policy?.areTagsEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.tags', icon: Expensicons.Tag, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.TAGS, - }, - ]; + }); + } + + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.moreFeatures', + icon: Expensicons.Gear, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.MORE_FEATURES, + }); const menuItems: WorkspaceMenuItem[] = [ { diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx new file mode 100644 index 000000000000..ffcf871ae70d --- /dev/null +++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx @@ -0,0 +1,80 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useThemeStyles from '@hooks/useThemeStyles'; +import navigateAfterJoinRequest from '@libs/navigateAfterJoinRequest'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import type {AuthScreensParamList} from '@navigation/types'; +import * as PolicyAction from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; + +type WorkspaceJoinUserPageOnyxProps = { + /** The list of this user's policies */ + policies: OnyxCollection; +}; + +type WorkspaceJoinUserPageRoute = {route: StackScreenProps['route']}; +type WorkspaceJoinUserPageProps = WorkspaceJoinUserPageRoute & WorkspaceJoinUserPageOnyxProps; + +let isJoinLinkUsed = false; + +function WorkspaceJoinUserPage({route, policies}: WorkspaceJoinUserPageProps) { + const styles = useThemeStyles(); + const policyID = route?.params?.policyID; + const inviterEmail = route?.params?.email; + const policy = ReportUtils.getPolicy(policyID); + const isUnmounted = useRef(false); + + useEffect(() => { + if (!isJoinLinkUsed) { + return; + } + navigateAfterJoinRequest(); + }, []); + + useEffect(() => { + if (!policy || !policies || isUnmounted.current || isJoinLinkUsed) { + return; + } + const isPolicyMember = PolicyUtils.isPolicyMember(policyID, policies as Record); + if (isPolicyMember) { + Navigation.goBack(undefined, false, true); + return; + } + PolicyAction.inviteMemberToWorkspace(policyID, inviterEmail); + isJoinLinkUsed = true; + Navigation.isNavigationReady().then(() => { + if (isUnmounted.current) { + return; + } + navigateAfterJoinRequest(); + }); + }, [policy, policyID, policies, inviterEmail]); + + useEffect( + () => () => { + isUnmounted.current = true; + }, + [], + ); + + return ( + + + + ); +} + +WorkspaceJoinUserPage.displayName = 'WorkspaceJoinUserPage'; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(WorkspaceJoinUserPage); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 100044344d4e..1aae3294be0d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -87,6 +87,10 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se const {isSmallScreenWidth} = useWindowDimensions(); const dropdownButtonRef = useRef(null); const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const isLoading = useMemo( + () => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers)), + [isOfflineAndNoMemberDataAvailable, personalDetails, policyMembers], + ); /** * Get filtered personalDetails list with current policyMembers @@ -254,6 +258,19 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se [selectedEmployees, addUser, removeUser], ); + /** Opens the member details page */ + const openMemberDetails = useCallback( + (item: MemberOption) => { + if (!isPolicyAdmin || !PolicyUtils.isPaidGroupPolicy(policy)) { + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID, Navigation.getActiveRoute())); + }, + [isPolicyAdmin, policy, route.params.policyID], + ); + /** * Dismisses the errors on one item */ @@ -349,7 +366,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se }); }); - result = result.sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase())); + result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase())); return result; }; @@ -359,7 +376,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se if (isOfflineAndNoMemberDataAvailable) { return translate('workspace.common.mustBeOnlineToViewMembers'); } - return !data.length ? translate('workspace.common.memberNotFound') : ''; + + return !isLoading && isEmptyObject(policyMembers) ? translate('workspace.common.memberNotFound') : ''; }; const getHeaderContent = () => ( @@ -451,7 +469,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se shouldAlwaysShowDropdownMenu pressOnEnter - customText={translate('workspace.people.selected', {selectedNumber: selectedEmployees.length})} + customText={translate('workspace.common.selected', {selectedNumber: selectedEmployees.length})} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} onPress={() => null} options={getBulkActionsButtonOptions()} @@ -465,7 +483,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se onPress={inviteUser} text={translate('workspace.invite.member')} icon={Expensicons.Plus} - iconStyles={{transform: [{scale: 0.6}]}} + iconStyles={StyleUtils.getTransformScaleStyle(0.6)} innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]} style={[isSmallScreenWidth && styles.flexGrow1]} /> @@ -525,16 +543,11 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} - onSelectRow={(item) => { - if (!isPolicyAdmin) { - Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); - return; - } - toggleUser(item.accountID); - }} + onSelectRow={openMemberDetails} + onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} - showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))} + showLoadingPlaceholder={isLoading} showScrollIndicator shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} ref={textInputRef} diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx new file mode 100644 index 000000000000..45a950e0fafb --- /dev/null +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -0,0 +1,168 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import * as Policy from '@userActions/Policy'; +import type {TranslationPaths} from '@src/languages/types'; +import type SCREENS from '@src/SCREENS'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type IconAsset from '@src/types/utils/IconAsset'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from './PaidPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow'; + +type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; + +type Item = { + icon: IconAsset; + titleTranslationKey: TranslationPaths; + subtitleTranslationKey: TranslationPaths; + isActive: boolean; + action: (isEnabled: boolean) => void; + pendingAction: PendingAction | undefined; +}; + +type SectionObject = { + titleTranslationKey: TranslationPaths; + subtitleTranslationKey: TranslationPaths; + items: Item[]; +}; + +function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPageProps) { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + + const spendItems: Item[] = [ + { + icon: Illustrations.Car, + titleTranslationKey: 'workspace.moreFeatures.distanceRates.title', + subtitleTranslationKey: 'workspace.moreFeatures.distanceRates.subtitle', + isActive: policy?.areDistanceRatesEnabled ?? false, + pendingAction: policy?.pendingFields?.areDistanceRatesEnabled, + action: (isEnabled: boolean) => { + Policy.enablePolicyDistanceRates(policy?.id ?? '', isEnabled); + }, + }, + { + icon: Illustrations.Workflows, + titleTranslationKey: 'workspace.moreFeatures.workflows.title', + subtitleTranslationKey: 'workspace.moreFeatures.workflows.subtitle', + isActive: policy?.areWorkflowsEnabled ?? false, + pendingAction: policy?.pendingFields?.areWorkflowsEnabled, + action: (isEnabled: boolean) => { + Policy.enablePolicyWorkflows(policy?.id ?? '', isEnabled); + }, + }, + ]; + + const organizeItems: Item[] = [ + { + icon: Illustrations.FolderOpen, + titleTranslationKey: 'workspace.moreFeatures.categories.title', + subtitleTranslationKey: 'workspace.moreFeatures.categories.subtitle', + isActive: policy?.areCategoriesEnabled ?? false, + pendingAction: policy?.pendingFields?.areCategoriesEnabled, + action: (isEnabled: boolean) => { + Policy.enablePolicyCategories(policy?.id ?? '', isEnabled); + }, + }, + { + icon: Illustrations.Tag, + titleTranslationKey: 'workspace.moreFeatures.tags.title', + subtitleTranslationKey: 'workspace.moreFeatures.tags.subtitle', + isActive: policy?.areTagsEnabled ?? false, + pendingAction: policy?.pendingFields?.areTagsEnabled, + action: (isEnabled: boolean) => { + Policy.enablePolicyTags(policy?.id ?? '', isEnabled); + }, + }, + ]; + + const sections: SectionObject[] = [ + { + titleTranslationKey: 'workspace.moreFeatures.spendSection.title', + subtitleTranslationKey: 'workspace.moreFeatures.spendSection.subtitle', + items: spendItems, + }, + { + titleTranslationKey: 'workspace.moreFeatures.organizeSection.title', + subtitleTranslationKey: 'workspace.moreFeatures.organizeSection.subtitle', + items: organizeItems, + }, + ]; + + const renderItem = useCallback( + (item: Item) => ( + + + + ), + [styles, translate], + ); + + const renderSection = useCallback( + (section: SectionObject) => ( + +
+ {section.items.map(renderItem)} +
+
+ ), + [isSmallScreenWidth, styles, renderItem, translate], + ); + + return ( + + + + + + {sections.map(renderSection)} + + + + ); +} + +WorkspaceMoreFeaturesPage.displayName = 'WorkspaceMoreFeaturesPage'; + +export default withPolicyAndFullscreenLoading(WorkspaceMoreFeaturesPage); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index f8c2ad905fc4..948cd2fd83c4 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -52,6 +52,7 @@ type WorkspaceItem = Required> & policyID?: string; adminRoom?: string | null; announceRoom?: string | null; + isJoinRequestPending?: boolean; }; // eslint-disable-next-line react/no-unused-prop-types @@ -116,11 +117,12 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [policyIDToDelete, setPolicyIDToDelete] = useState(); const [policyNameToDelete, setPolicyNameToDelete] = useState(); + const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth; const confirmDeleteAndHideModal = () => { if (!policyIDToDelete || !policyNameToDelete) { @@ -192,8 +194,9 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r workspaceIcon={item.icon} ownerAccountID={item.ownerAccountID} workspaceType={item.type} + isJoinRequestPending={item?.isJoinRequestPending} rowStyles={hovered && styles.hoveredComponentBG} - layoutWidth={isSmallScreenWidth ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE} + layoutWidth={isLessThanMediumScreen ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE} brickRoadIndicator={item.brickRoadIndicator} shouldDisableThreeDotsMenu={item.disabled} /> @@ -202,11 +205,11 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r ); }, - [isSmallScreenWidth, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate], + [isLessThanMediumScreen, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate], ); const listHeaderComponent = useCallback(() => { - if (isSmallScreenWidth) { + if (isLessThanMediumScreen) { return ; } @@ -239,7 +242,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r ); - }, [isSmallScreenWidth, styles, translate]); + }, [isLessThanMediumScreen, styles, translate]); const policyRooms = useMemo(() => { if (!reports || isEmptyObject(reports)) { @@ -284,8 +287,28 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r return Object.values(policies) .filter((policy): policy is PolicyType => PolicyUtils.shouldShowPolicy(policy, !!isOffline)) - .map( - (policy): WorkspaceItem => ({ + .map((policy): WorkspaceItem => { + if (policy?.isJoinRequestPending && policy?.policyDetailsForNonMembers) { + const policyInfo = Object.values(policy.policyDetailsForNonMembers)[0]; + const id = Object.keys(policy.policyDetailsForNonMembers)[0]; + return { + title: policyInfo.name, + icon: policyInfo.avatar ? policyInfo.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name), + disabled: true, + ownerAccountID: policyInfo.ownerAccountID, + type: policyInfo.type, + iconType: policyInfo.avatar ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_ICON, + iconFill: theme.textLight, + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, + policyID: id, + role: CONST.POLICY.ROLE.USER, + errors: null, + action: () => null, + dismissError: () => null, + isJoinRequestPending: true, + }; + } + return { title: policy.name, icon: policy.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name), action: () => Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)), @@ -308,8 +331,8 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r ownerAccountID: policy.ownerAccountID, role: policy.role, type: policy.type, - }), - ) + }; + }) .sort((a, b) => localeCompare(a.title, b.title)); }, [reimbursementAccount?.errors, policies, isOffline, theme.textLight, allPolicyMembers, policyRooms]); @@ -324,7 +347,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r > Navigation.goBack()} >