diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index b4fc05c7ebe9..0c5f70929c27 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -14,6 +14,24 @@ inputs: MAPBOX_SDK_DOWNLOAD_TOKEN: description: The token to use to download the MapBox SDK required: true + PATH_ENV_FILE: + description: The path to the .env file to use for the build + required: true + EXPENSIFY_PARTNER_NAME: + description: The name of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_PASSWORD: + description: The password of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_USER_ID: + description: The user ID of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_USER_SECRET: + description: The user secret of the Expensify partner to use for the build + required: true + EXPENSIFY_PARTNER_PASSWORD_EMAIL: + description: The email address of the Expensify partner to use for the build + required: true runs: using: composite @@ -37,9 +55,24 @@ runs: - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef + - name: Append environment variables to env file + shell: bash + run: | + echo "EXPENSIFY_PARTNER_NAME=${EXPENSIFY_PARTNER_NAME}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_PASSWORD=${EXPENSIFY_PARTNER_PASSWORD}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_USER_ID=${EXPENSIFY_PARTNER_USER_ID}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_USER_SECRET=${EXPENSIFY_PARTNER_USER_SECRET}" >> ${{ inputs.PATH_ENV_FILE }} + echo "EXPENSIFY_PARTNER_PASSWORD_EMAIL=${EXPENSIFY_PARTNER_PASSWORD_EMAIL}" >> ${{ inputs.PATH_ENV_FILE }} + - name: Build APK run: npm run ${{ inputs.PACKAGE_SCRIPT_NAME }} shell: bash + env: + EXPENSIFY_PARTNER_NAME: ${{ inputs.EXPENSIFY_PARTNER_NAME }} + EXPENSIFY_PARTNER_PASSWORD: ${{ inputs.EXPENSIFY_PARTNER_PASSWORD }} + EXPENSIFY_PARTNER_USER_ID: ${{ inputs.EXPENSIFY_PARTNER_USER_ID }} + EXPENSIFY_PARTNER_USER_SECRET: ${{ inputs.EXPENSIFY_PARTNER_USER_SECRET }} + EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ inputs.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} - name: Upload APK uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 diff --git a/.github/scripts/createHelpRedirects.sh b/.github/scripts/createHelpRedirects.sh index 04f55e19b4fb..1ae2220253c4 100755 --- a/.github/scripts/createHelpRedirects.sh +++ b/.github/scripts/createHelpRedirects.sh @@ -41,13 +41,13 @@ while read -r line; do # Basic sanity checking to make sure that the source and destination are in expected # subdomains. - if ! [[ $SOURCE_URL =~ ^https://community\.expensify\.com ]]; then - error "Found source URL that is not a community URL: $SOURCE_URL" + if ! [[ $SOURCE_URL =~ ^https://(community|help)\.expensify\.com ]]; then + error "Found source URL that is not a communityDot or helpDot URL: $SOURCE_URL" exit 1 fi - if ! [[ $DEST_URL =~ ^https://help\.expensify\.com ]]; then - error "Found destination URL that is not a help URL: $DEST_URL" + if ! [[ $DEST_URL =~ ^https://(help|use)\.expensify\.com ]]; then + error "Found destination URL that is not a helpDot or useDot URL: $DEST_URL" exit 1 fi diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index bd3af08ae25e..70f70fca60de 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -52,6 +52,12 @@ jobs: PACKAGE_SCRIPT_NAME: android-build-e2e APP_OUTPUT_PATH: android/app/build/outputs/apk/e2e/release/app-e2e-release.apk MAPBOX_SDK_DOWNLOAD_TOKEN: ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + EXPENSIFY_PARTNER_NAME: ${{ secrets.EXPENSIFY_PARTNER_NAME }} + EXPENSIFY_PARTNER_PASSWORD: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD }} + EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} + EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} + EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} + PATH_ENV_FILE: tests/e2e/.env.e2e buildDelta: runs-on: ubuntu-latest-xl @@ -114,6 +120,12 @@ jobs: PACKAGE_SCRIPT_NAME: android-build-e2edelta APP_OUTPUT_PATH: android/app/build/outputs/apk/e2edelta/release/app-e2edelta-release.apk MAPBOX_SDK_DOWNLOAD_TOKEN: ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + EXPENSIFY_PARTNER_NAME: ${{ secrets.EXPENSIFY_PARTNER_NAME }} + EXPENSIFY_PARTNER_PASSWORD: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD }} + EXPENSIFY_PARTNER_USER_ID: ${{ secrets.EXPENSIFY_PARTNER_USER_ID }} + EXPENSIFY_PARTNER_USER_SECRET: ${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }} + EXPENSIFY_PARTNER_PASSWORD_EMAIL: ${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }} + PATH_ENV_FILE: tests/e2e/.env.e2edelta runTestsInAWS: runs-on: ubuntu-latest diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 64b4536d9241..ebe31f41f3d8 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -33,6 +33,7 @@ jobs: npx reassure --baseline git switch --force --detach - git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours + git checkout --ours . npm install --force npx reassure --branch diff --git a/.nvmrc b/.nvmrc index 43bff1f8cf98..d5a159609d09 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9.0 \ No newline at end of file +20.10.0 diff --git a/README.md b/README.md index f6629af8604d..b69786d64f13 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ If you're using another operating system, you will need to ensure `mkcert` is in ## Running the iOS app 📱 For an M1 Mac, read this [SO](https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1) for installing cocoapods. +* If you haven't already, install Xcode tools and make sure to install the optional "iOS Platform" package as well. This installation may take awhile. * Install project gems, including cocoapods, using bundler to ensure everyone uses the same versions. In the project root, run: `bundle install` * If you get the error `Could not find 'bundler'`, install the bundler gem first: `gem install bundler` and try again. * If you are using MacOS and get the error `Gem::FilePermissionError` when trying to install the bundler gem, you're likely using system Ruby, which requires administrator permission to modify. To get around this, install another version of Ruby with a version manager like [rbenv](https://github.com/rbenv/rbenv#installation). diff --git a/android/app/build.gradle b/android/app/build.gradle index e135d44eb834..df6e0db76efa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -71,7 +71,7 @@ project.ext.envConfigFiles = [ /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ -def enableProguardInReleaseBuilds = false +def enableProguardInReleaseBuilds = true /** * The preferred build flavor of JavaScriptCore (JSC) @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043202 - versionName "1.4.32-2" + versionCode 1001043303 + versionName "1.4.33-3" } flavorDimensions "default" @@ -152,8 +152,9 @@ android { } release { productFlavors.production.signingConfig signingConfigs.release + shrinkResources enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig null // buildTypes take precedence over productFlavors when it comes to the signing configuration, diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 7dab035002a2..e553222dd682 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -8,5 +8,31 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: --keep class com.facebook.hermes.unicode.** { *; } --keep class com.facebook.jni.** { *; } +-keep class com.expensify.chat.BuildConfig { *; } +-keep, allowoptimization, allowobfuscation class expo.modules.** { *; } + +# Added from auto-generated missingrules.txt to allow build to succeed +-dontwarn com.onfido.javax.inject.Inject +-dontwarn javax.lang.model.element.Element +-dontwarn javax.lang.model.type.TypeMirror +-dontwarn javax.lang.model.type.TypeVisitor +-dontwarn javax.lang.model.util.SimpleTypeVisitor7 +-dontwarn net.sf.scuba.data.Gender +-dontwarn net.sf.scuba.smartcards.CardFileInputStream +-dontwarn net.sf.scuba.smartcards.CardService +-dontwarn net.sf.scuba.smartcards.CardServiceException +-dontwarn org.jmrtd.AccessKeySpec +-dontwarn org.jmrtd.BACKey +-dontwarn org.jmrtd.BACKeySpec +-dontwarn org.jmrtd.PACEKeySpec +-dontwarn org.jmrtd.PassportService +-dontwarn org.jmrtd.lds.CardAccessFile +-dontwarn org.jmrtd.lds.PACEInfo +-dontwarn org.jmrtd.lds.SecurityInfo +-dontwarn org.jmrtd.lds.icao.DG15File +-dontwarn org.jmrtd.lds.icao.DG1File +-dontwarn org.jmrtd.lds.icao.MRZInfo +-dontwarn org.jmrtd.protocol.AAResult +-dontwarn org.jmrtd.protocol.BACResult +-dontwarn org.jmrtd.protocol.PACEResult +-dontwarn org.spongycastle.jce.provider.BouncyCastleProvider \ No newline at end of file diff --git a/android/app/src/main/res/raw/keep.xml b/android/app/src/main/res/raw/keep.xml new file mode 100644 index 000000000000..972e0416855c --- /dev/null +++ b/android/app/src/main/res/raw/keep.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index d9d1e903423c..40aefc6f2405 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -17,14 +17,5 @@ apply from: file("../node_modules/@react-native-community/cli-platform-android/n include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') -includeBuild('../node_modules/react-native') { - dependencySubstitution { - substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid")) - substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid")) - substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) - substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) - } -} - apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") -useExpoModules() \ No newline at end of file +useExpoModules() diff --git a/assets/animations/Update.lottie b/assets/animations/Update.lottie new file mode 100644 index 000000000000..363486ec2267 Binary files /dev/null and b/assets/animations/Update.lottie differ diff --git a/assets/images/eReceiptIcon.svg b/assets/images/eReceiptIcon.svg index 9b0612a03231..f0e206995809 100644 --- a/assets/images/eReceiptIcon.svg +++ b/assets/images/eReceiptIcon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.js index 9c4ded572804..2612e2b190fa 100644 --- a/config/webpack/webpack.desktop.js +++ b/config/webpack/webpack.desktop.js @@ -54,15 +54,6 @@ module.exports = (env) => { loader: 'babel-loader', exclude: /node_modules/, }, - { - test: /react-native-onyx/, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-react'], - }, - }, - }, ], }, }; diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 9eb16099f535..25f54c668b24 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -123,7 +123,7 @@ Additionally if you want to discuss an idea with the open source community witho ``` 11. [Open a pull request](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork), and make sure to fill in the required fields. 12. An Expensify engineer and a member from the Contributor-Plus team will be assigned to your pull request automatically to review. -13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates for > 1 week, please comment on the PR or issue how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 1 full week may be considered abandoned and the original contract terminated. +13. Daily updates on weekdays are highly recommended. If you know you won’t be able to provide updates within 48 hours, please comment on the PR or issue stating how long you plan to be out so that we may plan accordingly. We understand everyone needs a little vacation here and there. Any issue that doesn't receive an update for 5 days (including weekend days) may be considered abandoned and the original contract terminated. #### Submit your pull request for final review 14. When you are ready to submit your pull request for final review, make sure the following checks pass: diff --git a/docs/articles/expensify-classic/getting-started/Plan-Types.md b/docs/articles/expensify-classic/getting-started/Plan-Types.md deleted file mode 100644 index 4f8c52c2e1a1..000000000000 --- a/docs/articles/expensify-classic/getting-started/Plan-Types.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Plan Types -description: Learn which Expensify plan is the best fit for you ---- -# Overview -You can access comprehensive information about Expensify's plans and pricing by visiting www.expensify.com/pricing. Below, we provide an overview of each plan type to assist you in selecting the one that best suits your business or personal requirements. - -## Free Plan -The Free plan is suited for small businesses, offering a dedicated workspace for efficiently handling Expensify card management, expense reimbursement, invoicing, and bill payment. This plan includes unlimited receipt scanning for all users within the company and the potential to earn up to 1% cashback on card spending exceeding $25,000 per month (across all cards). - -## Collect Workspace Plan -The Collect Workspace Plan is designed with small companies in mind, providing essential features like a single layer of expense approvals, reimbursement capabilities, corporate card management, and basic integration options such as QuickBooks Online, QuickBooks Desktop, and Xero. This plan is ideal for those who require simple expense management functions. - -## Control Workspace Plan -Our most popular option, the Control Workspace plan, offers a heightened level of control and Workspace customization. With a Control Workspace, you gain access to multi-level approval workflows, comprehensive corporate card management, advanced accounting integration, tax tracking capabilities, and advanced expense rules that facilitate the enforcement of your internal expense policy. This plan provides a robust set of features for effective expense management. - -## Individual Track Plan -The Track plan is tailored for solo Expensify users who don't require expense submission to others. Individuals or sole proprietors can choose the Track plan to customize their Individual Workspace to align with their personal expense tracking requirements. - -## Individual Submit Plan -The Submit plan is designed for individuals who need to keep track of their expenses and share them with someone else, such as their boss, accountant, or even a housemate. It's specifically tailored for single users who want to both track and submit their expenses efficiently. - -{% include faq-begin.md %} - -## How can I change Individual plans? -You have the flexibility to switch between a Track and Submit plan, or vice versa, at any time by navigating to **Settings > Workspaces > Individual > *Workspace Name* > Plan**. This allows you to adapt your expense management approach as needed. - -## How can I upgrade Group plans? -You can easily upgrade from a Collect to a Control plan at any time by going to **Settings > Workspaces > Group > *Workspace Name* > Plan**. However, it's important to note that if you have an active Annual Subscription, downgrading from Control to Collect is not possible until your current commitment period expires. - -## How does pricing work if I have two types of Group Workspace plans? -If you have a Control and Collect Workspace, you will be charged at the Control Workspace rate. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md deleted file mode 100644 index 189ff671b213..000000000000 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Expensify Card revenue share for ExpensifyApproved! partners -description: Earn money when your clients adopt the Expensify Card -redirect_from: articles/other/Card-Revenue-Share-for-ExpensifyApproved!-Partners/ ---- - - -Start making more with us! We're thrilled to announce a new incentive for our US-based ExpensifyApproved! partner accountants. You can now earn additional income for your firm every time your client uses their Expensify Card. **In short, your firm gets 0.5% of your clients’ total Expensify Card spend as cash back**. The more your clients spend, the more cashback your firm receives!
-
This program is currently only available to US-based ExpensifyApproved! partner accountants. - -# How-to -To benefit from this program, all you need to do is ensure that you are listed as a domain admin on your client's Expensify account. If you're not currently a domain admin, your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role. -{% include faq-begin.md %} -- What if my firm is not permitted to accept revenue share from our clients?
-
We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.

-- What if my firm does not wish to participate in the program?
-
Please reach out to your assigned partner manager at new.expensify.com to inform them you would not like to accept the revenue share nor do you want to pass the revenue share to your clients. \ No newline at end of file diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md deleted file mode 100644 index fb3cb5341f61..000000000000 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Your Expensify Partner Manager -description: Everything you need to know about your Expensify Partner Manager -redirect_from: articles/other/Your-Expensify-Partner-Manager/ ---- - - -# What is a Partner Manager? -A Partner Manager is a dedicated point of contact to support our ExpensifyApproved! Accountants with questions about their Expensify account. Partner Managers support our accounting partners by providing recommendations for client's accounts, assisting with firm-wide training, and ensuring partners receive the full benefits of our partnership program. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency and minimize time spent on expense management. - -Unlike Concierge, a Partner Manager’s support will not be real-time, 24 hours a day. A benefit of Concierge is that you get real-time support every day. Your partner manager will be super responsive when online, but anything sent when they’re offline will not be responded to until they’re online again. - -For real-time responses and simple troubleshooting issues, you can always message our general support by writing to Concierge via the in-product chat or by emailing concierge@expensify.com. - -# How do I know if I have a Partner Manager? -For your firm to be assigned a Partner Manager, you must complete the [ExpensifyApproved! University](https://use.expensify.com/accountants) training course. Every external accountant or bookkeeper who completes the training is automatically enrolled in our program and receives all the benefits, including access to the Partner Manager. So everyone at your firm must complete the training to receive the maximum benefit. - -You can check to see if you’ve completed the course and enrolled in the ExpensifyApproved! Accountants program simply by logging into your Expensify account. In the bottom left-hand corner of the website, you will see the ExpensifyApproved! logo. - -# How do I contact my Partner Manager? -You can contact your Partner Manager by: -- Signing in to new.expensify.com and searching for your Partner Manager -- Replying to or clicking the chat link on any email you get from your Partner Manager - -{% include faq-begin.md %} -## How do I know if my Partner Manager is online? -You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours. - -## What if I’m unable to reach my Partner Manager? -If you’re unable to contact your Partner Manager (i.e., they're out of office for the day) you can reach out to Concierge for assistance. Your Partner Manager will get back to you when they’re online again. - -## Can I get on a call with my Partner Manager? -Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm wide training, and client setups. - -We recommend continuing to work with Concierge for **general support questions**, as this team is always online and available to help immediately. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md index 30adac589dc0..b3f0ad3c6f6f 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md @@ -44,7 +44,7 @@ Expensify’s Budgets feature allows you to: {% include faq-begin.md %} ## Can I import budgets as a CSV? -At this time, you cannot import budgets via CSV since we don’t import categories or tags from direct accounting integrations. +At this time, you cannot import budgets via CSV. ## When will I be notified as a budget is hit? Notifications are sent twice: diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md index ea701dc09d3e..4a2dc56c430f 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md @@ -87,7 +87,7 @@ Concierge Receipt Audit is a real-time audit and compliance of receipts submitte - To make sure you don't miss any risky expenses that need human oversight. - To avoid needing to manually review all your company receipts. -- It's included for free with the [Control Plan](https://www.expensify.com/pricing). +- It's included at no extra cost with the [Control Plan](https://www.expensify.com/pricing). - Instead of paying someone to audit your company expenses or being concerned that your expenses might be audited by a government agency. - It's easy to use! Concierge will alert you to the risky expense and present it to you in an easy-to-follow review tutorial. - In addition to the risky expense alerts, Expensify will include a Note with audit details on every report. diff --git a/docs/redirects.csv b/docs/redirects.csv index d3a7fdd695a3..2571cb1156eb 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -24,9 +24,12 @@ https://community.expensify.com/discussion/5655/deep-dive-what-is-a-vacation-del 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/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#gsc.tab=0 -https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 -https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#gsc.tab=0 +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/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details +https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Perks.html,https://use.expensify.com/company-credit-card +https://help.expensify.com/articles/expensify-classic/expensify-partner-program/How-to-Join-the-ExpensifyApproved!-Partner-Program.html,https://use.expensify.com/accountants-program +https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners, https://use.expensify.com/blog/maximizing-rewards-expensifyapproved-accounting-partners-now-earn-0-5-revenue-share diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index acd08500fc11..41f53a0b8f7d 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -1043,7 +1043,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1064,7 +1063,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -1075,7 +1074,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1130,7 +1128,6 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1151,7 +1148,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -1162,7 +1159,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1218,7 +1214,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1239,7 +1234,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -1250,7 +1245,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1306,7 +1300,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1321,7 +1314,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -1332,7 +1325,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; @@ -1386,7 +1378,6 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1401,7 +1392,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -1412,7 +1403,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; @@ -1467,7 +1457,6 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; @@ -1482,7 +1471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -1493,7 +1482,6 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c636ced8e7f9..8ed1ada207a5 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.32 + 1.4.33 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.32.2 + 1.4.33.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ef1ef0d998d5..268b99e55b7e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.32 + 1.4.33 CFBundleSignature ???? CFBundleVersion - 1.4.32.2 + 1.4.33.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 16439b1d24d9..4ce550d62709 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -2,10 +2,18 @@ + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.32 + 1.4.33 CFBundleVersion - 1.4.32.2 + 1.4.33.3 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 776dcb544ee6..4cdf61554a6b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1967,7 +1967,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7 - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 diff --git a/jest/setup.js b/jest/setup.js index 38b4b55a68b3..e82bf678941d 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -19,7 +19,7 @@ jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); // Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, // and we only want to mock the storage layer, not the whole Onyx module. -jest.mock('react-native-onyx/lib/storage', () => require('react-native-onyx/lib/storage/__mocks__')); +jest.mock('react-native-onyx/dist/storage', () => require('react-native-onyx/dist/storage/__mocks__')); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params) => { diff --git a/metro.config.js b/metro.config.js index a4d0da1d85f4..2422d29aaacf 100644 --- a/metro.config.js +++ b/metro.config.js @@ -7,12 +7,6 @@ require('dotenv').config(); const defaultConfig = getDefaultConfig(__dirname); const isE2ETesting = process.env.E2E_TESTING === 'true'; - -if (isE2ETesting) { - // eslint-disable-next-line no-console - console.log('⚠️⚠️⚠️⚠️ Using mock API ⚠️⚠️⚠️⚠️'); -} - const e2eSourceExts = ['e2e.js', 'e2e.ts']; /** @@ -26,21 +20,6 @@ const config = { assetExts: [...defaultAssetExts, 'lottie'], // When we run the e2e tests we want files that have the extension e2e.js to be resolved as source files sourceExts: [...(isE2ETesting ? e2eSourceExts : []), ...defaultSourceExts, 'jsx'], - resolveRequest: (context, moduleName, platform) => { - const resolution = context.resolveRequest(context, moduleName, platform); - if (isE2ETesting && moduleName.includes('/API')) { - const originalPath = resolution.filePath; - const mockPath = originalPath.replace('src/libs/API.ts', 'src/libs/E2E/API.mock.ts').replace('/src/libs/API.ts/', 'src/libs/E2E/API.mock.ts'); - // eslint-disable-next-line no-console - console.log('⚠️⚠️⚠️⚠️ Replacing resolution path', originalPath, ' => ', mockPath); - - return { - ...resolution, - filePath: mockPath, - }; - } - return resolution; - }, }, }; diff --git a/package-lock.json b/package-lock.json index 543a1366f8d6..b0a9e09c4e16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.32-2", + "version": "1.4.33-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.32-2", + "version": "1.4.33-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -40,7 +40,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", "@shopify/flash-list": "^1.6.3", - "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.3.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -51,7 +50,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4a61536649cbfe49236a35bc7542b5dfd0767e4a", "expo": "^50.0.0-preview.7", "expo-image": "1.10.1", "fbjs": "^3.0.2", @@ -91,10 +90,11 @@ "react-native-image-picker": "^5.1.0", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", "react-native-key-command": "^1.0.6", + "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "2.0.1", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -169,6 +169,7 @@ "@types/js-yaml": "^4.0.5", "@types/lodash": "^4.14.195", "@types/mapbox-gl": "^2.7.13", + "@types/node": "^20.11.5", "@types/pusher-js": "^5.1.0", "@types/react": "18.2.45", "@types/react-beautiful-dnd": "^13.1.4", @@ -240,8 +241,8 @@ "yaml": "^2.2.1" }, "engines": { - "node": "20.9.0", - "npm": "10.1.0" + "node": "20.10.0", + "npm": "10.2.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4001,6 +4002,26 @@ "node": ">=8" } }, + "node_modules/@expo/config-plugins/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@expo/config-types": { "version": "45.0.0", "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-45.0.0.tgz", @@ -20646,9 +20667,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", - "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==" + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -22072,7 +22096,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "license": "BSD-3-Clause" + "deprecated": "Use your platform's native atob() and btoa() methods instead" }, "node_modules/abbrev": { "version": "1.1.1", @@ -22139,6 +22163,34 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -22753,6 +22805,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -22932,6 +22985,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -25706,10 +25760,9 @@ } }, "node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "license": "MIT" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz", + "integrity": "sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA==" }, "node_modules/clean-css": { "version": "5.3.2", @@ -27521,26 +27574,7 @@ "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "license": "MIT", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "license": "MIT" + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, "node_modules/csstype": { "version": "3.1.1", @@ -27585,20 +27619,6 @@ "node": ">= 6" } }, - "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -27819,6 +27839,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -28329,7 +28350,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "license": "MIT", + "deprecated": "Use your platform's native DOMException instead", "dependencies": { "webidl-conversions": "^7.0.0" }, @@ -28752,6 +28773,15 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz", "integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==" }, + "node_modules/electron/node_modules/@types/node": { + "version": "18.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", + "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/element-resize-detector": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", @@ -29017,6 +29047,7 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.1", @@ -29121,6 +29152,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3", "has": "^1.0.3", @@ -29144,6 +29176,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.1.4", @@ -31099,11 +31132,11 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", - "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#4a61536649cbfe49236a35bc7542b5dfd0767e4a", + "integrity": "sha512-UOy3btYvKRZ1kS4etLPw6Lgfqx+yiM3GMd340K06YLasn24alKgMOmg2dqSRTApF7RltS2FjOXRddAhzgvJZ3w==", "license": "MIT", "dependencies": { - "classnames": "2.3.1", + "classnames": "2.4.0", "clipboard": "2.0.11", "html-entities": "^2.4.0", "jquery": "3.6.0", @@ -31114,8 +31147,7 @@ "react-dom": "16.12.0", "semver": "^7.5.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "string.prototype.replaceall": "^1.0.8", - "ua-parser-js": "^1.0.35", + "ua-parser-js": "^1.0.37", "underscore": "1.13.6" } }, @@ -31159,9 +31191,9 @@ } }, "node_modules/expensify-common/node_modules/ua-parser-js": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", - "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", "funding": [ { "type": "opencollective", @@ -31170,9 +31202,12 @@ { "type": "paypal", "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } ], - "license": "MIT", "engines": { "node": "*" } @@ -32711,6 +32746,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -32736,6 +32772,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -32863,6 +32900,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -33000,6 +33038,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.1.3" @@ -33193,6 +33232,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -33237,6 +33277,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.1" @@ -33641,18 +33682,6 @@ "wbuf": "^1.1.0" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/html-entities": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", @@ -34754,6 +34783,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -34883,6 +34913,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -34917,6 +34948,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" @@ -34942,6 +34974,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35044,6 +35077,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -35294,6 +35328,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35315,6 +35350,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -35388,6 +35424,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -35414,6 +35451,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2" @@ -35438,6 +35476,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" @@ -35453,6 +35492,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" @@ -35520,6 +35560,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2" @@ -35590,6 +35631,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { @@ -36616,6 +36658,17 @@ "@types/yargs-parser": "*" } }, + "node_modules/jest-environment-jsdom/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -36665,6 +36718,59 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-environment-jsdom/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -36674,6 +36780,72 @@ "node": ">=8" } }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/jest-environment-jsdom/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -36686,6 +36858,67 @@ "node": ">=8" } }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "engines": { + "node": ">=12" + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -38698,130 +38931,6 @@ "node": ">=12.0.0" } }, - "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "license": "MIT", - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsdom/node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "license": "MIT", - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "node_modules/jsdom/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsdom/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "license": "MIT", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -42047,8 +42156,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.2", - "license": "MIT" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, "node_modules/ob1": { "version": "0.80.3", @@ -42195,6 +42305,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -42217,6 +42328,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -43959,8 +44071,7 @@ "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "license": "MIT" + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/public-encrypt": { "version": "4.0.3", @@ -44014,9 +44125,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } @@ -44979,6 +45090,15 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, + "node_modules/react-native-launch-arguments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-launch-arguments/-/react-native-launch-arguments-4.0.2.tgz", + "integrity": "sha512-OaXXOG9jVrmb66cTV8wPdhKxaSVivOBKuYr8wgKCM5uAHkY5B6SkvydgJ3B/x8uGoWqtr87scSYYDm4MMU4rSg==", + "peerDependencies": { + "react": ">=16.8.1", + "react-native": ">=0.60.0-rc.0 <1.0.x" + } + }, "node_modules/react-native-linear-gradient": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz", @@ -45023,17 +45143,17 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.1.tgz", + "integrity": "sha512-o6QNvq91qg8hFXIhmHjBqlNXD/YZxBZSRN8Vkq7xD2NYskzxK2mLqhBdhB8yMMwe6Cd8sVUK4vlZax/JU79xYw==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=16.15.1 <=20.9.0", - "npm": ">=8.11.0 <=10.1.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -46635,6 +46755,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -47344,6 +47465,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -47383,6 +47505,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -47423,6 +47546,17 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "license": "ISC" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", @@ -49073,26 +49207,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.replaceall": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz", - "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -49109,6 +49228,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -49122,6 +49242,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -50464,8 +50585,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", - "license": "BSD-3-Clause", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -50480,23 +50602,10 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, - "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/traverse": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", @@ -50753,6 +50862,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1", @@ -50766,6 +50876,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -50783,6 +50894,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -50801,6 +50913,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -50894,6 +51007,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -50910,6 +51024,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unfetch": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", @@ -51709,18 +51828,6 @@ "pbf": "^3.2.1" } }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/wait-port": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", @@ -52131,7 +52238,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", "engines": { "node": ">=12" } @@ -52820,45 +52926,11 @@ "node": ">=0.8.0" } }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "license": "MIT", - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/whatwg-url-without-unicode": { "version": "8.0.0-3", "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", @@ -52899,6 +52971,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", @@ -53192,9 +53265,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { "node": ">=10.0.0" }, @@ -53246,37 +53319,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "license": "Apache-2.0", - "engines": { - "node": ">=12" - } - }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", @@ -56315,6 +56357,20 @@ "requires": { "has-flag": "^4.0.0" } + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" } } }, @@ -68307,9 +68363,12 @@ "dev": true }, "@types/node": { - "version": "18.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", - "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==" + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/node-fetch": { "version": "2.6.4", @@ -69434,6 +69493,27 @@ } } }, + "acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "requires": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + }, + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" + }, + "acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==" + } + } + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -69896,6 +69976,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "requires": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -70017,6 +70098,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -72054,9 +72136,9 @@ } }, "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz", + "integrity": "sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA==" }, "clean-css": { "version": "5.3.2", @@ -73360,21 +73442,6 @@ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - } - } - }, "csstype": { "version": "3.1.1" }, @@ -73408,16 +73475,6 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" }, - "data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "requires": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - } - }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -73565,6 +73622,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -74053,6 +74111,17 @@ "@electron/get": "^2.0.0", "@types/node": "^18.11.18", "extract-zip": "^2.0.1" + }, + "dependencies": { + "@types/node": { + "version": "18.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", + "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + } } }, "electron-builder": { @@ -74451,6 +74520,7 @@ "version": "1.22.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.1", @@ -74544,6 +74614,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, "requires": { "get-intrinsic": "^1.1.3", "has": "^1.0.3", @@ -74563,6 +74634,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -75952,11 +76024,11 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", - "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#4a61536649cbfe49236a35bc7542b5dfd0767e4a", + "integrity": "sha512-UOy3btYvKRZ1kS4etLPw6Lgfqx+yiM3GMd340K06YLasn24alKgMOmg2dqSRTApF7RltS2FjOXRddAhzgvJZ3w==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#4a61536649cbfe49236a35bc7542b5dfd0767e4a", "requires": { - "classnames": "2.3.1", + "classnames": "2.4.0", "clipboard": "2.0.11", "html-entities": "^2.4.0", "jquery": "3.6.0", @@ -75967,8 +76039,7 @@ "react-dom": "16.12.0", "semver": "^7.5.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "string.prototype.replaceall": "^1.0.8", - "ua-parser-js": "^1.0.35", + "ua-parser-js": "^1.0.37", "underscore": "1.13.6" }, "dependencies": { @@ -76003,9 +76074,9 @@ } }, "ua-parser-js": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", - "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==" + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==" } } }, @@ -77137,6 +77208,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", @@ -77153,7 +77225,8 @@ "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true }, "gauge": { "version": "3.0.2", @@ -77241,6 +77314,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -77335,6 +77409,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, "requires": { "define-properties": "^1.1.3" } @@ -77466,7 +77541,8 @@ "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true }, "has-flag": { "version": "3.0.0", @@ -77497,6 +77573,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "requires": { "get-intrinsic": "^1.1.1" } @@ -77790,14 +77867,6 @@ "wbuf": "^1.1.0" } }, - "html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "requires": { - "whatwg-encoding": "^2.0.0" - } - }, "html-entities": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", @@ -78559,6 +78628,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "requires": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -78644,6 +78714,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -78668,6 +78739,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -78685,6 +78757,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -78740,6 +78813,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -78895,7 +78969,8 @@ "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true }, "is-number": { "version": "7.0.0", @@ -78906,6 +78981,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -78951,6 +79027,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -78966,6 +79043,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -78979,6 +79057,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -78987,6 +79066,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -79027,6 +79107,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -79070,7 +79151,8 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "isbinaryfile": { "version": "5.0.0", @@ -79840,6 +79922,11 @@ "@types/yargs-parser": "*" } }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -79870,11 +79957,100 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "requires": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "requires": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -79882,6 +80058,49 @@ "requires": { "has-flag": "^4.0.0" } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "requires": { + "xml-name-validator": "^4.0.0" + } + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + }, + "xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" } } }, @@ -81296,91 +81515,6 @@ "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true }, - "jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "requires": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" - }, - "acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "requires": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" - }, - "entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "requires": { - "entities": "^4.4.0" - } - }, - "saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "requires": { - "xmlchars": "^2.2.0" - } - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -83729,7 +83863,9 @@ "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" }, "nwsapi": { - "version": "2.2.2" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, "ob1": { "version": "0.80.3", @@ -83833,7 +83969,8 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -83848,6 +83985,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -85111,9 +85249,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "pusher-js": { "version": "8.3.0", @@ -85884,6 +86022,12 @@ } } }, + "react-native-launch-arguments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-launch-arguments/-/react-native-launch-arguments-4.0.2.tgz", + "integrity": "sha512-OaXXOG9jVrmb66cTV8wPdhKxaSVivOBKuYr8wgKCM5uAHkY5B6SkvydgJ3B/x8uGoWqtr87scSYYDm4MMU4rSg==", + "requires": {} + }, "react-native-linear-gradient": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz", @@ -85906,9 +86050,9 @@ } }, "react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.1.tgz", + "integrity": "sha512-o6QNvq91qg8hFXIhmHjBqlNXD/YZxBZSRN8Vkq7xD2NYskzxK2mLqhBdhB8yMMwe6Cd8sVUK4vlZax/JU79xYw==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -86929,6 +87073,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -87437,6 +87582,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -87468,6 +87614,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -87502,6 +87649,14 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "requires": { + "xmlchars": "^2.2.0" + } + }, "scheduler": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", @@ -88749,23 +88904,11 @@ "es-abstract": "^1.19.1" } }, - "string.prototype.replaceall": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz", - "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "is-regex": "^1.1.4" - } - }, "string.prototype.trim": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -88776,6 +88919,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -88786,6 +88930,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -89753,7 +89898,9 @@ "dev": true }, "tough-cookie": { - "version": "4.1.2", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -89768,14 +89915,6 @@ } } }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "requires": { - "punycode": "^2.1.1" - } - }, "traverse": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", @@ -89954,6 +90093,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1", @@ -89964,6 +90104,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -89975,6 +90116,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -89987,6 +90129,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, "requires": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -90046,6 +90189,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -90058,6 +90202,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "unfetch": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", @@ -90623,14 +90772,6 @@ "pbf": "^3.2.1" } }, - "w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "requires": { - "xml-name-validator": "^4.0.0" - } - }, "wait-port": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", @@ -91410,33 +91551,11 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "requires": { - "iconv-lite": "0.6.3" - } - }, "whatwg-fetch": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" }, - "whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" - }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - }, "whatwg-url-without-unicode": { "version": "8.0.0-3", "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", @@ -91466,6 +91585,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -91684,9 +91804,9 @@ } }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "requires": {} }, "x-default-browser": { @@ -91714,27 +91834,6 @@ } } }, - "xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "dependencies": { - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - } - } - }, "xmlbuilder": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", diff --git a/package.json b/package.json index 8ceac3912660..a57ab432d5fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.32-2", + "version": "1.4.33-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -88,7 +88,6 @@ "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", "@shopify/flash-list": "^1.6.3", - "@types/node": "^18.14.0", "@ua/react-native-airship": "^15.3.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", @@ -99,7 +98,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4a61536649cbfe49236a35bc7542b5dfd0767e4a", "expo": "^50.0.0-preview.7", "expo-image": "1.10.1", "fbjs": "^3.0.2", @@ -139,10 +138,11 @@ "react-native-image-picker": "^5.1.0", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b", "react-native-key-command": "^1.0.6", + "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "2.0.1", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -217,6 +217,7 @@ "@types/js-yaml": "^4.0.5", "@types/lodash": "^4.14.195", "@types/mapbox-gl": "^2.7.13", + "@types/node": "^20.11.5", "@types/pusher-js": "^5.1.0", "@types/react": "18.2.45", "@types/react-beautiful-dnd": "^13.1.4", @@ -315,7 +316,7 @@ ] }, "engines": { - "node": "20.9.0", - "npm": "10.1.0" + "node": "20.10.0", + "npm": "10.2.3" } } diff --git a/patches/react-native+0.73.2+001+NumberOfLines.patch b/patches/react-native+0.73.2+001+NumberOfLines.patch deleted file mode 100644 index c9ce92f8e1ef..000000000000 --- a/patches/react-native+0.73.2+001+NumberOfLines.patch +++ /dev/null @@ -1,968 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 55b770d..4073836 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -@@ -179,6 +179,13 @@ export type NativeProps = $ReadOnly<{| - */ - numberOfLines?: ?Int32, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ * @platform android -+ */ -+ maximumNumberOfLines?: ?Int32, -+ - /** - * When `false`, if there is a small amount of space available around a text input - * (e.g. landscape orientation on a phone), the OS may choose to have the user edit -diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index 88d3cc8..664d37d 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -@@ -144,6 +144,8 @@ const RCTTextInputViewConfig = { - placeholder: true, - autoCorrect: true, - multiline: true, -+ numberOfLines: true, -+ maximumNumberOfLines: true, - textContentType: true, - maxLength: true, - autoCapitalize: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -index 2c0c099..5cb6bf1 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -@@ -695,11 +695,29 @@ export interface TextInputProps - */ - maxLength?: number | undefined; - -+ /** -+ * Sets the maximum number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ maxNumberOfLines?: number | undefined; -+ - /** - * If true, the text input can be multiple lines. The default value is false. - */ - multiline?: boolean | undefined; - -+ /** -+ * Sets the number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ numberOfLines?: number | undefined; -+ -+ /** -+ * Sets the number of rows for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ rows?: number | undefined; -+ - /** - * Callback that is called when the text input is blurred - */ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -index 9adbfe9..dc52051 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -@@ -366,26 +366,12 @@ type AndroidProps = $ReadOnly<{| - */ - inlineImagePadding?: ?number, - -- /** -- * Sets the number of lines for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- numberOfLines?: ?number, -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android - */ - returnKeyLabel?: ?string, - -- /** -- * Sets the number of rows for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- rows?: ?number, -- - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -680,12 +666,24 @@ export type Props = $ReadOnly<{| - */ - maxLength?: ?number, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * If `true`, the text input can be multiple lines. - * The default value is `false`. - */ - multiline?: ?boolean, - -+ /** -+ * Sets the number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ numberOfLines?: ?number, -+ - /** - * Callback that is called when the text input is blurred. - */ -@@ -847,6 +845,13 @@ export type Props = $ReadOnly<{| - */ - returnKeyType?: ?ReturnKeyType, - -+ /** -+ * Sets the number of rows for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ rows?: ?number, -+ -+ - /** - * If `true`, the text input obscures the text entered so that sensitive text - * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 481938f..3ce7422 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -413,7 +413,6 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of lines for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - numberOfLines?: ?number, - -@@ -426,10 +425,15 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of rows for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - rows?: ?number, - -+ /** -+ * Sets the maximum number of lines the TextInput can have. -+ */ -+ maxNumberOfLines?: ?number, -+ -+ - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -1102,6 +1106,9 @@ function InternalTextInput(props: Props): React.Node { - accessibilityState, - id, - tabIndex, -+ rows, -+ numberOfLines, -+ maxNumberOfLines, - selection: propsSelection, - ...otherProps - } = props; -@@ -1460,6 +1467,8 @@ function InternalTextInput(props: Props): React.Node { - focusable={tabIndex !== undefined ? !tabIndex : focusable} - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} -+ numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onKeyPressSync={props.unstable_onKeyPressSync} - onChange={_onChange} -@@ -1515,6 +1524,7 @@ function InternalTextInput(props: Props): React.Node { - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} - numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onChange={_onChange} - onFocus={_onFocus} -diff --git a/node_modules/react-native/Libraries/Text/Text.js b/node_modules/react-native/Libraries/Text/Text.js -index d737ccc..beee7ce 100644 ---- a/node_modules/react-native/Libraries/Text/Text.js -+++ b/node_modules/react-native/Libraries/Text/Text.js -@@ -17,7 +17,11 @@ import flattenStyle from '../StyleSheet/flattenStyle'; - import processColor from '../StyleSheet/processColor'; - import Platform from '../Utilities/Platform'; - import TextAncestor from './TextAncestor'; --import {NativeText, NativeVirtualText} from './TextNativeComponent'; -+import { -+ CONTAINS_MAX_NUMBER_OF_LINES_RENAME, -+ NativeText, -+ NativeVirtualText, -+} from './TextNativeComponent'; - import * as React from 'react'; - import {useContext, useMemo, useState} from 'react'; - -@@ -56,6 +60,7 @@ const Text: React.AbstractComponent< - onStartShouldSetResponder, - pressRetentionOffset, - suppressHighlighting, -+ numberOfLines, - ...restProps - } = props; - -@@ -195,14 +200,34 @@ const Text: React.AbstractComponent< - } - } - -- let numberOfLines = restProps.numberOfLines; -+ let numberOfLinesValue = numberOfLines; - if (numberOfLines != null && !(numberOfLines >= 0)) { - console.error( - `'numberOfLines' in must be a non-negative number, received: ${numberOfLines}. The value will be set to 0.`, - ); -- numberOfLines = 0; -+ numberOfLinesValue = 0; - } - -+ const numberOfLinesProps = useMemo((): { -+ maximumNumberOfLines?: ?number, -+ numberOfLines?: ?number, -+ } => { -+ // FIXME: Current logic is breaking all Text components. -+ // if (CONTAINS_MAX_NUMBER_OF_LINES_RENAME) { -+ // return { -+ // maximumNumberOfLines: numberOfLinesValue, -+ // }; -+ // } else { -+ // return { -+ // numberOfLines: numberOfLinesValue, -+ // }; -+ // } -+ return { -+ maximumNumberOfLines: numberOfLinesValue, -+ }; -+ }, [numberOfLinesValue]); -+ -+ - const hasTextAncestor = useContext(TextAncestor); - - const _accessible = Platform.select({ -@@ -251,7 +276,6 @@ const Text: React.AbstractComponent< - isHighlighted={isHighlighted} - isPressable={isPressable} - nativeID={id ?? nativeID} -- numberOfLines={numberOfLines} - ref={forwardedRef} - selectable={_selectable} - selectionColor={selectionColor} -@@ -262,6 +286,7 @@ const Text: React.AbstractComponent< - - #import -+#import -+#import - - @implementation RCTMultilineTextInputViewManager - -@@ -17,8 +19,21 @@ - (UIView *)view - return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; - } - -+- (RCTShadowView *)shadowView -+{ -+ RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; -+ -+ shadowView.maximumNumberOfLines = 0; -+ shadowView.exactNumberOfLines = 0; -+ -+ return shadowView; -+} -+ - #pragma mark - Multiline (aka TextView) specific properties - - RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) - -+RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) -+RCT_REMAP_SHADOW_PROPERTY(numberOfLines, exactNumberOfLines, NSInteger) -+ - @end -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -index 8f4cf7e..6238ebc 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -@@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - @property (nonatomic, copy, nullable) NSString *text; - @property (nonatomic, copy, nullable) NSString *placeholder; - @property (nonatomic, assign) NSInteger maximumNumberOfLines; -+@property (nonatomic, assign) NSInteger exactNumberOfLines; - @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; - - - (void)uiManagerWillPerformMounting; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm -index 1f06b79..48172ce 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm -@@ -218,7 +218,22 @@ - (NSAttributedString *)measurableAttributedText - - - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize - { -- NSAttributedString *attributedText = [self measurableAttributedText]; -+ NSMutableAttributedString *attributedText = [[self measurableAttributedText] mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. -+ */ -+ if (self.exactNumberOfLines) { -+ NSMutableString *newLines = [NSMutableString stringWithCapacity:self.exactNumberOfLines]; -+ for (NSUInteger i = 0UL; i < self.exactNumberOfLines; ++i) { -+ [newLines appendString:@"\n"]; -+ } -+ [attributedText insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:self.textAttributes.effectiveTextAttributes] atIndex:0]; -+ _maximumNumberOfLines = self.exactNumberOfLines; -+ } - - if (!_textStorage) { - _textContainer = [NSTextContainer new]; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm -index 413ac42..56d039c 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.mm -@@ -19,6 +19,7 @@ - (RCTShadowView *)shadowView - RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; - - shadowView.maximumNumberOfLines = 1; -+ shadowView.exactNumberOfLines = 0; - - return shadowView; - } -diff --git a/node_modules/react-native/Libraries/Text/TextNativeComponent.js b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -index 0d59904..3216e43 100644 ---- a/node_modules/react-native/Libraries/Text/TextNativeComponent.js -+++ b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -@@ -9,6 +9,7 @@ - */ - - import {createViewConfig} from '../NativeComponent/ViewConfig'; -+import getNativeComponentAttributes from '../ReactNative/getNativeComponentAttributes'; - import UIManager from '../ReactNative/UIManager'; - import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; - import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; -@@ -18,6 +19,7 @@ import {type TextProps} from './TextProps'; - - type NativeTextProps = $ReadOnly<{ - ...TextProps, -+ maximumNumberOfLines?: ?number, - isHighlighted?: ?boolean, - selectionColor?: ?ProcessedColorValue, - onClick?: ?(event: PressEvent) => mixed, -@@ -31,7 +33,7 @@ const textViewConfig = { - validAttributes: { - isHighlighted: true, - isPressable: true, -- numberOfLines: true, -+ maximumNumberOfLines: true, - ellipsizeMode: true, - allowFontScaling: true, - dynamicTypeRamp: true, -@@ -73,6 +75,12 @@ export const NativeText: HostComponent = - createViewConfig(textViewConfig), - ): any); - -+const jestIsDefined = typeof jest !== 'undefined'; -+export const CONTAINS_MAX_NUMBER_OF_LINES_RENAME: boolean = jestIsDefined -+ ? true -+ : getNativeComponentAttributes('RCTText')?.NativeProps -+ ?.maximumNumberOfLines === 'number'; -+ - export const NativeVirtualText: HostComponent = - !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') - ? NativeText -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -index 8cab407..ad5fa96 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -@@ -12,5 +12,6 @@ public class ViewDefaults { - - public static final float FONT_SIZE_SP = 14.0f; - public static final int LINE_HEIGHT = 0; -- public static final int NUMBER_OF_LINES = Integer.MAX_VALUE; -+ public static final int NUMBER_OF_LINES = -1; -+ public static final int MAXIMUM_NUMBER_OF_LINES = Integer.MAX_VALUE; - } -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -index fa6eae3..f524753 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -@@ -96,6 +96,7 @@ public class ViewProps { - public static final String LETTER_SPACING = "letterSpacing"; - public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing"; - public static final String NUMBER_OF_LINES = "numberOfLines"; -+ public static final String MAXIMUM_NUMBER_OF_LINES = "maximumNumberOfLines"; - public static final String ELLIPSIZE_MODE = "ellipsizeMode"; - public static final String ADJUSTS_FONT_SIZE_TO_FIT = "adjustsFontSizeToFit"; - public static final String MINIMUM_FONT_SCALE = "minimumFontScale"; -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -index d2c2d6e..e4dec5d 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -@@ -311,6 +311,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { - protected @Nullable Role mRole = null; - - protected int mNumberOfLines = UNSET; -+ protected int mMaxNumberOfLines = UNSET; - protected int mTextAlign = Gravity.NO_GRAVITY; - protected int mTextBreakStrategy = - (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY; -@@ -395,6 +396,12 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { - markUpdated(); - } - -+ @ReactProp(name = ViewProps.MAXIMUM_NUMBER_OF_LINES, defaultInt = UNSET) -+ public void setMaxNumberOfLines(int numberOfLines) { -+ mMaxNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; -+ markUpdated(); -+ } -+ - @ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN) - public void setLineHeight(float lineHeight) { - mTextAttributes.setLineHeight(lineHeight); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -index f683c24..b5f6f7d 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -@@ -49,8 +49,8 @@ public abstract class ReactTextAnchorViewManager minimumFontSize -- && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines -+ && (mMaxNumberOfLines != UNSET && layout.getLineCount() > mMaxNumberOfLines - || heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) { - // TODO: We could probably use a smarter algorithm here. This will require 0(n) - // measurements -@@ -124,9 +124,9 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { - } - - final int lineCount = -- mNumberOfLines == UNSET -+ mMaxNumberOfLines == UNSET - ? layout.getLineCount() -- : Math.min(mNumberOfLines, layout.getLineCount()); -+ : Math.min(mMaxNumberOfLines, layout.getLineCount()); - - // Instead of using `layout.getWidth()` (which may yield a significantly larger width for - // text that is wrapping), compute width using the longest line. -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -index 4af5729..64474af 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -@@ -90,7 +90,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - - mReactBackgroundManager = new ReactViewBackgroundManager(this); - -- mNumberOfLines = ViewDefaults.NUMBER_OF_LINES; -+ mNumberOfLines = ViewDefaults.MAXIMUM_NUMBER_OF_LINES; - mAdjustsFontSizeToFit = false; - mLinkifyMaskType = 0; - mNotifyOnInlineViewLayout = false; -@@ -579,7 +579,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - } - - public void setNumberOfLines(int numberOfLines) { -- mNumberOfLines = numberOfLines == 0 ? ViewDefaults.NUMBER_OF_LINES : numberOfLines; -+ mNumberOfLines = numberOfLines == 0 ? ViewDefaults.MAXIMUM_NUMBER_OF_LINES : numberOfLines; - setMaxLines(mNumberOfLines); - } - -@@ -621,7 +621,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - public void updateView() { - @Nullable - TextUtils.TruncateAt ellipsizeLocation = -- mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit -+ mNumberOfLines == ViewDefaults.MAXIMUM_NUMBER_OF_LINES || mAdjustsFontSizeToFit - ? null - : mEllipsizeLocation; - setEllipsize(ellipsizeLocation); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -index ffd5b2f..e9a8b0b 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -@@ -19,6 +19,7 @@ import android.text.SpannableStringBuilder; - import android.text.Spanned; - import android.text.StaticLayout; - import android.text.TextPaint; -+import android.text.TextUtils; - import android.util.LayoutDirection; - import android.util.LruCache; - import android.view.View; -@@ -68,6 +69,7 @@ public class TextLayoutManager { - private static final String TEXT_BREAK_STRATEGY_KEY = "textBreakStrategy"; - private static final String HYPHENATION_FREQUENCY_KEY = "android_hyphenationFrequency"; - private static final String MAXIMUM_NUMBER_OF_LINES_KEY = "maximumNumberOfLines"; -+ private static final String NUMBER_OF_LINES_KEY = "numberOfLines"; - private static final LruCache sSpannableCache = - new LruCache<>(spannableCacheSize); - private static final ConcurrentHashMap sTagToSpannableCache = -@@ -395,6 +397,48 @@ public class TextLayoutManager { - ? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY) - : UNSET; - -+ int numberOfLines = -+ paragraphAttributes.hasKey(NUMBER_OF_LINES_KEY) -+ ? paragraphAttributes.getInt(NUMBER_OF_LINES_KEY) -+ : UNSET; -+ -+ int lines = layout.getLineCount(); -+ if (numberOfLines != UNSET && numberOfLines != 0 && numberOfLines >= lines && text.length() > 0) { -+ int numberOfEmptyLines = numberOfLines - lines; -+ SpannableStringBuilder ssb = new SpannableStringBuilder(); -+ -+ // for some reason a newline on end causes issues with computing height so we add a character -+ if (text.toString().endsWith("\n")) { -+ ssb.append("A"); -+ } -+ -+ for (int i = 0; i < numberOfEmptyLines; ++i) { -+ ssb.append("\nA"); -+ } -+ -+ Object[] spans = text.getSpans(0, 0, Object.class); -+ for (Object span : spans) { // It's possible we need to set exl-exl -+ ssb.setSpan(span, 0, ssb.length(), text.getSpanFlags(span)); -+ }; -+ -+ text = new SpannableStringBuilder(TextUtils.concat(text, ssb)); -+ boring = null; -+ layout = createLayout( -+ text, -+ boring, -+ width, -+ widthYogaMeasureMode, -+ includeFontPadding, -+ textBreakStrategy, -+ hyphenationFrequency); -+ } -+ -+ -+ if (numberOfLines != UNSET && numberOfLines != 0) { -+ maximumNumberOfLines = numberOfLines; -+ } -+ -+ - int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 - ? layout.getLineCount() -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -index 8cd5764..35a2e9e 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -@@ -20,6 +20,7 @@ import android.text.SpannableStringBuilder; - import android.text.Spanned; - import android.text.StaticLayout; - import android.text.TextPaint; -+import android.text.TextUtils; - import android.util.LayoutDirection; - import android.util.LruCache; - import android.view.View; -@@ -66,6 +67,7 @@ public class TextLayoutManagerMapBuffer { - public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - public static final short PA_KEY_INCLUDE_FONT_PADDING = 4; - public static final short PA_KEY_HYPHENATION_FREQUENCY = 5; -+ public static final short PA_KEY_NUMBER_OF_LINES = 6; - - private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false; - -@@ -417,6 +419,46 @@ public class TextLayoutManagerMapBuffer { - ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) - : UNSET; - -+ int numberOfLines = -+ paragraphAttributes.contains(PA_KEY_NUMBER_OF_LINES) -+ ? paragraphAttributes.getInt(PA_KEY_NUMBER_OF_LINES) -+ : UNSET; -+ -+ int lines = layout.getLineCount(); -+ if (numberOfLines != UNSET && numberOfLines != 0 && numberOfLines > lines && text.length() > 0) { -+ int numberOfEmptyLines = numberOfLines - lines; -+ SpannableStringBuilder ssb = new SpannableStringBuilder(); -+ -+ // for some reason a newline on end causes issues with computing height so we add a character -+ if (text.toString().endsWith("\n")) { -+ ssb.append("A"); -+ } -+ -+ for (int i = 0; i < numberOfEmptyLines; ++i) { -+ ssb.append("\nA"); -+ } -+ -+ Object[] spans = text.getSpans(0, 0, Object.class); -+ for (Object span : spans) { // It's possible we need to set exl-exl -+ ssb.setSpan(span, 0, ssb.length(), text.getSpanFlags(span)); -+ }; -+ -+ text = new SpannableStringBuilder(TextUtils.concat(text, ssb)); -+ boring = null; -+ layout = createLayout( -+ text, -+ boring, -+ width, -+ widthYogaMeasureMode, -+ includeFontPadding, -+ textBreakStrategy, -+ hyphenationFrequency); -+ } -+ -+ if (numberOfLines != UNSET && numberOfLines != 0) { -+ maximumNumberOfLines = numberOfLines; -+ } -+ - int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 - ? layout.getLineCount() -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -index 081f2b8..0659179 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -@@ -546,9 +546,15 @@ public class ReactEditText extends AppCompatEditText { - * android.widget.TextView#isMultilineInputType(int)}} Source: {@Link TextView.java} - */ -- if (isMultiline()) { -- setSingleLine(false); -- } -+ if (isMultiline()) { -+ // we save max lines as setSingleLines overwrites it -+ // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/TextView.java#10671 -+ int maxLines = getMaxLines(); -+ setSingleLine(false); -+ if (maxLines != -1) { -+ setMaxLines(maxLines); -+ } -+ } - - // We override the KeyListener so that all keys on the soft input keyboard as well as hardware - // keyboards work. Some KeyListeners like DigitsKeyListener will display the keyboard but not -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -index a850510..c59be1d 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -@@ -41,9 +41,9 @@ public final class ReactTextInputLocalData { - public void apply(EditText editText) { - editText.setText(mText); - editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); -+ editText.setInputType(mInputType); - editText.setMinLines(mMinLines); - editText.setMaxLines(mMaxLines); -- editText.setInputType(mInputType); - editText.setHint(mPlaceholder); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - editText.setBreakStrategy(mBreakStrategy); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 8496a7d..e4d975b 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -736,9 +736,18 @@ public class ReactTextInputManager extends BaseViewManager= Build.VERSION_CODES.M -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -index f2317ba..10f342c 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -@@ -16,6 +16,7 @@ namespace facebook::react { - - bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const { - return std::tie( -+ numberOfLines, - maximumNumberOfLines, - ellipsizeMode, - textBreakStrategy, -@@ -23,6 +24,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes& rhs) const { - includeFontPadding, - android_hyphenationFrequency) == - std::tie( -+ rhs.numberOfLines, - rhs.maximumNumberOfLines, - rhs.ellipsizeMode, - rhs.textBreakStrategy, -@@ -42,6 +44,7 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes& rhs) const { - #if RN_DEBUG_STRING_CONVERTIBLE - SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { - return { -+ debugStringConvertibleItem("numberOfLines", numberOfLines), - debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines), - debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), - debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -index d73f863..1f85b22 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -@@ -29,6 +29,11 @@ class ParagraphAttributes : public DebugStringConvertible { - public: - #pragma mark - Fields - -+ /* -+ * Number of lines which paragraph takes. -+ */ -+ int numberOfLines{}; -+ - /* - * Maximum number of lines which paragraph can take. - * Zero value represents "no limit". -@@ -89,6 +94,7 @@ struct hash { - size_t operator()( - const facebook::react::ParagraphAttributes& attributes) const { - return facebook::react::hash_combine( -+ attributes.numberOfLines, - attributes.maximumNumberOfLines, - attributes.ellipsizeMode, - attributes.textBreakStrategy, -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -index 445e452..3f0bb36 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -@@ -692,10 +692,16 @@ inline ParagraphAttributes convertRawProp( - const ParagraphAttributes& defaultParagraphAttributes) { - auto paragraphAttributes = ParagraphAttributes{}; - -- paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ paragraphAttributes.numberOfLines = convertRawProp( - context, - rawProps, - "numberOfLines", -+ sourceParagraphAttributes.numberOfLines, -+ defaultParagraphAttributes.numberOfLines); -+ paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ context, -+ rawProps, -+ "maximumNumberOfLines", - sourceParagraphAttributes.maximumNumberOfLines, - defaultParagraphAttributes.maximumNumberOfLines); - paragraphAttributes.ellipsizeMode = convertRawProp( -@@ -770,6 +776,7 @@ inline std::string toString(const AttributedString::Range& range) { - inline folly::dynamic toDynamic( - const ParagraphAttributes& paragraphAttributes) { - auto values = folly::dynamic::object(); -+ values("numberOfLines", paragraphAttributes.numberOfLines); - values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); - values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); - values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); -@@ -979,6 +986,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; - constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; - constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; -+constexpr static MapBuffer::Key PA_KEY_NUMBER_OF_LINES = 6; - - inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) { - auto builder = MapBufferBuilder(); -@@ -996,6 +1004,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes& paragraphAttributes) { - builder.putString( - PA_KEY_HYPHENATION_FREQUENCY, - toString(paragraphAttributes.android_hyphenationFrequency)); -+ builder.putInt( -+ PA_KEY_NUMBER_OF_LINES, paragraphAttributes.numberOfLines); - - return builder.build(); - } -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -index 116284f..5749c57 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -@@ -56,6 +56,10 @@ AndroidTextInputProps::AndroidTextInputProps( - "numberOfLines", - sourceProps.numberOfLines, - {0})), -+ maximumNumberOfLines(CoreFeatures::enablePropIteratorSetter? sourceProps.maximumNumberOfLines : convertRawProp(context, rawProps, -+ "maximumNumberOfLines", -+ sourceProps.maximumNumberOfLines, -+ {0})), - disableFullscreenUI(CoreFeatures::enablePropIteratorSetter? sourceProps.disableFullscreenUI : convertRawProp(context, rawProps, - "disableFullscreenUI", - sourceProps.disableFullscreenUI, -@@ -281,6 +285,12 @@ void AndroidTextInputProps::setProp( - value, - paragraphAttributes, - maximumNumberOfLines, -+ "maximumNumberOfLines"); -+ REBUILD_FIELD_SWITCH_CASE( -+ paDefaults, -+ value, -+ paragraphAttributes, -+ numberOfLines, - "numberOfLines"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); -@@ -323,6 +333,7 @@ void AndroidTextInputProps::setProp( - } - - switch (hash) { -+ RAW_SET_PROP_SWITCH_CASE_BASIC(maximumNumberOfLines); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); - RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); - RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); -@@ -422,6 +433,7 @@ void AndroidTextInputProps::setProp( - // TODO T53300085: support this in codegen; this was hand-written - folly::dynamic AndroidTextInputProps::getDynamic() const { - folly::dynamic props = folly::dynamic::object(); -+ props["maximumNumberOfLines"] = maximumNumberOfLines; - props["autoComplete"] = autoComplete; - props["returnKeyLabel"] = returnKeyLabel; - props["numberOfLines"] = numberOfLines; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -index 43cbb68..0bf63e7 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -@@ -81,6 +81,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { - std::string autoComplete{}; - std::string returnKeyLabel{}; - int numberOfLines{0}; -+ int maximumNumberOfLines{0}; - bool disableFullscreenUI{false}; - std::string textBreakStrategy{}; - SharedColor underlineColorAndroid{}; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -index 368c334..ef9ec17 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -@@ -244,26 +244,49 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString - - #pragma mark - Private - --- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString -+- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)inputAttributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - size:(CGSize)size - { -- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; -+NSMutableAttributedString *attributedString = [ inputAttributedString mutableCopy]; -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. This method is used for drawing only for Paragraph component -+ * but we set exact height in lines only on TextInput that doesn't use it. -+ */ -+ if (paragraphAttributes.numberOfLines) { -+ paragraphAttributes.maximumNumberOfLines = paragraphAttributes.numberOfLines; -+ NSMutableString *newLines = [NSMutableString stringWithCapacity: paragraphAttributes.numberOfLines]; -+ for (NSUInteger i = 0UL; i < paragraphAttributes.numberOfLines; ++i) { -+ // K is added on purpose. New line seems to be not enough for NTtextContainer -+ [newLines appendString:@"K\n"]; -+ } -+ NSDictionary * attributesOfFirstCharacter = [inputAttributedString attributesAtIndex:0 effectiveRange:NULL]; - -- textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -- textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -- ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -- : NSLineBreakByClipping; -- textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:attributesOfFirstCharacter] atIndex:0]; -+ } -+ -+ NSTextContainer *textContainer = [NSTextContainer new]; - - NSLayoutManager *layoutManager = [NSLayoutManager new]; - layoutManager.usesFontLeading = NO; - [layoutManager addTextContainer:textContainer]; - -- NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; -+ NSTextStorage *textStorage = [NSTextStorage new]; - - [textStorage addLayoutManager:layoutManager]; - -+ textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -+ textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -+ ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -+ : NSLineBreakByClipping; -+ textContainer.size = size; -+ textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ -+ [textStorage replaceCharactersInRange:(NSRange){0, textStorage.length} withAttributedString:attributedString]; -+ - if (paragraphAttributes.adjustsFontSizeToFit) { - CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; - CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; diff --git a/src/CONST.ts b/src/CONST.ts index a0696560dc56..f9537d15e46e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -458,6 +458,8 @@ const CONST = { NEW_ZOOM_MEETING_URL: 'https://zoom.us/start/videomeeting', NEW_GOOGLE_MEET_MEETING_URL: 'https://meet.google.com/new', GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', + GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', + IMAGE_BASE64_MATCH: 'base64', DEEPLINK_BASE_URL: 'new-expensify://', PDF_VIEWER_URL: '/pdf/web/viewer.html', CLOUDFRONT_DOMAIN_REGEX: /^https:\/\/\w+\.cloudfront\.net/i, @@ -606,7 +608,6 @@ const CONST = { ROOMCHANGELOG: { INVITE_TO_ROOM: 'INVITETOROOM', REMOVE_FROM_ROOM: 'REMOVEFROMROOM', - JOIN_ROOM: 'JOINROOM', }, }, THREAD_DISABLED: ['CREATED'], @@ -790,6 +791,7 @@ const CONST = { EXP_ERROR: 666, MANY_WRITES_ERROR: 665, UNABLE_TO_RETRY: 'unableToRetry', + UPDATE_REQUIRED: 426, }, HTTP_STATUS: { // When Cloudflare throttles @@ -820,6 +822,9 @@ const CONST = { GATEWAY_TIMEOUT: 'Gateway Timeout', EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted', DUPLICATE_RECORD: 'A record already exists with this ID', + + // The "Upgrade" is intentional as the 426 HTTP code means "Upgrade Required" and sent by the API. We use the "Update" language everywhere else in the front end when this gets returned. + UPDATE_REQUIRED: 'Upgrade Required', }, ERROR_TYPE: { SOCKET: 'Expensify\\Auth\\Error\\Socket', @@ -920,6 +925,7 @@ const CONST = { KEYBOARD_TYPE: { VISIBLE_PASSWORD: 'visible-password', ASCII_CAPABLE: 'ascii-capable', + NUMBER_PAD: 'number-pad', }, INPUT_MODE: { @@ -3057,13 +3063,6 @@ const CONST = { */ MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500, - /** - * Performance test setup - run the same test multiple times to get a more accurate result - */ - PERFORMANCE_TESTS: { - RUNS: 20, - }, - /** * Bank account names */ diff --git a/src/Expensify.js b/src/Expensify.js index 0707ba069241..12003968b284 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -13,6 +13,7 @@ import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; import UpdateAppModal from './components/UpdateAppModal'; import withLocalize, {withLocalizePropTypes} from './components/withLocalize'; +import CONST from './CONST'; import * as EmojiPickerAction from './libs/actions/EmojiPickerAction'; import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; @@ -76,6 +77,9 @@ const propTypes = { /** Whether the app is waiting for the server's response to determine if a room is public */ isCheckingPublicRoom: PropTypes.bool, + /** True when the user must update to the latest minimum version of the app */ + updateRequired: PropTypes.bool, + /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */ focusModeNotification: PropTypes.bool, @@ -91,6 +95,7 @@ const defaultProps = { isSidebarLoaded: false, screenShareRequest: null, isCheckingPublicRoom: true, + updateRequired: false, focusModeNotification: false, }; @@ -204,6 +209,10 @@ function Expensify(props) { return null; } + if (props.updateRequired) { + throw new Error(CONST.ERROR.UPDATE_REQUIRED); + } + return ( {/* We include the modal for showing a new update at the top level so the option is always present. */} - {props.updateAvailable ? : null} + {/* If the update is required we won't show this option since a full screen update view will be displayed instead. */} + {props.updateAvailable && !props.updateRequired ? : null} {props.screenShareRequest ? ( element MAX_CANVAS_WIDTH: 'maxCanvasWidth', + /** Indicates whether an forced upgrade is required */ + UPDATE_REQUIRED: 'updateRequired', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -358,13 +358,15 @@ const ONYXKEYS = { GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft', POLICY_REPORT_FIELD_EDIT_FORM: 'policyReportFieldEditForm', POLICY_REPORT_FIELD_EDIT_FORM_DRAFT: 'policyReportFieldEditFormDraft', + REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', + REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', }, } as const; type OnyxKeysMap = typeof ONYXKEYS; type OnyxCollectionKey = ValueOf; type OnyxKey = DeepValueOf>; -type OnyxFormKey = ValueOf | OnyxKeysMap['REIMBURSEMENT_ACCOUNT'] | OnyxKeysMap['REIMBURSEMENT_ACCOUNT_DRAFT']; +type OnyxFormKey = ValueOf; type OnyxValues = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; @@ -419,7 +421,6 @@ type OnyxValues = { [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; - [ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT]: OnyxTypes.ReimbursementAccountDraft; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID]: string; @@ -442,6 +443,7 @@ type OnyxValues = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; + [ONYXKEYS.UPDATE_REQUIRED]: boolean; // Collections [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; @@ -486,8 +488,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.DisplayNameForm; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.DisplayNameForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; @@ -500,8 +502,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.NewRoomForm; + [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.NewRoomForm; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_TASK_FORM]: OnyxTypes.Form; @@ -526,20 +528,23 @@ type OnyxValues = { [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.PrivateNotesForm; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.PrivateNotesForm; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.IKnowATeacherForm; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.IKnowATeacherForm; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.IntroSchoolPrincipalForm; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.IntroSchoolPrincipalForm; [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form | undefined; + [ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form; + // @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm + [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 0f3416076cc0..05080fcdd21c 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {ForwardedRef} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; @@ -34,7 +35,7 @@ type AmountTextInputProps = { function AmountTextInput( {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress}: AmountTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const styles = useThemeStyles(); return ( diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 04e8a5f8d55b..7d00bac54dca 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,11 +1,10 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@userActions/Session'; -import type {PersonalDetails, Report} from '@src/types/onyx'; +import type {PersonalDetailsList, Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; @@ -19,7 +18,7 @@ type AnonymousReportFooterProps = { isSmallSizeLayout?: boolean; /** Personal details of all the users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; function AnonymousReportFooter({isSmallSizeLayout = false, personalDetails, report}: AnonymousReportFooterProps) { diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 4988c33ed8ce..46ce4cf63f26 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -501,7 +501,6 @@ function AttachmentModal(props) { report={props.report} onNavigate={onNavigate} source={props.source} - onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} setDownloadButtonVisibility={setDownloadButtonVisibility} /> diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js deleted file mode 100644 index abaf06900853..000000000000 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.js +++ /dev/null @@ -1,5 +0,0 @@ -import {createContext} from 'react'; - -const AttachmentCarouselPagerContext = createContext(null); - -export default AttachmentCarouselPagerContext; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts new file mode 100644 index 000000000000..270e0b04909c --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -0,0 +1,17 @@ +import type {ForwardedRef} from 'react'; +import {createContext} from 'react'; +import type PagerView from 'react-native-pager-view'; +import type {SharedValue} from 'react-native-reanimated'; + +type AttachmentCarouselPagerContextValue = { + pagerRef: ForwardedRef; + isPagerScrolling: SharedValue; + isScrollEnabled: SharedValue; + onTap: () => void; + onScaleChanged: (scale: number) => void; +}; + +const AttachmentCarouselPagerContext = createContext(null); + +export default AttachmentCarouselPagerContext; +export type {AttachmentCarouselPagerContextValue}; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js deleted file mode 100644 index 553e963a3461..000000000000 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.js +++ /dev/null @@ -1,172 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {createNativeWrapper} from 'react-native-gesture-handler'; -import PagerView from 'react-native-pager-view'; -import Animated, {runOnJS, useAnimatedProps, useAnimatedReaction, useEvent, useHandler, useSharedValue} from 'react-native-reanimated'; -import _ from 'underscore'; -import refPropTypes from '@components/refPropTypes'; -import useThemeStyles from '@hooks/useThemeStyles'; -import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; - -const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView)); - -function usePageScrollHandler(handlers, dependencies) { - const {context, doDependenciesDiffer} = useHandler(handlers, dependencies); - const subscribeForEvents = ['onPageScroll']; - - return useEvent( - (event) => { - 'worklet'; - - const {onPageScroll} = handlers; - if (onPageScroll && event.eventName.endsWith('onPageScroll')) { - onPageScroll(event, context); - } - }, - subscribeForEvents, - doDependenciesDiffer, - ); -} - -const noopWorklet = () => { - 'worklet'; - - // noop -}; - -const pagerPropTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string, - url: PropTypes.string, - }), - ).isRequired, - renderItem: PropTypes.func.isRequired, - initialIndex: PropTypes.number, - onPageSelected: PropTypes.func, - onTap: PropTypes.func, - onSwipe: PropTypes.func, - onSwipeSuccess: PropTypes.func, - onSwipeDown: PropTypes.func, - onPinchGestureChange: PropTypes.func, - forwardedRef: refPropTypes, -}; - -const pagerDefaultProps = { - initialIndex: 0, - onPageSelected: () => {}, - onTap: () => {}, - onSwipe: noopWorklet, - onSwipeSuccess: () => {}, - onSwipeDown: () => {}, - onPinchGestureChange: () => {}, - forwardedRef: null, -}; - -function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onTap, onSwipe = noopWorklet, onSwipeSuccess, onSwipeDown, onPinchGestureChange, forwardedRef}) { - const styles = useThemeStyles(); - const shouldPagerScroll = useSharedValue(true); - const pagerRef = useRef(null); - - const isScrolling = useSharedValue(false); - const activeIndex = useSharedValue(initialIndex); - - const pageScrollHandler = usePageScrollHandler( - { - onPageScroll: (e) => { - 'worklet'; - - activeIndex.value = e.position; - isScrolling.value = e.offset !== 0; - }, - }, - [], - ); - - const [activePage, setActivePage] = useState(initialIndex); - - useEffect(() => { - setActivePage(initialIndex); - activeIndex.value = initialIndex; - }, [activeIndex, initialIndex]); - - // we use reanimated for this since onPageSelected is called - // in the middle of the pager animation - useAnimatedReaction( - () => isScrolling.value, - (stillScrolling) => { - if (stillScrolling) { - return; - } - - runOnJS(setActivePage)(activeIndex.value); - }, - ); - - useImperativeHandle( - forwardedRef, - () => ({ - setPage: (...props) => pagerRef.current.setPage(...props), - }), - [], - ); - - const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: shouldPagerScroll.value, - })); - - const contextValue = useMemo( - () => ({ - isScrolling, - pagerRef, - shouldPagerScroll, - onPinchGestureChange, - onTap, - onSwipe, - onSwipeSuccess, - onSwipeDown, - }), - [isScrolling, pagerRef, shouldPagerScroll, onPinchGestureChange, onTap, onSwipe, onSwipeSuccess, onSwipeDown], - ); - - return ( - - - {_.map(items, (item, index) => ( - - {renderItem({item, index, isActive: index === activePage})} - - ))} - - - ); -} - -AttachmentCarouselPager.propTypes = pagerPropTypes; -AttachmentCarouselPager.defaultProps = pagerDefaultProps; -AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; - -const AttachmentCarouselPagerWithRef = React.forwardRef((props, ref) => ( - -)); - -AttachmentCarouselPagerWithRef.displayName = 'AttachmentCarouselPagerWithRef'; - -export default AttachmentCarouselPagerWithRef; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx new file mode 100644 index 000000000000..490afb6614ac --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -0,0 +1,150 @@ +import type {ForwardedRef} from 'react'; +import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {NativeViewGestureHandlerProps} from 'react-native-gesture-handler'; +import {createNativeWrapper} from 'react-native-gesture-handler'; +import type {PagerViewProps} from 'react-native-pager-view'; +import PagerView from 'react-native-pager-view'; +import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated'; +import useThemeStyles from '@hooks/useThemeStyles'; +import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; +import usePageScrollHandler from './usePageScrollHandler'; + +const WrappedPagerView = createNativeWrapper(PagerView) as React.ForwardRefExoticComponent< + PagerViewProps & NativeViewGestureHandlerProps & React.RefAttributes> +>; +const AnimatedPagerView = Animated.createAnimatedComponent(WrappedPagerView); + +type AttachmentCarouselPagerHandle = { + setPage: (selectedPage: number) => void; +}; + +type PagerItem = { + key: string; + url: string; + source: string; +}; + +type AttachmentCarouselPagerProps = { + items: PagerItem[]; + renderItem: (props: {item: PagerItem; index: number; isActive: boolean}) => React.ReactNode; + initialIndex: number; + onPageSelected: () => void; + onRequestToggleArrows: (showArrows?: boolean) => void; +}; + +function AttachmentCarouselPager({items, renderItem, initialIndex, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + const pagerRef = useRef(null); + + const scale = useRef(1); + const isPagerScrolling = useSharedValue(false); + const isScrollEnabled = useSharedValue(true); + + const activePage = useSharedValue(initialIndex); + const [activePageState, setActivePageState] = useState(initialIndex); + + const pageScrollHandler = usePageScrollHandler((e) => { + 'worklet'; + + activePage.value = e.position; + isPagerScrolling.value = e.offset !== 0; + }, []); + + useEffect(() => { + setActivePageState(initialIndex); + activePage.value = initialIndex; + }, [activePage, initialIndex]); + + /** + * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. + * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager, + * as well as enabling/disabling the carousel buttons. + */ + const handleScaleChange = useCallback( + (newScale: number) => { + if (newScale === scale.current) { + return; + } + + scale.current = newScale; + + const newIsScrollEnabled = newScale === 1; + if (isScrollEnabled.value === newIsScrollEnabled) { + return; + } + + isScrollEnabled.value = newIsScrollEnabled; + onRequestToggleArrows(newIsScrollEnabled); + }, + [isScrollEnabled, onRequestToggleArrows], + ); + + /** + * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. + * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox. + */ + const handleTap = useCallback(() => { + if (!isScrollEnabled.value) { + return; + } + + onRequestToggleArrows(); + }, [isScrollEnabled.value, onRequestToggleArrows]); + + const contextValue = useMemo( + () => ({ + pagerRef, + isPagerScrolling, + isScrollEnabled, + onTap: handleTap, + onScaleChanged: handleScaleChange, + }), + [isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange], + ); + + const animatedProps = useAnimatedProps(() => ({ + scrollEnabled: isScrollEnabled.value, + })); + + /** + * This "useImperativeHandle" call is needed to expose certain imperative methods via the pager's ref. + * setPage: can be used to programmatically change the page from a parent component + */ + useImperativeHandle( + ref, + () => ({ + setPage: (selectedPage) => { + pagerRef.current?.setPage(selectedPage); + }, + }), + [], + ); + + return ( + + + {items.map((item, index) => ( + + {renderItem({item, index, isActive: index === activePageState})} + + ))} + + + ); +} +AttachmentCarouselPager.displayName = 'AttachmentCarouselPager'; + +export default React.forwardRef(AttachmentCarouselPager); diff --git a/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts new file mode 100644 index 000000000000..ab7f0d99b7f0 --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/Pager/usePageScrollHandler.ts @@ -0,0 +1,41 @@ +import type {PagerViewProps} from 'react-native-pager-view'; +import {useEvent, useHandler} from 'react-native-reanimated'; + +type PageScrollHandler = NonNullable; + +type PageScrollEventData = Parameters[0]['nativeEvent']; +type PageScrollContext = Record; + +// Reanimated doesn't expose the type for animated event handlers, therefore we must infer it from the useHandler hook. +// The AnimatedPageScrollHandler type is the type of the onPageScroll prop from react-native-pager-view as an animated handler. +type AnimatedHandlers = Parameters>[0]; +type AnimatedPageScrollHandler = AnimatedHandlers[string]; + +type Dependencies = Parameters[1]; + +/** + * This hook is used to create a wrapped handler for the onPageScroll event from react-native-pager-view. + * The produced handler can react to the onPageScroll event and allows to use it with animated shared values (from REA) + * This hook is a wrapper around the useHandler and useEvent hooks from react-native-reanimated. + * @param onPageScroll The handler for the onPageScroll event from react-native-pager-view + * @param dependencies The dependencies for the useHandler hook + * @returns A wrapped/animated handler for the onPageScroll event from react-native-pager-view + */ +const usePageScrollHandler = (onPageScroll: AnimatedPageScrollHandler, dependencies: Dependencies): PageScrollHandler => { + const {context, doDependenciesDiffer} = useHandler({onPageScroll}, dependencies); + const subscribeForEvents = ['onPageScroll']; + + return useEvent( + (event) => { + 'worklet'; + + if (onPageScroll && event.eventName.endsWith('onPageScroll')) { + onPageScroll(event, context); + } + }, + subscribeForEvents, + doDependenciesDiffer, + ); +}; + +export default usePageScrollHandler; diff --git a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js index 72a554de68be..5aa665683162 100644 --- a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js +++ b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js @@ -10,9 +10,6 @@ const propTypes = { /** Callback to update the parent modal's state with a source and name from the attachments array */ onNavigate: PropTypes.func, - /** Callback to close carousel when user swipes down (on native) */ - onClose: PropTypes.func, - /** Function to change the download button Visibility */ setDownloadButtonVisibility: PropTypes.func, @@ -39,7 +36,6 @@ const defaultProps = { parentReportActions: {}, transaction: {}, onNavigate: () => {}, - onClose: () => {}, setDownloadButtonVisibility: () => {}, }; diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index f5479b73abdb..fa24ccd0ef53 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -18,12 +18,11 @@ import extractAttachmentsFromReport from './extractAttachmentsFromReport'; import AttachmentCarouselPager from './Pager'; import useCarouselArrows from './useCarouselArrows'; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) { const styles = useThemeStyles(); const pagerRef = useRef(null); const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); - const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); const [activeSource, setActiveSource] = useState(source); @@ -88,6 +87,22 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [autoHideArrows, page, updatePage], ); + /** + * Toggles the arrows visibility + * @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value + */ + const toggleArrows = useCallback( + (showArrows) => { + if (showArrows === undefined) { + setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows); + return; + } + + setShouldShowArrows(showArrows); + }, + [setShouldShowArrows], + ); + /** * Defines how a single attachment should be rendered * @param {{ reportActionID: String, isAuthTokenRequired: Boolean, source: String, file: { name: String }, hasBeenFlagged: Boolean }} item @@ -101,18 +116,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, index={index} activeIndex={page} isFocused={isActive && activeSource === item.source} - onPress={() => setShouldShowArrows(!shouldShowArrows)} /> ), - [activeSource, attachments.length, page, setShouldShowArrows, shouldShowArrows], + [activeSource, attachments.length, page], ); return ( - setShouldShowArrows(true)} - onMouseLeave={() => setShouldShowArrows(false)} - > + {page == null ? ( ) : ( @@ -127,7 +137,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ) : ( <> cycleThroughAttachments(-1)} @@ -140,14 +150,8 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, items={attachments} renderItem={renderItem} initialIndex={page} + onRequestToggleArrows={toggleArrows} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} - onPinchGestureChange={(newIsPinchGestureRunning) => { - setIsPinchGestureRunning(newIsPinchGestureRunning); - if (!newIsPinchGestureRunning && !shouldShowArrows) { - setShouldShowArrows(true); - } - }} - onSwipeDown={onClose} ref={pagerRef} /> diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index f53b993f6053..14c60458b044 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -13,7 +13,7 @@ const propTypes = { }; function AttachmentViewImage({ - source, + url, file, isAuthTokenRequired, isUsedInCarousel, @@ -25,15 +25,13 @@ function AttachmentViewImage({ onPress, onError, isImage, - onScaleChanged, translate, }) { const styles = useThemeStyles(); const children = ( { if (!attachmentCarouselPagerContext) { return; } - attachmentCarouselPagerContext.onPinchGestureChange(false); + attachmentCarouselPagerContext.onScaleChanged(1); // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); + /** + * When the PDF's onScaleChanged event is triggered, we must call the context's onScaleChanged callback, + * because we want to disable the pager scroll when the pdf is zoomed in, + * as well as call the onScaleChanged prop of the AttachmentViewPdf component if defined. + */ const onScaleChanged = useCallback( - (scale) => { - onScaleChangedProp(scale); + (newScale) => { + if (onScaleChangedProp !== undefined) { + onScaleChangedProp(newScale); + } // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in if (isUsedInCarousel && attachmentCarouselPagerContext) { - const shouldPagerScroll = scale === 1; - - attachmentCarouselPagerContext.onPinchGestureChange(!shouldPagerScroll); + attachmentCarouselPagerContext.onScaleChanged(newScale); + } + }, + [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], + ); - if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) { - return; - } + /** + * This callback is used to pass-through the onPress event from the AttachmentViewPdf's props + * as well trigger the onTap event from the context. + * The onTap event should only be triggered, if the pager is currently scrollable. + * Otherwise it means that the PDF is currently zoomed in, therefore the onTap callback should be ignored + */ + const onPress = useCallback( + (e) => { + if (onPressProp !== undefined) { + onPressProp(e); + } - attachmentCarouselPagerContext.shouldPagerScroll.value = shouldPagerScroll; + if (attachmentCarouselPagerContext !== null && isScrollEnabled.value) { + attachmentCarouselPagerContext.onTap(e); } }, - [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], + [attachmentCarouselPagerContext, isScrollEnabled, onPressProp], ); return ( @@ -60,8 +93,8 @@ function BaseAttachmentViewPdf({ ); } -BaseAttachmentViewPdf.propTypes = attachmentViewPdfPropTypes; -BaseAttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps; +BaseAttachmentViewPdf.propTypes = baseAttachmentViewPdfPropTypes; +BaseAttachmentViewPdf.defaultProps = baseAttachmentViewPdfDefaultProps; BaseAttachmentViewPdf.displayName = 'BaseAttachmentViewPdf'; export default memo(BaseAttachmentViewPdf); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js index db4f4f11d68c..07cd8ecf61e7 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js @@ -1,4 +1,4 @@ -import React, {memo, useCallback, useContext} from 'react'; +import React, {memo, useContext, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {useSharedValue} from 'react-native-reanimated'; @@ -7,13 +7,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +// If the user pans less than this threshold, we'll not enable/disable the pager scroll, since the thouch will most probably be a tap. +// If the user moves their finger more than this threshold in the X direction, we'll enable the pager scroll. Otherwise if in the Y direction, we'll disable it. +const SCROLL_THRESHOLD = 10; + function AttachmentViewPdf(props) { const styles = useThemeStyles(); - const {onScaleChanged, ...restProps} = props; const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const scaleRef = useSharedValue(1); - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); + const scale = useSharedValue(1); // Reanimated freezes all objects captured in the closure of a worklet. // Since Reanimated 3, entire objects are captured instead of just the relevant properties. @@ -22,30 +23,53 @@ function AttachmentViewPdf(props) { // frozen, which combined with Reanimated using strict mode since 3.6.0 was resulting in errors. // Without strict mode, it would just silently fail. // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#description - const shouldPagerScroll = attachmentCarouselPagerContext !== null ? attachmentCarouselPagerContext.shouldPagerScroll : undefined; + const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; + + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); + const isPanGestureActive = useSharedValue(false); const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && shouldPagerScroll) { + if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) { + const translateX = Math.abs(evt.allTouches[0].absoluteX - offsetX.value); + const translateY = Math.abs(evt.allTouches[0].absoluteY - offsetY.value); + const allowEnablingScroll = !isPanGestureActive.value || isScrollEnabled.value; + // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. - if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - shouldPagerScroll.value = true; - } else { - shouldPagerScroll.value = false; + if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) { + isScrollEnabled.value = true; + } else if (translateY > SCROLL_THRESHOLD) { + isScrollEnabled.value = false; } } + + isPanGestureActive.value = true; offsetX.value = evt.allTouches[0].absoluteX; offsetY.value = evt.allTouches[0].absoluteY; + }) + .onTouchesUp(() => { + isPanGestureActive.value = false; + isScrollEnabled.value = true; }); - const updateScale = useCallback( - (scale) => { - scaleRef.value = scale; - }, - [scaleRef], + const Content = useMemo( + () => ( + { + // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, + // even though we're not pinching/zooming + // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. + scale.value = Math.round(newScale * 1e2) / 1e2; + }} + /> + ), + [props, scale], ); return ( @@ -53,21 +77,18 @@ function AttachmentViewPdf(props) { collapsable={false} style={styles.flex1} > - - - { - updateScale(scale); - onScaleChanged(); - }} - /> - - + {attachmentCarouselPagerContext === null ? ( + Content + ) : ( + + + {Content} + + + )} ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index c3d1423b17c9..d6a402613c34 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { return ( diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index b0060afdb813..67f6dd95568e 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -4,6 +4,7 @@ import React, {memo, useState} from 'react'; import {ActivityIndicator, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; @@ -28,6 +29,9 @@ const propTypes = { ...attachmentViewPropTypes, ...withLocalizePropTypes, + /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ + source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, + /** Flag to show/hide download icon */ shouldShowDownloadIcon: PropTypes.bool, @@ -67,7 +71,6 @@ function AttachmentView({ shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles, - onScaleChanged, onToggleKeyboard, translate, isFocused, @@ -141,7 +144,6 @@ function AttachmentView({ carouselItemIndex={carouselItemIndex} carouselActiveItemIndex={carouselActiveItemIndex} onPress={onPress} - onScaleChanged={onScaleChanged} onToggleKeyboard={onToggleKeyboard} onLoadComplete={() => !loadComplete && setLoadComplete(true)} errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} @@ -163,7 +165,7 @@ function AttachmentView({ if (isImage || (file && Str.isImage(file.name))) { return ( { setImageError(true); }} diff --git a/src/components/Attachments/AttachmentView/propTypes.js b/src/components/Attachments/AttachmentView/propTypes.js index 286c903ccf5b..d78bed8526b8 100644 --- a/src/components/Attachments/AttachmentView/propTypes.js +++ b/src/components/Attachments/AttachmentView/propTypes.js @@ -5,9 +5,6 @@ const attachmentViewPropTypes = { /** Whether source url requires authentication */ isAuthTokenRequired: PropTypes.bool, - /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, - /** File object can be an instance of File or Object */ file: AttachmentsPropTypes.attachmentFilePropType, diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 4da91c2e7d19..8ea8a1bb6f64 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -124,3 +124,4 @@ function Avatar({ Avatar.displayName = 'Avatar'; export default Avatar; +export {type AvatarProps}; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index e9e1054427b9..d42d47caafc9 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -12,7 +12,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; @@ -36,7 +36,7 @@ type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { size?: ValueOf; /** Personal details of all the users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; /** Whether if it's an unauthenticated user */ isAnonymous?: boolean; @@ -63,7 +63,7 @@ function AvatarWithDisplayName({ const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); const isExpenseRequest = ReportUtils.isExpenseRequest(report); const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG; diff --git a/src/components/BigNumberPad.tsx b/src/components/BigNumberPad.tsx index 8b840f9d1b57..80f7794d0ad3 100644 --- a/src/components/BigNumberPad.tsx +++ b/src/components/BigNumberPad.tsx @@ -31,7 +31,7 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i const {toLocaleDigit} = useLocalize(); const styles = useThemeStyles(); - const [timer, setTimer] = useState(null); + const [timer, setTimer] = useState(null); const {isExtraSmallScreenHeight} = useWindowDimensions(); /** diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx deleted file mode 100644 index ade1513c8613..000000000000 --- a/src/components/Composer/index.android.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import type {TextInput} from 'react-native'; -import {StyleSheet} from 'react-native'; -import RNTextInput from '@components/RNTextInput'; -import useResetComposerFocus from '@hooks/useResetComposerFocus'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as ComposerUtils from '@libs/ComposerUtils'; -import type {ComposerProps} from './types'; - -function Composer( - { - shouldClear = false, - onClear = () => {}, - isDisabled = false, - maxLines, - isComposerFullSize = false, - setIsFullComposerAvailable = () => {}, - style, - autoFocus = false, - selection = { - start: 0, - end: 0, - }, - isFullComposerAvailable = false, - ...props - }: ComposerProps, - ref: ForwardedRef, -) { - const textInput = useRef(null); - const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); - - const styles = useThemeStyles(); - const theme = useTheme(); - - /** - * Set the TextInput Ref - */ - const setTextInputRef = useCallback((el: TextInput) => { - textInput.current = el; - if (typeof ref !== 'function' || textInput.current === null) { - return; - } - - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - ref(textInput.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!shouldClear) { - return; - } - textInput.current?.clear(); - onClear(); - }, [shouldClear, onClear]); - - /** - * Set maximum number of lines - */ - const maxNumberOfLines = useMemo(() => { - if (isComposerFullSize) { - return 1000000; - } - return maxLines; - }, [isComposerFullSize, maxLines]); - - const composerStyles = useMemo(() => StyleSheet.flatten(style), [style]); - - return ( - ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)} - rejectResponderTermination={false} - // Setting a really high number here fixes an issue with the `maxNumberOfLines` prop on TextInput, where on Android the text input would collapse to only one line, - // when it should actually expand to the container (https://github.com/Expensify/App/issues/11694#issuecomment-1560520670) - // @Szymon20000 is working on fixing this (android-only) issue in the in the upstream PR (https://github.com/facebook/react-native/pulls?q=is%3Apr+is%3Aopen+maxNumberOfLines) - // TODO: remove this comment once upstream PR is merged and available in a future release - maxNumberOfLines={maxNumberOfLines} - textAlignVertical="center" - style={[composerStyles]} - autoFocus={autoFocus} - selection={selection} - isFullComposerAvailable={isFullComposerAvailable} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...props} - readOnly={isDisabled} - onBlur={(e) => { - if (!isFocused) { - shouldResetFocus.current = true; // detect the input is blurred when the page is hidden - } - props?.onBlur?.(e); - }} - /> - ); -} - -Composer.displayName = 'Composer'; - -export default React.forwardRef(Composer); diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.native.tsx similarity index 82% rename from src/components/Composer/index.ios.tsx rename to src/components/Composer/index.native.tsx index 07736e5ddcba..c7b020a5c6dd 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.native.tsx @@ -2,8 +2,10 @@ import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import useResetComposerFocus from '@hooks/useResetComposerFocus'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; @@ -28,15 +30,17 @@ function Composer( }: ComposerProps, ref: ForwardedRef, ) { - const textInput = useRef(null); + const textInput = useRef(null); const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); - const styles = useThemeStyles(); const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); /** * Set the TextInput Ref + * @param {Element} el */ - const setTextInputRef = useCallback((el: TextInput) => { + const setTextInputRef = useCallback((el: AnimatedTextInputRef) => { textInput.current = el; if (typeof ref !== 'function' || textInput.current === null) { return; @@ -58,28 +62,20 @@ function Composer( onClear(); }, [shouldClear, onClear]); - /** - * Set maximum number of lines - */ - const maxNumberOfLines = useMemo(() => { - if (isComposerFullSize) { - return; - } - return maxLines; - }, [isComposerFullSize, maxLines]); - - const composerStyles = useMemo(() => StyleSheet.flatten(style), [style]); + const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); + const composerStyle = useMemo(() => StyleSheet.flatten(style), [style]); return ( ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)} rejectResponderTermination={false} smartInsertDelete={false} - style={[composerStyles, styles.verticalAlignMiddle]} - maxNumberOfLines={maxNumberOfLines} + textAlignVertical="center" + style={[composerStyle, maxHeightStyle]} autoFocus={autoFocus} isFullComposerAvailable={isFullComposerAvailable} /* eslint-disable-next-line react/jsx-props-no-spreading */ diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3c2caf020ef7..3568aee6eebb 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -4,9 +4,9 @@ import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports -import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; +import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -17,6 +17,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; +import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; @@ -75,7 +76,7 @@ function Composer( shouldContainScroll = false, ...props }: ComposerProps, - ref: ForwardedRef>>, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -83,7 +84,7 @@ function Composer( const {windowWidth} = useWindowDimensions(); const navigation = useNavigation(); const textRef = useRef(null); - const textInput = useRef<(HTMLTextAreaElement & TextInput) | null>(null); + const textInput = useRef(null); const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { @@ -216,6 +217,9 @@ function Composer( const TEXT_HTML = 'text/html'; + const clipboardDataHtml = event.clipboardData?.getData(TEXT_HTML) ?? ''; + const clipboardDataTypesHtml = event.clipboardData?.types.includes(TEXT_HTML) ?? false; + // If paste contains files, then trigger file management if (event.clipboardData?.files.length && event.clipboardData.files.length > 0) { // Prevent the default so we do not post the file name into the text box @@ -223,9 +227,43 @@ function Composer( return; } + // If paste contains base64 image + if (clipboardDataHtml?.includes(CONST.IMAGE_BASE64_MATCH)) { + const domparser = new DOMParser(); + const pastedHTML = clipboardDataHtml; + const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML)?.images; + + if (embeddedImages.length > 0 && embeddedImages[0].src) { + const src = embeddedImages[0].src; + const file = FileUtils.base64ToFile(src, 'image.png'); + onPasteFile(file); + return; + } + } + + // If paste contains image from Google Workspaces ex: Sheets, Docs, Slide, etc + if (clipboardDataHtml?.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { + const domparser = new DOMParser(); + const pastedHTML = clipboardDataHtml; + const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; + + if (embeddedImages.length > 0 && embeddedImages[0]?.src) { + const src = embeddedImages[0].src; + if (src.includes(CONST.GOOGLE_DOC_IMAGE_LINK_MATCH)) { + fetch(src) + .then((response) => response.blob()) + .then((blob) => { + const file = new File([blob], 'image.jpg', {type: 'image/jpeg'}); + onPasteFile(file); + }); + return; + } + } + } + // If paste contains HTML - if (event.clipboardData?.types.includes(TEXT_HTML)) { - const pastedHTML = event.clipboardData?.getData(TEXT_HTML); + if (clipboardDataTypesHtml) { + const pastedHTML = clipboardDataHtml; const domparser = new DOMParser(); const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; @@ -359,7 +397,7 @@ function Composer( autoComplete="off" autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={theme.placeholderText} - ref={(el: TextInput & HTMLTextAreaElement) => (textInput.current = el)} + ref={(el) => (textInput.current = el)} selection={selection} style={inputStyleMemo} value={value} @@ -368,7 +406,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - rows={numberOfLines} + numberOfLines={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index c01f7c6250f4..c38241b275ba 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect} from 'react'; import type {ReactNode} from 'react'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 0241eea44063..3418aa55e22d 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -97,7 +97,7 @@ function DistanceEReceipt({transaction}) { > {translate(descriptionKey)} {waypoint.name && {waypoint.name}} - {waypoint.address && {waypoint.address}} + {waypoint.address && {waypoint.address}} ); })} diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index bfcb66aeefbb..7f60b0615785 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -60,7 +60,12 @@ function EmojiPickerButtonDropdown(props) { style={styles.emojiPickerButtonDropdownIcon} numberOfLines={1} > - {props.value} + {props.value || ( + + )} {}, errorMessage, children}: BaseErrorBoundaryProps) { - const catchError = (error: Error, errorInfo: React.ErrorInfo) => { - logError(errorMessage, error, JSON.stringify(errorInfo)); + const [errorContent, setErrorContent] = useState(''); + const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => { + logError(errorMessage, errorObject, JSON.stringify(errorInfo)); // We hide the splash screen since the error might happened during app init BootSplash.hide(); + setErrorContent(errorObject.message); }; + const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED; return ( } + fallback={updateRequired ? : } onError={catchError} > {children} diff --git a/src/components/Form/FormContext.js b/src/components/Form/FormContext.js deleted file mode 100644 index 40edaa7cca69..000000000000 --- a/src/components/Form/FormContext.js +++ /dev/null @@ -1,4 +0,0 @@ -import {createContext} from 'react'; - -const FormContext = createContext({}); -export default FormContext; diff --git a/src/components/Form/FormContext.tsx b/src/components/Form/FormContext.tsx new file mode 100644 index 000000000000..47e0de8b497c --- /dev/null +++ b/src/components/Form/FormContext.tsx @@ -0,0 +1,12 @@ +import {createContext} from 'react'; +import type {RegisterInput} from './types'; + +type FormContext = { + registerInput: RegisterInput; +}; + +export default createContext({ + registerInput: () => { + throw new Error('Registered input should be wrapped with FormWrapper'); + }, +}); diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js deleted file mode 100644 index 4d1630dbbe06..000000000000 --- a/src/components/Form/FormProvider.js +++ /dev/null @@ -1,408 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import compose from '@libs/compose'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import Visibility from '@libs/Visibility'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as FormActions from '@userActions/FormActions'; -import CONST from '@src/CONST'; -import FormContext from './FormContext'; -import FormWrapper from './FormWrapper'; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string.isRequired, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to validate the form */ - validate: PropTypes.func, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Contains draft values for each input in the form */ - draftValues: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number, PropTypes.objectOf(Date)])), - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Should validate function be called when input loose focus */ - shouldValidateOnBlur: PropTypes.bool, - - /** Should validate function be called when the value of the input is changed */ - shouldValidateOnChange: PropTypes.bool, - - /** Should fix the errors alert be displayed when there is an error in the form */ - shouldHideFixErrorsAlert: PropTypes.bool, -}; - -// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. -// 200ms delay was chosen as a result of empirical testing. -// More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 -const VALIDATE_DELAY = 200; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - draftValues: {}, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - footerContent: null, - style: [], - validate: () => {}, - shouldValidateOnBlur: true, - shouldValidateOnChange: true, - shouldHideFixErrorsAlert: false, -}; - -function getInitialValueByType(valueType) { - switch (valueType) { - case 'string': - return ''; - case 'boolean': - return false; - case 'date': - return new Date(); - default: - return ''; - } -} - -const FormProvider = forwardRef( - ({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}, forwardedRef) => { - const inputRefs = useRef({}); - const touchedInputs = useRef({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); - const [errors, setErrors] = useState({}); - const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); - - const onValidate = useCallback( - (values, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); - - if (shouldClearServerError) { - FormActions.setErrors(formID, null); - } - FormActions.setErrorFields(formID, null); - - const validateErrors = validate(trimmedStringValues) || {}; - - // Validate the input for html tags. It should supercede any other error - _.each(trimmedStringValues, (inputValue, inputID) => { - // If the input value is empty OR is non-string, we don't need to validate it for HTML tags - if (!inputValue || !_.isString(inputValue)) { - return; - } - const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); - - // Return early if there are no HTML characters - if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { - return; - } - - const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); - // Check for any matches that the original regex (foundHtmlTagIndex) matched - if (matchedHtmlTags) { - // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. - for (let i = 0; i < matchedHtmlTags.length; i++) { - const htmlTag = matchedHtmlTags[i]; - isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); - if (!isMatch) { - break; - } - } - } - - if (isMatch && leadingSpaceIndex === -1) { - return; - } - - // Add a validation error here because it is a string value that contains HTML characters - validateErrors[inputID] = 'common.error.invalidCharacter'; - }); - - if (!_.isObject(validateErrors)) { - throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); - } - - const touchedInputErrors = _.pick(validateErrors, (inputValue, inputID) => Boolean(touchedInputs.current[inputID])); - - if (!_.isEqual(errors, touchedInputErrors)) { - setErrors(touchedInputErrors); - } - - return touchedInputErrors; - }, - [errors, formID, validate], - ); - - /** - * @param {String} inputID - The inputID of the input being touched - */ - const setTouchedInput = useCallback( - (inputID) => { - touchedInputs.current[inputID] = true; - }, - [touchedInputs], - ); - - const submit = useCallback(() => { - // Return early if the form is already submitting to avoid duplicate submission - if (formState.isLoading) { - return; - } - - // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); - - // Touches all form inputs so we can validate the entire form - _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); - - // Validate form and return early if any errors are found - if (!_.isEmpty(onValidate(trimmedStringValues))) { - return; - } - - // Do not submit form if network is offline and the form is not enabled when offline - if (network.isOffline && !enabledWhenOffline) { - return; - } - - onSubmit(trimmedStringValues); - }, [enabledWhenOffline, formState.isLoading, inputValues, network.isOffline, onSubmit, onValidate]); - - /** - * Resets the form - */ - const resetForm = useCallback( - (optionalValue) => { - _.each(inputValues, (inputRef, inputID) => { - setInputValues((prevState) => { - const copyPrevState = _.clone(prevState); - - touchedInputs.current[inputID] = false; - copyPrevState[inputID] = optionalValue[inputID] || ''; - - return copyPrevState; - }); - }); - setErrors({}); - }, - [inputValues], - ); - useImperativeHandle(forwardedRef, () => ({ - resetForm, - })); - - const registerInput = useCallback( - (inputID, propsToParse = {}) => { - const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); - if (inputRefs.current[inputID] !== newRef) { - inputRefs.current[inputID] = newRef; - } - - if (!_.isUndefined(propsToParse.value)) { - inputValues[inputID] = propsToParse.value; - } else if (propsToParse.shouldSaveDraft && !_.isUndefined(draftValues[inputID]) && _.isUndefined(inputValues[inputID])) { - inputValues[inputID] = draftValues[inputID]; - } else if (propsToParse.shouldUseDefaultValue && _.isUndefined(inputValues[inputID])) { - // We force the form to set the input value from the defaultValue props if there is a saved valid value - inputValues[inputID] = propsToParse.defaultValue; - } else if (_.isUndefined(inputValues[inputID])) { - // We want to initialize the input value if it's undefined - inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; - } - - const errorFields = lodashGet(formState, 'errorFields', {}); - const fieldErrorMessage = - _.chain(errorFields[inputID]) - .keys() - .sortBy() - .reverse() - .map((key) => errorFields[inputID][key]) - .first() - .value() || ''; - - return { - ...propsToParse, - ref: - typeof propsToParse.ref === 'function' - ? (node) => { - propsToParse.ref(node); - newRef.current = node; - } - : newRef, - inputID, - key: propsToParse.key || inputID, - errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], - // As the text input is controlled, we never set the defaultValue prop - // as this is already happening by the value prop. - defaultValue: undefined, - onTouched: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onTouched)) { - propsToParse.onTouched(event); - } - }, - onPress: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPress)) { - propsToParse.onPress(event); - } - }, - onPressOut: (event) => { - // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time - // as the onValidate is delayed - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPressIn)) { - propsToParse.onPressIn(event); - } - }, - onBlur: (event) => { - // Only run validation when user proactively blurs the input. - if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); - // We delay the validation in order to prevent Checkbox loss of focus when - // the user is focusing a TextInput and proceeds to toggle a CheckBox in - // web and mobile web platforms. - - setTimeout(() => { - if ( - relatedTargetId && - _.includes([CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID, CONST.OVERLAY.TOP_BUTTON_NATIVE_ID, CONST.BACK_BUTTON_NATIVE_ID], relatedTargetId) - ) { - return; - } - setTouchedInput(inputID); - if (shouldValidateOnBlur) { - onValidate(inputValues, !hasServerError); - } - }, VALIDATE_DELAY); - } - - if (_.isFunction(propsToParse.onBlur)) { - propsToParse.onBlur(event); - } - }, - onInputChange: (value, key) => { - const inputKey = key || inputID; - setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; - - if (shouldValidateOnChange) { - onValidate(newState); - } - return newState; - }); - - if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); - } - - if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(value, inputKey); - } - }, - }; - }, - [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], - ); - const value = useMemo(() => ({registerInput}), [registerInput]); - - return ( - - {/* eslint-disable react/jsx-props-no-spreading */} - - {_.isFunction(children) ? children({inputValues}) : children} - - - ); - }, -); - -FormProvider.displayName = 'Form'; -FormProvider.propTypes = propTypes; -FormProvider.defaultProps = defaultProps; - -export default compose( - withNetwork(), - withOnyx({ - formState: { - key: (props) => props.formID, - }, - draftValues: { - key: (props) => `${props.formID}Draft`, - }, - }), -)(FormProvider); diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx new file mode 100644 index 000000000000..424fd989291a --- /dev/null +++ b/src/components/Form/FormProvider.tsx @@ -0,0 +1,358 @@ +import lodashIsEqual from 'lodash/isEqual'; +import type {ForwardedRef, MutableRefObject, ReactNode} from 'react'; +import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Visibility from '@libs/Visibility'; +import * as FormActions from '@userActions/FormActions'; +import CONST from '@src/CONST'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Form, Network} from '@src/types/onyx'; +import type {FormValueType} from '@src/types/onyx/Form'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import FormContext from './FormContext'; +import FormWrapper from './FormWrapper'; +import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; + +// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. +// 200ms delay was chosen as a result of empirical testing. +// More details: https://github.com/Expensify/App/pull/16444#issuecomment-1482983426 +const VALIDATE_DELAY = 200; + +type InitialDefaultValue = false | Date | ''; + +function getInitialValueByType(valueType?: ValueTypeKey): InitialDefaultValue { + switch (valueType) { + case 'string': + return ''; + case 'boolean': + return false; + case 'date': + return new Date(); + default: + return ''; + } +} + +type FormProviderOnyxProps = { + /** Contains the form state that must be accessed outside the component */ + formState: OnyxEntry
; + + /** Contains draft values for each input in the form */ + draftValues: OnyxEntry; + + /** Information about the network */ + network: OnyxEntry; +}; + +type FormProviderProps = FormProviderOnyxProps & + FormProps & { + /** Children to render. */ + children: ((props: {inputValues: OnyxFormValues}) => ReactNode) | ReactNode; + + /** Callback to validate the form */ + validate?: (values: OnyxFormValuesFields) => Errors; + + /** Should validate function be called when input loose focus */ + shouldValidateOnBlur?: boolean; + + /** Should validate function be called when the value of the input is changed */ + shouldValidateOnChange?: boolean; + }; + +type FormRef = { + resetForm: (optionalValue: OnyxFormValues) => void; +}; + +function FormProvider( + { + formID, + validate, + shouldValidateOnBlur = true, + shouldValidateOnChange = true, + children, + formState, + network, + enabledWhenOffline = false, + draftValues, + onSubmit, + ...rest + }: FormProviderProps, + forwardedRef: ForwardedRef, +) { + const inputRefs = useRef({}); + const touchedInputs = useRef>({}); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [errors, setErrors] = useState({}); + const hasServerError = useMemo(() => !!formState && !isEmptyObject(formState?.errors), [formState]); + + const onValidate = useCallback( + (values: OnyxFormValuesFields, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values); + + if (shouldClearServerError) { + FormActions.clearErrors(formID); + } + FormActions.clearErrorFields(formID); + + const validateErrors = validate?.(trimmedStringValues) ?? {}; + + // Validate the input for html tags. It should supersede any other error + Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => { + // If the input value is empty OR is non-string, we don't need to validate it for HTML tags + if (!inputValue || typeof inputValue !== 'string') { + return; + } + const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); + + // Return early if there are no HTML characters + if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { + return; + } + + const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + let isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(inputValue)); + // Check for any matches that the original regex (foundHtmlTagIndex) matched + if (matchedHtmlTags) { + // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. + for (const htmlTag of matchedHtmlTags) { + isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(htmlTag)); + if (!isMatch) { + break; + } + } + } + + if (isMatch && leadingSpaceIndex === -1) { + return; + } + + // Add a validation error here because it is a string value that contains HTML characters + validateErrors[inputID] = 'common.error.invalidCharacter'; + }); + + if (typeof validateErrors !== 'object') { + throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); + } + + const touchedInputErrors = Object.fromEntries(Object.entries(validateErrors).filter(([inputID]) => touchedInputs.current[inputID])); + + if (!lodashIsEqual(errors, touchedInputErrors)) { + setErrors(touchedInputErrors); + } + + return touchedInputErrors; + }, + [errors, formID, validate], + ); + + /** @param inputID - The inputID of the input being touched */ + const setTouchedInput = useCallback( + (inputID: keyof Form) => { + touchedInputs.current[inputID] = true; + }, + [touchedInputs], + ); + + const submit = useCallback(() => { + // Return early if the form is already submitting to avoid duplicate submission + if (formState?.isLoading) { + return; + } + + // Prepare values before submitting + const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + + // Touches all form inputs, so we can validate the entire form + Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); + + // Validate form and return early if any errors are found + if (!isEmptyObject(onValidate(trimmedStringValues))) { + return; + } + + // Do not submit form if network is offline and the form is not enabled when offline + if (network?.isOffline && !enabledWhenOffline) { + return; + } + + onSubmit(trimmedStringValues); + }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); + + const resetForm = useCallback( + (optionalValue: OnyxFormValuesFields) => { + Object.keys(inputValues).forEach((inputID) => { + setInputValues((prevState) => { + const copyPrevState = {...prevState}; + + touchedInputs.current[inputID] = false; + copyPrevState[inputID] = optionalValue[inputID as keyof OnyxFormValuesFields] || ''; + + return copyPrevState; + }); + }); + setErrors({}); + }, + [inputValues], + ); + useImperativeHandle(forwardedRef, () => ({ + resetForm, + })); + + const registerInput = useCallback( + (inputID: keyof Form, inputProps: TInputProps): TInputProps => { + const newRef: MutableRefObject = inputRefs.current[inputID] ?? inputProps.ref ?? createRef(); + if (inputRefs.current[inputID] !== newRef) { + inputRefs.current[inputID] = newRef; + } + if (inputProps.value !== undefined) { + inputValues[inputID] = inputProps.value; + } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { + inputValues[inputID] = draftValues[inputID]; + } else if (inputProps.shouldUseDefaultValue && inputProps.defaultValue !== undefined && inputValues[inputID] === undefined) { + // We force the form to set the input value from the defaultValue props if there is a saved valid value + inputValues[inputID] = inputProps.defaultValue; + } else if (inputValues[inputID] === undefined) { + // We want to initialize the input value if it's undefined + inputValues[inputID] = inputProps.defaultValue ?? getInitialValueByType(inputProps.valueType); + } + + const errorFields = formState?.errorFields?.[inputID] ?? {}; + const fieldErrorMessage = + Object.keys(errorFields) + .sort() + .map((key) => errorFields[key]) + .at(-1) ?? ''; + + const inputRef = inputProps.ref; + + return { + ...inputProps, + ref: + typeof inputRef === 'function' + ? (node: BaseInputProps) => { + inputRef(node); + newRef.current = node; + } + : newRef, + inputID, + key: inputProps.key ?? inputID, + errorText: errors[inputID] ?? fieldErrorMessage, + value: inputValues[inputID], + // As the text input is controlled, we never set the defaultValue prop + // as this is already happening by the value prop. + defaultValue: undefined, + onTouched: (event) => { + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + inputProps.onTouched?.(event); + }, + onPress: (event) => { + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + inputProps.onPress?.(event); + }, + onPressOut: (event) => { + // To prevent validating just pressed inputs, we need to set the touched input right after + // onValidate and to do so, we need to delay setTouchedInput of the same amount of time + // as the onValidate is delayed + if (!inputProps.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + inputProps.onPressOut?.(event); + }, + onBlur: (event) => { + // Only run validation when user proactively blurs the input. + if (Visibility.isVisible() && Visibility.hasFocus()) { + const relatedTarget = event && 'relatedTarget' in event.nativeEvent && event?.nativeEvent?.relatedTarget; + const relatedTargetId = relatedTarget && 'id' in relatedTarget && typeof relatedTarget.id === 'string' && relatedTarget.id; + // We delay the validation in order to prevent Checkbox loss of focus when + // the user is focusing a TextInput and proceeds to toggle a CheckBox in + // web and mobile web platforms. + + setTimeout(() => { + if ( + relatedTargetId === CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID || + relatedTargetId === CONST.OVERLAY.TOP_BUTTON_NATIVE_ID || + relatedTargetId === CONST.BACK_BUTTON_NATIVE_ID + ) { + return; + } + setTouchedInput(inputID); + if (shouldValidateOnBlur) { + onValidate(inputValues, !hasServerError); + } + }, VALIDATE_DELAY); + } + inputProps.onBlur?.(event); + }, + onInputChange: (value: FormValueType, key?: string) => { + const inputKey = key ?? inputID; + setInputValues((prevState) => { + const newState = { + ...prevState, + [inputKey]: value, + }; + + if (shouldValidateOnChange) { + onValidate(newState); + } + return newState as Form; + }); + + if (inputProps.shouldSaveDraft && !formID.includes('Draft')) { + FormActions.setDraftValues(formID as OnyxFormKeyWithoutDraft, {[inputKey]: value}); + } + inputProps.onValueChange?.(value, inputKey); + }, + }; + }, + [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + ); + const value = useMemo(() => ({registerInput}), [registerInput]); + + return ( + + {/* eslint-disable react/jsx-props-no-spreading */} + + {typeof children === 'function' ? children({inputValues}) : children} + + + ); +} + +FormProvider.displayName = 'Form'; + +export default withOnyx({ + network: { + key: ONYXKEYS.NETWORK, + }, + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any + formState: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: ({formID}) => formID as any, + }, + draftValues: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: (props) => `${props.formID}Draft` as any, + }, +})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js deleted file mode 100644 index f1c5d6de9071..000000000000 --- a/src/components/Form/FormWrapper.js +++ /dev/null @@ -1,223 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import FormSubmit from '@components/FormSubmit'; -import refPropTypes from '@components/refPropTypes'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import ScrollViewWithContext from '@components/ScrollViewWithContext'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -import errorsPropType from './errorsPropType'; - -const propTypes = { - /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, - - /** Text to be displayed in the submit button */ - submitButtonText: PropTypes.string.isRequired, - - /** Controls the submit button's visibility */ - isSubmitButtonVisible: PropTypes.bool, - - /** Callback to submit the form */ - onSubmit: PropTypes.func.isRequired, - - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* Onyx Props */ - - /** Contains the form state that must be accessed outside of the component */ - formState: PropTypes.shape({ - /** Controls the loading state of the form */ - isLoading: PropTypes.bool, - - /** Server side errors keyed by microtime */ - errors: errorsPropType, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - - /** Should the button be enabled when offline */ - enabledWhenOffline: PropTypes.bool, - - /** Whether the form submit action is dangerous */ - isSubmitActionDangerous: PropTypes.bool, - - /** Whether ScrollWithContext should be used instead of regular ScrollView. - * Set to true when there's a nested Picker component in Form. - */ - scrollContextEnabled: PropTypes.bool, - - /** Container styles */ - style: stylePropTypes, - - /** Submit button styles */ - submitButtonStyles: stylePropTypes, - - /** Custom content to display in the footer after submit button */ - footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), - - errors: errorsPropType.isRequired, - - inputRefs: PropTypes.objectOf(refPropTypes).isRequired, - - shouldHideFixErrorsAlert: PropTypes.bool, -}; - -const defaultProps = { - isSubmitButtonVisible: true, - formState: { - isLoading: false, - }, - enabledWhenOffline: false, - isSubmitActionDangerous: false, - scrollContextEnabled: false, - footerContent: null, - style: [], - submitButtonStyles: [], - shouldHideFixErrorsAlert: false, -}; - -function FormWrapper(props) { - const styles = useThemeStyles(); - const { - onSubmit, - children, - formState, - errors, - inputRefs, - submitButtonText, - footerContent, - isSubmitButtonVisible, - style, - submitButtonStyles, - enabledWhenOffline, - isSubmitActionDangerous, - formID, - shouldHideFixErrorsAlert, - } = props; - const formRef = useRef(null); - const formContentRef = useRef(null); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [formState]); - - const scrollViewContent = useCallback( - (safeAreaPaddingBottomStyle) => ( - - {children} - {isSubmitButtonVisible && ( - 0 || !_.isEmpty(formState.errorFields)) && !shouldHideFixErrorsAlert) || Boolean(errorMessage)} - isLoading={formState.isLoading} - message={_.isEmpty(formState.errorFields) ? errorMessage : null} - onSubmit={onSubmit} - footerContent={footerContent} - onFixTheErrorsLinkPressed={() => { - const errorFields = !_.isEmpty(errors) ? errors : formState.errorFields; - const focusKey = _.find(_.keys(inputRefs.current), (key) => _.keys(errorFields).includes(key)); - const focusInput = inputRefs.current[focusKey].current; - - // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. - if (typeof focusInput.isFocused !== 'function') { - Keyboard.dismiss(); - } - - // We subtract 10 to scroll slightly above the input - if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') { - // We measure relative to the content root, not the scroll view, as that gives - // consistent results across mobile and web - focusInput.measureLayout(formContentRef.current, (x, y) => - formRef.current.scrollTo({ - y: y - 10, - animated: false, - }), - ); - } - - // Focus the input after scrolling, as on the Web it gives a slightly better visual result - if (focusInput.focus && typeof focusInput.focus === 'function') { - focusInput.focus(); - } - }} - containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...submitButtonStyles]} - enabledWhenOffline={enabledWhenOffline} - isSubmitActionDangerous={isSubmitActionDangerous} - disablePressOnEnter - shouldHideFixErrorsAlert={shouldHideFixErrorsAlert} - /> - )} - - ), - [ - children, - enabledWhenOffline, - errorMessage, - errors, - footerContent, - formID, - formState.errorFields, - formState.isLoading, - inputRefs, - isSubmitActionDangerous, - isSubmitButtonVisible, - onSubmit, - style, - styles.flex1, - styles.mh0, - styles.mt5, - submitButtonStyles, - submitButtonText, - shouldHideFixErrorsAlert, - ], - ); - - return ( - - {({safeAreaPaddingBottomStyle}) => - props.scrollContextEnabled ? ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) : ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) - } - - ); -} - -FormWrapper.displayName = 'FormWrapper'; -FormWrapper.propTypes = propTypes; -FormWrapper.defaultProps = defaultProps; - -export default withOnyx({ - formState: { - key: (props) => props.formID, - }, -})(FormWrapper); diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx new file mode 100644 index 000000000000..d5b47761e4c0 --- /dev/null +++ b/src/components/Form/FormWrapper.tsx @@ -0,0 +1,178 @@ +import React, {useCallback, useMemo, useRef} from 'react'; +import type {RefObject} from 'react'; +import type {StyleProp, View, ViewStyle} from 'react-native'; +import {Keyboard, ScrollView} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import FormSubmit from '@components/FormSubmit'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; +import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import type {Form} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {FormProps, InputRefs} from './types'; + +type FormWrapperOnyxProps = { + /** Contains the form state that must be accessed outside the component */ + formState: OnyxEntry; +}; + +type FormWrapperProps = ChildrenProps & + FormWrapperOnyxProps & + FormProps & { + /** Submit button styles */ + submitButtonStyles?: StyleProp; + + /** Server side errors keyed by microtime */ + errors: Errors; + + /** Assuming refs are React refs */ + inputRefs: RefObject; + + /** Callback to submit the form */ + onSubmit: () => void; + }; + +function FormWrapper({ + onSubmit, + children, + formState, + errors, + inputRefs, + submitButtonText, + footerContent, + isSubmitButtonVisible = true, + style, + submitButtonStyles, + enabledWhenOffline, + isSubmitActionDangerous = false, + formID, + scrollContextEnabled = false, + shouldHideFixErrorsAlert = false, +}: FormWrapperProps) { + const styles = useThemeStyles(); + const formRef = useRef(null); + const formContentRef = useRef(null); + const errorMessage = useMemo(() => (formState ? ErrorUtils.getLatestErrorMessage(formState) : undefined), [formState]); + + const onFixTheErrorsLinkPressed = useCallback(() => { + const errorFields = !isEmptyObject(errors) ? errors : formState?.errorFields ?? {}; + const focusKey = Object.keys(inputRefs.current ?? {}).find((key) => Object.keys(errorFields).includes(key)); + + if (!focusKey) { + return; + } + + const focusInput = inputRefs.current?.[focusKey]?.current; + + // Dismiss the keyboard for non-text fields by checking if the component has the isFocused method, as only TextInput has this method. + if (typeof focusInput?.isFocused !== 'function') { + Keyboard.dismiss(); + } + + // We subtract 10 to scroll slightly above the input + if (formContentRef.current) { + // We measure relative to the content root, not the scroll view, as that gives + // consistent results across mobile and web + focusInput?.measureLayout?.(formContentRef.current, (X: number, y: number) => + formRef.current?.scrollTo({ + y: y - 10, + animated: false, + }), + ); + } + + // Focus the input after scrolling, as on the Web it gives a slightly better visual result + focusInput?.focus?.(); + }, [errors, formState?.errorFields, inputRefs]); + + const scrollViewContent = useCallback( + (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( + + {children} + {isSubmitButtonVisible && ( + + )} + + ), + [ + children, + enabledWhenOffline, + errorMessage, + errors, + footerContent, + formID, + formState?.errorFields, + formState?.isLoading, + isSubmitActionDangerous, + isSubmitButtonVisible, + onSubmit, + style, + styles.flex1, + styles.mh0, + styles.mt5, + submitButtonStyles, + submitButtonText, + shouldHideFixErrorsAlert, + onFixTheErrorsLinkPressed, + ], + ); + + return ( + + {({safeAreaPaddingBottomStyle}) => + scrollContextEnabled ? ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) : ( + + {scrollViewContent(safeAreaPaddingBottomStyle)} + + ) + } + + ); +} + +FormWrapper.displayName = 'FormWrapper'; + +export default withOnyx({ + formState: { + // withOnyx typings are not able to handle such generic cases like this one, since it's a generic component we need to cast the keys to any + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any + key: (props) => props.formID as any, + }, +})(FormWrapper); diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js deleted file mode 100644 index 9a31210195c4..000000000000 --- a/src/components/Form/InputWrapper.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {forwardRef, useContext} from 'react'; -import refPropTypes from '@components/refPropTypes'; -import TextInput from '@components/TextInput'; -import FormContext from './FormContext'; - -const propTypes = { - InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, - inputID: PropTypes.string.isRequired, - valueType: PropTypes.string, - forwardedRef: refPropTypes, -}; - -const defaultProps = { - forwardedRef: undefined, - valueType: 'string', -}; - -function InputWrapper(props) { - const {InputComponent, inputID, forwardedRef, ...rest} = props; - const {registerInput} = useContext(FormContext); - // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to - // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were - // calling some methods too early or twice, so we had to add this check to prevent that side effect. - // For now this side effect happened only in `TextInput` components. - const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} - -InputWrapper.propTypes = propTypes; -InputWrapper.defaultProps = defaultProps; -InputWrapper.displayName = 'InputWrapper'; - -const InputWrapperWithRef = forwardRef((props, ref) => ( - -)); - -InputWrapperWithRef.displayName = 'InputWrapperWithRef'; - -export default InputWrapperWithRef; diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx new file mode 100644 index 000000000000..ae78e909753b --- /dev/null +++ b/src/components/Form/InputWrapper.tsx @@ -0,0 +1,23 @@ +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useContext} from 'react'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import TextInput from '@components/TextInput'; +import FormContext from './FormContext'; +import type {InputWrapperProps, ValidInputs} from './types'; + +function InputWrapper({InputComponent, inputID, valueType = 'string', ...rest}: InputWrapperProps, ref: ForwardedRef) { + const {registerInput} = useContext(FormContext); + // There are inputs that don't have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to + // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were + // calling some methods too early or twice, so we had to add this check to prevent that side effect. + // For now this side effect happened only in `TextInput` components. + const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; + + // TODO: Sometimes we return too many props with register input, so we need to consider if it's better to make the returned type more general and disregard the issue, or we would like to omit the unused props somehow. + // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any + return ; +} + +InputWrapper.displayName = 'InputWrapper'; + +export default forwardRef(InputWrapper); diff --git a/src/components/Form/errorsPropType.js b/src/components/Form/errorsPropType.js deleted file mode 100644 index 3a02bb74e942..000000000000 --- a/src/components/Form/errorsPropType.js +++ /dev/null @@ -1,11 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOfType([ - PropTypes.string, - PropTypes.objectOf( - PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]))])), - ]), - ), -]); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts new file mode 100644 index 000000000000..447f3205ad68 --- /dev/null +++ b/src/components/Form/types.ts @@ -0,0 +1,93 @@ +import type {ComponentProps, FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react'; +import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type AddressSearch from '@components/AddressSearch'; +import type AmountTextInput from '@components/AmountTextInput'; +import type CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type Picker from '@components/Picker'; +import type SingleChoiceQuestion from '@components/SingleChoiceQuestion'; +import type TextInput from '@components/TextInput'; +import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; +import type Form from '@src/types/onyx/Form'; +import type {BaseForm, FormValueType} from '@src/types/onyx/Form'; + +/** + * This type specifies all the inputs that can be used with `InputWrapper` component. Make sure to update it + * when adding new inputs or removing old ones. + * + * TODO: Add remaining inputs here once these components are migrated to Typescript: + * CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker + */ +type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch; + +type ValueTypeKey = 'string' | 'boolean' | 'date'; + +type MeasureLayoutOnSuccessCallback = (left: number, top: number, width: number, height: number) => void; + +type BaseInputProps = { + shouldSetTouchedOnBlurOnly?: boolean; + onValueChange?: (value: unknown, key: string) => void; + onTouched?: (event: GestureResponderEvent) => void; + valueType?: ValueTypeKey; + value?: FormValueType; + defaultValue?: FormValueType; + onBlur?: (event: FocusEvent | NativeSyntheticEvent) => void; + onPressOut?: (event: GestureResponderEvent) => void; + onPress?: (event: GestureResponderEvent) => void; + shouldSaveDraft?: boolean; + shouldUseDefaultValue?: boolean; + key?: Key | null | undefined; + ref?: Ref; + isFocused?: boolean; + measureLayout?: (ref: unknown, callback: MeasureLayoutOnSuccessCallback) => void; + focus?: () => void; +}; + +type InputWrapperProps = Omit & + ComponentProps & { + InputComponent: TInput; + inputID: string; + }; + +type ExcludeDraft = T extends `${string}Draft` ? never : T; +type OnyxFormKeyWithoutDraft = ExcludeDraft; + +type OnyxFormValues = OnyxValues[TOnyxKey]; +type OnyxFormValuesFields = Omit, keyof BaseForm>; + +type FormProps = { + /** A unique Onyx key identifying the form */ + formID: TFormID; + + /** Text to be displayed in the submit button */ + submitButtonText: string; + + /** Controls the submit button's visibility */ + isSubmitButtonVisible?: boolean; + + /** Callback to submit the form */ + onSubmit: (values: OnyxFormValuesFields) => void; + + /** Should the button be enabled when offline */ + enabledWhenOffline?: boolean; + + /** Whether the form submit action is dangerous */ + isSubmitActionDangerous?: boolean; + + /** Should fix the errors alert be displayed when there is an error in the form */ + shouldHideFixErrorsAlert?: boolean; + + /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ + scrollContextEnabled?: boolean; + + /** Container styles */ + style?: StyleProp; + + /** Custom content to display in the footer after submit button */ + footerContent?: ReactNode; +}; + +type RegisterInput = (inputID: keyof Form, inputProps: TInputProps) => TInputProps; + +type InputRefs = Record>; + +export type {InputWrapperProps, FormProps, RegisterInput, ValidInputs, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 512d2063dc0f..ae96aa6c5359 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -65,7 +65,7 @@ function FormAlertWithSubmitButton({ enabledWhenOffline = false, disablePressOnEnter = false, isSubmitActionDangerous = false, - footerContent = null, + footerContent, buttonStyles, buttonText, isAlertVisible, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 11ffabe4fe6a..3646d9148b3a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -1,3 +1,4 @@ +import {cloneDeep} from 'lodash'; import lodashGet from 'lodash/get'; import React from 'react'; import {TNodeChildrenRenderer} from 'react-native-render-html'; @@ -16,6 +17,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; import CONST from '@src/CONST'; +import * as LoginUtils from '@src/libs/LoginUtils'; import ROUTES from '@src/ROUTES'; import htmlRendererPropTypes from './htmlRendererPropTypes'; @@ -31,21 +33,41 @@ function MentionUserRenderer(props) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); - const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid'); + const htmlAttributeAccountID = lodashGet(props.tnode.attributes, 'accountid'); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; let accountID; let displayNameOrLogin; let navigationRoute; + const tnode = cloneDeep(props.tnode); - if (!_.isEmpty(htmlAttribAccountID)) { - const user = lodashGet(personalDetails, htmlAttribAccountID); - accountID = parseInt(htmlAttribAccountID, 10); + const getMentionDisplayText = (displayText, userAccountID, userLogin = '') => { + // If the userAccountID does not exist, this is an email-based mention so the displayText must be an email. + // If the userAccountID exists but userLogin is different from displayText, this means the displayText is either user display name, Hidden, or phone number, in which case we should return it as is. + if (userAccountID && userLogin !== displayText) { + return displayText; + } + + // If the emails are not in the same private domain, we also return the displayText + if (!LoginUtils.areEmailsFromSamePrivateDomain(displayText, props.currentUserPersonalDetails.login)) { + return displayText; + } + + // Otherwise, the emails must be of the same private domain, so we should remove the domain part + return displayText.split('@')[0]; + }; + + if (!_.isEmpty(htmlAttributeAccountID)) { + const user = lodashGet(personalDetails, htmlAttributeAccountID); + accountID = parseInt(htmlAttributeAccountID, 10); displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(lodashGet(user, 'login', '')) || lodashGet(user, 'displayName', '') || translate('common.hidden'); - navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); - } else if (!_.isEmpty(props.tnode.data)) { + displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID, lodashGet(user, 'login', '')); + navigationRoute = ROUTES.PROFILE.getRoute(htmlAttributeAccountID); + } else if (!_.isEmpty(tnode.data)) { // We need to remove the LTR unicode and leading @ from data as it is not part of the login - displayNameOrLogin = props.tnode.data.replace(CONST.UNICODE.LTR, '').slice(1); + displayNameOrLogin = tnode.data.replace(CONST.UNICODE.LTR, '').slice(1); + // We need to replace tnode.data here because we will pass it to TNodeChildrenRenderer below + tnode.data = tnode.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID)); accountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])); navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); @@ -83,7 +105,7 @@ function MentionUserRenderer(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...defaultRendererProps} > - {!_.isEmpty(htmlAttribAccountID) ? `@${displayNameOrLogin}` : } + {!_.isEmpty(htmlAttributeAccountID) ? `@${displayNameOrLogin}` : } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index a0f24b06db7f..2d5ad0c45536 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -39,6 +39,7 @@ function HeaderWithBackButton({ shouldShowGetAssistanceButton = false, shouldDisableGetAssistanceButton = false, shouldShowPinButton = false, + shouldSetModalVisibility = true, shouldShowThreeDotsButton = false, shouldDisableThreeDotsButton = false, stepCounter, @@ -165,6 +166,7 @@ function HeaderWithBackButton({ onIconPress={onThreeDotsButtonPress} anchorPosition={threeDotsAnchorPosition} shouldOverlay={shouldOverlayDots} + shouldSetModalVisibility={shouldSetModalVisibility} /> )} {shouldShowCloseButton && ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 725d14e041a7..88f7e717a44d 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -61,6 +61,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should disable threedots button */ shouldDisableThreeDotsButton?: boolean; + /** Whether we should set modal visibility when three dot menu opens */ + shouldSetModalVisibility?: boolean; + /** List of menu items for more(three dots) menu */ threeDotsMenuItems?: ThreeDotsMenuItem[]; diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx index e36bb39d2bed..8de1946ef554 100644 --- a/src/components/ImageView/index.native.tsx +++ b/src/components/ImageView/index.native.tsx @@ -1,15 +1,13 @@ import React from 'react'; import Lightbox from '@components/Lightbox'; -import {zoomRangeDefaultProps} from '@components/MultiGestureCanvas/propTypes'; +import {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; import type {ImageViewProps} from './types'; function ImageView({ isAuthTokenRequired = false, url, - onScaleChanged, - onPress, style, - zoomRange = zoomRangeDefaultProps.zoomRange, + zoomRange = DEFAULT_ZOOM_RANGE, onError, isUsedInCarousel = false, isSingleCarouselItem = false, @@ -20,11 +18,9 @@ function ImageView({ return ( void; - /** URL to full-sized image */ url: string; @@ -29,9 +26,6 @@ type ImageViewProps = { /** The index of the currently active carousel item */ carouselActiveItemIndex?: number; - /** Function for handle on press */ - onPress?: () => void; - /** Additional styles to add to the component */ style?: StyleProp; diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index c65faef53748..d0559327274a 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -1,6 +1,6 @@ import delay from 'lodash/delay'; import React, {useEffect, useRef, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; @@ -19,7 +19,7 @@ type OnLoadNativeEvent = { type ImageWithSizeCalculationProps = { /** Url for image to display */ - url: string | number; + url: string | ImageSourcePropType; /** Any additional styles to apply */ style?: StyleProp; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 5bf93eb8a6b3..486189c66710 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -1,8 +1,7 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; diff --git a/src/components/InvertedFlatList/CellRendererComponent.tsx b/src/components/InvertedFlatList/CellRendererComponent.tsx index b95fbf42cbb4..1199fb2a594c 100644 --- a/src/components/InvertedFlatList/CellRendererComponent.tsx +++ b/src/components/InvertedFlatList/CellRendererComponent.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import type {StyleProp, ViewProps} from 'react-native'; +import type {StyleProp, ViewProps, ViewStyle} from 'react-native'; import {View} from 'react-native'; type CellRendererComponentProps = ViewProps & { index: number; - style?: StyleProp; + style?: StyleProp; }; function CellRendererComponent(props: CellRendererComponentProps) { diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index ecf320807b48..15c12afb2609 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -32,9 +32,23 @@ function LHNOptionsList({ currentReportID = '', draftComments = {}, transactionViolations = {}, + onFirstItemRendered = () => {}, }: LHNOptionsListProps) { const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); + + // When the first item renders we want to call the onFirstItemRendered callback. + // At this point in time we know that the list is actually displaying items. + const hasCalledOnLayout = React.useRef(false); + const onLayoutItem = useCallback(() => { + if (hasCalledOnLayout.current) { + return; + } + hasCalledOnLayout.current = true; + + onFirstItemRendered(); + }, [onFirstItemRendered]); + /** * Function which renders a row in the list */ @@ -48,7 +62,7 @@ function LHNOptionsList({ const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; - const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID]; + const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID].filter(Boolean) as number[]; const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); return ( @@ -58,7 +72,6 @@ function LHNOptionsList({ reportActions={itemReportActions} parentReportAction={itemParentReportAction} policy={itemPolicy} - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. personalDetails={participantsPersonalDetails} transaction={itemTransaction} receiptTransactions={transactions} @@ -69,6 +82,7 @@ function LHNOptionsList({ comment={itemComment} transactionViolations={transactionViolations} canUseViolations={canUseViolations} + onLayout={onLayoutItem} /> ); }, @@ -86,6 +100,7 @@ function LHNOptionsList({ transactions, transactionViolations, canUseViolations, + onLayoutItem, ], ); diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 4123e9d20d58..ae225b3db9e9 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -28,7 +28,7 @@ import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OptionRowLHNProps} from './types'; -function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style}: OptionRowLHNProps) { +function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style, onLayout = () => {}}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); @@ -164,6 +164,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti ]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} + onLayout={onLayout} needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} > diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 24cebb8e3da2..1f2c98301f9a 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,6 +1,6 @@ import type {ContentStyle} from '@shopify/flash-list'; import type {RefObject} from 'react'; -import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; @@ -47,13 +47,16 @@ type CustomLHNOptionsListProps = { data: string[]; /** Callback to fire when a row is selected */ - onSelectRow: (reportID: string) => void; + onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; /** Toggle between compact and default view of the option */ optionMode: OptionMode; /** Whether to allow option focus or not */ shouldDisableFocusOptions?: boolean; + + /** Callback to fire when the list is laid out */ + onFirstItemRendered: () => void; }; type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; @@ -97,6 +100,15 @@ type OptionRowLHNDataProps = { /** Whether the user can use violations */ canUseViolations: boolean | undefined; + + /** Toggle between compact and default view */ + viewMode?: OptionMode; + + /** A function that is called when an option is selected. Selected option is passed as a param */ + onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; + + /** Callback to execute when the OptionList lays out */ + onLayout?: (event: LayoutChangeEvent) => void; }; type OptionRowLHNProps = { @@ -117,6 +129,8 @@ type OptionRowLHNProps = { /** The item that should be rendered */ optionItem?: OptionData; + + onLayout?: (event: LayoutChangeEvent) => void; }; type RenderItemProps = {item: string}; diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js deleted file mode 100644 index 8b7d68befafd..000000000000 --- a/src/components/Lightbox.js +++ /dev/null @@ -1,239 +0,0 @@ -/* eslint-disable es/no-optional-chaining */ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; -import useStyleUtils from '@hooks/useStyleUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as AttachmentsPropTypes from './Attachments/propTypes'; -import Image from './Image'; -import MultiGestureCanvas from './MultiGestureCanvas'; -import getCanvasFitScale from './MultiGestureCanvas/getCanvasFitScale'; -import {zoomRangeDefaultProps, zoomRangePropTypes} from './MultiGestureCanvas/propTypes'; - -// Increase/decrease this number to change the number of concurrent lightboxes -// The more concurrent lighboxes, the worse performance gets (especially on low-end devices) -// -1 means unlimited -const NUMBER_OF_CONCURRENT_LIGHTBOXES = 3; - -const cachedDimensions = new Map(); - -/** - * On the native layer, we use a image library to handle zoom functionality - */ -const propTypes = { - ...zoomRangePropTypes, - - /** Function for handle on press */ - onPress: PropTypes.func, - - /** Handles errors while displaying the image */ - onError: PropTypes.func, - - /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, - - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ - hasSiblingCarouselItems: PropTypes.bool, - - /** The index of the carousel item */ - index: PropTypes.number, - - /** The index of the currently active carousel item */ - activeIndex: PropTypes.number, - - /** Additional styles to add to the component */ - style: stylePropTypes, -}; - -const defaultProps = { - ...zoomRangeDefaultProps, - - isAuthTokenRequired: false, - index: 0, - activeIndex: 0, - hasSiblingCarouselItems: false, - onPress: () => {}, - onError: () => {}, - style: {}, -}; - -const DEFAULT_IMAGE_SIZE = 200; - -function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError, style, index, activeIndex, hasSiblingCarouselItems, zoomRange}) { - const StyleUtils = useStyleUtils(); - - const [containerSize, setContainerSize] = useState({width: 0, height: 0}); - const isContainerLoaded = containerSize.width !== 0 && containerSize.height !== 0; - - const [imageDimensions, _setImageDimensions] = useState(() => cachedDimensions.get(source)); - const setImageDimensions = (newDimensions) => { - _setImageDimensions(newDimensions); - cachedDimensions.set(source, newDimensions); - }; - - const isItemActive = index === activeIndex; - const [isActive, setActive] = useState(isItemActive); - const [isImageLoaded, setImageLoaded] = useState(false); - - const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; - const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); - const [isFallbackLoaded, setFallbackLoaded] = useState(false); - - const isLightboxLoaded = imageDimensions?.lightboxSize != null; - const isLightboxInRange = useMemo(() => { - if (NUMBER_OF_CONCURRENT_LIGHTBOXES === -1) { - return true; - } - - const indexCanvasOffset = Math.floor((NUMBER_OF_CONCURRENT_LIGHTBOXES - 1) / 2) || 0; - const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; - return !indexOutOfRange; - }, [activeIndex, index]); - const isLightboxVisible = isLightboxInRange && (isActive || isLightboxLoaded || isFallbackLoaded); - - const isLoading = isActive && (!isContainerLoaded || !isImageLoaded); - - const updateCanvasSize = useCallback( - ({nativeEvent}) => setContainerSize({width: PixelRatio.roundToNearestPixel(nativeEvent.layout.width), height: PixelRatio.roundToNearestPixel(nativeEvent.layout.height)}), - [], - ); - - // We delay setting a page to active state by a (few) millisecond(s), - // to prevent the image transformer from flashing while still rendering - // Instead, we show the fallback image while the image transformer is loading the image - useEffect(() => { - if (isItemActive) { - setTimeout(() => setActive(true), 1); - } else { - setActive(false); - } - }, [isItemActive]); - - useEffect(() => { - if (isLightboxVisible) { - return; - } - setImageLoaded(false); - }, [isLightboxVisible]); - - useEffect(() => { - if (!hasSiblingCarouselItems) { - return; - } - - if (isActive) { - if (isImageLoaded && isFallbackVisible) { - // We delay hiding the fallback image while image transformer is still rendering - setTimeout(() => { - setFallbackVisible(false); - setFallbackLoaded(false); - }, 100); - } - } else { - if (isLightboxVisible && isLightboxLoaded) { - return; - } - - // Show fallback when the image goes out of focus or when the image is loading - setFallbackVisible(true); - } - }, [hasSiblingCarouselItems, isActive, isImageLoaded, isFallbackVisible, isLightboxLoaded, isLightboxVisible]); - - const fallbackSize = useMemo(() => { - if (!hasSiblingCarouselItems || (imageDimensions?.lightboxSize == null && imageDimensions?.fallbackSize == null) || containerSize.width === 0 || containerSize.height === 0) { - return { - width: DEFAULT_IMAGE_SIZE, - height: DEFAULT_IMAGE_SIZE, - }; - } - - const imageSize = imageDimensions.lightboxSize || imageDimensions.fallbackSize; - - const {minScale} = getCanvasFitScale({canvasSize: containerSize, contentSize: imageSize}); - - return { - width: PixelRatio.roundToNearestPixel(imageSize.width * minScale), - height: PixelRatio.roundToNearestPixel(imageSize.height * minScale), - }; - }, [containerSize, hasSiblingCarouselItems, imageDimensions]); - - return ( - - {isContainerLoaded && ( - <> - {isLightboxVisible && ( - - - setImageLoaded(true)} - onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - setImageDimensions({...imageDimensions, lightboxSize: {width, height}}); - }} - /> - - - )} - - {/* Keep rendering the image without gestures as fallback if the carousel item is not active and while the lightbox is loading the image */} - {isFallbackVisible && ( - - setFallbackLoaded(true)} - onLoad={(e) => { - const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); - const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); - - if (imageDimensions?.lightboxSize != null) { - return; - } - - setImageDimensions({...imageDimensions, fallbackSize: {width, height}}); - }} - /> - - )} - - {/* Show activity indicator while the lightbox is still loading the image. */} - {isLoading && ( - - )} - - )} - - ); -} - -Lightbox.propTypes = propTypes; -Lightbox.defaultProps = defaultProps; -Lightbox.displayName = 'Lightbox'; - -export default Lightbox; diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx new file mode 100644 index 000000000000..aeec1876eb93 --- /dev/null +++ b/src/components/Lightbox/index.tsx @@ -0,0 +1,222 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; +import Image from '@components/Image'; +import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types'; +import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils'; +import useStyleUtils from '@hooks/useStyleUtils'; +import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes'; + +const DEFAULT_IMAGE_SIZE = 200; +const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; + +type ImageOnLoadEvent = NativeSyntheticEvent; + +const cachedImageDimensions = new Map(); + +type LightboxProps = { + /** Whether source url requires authentication */ + isAuthTokenRequired?: boolean; + + /** URI to full-sized attachment */ + uri: string; + + /** Triggers whenever the zoom scale changes */ + onScaleChanged?: OnScaleChangedCallback; + + /** Handles errors while displaying the image */ + onError?: () => void; + + /** Additional styles to add to the component */ + style?: StyleProp; + + /** The index of the carousel item */ + index?: number; + + /** The index of the currently active carousel item */ + activeIndex?: number; + + /** Whether the Lightbox is used within a carousel component and there are other sibling elements */ + hasSiblingCarouselItems?: boolean; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: Partial; +}; + +/** + * On the native layer, we use a image library to handle zoom functionality + */ +function Lightbox({ + isAuthTokenRequired = false, + uri, + onScaleChanged, + onError, + style, + index = 0, + activeIndex = 0, + hasSiblingCarouselItems = false, + zoomRange = DEFAULT_ZOOM_RANGE, +}: LightboxProps) { + const StyleUtils = useStyleUtils(); + + const [canvasSize, setCanvasSize] = useState(); + const isCanvasLoading = canvasSize === undefined; + const updateCanvasSize = useCallback( + ({ + nativeEvent: { + layout: {width, height}, + }, + }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}), + [], + ); + + const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); + const setContentSize = useCallback( + (newDimensions: ContentSize | undefined) => { + setInternalContentSize(newDimensions); + cachedImageDimensions.set(uri, newDimensions); + }, + [uri], + ); + const updateContentSize = useCallback( + ({nativeEvent: {width, height}}: ImageOnLoadEvent) => { + if (contentSize !== undefined) { + return; + } + + setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}); + }, + [contentSize, setContentSize], + ); + + // Enables/disables the lightbox based on the number of concurrent lightboxes + // On higher-end devices, we can show render lightboxes at the same time, + // while on lower-end devices we want to only render the active carousel item as a lightbox + // to avoid performance issues. + const isLightboxVisible = useMemo(() => { + if (!hasSiblingCarouselItems || NUMBER_OF_CONCURRENT_LIGHTBOXES === 'UNLIMITED') { + return true; + } + + const indexCanvasOffset = Math.floor((NUMBER_OF_CONCURRENT_LIGHTBOXES - 1) / 2) || 0; + const indexOutOfRange = index > activeIndex + indexCanvasOffset || index < activeIndex - indexCanvasOffset; + return !indexOutOfRange; + }, [activeIndex, hasSiblingCarouselItems, index]); + const [isLightboxImageLoaded, setLightboxImageLoaded] = useState(false); + + const [isFallbackVisible, setFallbackVisible] = useState(!isLightboxVisible); + const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); + const fallbackSize = useMemo(() => { + if (!hasSiblingCarouselItems || !contentSize || isCanvasLoading) { + return DEFAULT_IMAGE_DIMENSION; + } + + const {minScale} = getCanvasFitScale({canvasSize, contentSize}); + + return { + width: PixelRatio.roundToNearestPixel(contentSize.width * minScale), + height: PixelRatio.roundToNearestPixel(contentSize.height * minScale), + }; + }, [hasSiblingCarouselItems, contentSize, isCanvasLoading, canvasSize]); + + // If the fallback image is currently visible, we want to hide the Lightbox by setting the opacity to 0, + // until the fallback gets hidden so that we don't see two overlapping images at the same time. + // If there the Lightbox is not used within a carousel, we don't need to hide the Lightbox, + // because it's only going to be rendered after the fallback image is hidden. + const shouldShowLightbox = isLightboxImageLoaded && !isFallbackVisible; + + const isActive = index === activeIndex; + const isFallbackStillLoading = isFallbackVisible && !isFallbackImageLoaded; + const isLightboxStillLoading = isLightboxVisible && !isLightboxImageLoaded; + const isLoading = isActive && (isCanvasLoading || isFallbackStillLoading || isLightboxStillLoading); + + // Resets the lightbox when it becomes inactive + useEffect(() => { + if (isLightboxVisible) { + return; + } + setLightboxImageLoaded(false); + setContentSize(undefined); + }, [isLightboxVisible, setContentSize]); + + // Enables and disables the fallback image when the carousel item is active or not + useEffect(() => { + // When there are no other carousel items, we don't need to show the fallback image + if (!hasSiblingCarouselItems) { + return; + } + + // When the carousel item is active and the lightbox has finished loading, we want to hide the fallback image + if (isActive && isFallbackVisible && isLightboxVisible && isLightboxImageLoaded) { + setFallbackVisible(false); + setFallbackImageLoaded(false); + return; + } + + // If the carousel item has become inactive and the lightbox is not continued to be rendered, we want to show the fallback image + if (!isActive && !isLightboxVisible) { + setFallbackVisible(true); + } + }, [hasSiblingCarouselItems, isActive, isFallbackVisible, isLightboxImageLoaded, isLightboxVisible]); + + return ( + + {!isCanvasLoading && ( + <> + {isLightboxVisible && ( + + + { + setLightboxImageLoaded(true); + }} + /> + + + )} + + {/* Keep rendering the image without gestures as fallback if the carousel item is not active and while the lightbox is loading the image */} + {isFallbackVisible && ( + + setFallbackImageLoaded(true)} + /> + + )} + + {/* Show activity indicator while the lightbox is still loading the image. */} + {isLoading && ( + + )} + + )} + + ); +} + +Lightbox.displayName = 'Lightbox'; + +export default Lightbox; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts new file mode 100644 index 000000000000..1ce0d2cee405 --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ios.ts @@ -0,0 +1,8 @@ +import type LightboxConcurrencyLimit from './types'; + +// On iOS we can allow multiple lightboxes to be rendered at the same time. +// This enables faster time to interaction when swiping between pages in the carousel. +// When the lightbox is pre-rendered, we don't need to wait for the gestures to initialize. +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 3; + +export default NUMBER_OF_CONCURRENT_LIGHTBOXES; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts new file mode 100644 index 000000000000..f6f55a8913c7 --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/index.ts @@ -0,0 +1,8 @@ +import type LightboxConcurrencyLimit from './types'; + +// On web, this is not used. +// On Android, we don't want to allow rendering multiple lightboxes, +// because performance is typically slower than on iOS and this caused issues. +const NUMBER_OF_CONCURRENT_LIGHTBOXES: LightboxConcurrencyLimit = 1; + +export default NUMBER_OF_CONCURRENT_LIGHTBOXES; diff --git a/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts b/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts new file mode 100644 index 000000000000..57aaa53cca8c --- /dev/null +++ b/src/components/Lightbox/numberOfConcurrentLightboxes/types.ts @@ -0,0 +1,5 @@ +// Increase/decrease this number to change the number of concurrent lightboxes +// The more concurrent lighboxes, the worse performance gets (especially on low-end devices) +type LightboxConcurrencyLimit = number | 'UNLIMITED'; + +export default LightboxConcurrencyLimit; diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index d42d471eba5e..fd593421232d 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -1,3 +1,4 @@ +import variables from '@styles/variables'; import type DotLottieAnimation from './types'; const DotLottieAnimations: Record = { @@ -51,6 +52,11 @@ const DotLottieAnimations: Record = { w: 853, h: 480, }, + Update: { + file: require('@assets/animations/Update.lottie'), + w: variables.updateAnimationW, + h: variables.updateAnimationH, + }, Coin: { file: require('@assets/animations/Coin.lottie'), w: 375, diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.tsx similarity index 61% rename from src/components/MagicCodeInput.js rename to src/components/MagicCodeInput.tsx index 8614736d200f..4a6d87b48e38 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.tsx @@ -1,8 +1,8 @@ -import PropTypes from 'prop-types'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {NativeSyntheticEvent, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import _ from 'underscore'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,89 +10,75 @@ import * as Browser from '@libs/Browser'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; +import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; const TEXT_INPUT_EMPTY_STATE = ''; -const propTypes = { - /** Information about the network */ - network: networkPropTypes.isRequired, +type AutoCompleteVariant = 'sms-otp' | 'one-time-code' | 'off'; +type MagicCodeInputProps = { /** Name attribute for the input */ - name: PropTypes.string, + name?: string; /** Input value */ - value: PropTypes.string, + value?: string; /** Should the input auto focus */ - autoFocus: PropTypes.bool, + autoFocus?: boolean; /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus: PropTypes.bool, + shouldDelayFocus?: boolean; /** Error text to display */ - errorText: PropTypes.string, + errorText?: string; /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code', 'off']).isRequired, + autoComplete: AutoCompleteVariant; /* Should submit when the input is complete */ - shouldSubmitOnComplete: PropTypes.bool, - - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + shouldSubmitOnComplete?: boolean; /** Function to call when the input is changed */ - onChangeText: PropTypes.func, + onChangeText?: (value: string) => void; /** Function to call when the input is submitted or fully complete */ - onFulfill: PropTypes.func, + onFulfill?: (value: string) => void; /** Specifies if the input has a validation error */ - hasError: PropTypes.bool, + hasError?: boolean; /** Specifies the max length of the input */ - maxLength: PropTypes.number, + maxLength?: number; /** Specifies if the keyboard should be disabled */ - isDisableKeyboard: PropTypes.bool, + isDisableKeyboard?: boolean; /** Last pressed digit on BigDigitPad */ - lastPressedDigit: PropTypes.string, + lastPressedDigit?: string; /** TestID for test */ - testID: PropTypes.string, + testID?: string; }; -const defaultProps = { - value: '', - name: '', - autoFocus: true, - shouldDelayFocus: false, - errorText: '', - shouldSubmitOnComplete: true, - innerRef: null, - onChangeText: () => {}, - onFulfill: () => {}, - hasError: false, - maxLength: CONST.MAGIC_CODE_LENGTH, - isDisableKeyboard: false, - lastPressedDigit: '', - testID: '', +type MagicCodeInputHandle = { + focus: () => void; + focusLastSelected: () => void; + resetFocus: () => void; + clear: () => void; + blur: () => void; }; /** * Converts a given string into an array of numbers that must have the same * number of elements as the number of inputs. - * - * @param {String} value - * @param {Number} length - * @returns {Array} */ -const decomposeString = (value, length) => { - let arr = _.map(value.split('').slice(0, length), (v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)); +const decomposeString = (value: string, length: number): string[] => { + let arr = value + .split('') + .slice(0, length) + .map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)); if (arr.length < length) { arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); } @@ -102,33 +88,48 @@ const decomposeString = (value, length) => { /** * Converts an array of strings into a single string. If there are undefined or * empty values, it will replace them with a space. - * - * @param {Array} value - * @returns {String} */ -const composeToString = (value) => _.map(value, (v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); - -const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys()); - -function MagicCodeInput(props) { +const composeToString = (value: string[]): string => value.map((v) => v ?? CONST.MAGIC_CODE_EMPTY_CHAR).join(''); + +const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); + +function MagicCodeInput( + { + value = '', + name = '', + autoFocus = true, + shouldDelayFocus = false, + errorText = '', + shouldSubmitOnComplete = true, + onChangeText: onChangeTextProp = () => {}, + maxLength = CONST.MAGIC_CODE_LENGTH, + onFulfill = () => {}, + isDisableKeyboard = false, + lastPressedDigit = '', + autoComplete, + hasError = false, + testID = '', + }: MagicCodeInputProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const inputRefs = useRef(); + const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); - const [focusedIndex, setFocusedIndex] = useState(0); + const [focusedIndex, setFocusedIndex] = useState(0); const [editIndex, setEditIndex] = useState(0); const [wasSubmitted, setWasSubmitted] = useState(false); const shouldFocusLast = useRef(false); const inputWidth = useRef(0); const lastFocusedIndex = useRef(0); - const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); + const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); useEffect(() => { lastValue.current = input.length; }, [input]); const blurMagicCodeInput = () => { - inputRefs.current.blur(); + inputRefs.current?.blur(); setFocusedIndex(undefined); }; @@ -136,21 +137,21 @@ function MagicCodeInput(props) { setFocusedIndex(0); lastFocusedIndex.current = 0; setEditIndex(0); - inputRefs.current.focus(); + inputRefs.current?.focus(); }; - const setInputAndIndex = (index) => { + const setInputAndIndex = (index: number) => { setInput(TEXT_INPUT_EMPTY_STATE); setFocusedIndex(index); setEditIndex(index); }; - useImperativeHandle(props.innerRef, () => ({ + useImperativeHandle(ref, () => ({ focus() { focusMagicCodeInput(); }, focusLastSelected() { - inputRefs.current.focus(); + inputRefs.current?.focus(); }, resetFocus() { setInput(TEXT_INPUT_EMPTY_STATE); @@ -159,8 +160,8 @@ function MagicCodeInput(props) { clear() { lastFocusedIndex.current = 0; setInputAndIndex(0); - inputRefs.current.focus(); - props.onChangeText(''); + inputRefs.current?.focus(); + onChangeTextProp(''); }, blur() { blurMagicCodeInput(); @@ -168,8 +169,9 @@ function MagicCodeInput(props) { })); const validateAndSubmit = () => { - const numbers = decomposeString(props.value, props.maxLength); - if (wasSubmitted || !props.shouldSubmitOnComplete || _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || props.network.isOffline) { + const numbers = decomposeString(value, maxLength); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== maxLength || isOffline) { return; } if (!wasSubmitted) { @@ -178,28 +180,25 @@ function MagicCodeInput(props) { // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. blurMagicCodeInput(); - props.onFulfill(props.value); + onFulfill(value); lastValue.current = ''; }; - useNetwork({onReconnect: validateAndSubmit}); + const {isOffline} = useNetwork({onReconnect: validateAndSubmit}); useEffect(() => { validateAndSubmit(); // We have not added: // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. - // + the props.onFulfill as the dependency because props.onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. + // + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.value, props.shouldSubmitOnComplete]); + }, [value, shouldSubmitOnComplete]); /** * Focuses on the input when it is pressed. - * - * @param {Object} event - * @param {Number} index */ - const onFocus = (event) => { + const onFocus = (event: NativeSyntheticEvent) => { if (shouldFocusLast.current) { lastValue.current = TEXT_INPUT_EMPTY_STATE; setInputAndIndex(lastFocusedIndex.current); @@ -214,12 +213,12 @@ function MagicCodeInput(props) { const tapGesture = Gesture.Tap() .runOnJS(true) .onBegin((event) => { - const index = Math.floor(event.x / (inputWidth.current / props.maxLength)); + const index = Math.floor(event.x / (inputWidth.current / maxLength)); shouldFocusLast.current = false; // TapGestureHandler works differently on mobile web and native app // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually if (!Browser.isMobileChrome() && !Browser.isMobileSafari()) { - inputRefs.current.focus(); + inputRefs.current?.focus(); } setInputAndIndex(index); lastFocusedIndex.current = index; @@ -231,36 +230,34 @@ function MagicCodeInput(props) { * the focused input on the next empty one, if exists. * It handles both fast typing and only one digit at a time * in a specific position. - * - * @param {String} value */ - const onChangeText = (value) => { - if (_.isUndefined(value) || _.isEmpty(value) || !ValidationUtils.isNumeric(value)) { + const onChangeText = (textValue?: string) => { + if (!textValue?.length || !ValidationUtils.isNumeric(textValue)) { return; } // Checks if one new character was added, or if the content was replaced - const hasToSlice = value.length - 1 === lastValue.current.length && value.slice(0, value.length - 1) === lastValue.current; + const hasToSlice = typeof lastValue.current === 'string' && textValue.length - 1 === lastValue.current.length && textValue.slice(0, textValue.length - 1) === lastValue.current; - // Gets the new value added by the user - const addedValue = hasToSlice ? value.slice(lastValue.current.length, value.length) : value; + // Gets the new textValue added by the user + const addedValue = hasToSlice && typeof lastValue.current === 'string' ? textValue.slice(lastValue.current.length, textValue.length) : textValue; - lastValue.current = value; + lastValue.current = textValue; // Updates the focused input taking into consideration the last input // edited and the number of digits added by the user. const numbersArr = addedValue .trim() .split('') - .slice(0, props.maxLength - editIndex); - const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, props.maxLength - 1); + .slice(0, maxLength - editIndex); + const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, maxLength - 1); - let numbers = decomposeString(props.value, props.maxLength); - numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, props.maxLength)]; + let numbers = decomposeString(value, maxLength); + numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, maxLength)]; setInputAndIndex(updatedFocusedIndex); const finalInput = composeToString(numbers); - props.onChangeText(finalInput); + onChangeTextProp(finalInput); }; /** @@ -268,73 +265,73 @@ function MagicCodeInput(props) { * * NOTE: when using Android Emulator, this can only be tested using * hardware keyboard inputs. - * - * @param {Object} event */ - const onKeyPress = ({nativeEvent: {key: keyValue}}) => { + const onKeyPress = (event: Partial>) => { + const keyValue = event?.nativeEvent?.key; if (keyValue === 'Backspace' || keyValue === '<') { - let numbers = decomposeString(props.value, props.maxLength); + let numbers = decomposeString(value, maxLength); // If keyboard is disabled and no input is focused we need to remove // the last entered digit and focus on the correct input - if (props.isDisableKeyboard && focusedIndex === undefined) { + if (isDisableKeyboard && focusedIndex === undefined) { const indexBeforeLastEditIndex = editIndex === 0 ? editIndex : editIndex - 1; const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; - inputRefs.current[indexToFocus].focus(); - props.onChangeText(props.value.substring(0, indexToFocus)); + const formElement = inputRefs.current as HTMLFormElement | null; + (formElement?.[indexToFocus] as HTMLInputElement).focus(); + onChangeTextProp(value.substring(0, indexToFocus)); return; } // If the currently focused index already has a value, it will delete // that value but maintain the focus on the same input. - if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + if (focusedIndex !== undefined && numbers?.[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { setInput(TEXT_INPUT_EMPTY_STATE); - numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, props.maxLength)]; + numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, maxLength)]; setEditIndex(focusedIndex); - props.onChangeText(composeToString(numbers)); + onChangeTextProp(composeToString(numbers)); return; } - const hasInputs = _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== 0; + const hasInputs = numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== 0; // Fill the array with empty characters if there are no inputs. if (focusedIndex === 0 && !hasInputs) { - numbers = Array(props.maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); // Deletes the value of the previous input and focuses on it. - } else if (focusedIndex !== 0) { - numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, props.maxLength)]; + } else if (focusedIndex && focusedIndex !== 0) { + numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, maxLength)]; } - const newFocusedIndex = Math.max(0, focusedIndex - 1); + const newFocusedIndex = Math.max(0, (focusedIndex ?? 0) - 1); // Saves the input string so that it can compare to the change text // event that will be triggered, this is a workaround for mobile that // triggers the change text on the event after the key press. setInputAndIndex(newFocusedIndex); - props.onChangeText(composeToString(numbers)); + onChangeTextProp(composeToString(numbers)); - if (!_.isUndefined(newFocusedIndex)) { - inputRefs.current.focus(); + if (newFocusedIndex !== undefined) { + inputRefs.current?.focus(); } } - if (keyValue === 'ArrowLeft' && !_.isUndefined(focusedIndex)) { + if (keyValue === 'ArrowLeft' && focusedIndex !== undefined) { const newFocusedIndex = Math.max(0, focusedIndex - 1); setInputAndIndex(newFocusedIndex); - inputRefs.current.focus(); - } else if (keyValue === 'ArrowRight' && !_.isUndefined(focusedIndex)) { - const newFocusedIndex = Math.min(focusedIndex + 1, props.maxLength - 1); + inputRefs.current?.focus(); + } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { + const newFocusedIndex = Math.min(focusedIndex + 1, maxLength - 1); setInputAndIndex(newFocusedIndex); - inputRefs.current.focus(); + inputRefs.current?.focus(); } else if (keyValue === 'Enter') { // We should prevent users from submitting when it's offline. - if (props.network.isOffline) { + if (isOffline) { return; } setInput(TEXT_INPUT_EMPTY_STATE); - props.onFulfill(props.value); + onFulfill(value); } }; @@ -346,18 +343,18 @@ function MagicCodeInput(props) { */ useEffect(() => { - if (!props.isDisableKeyboard) { + if (!isDisableKeyboard) { return; } - const value = props.lastPressedDigit.charAt(0); - onKeyPress({nativeEvent: {key: value}}); - onChangeText(value); + const textValue = lastPressedDigit.charAt(0); + onKeyPress({nativeEvent: {key: textValue}}); + onChangeText(textValue); // We have not added: // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.lastPressedDigit, props.isDisableKeyboard]); + }, [lastPressedDigit, isDisableKeyboard]); return ( <> @@ -372,76 +369,62 @@ function MagicCodeInput(props) { onLayout={(e) => { inputWidth.current = e.nativeEvent.layout.width; }} - ref={(ref) => (inputRefs.current = ref)} - autoFocus={props.autoFocus} + ref={(inputRef) => (inputRefs.current = inputRef)} + autoFocus={autoFocus} inputMode="numeric" textContentType="oneTimeCode" - name={props.name} - maxLength={props.maxLength} + name={name} + maxLength={maxLength} value={input} hideFocusedState - autoComplete={input.length === 0 && props.autoComplete} - shouldDelayFocus={input.length === 0 && props.shouldDelayFocus} + autoComplete={input.length === 0 ? autoComplete : undefined} + shouldDelayFocus={input.length === 0 && shouldDelayFocus} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={(value) => { - onChangeText(value); - }} + onChangeText={onChangeText} onKeyPress={onKeyPress} onFocus={onFocus} onBlur={() => { shouldFocusLast.current = true; - lastFocusedIndex.current = focusedIndex; + lastFocusedIndex.current = focusedIndex ?? 0; setFocusedIndex(undefined); }} selectionColor="transparent" inputStyle={[styles.inputTransparent]} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} style={[styles.inputTransparent]} textInputContainerStyles={[styles.borderNone]} - testID={props.testID} + testID={testID} /> - {_.map(getInputPlaceholderSlots(props.maxLength), (index) => ( + {getInputPlaceholderSlots(maxLength).map((index) => ( - {decomposeString(props.value, props.maxLength)[index] || ''} + {decomposeString(value, maxLength)[index] || ''} ))} - {!_.isEmpty(props.errorText) && ( + {errorText && ( )} ); } -MagicCodeInput.propTypes = propTypes; -MagicCodeInput.defaultProps = defaultProps; MagicCodeInput.displayName = 'MagicCodeInput'; -const MagicCodeInputWithRef = forwardRef((props, ref) => ( - -)); - -MagicCodeInputWithRef.displayName = 'MagicCodeInputWithRef'; - -export default withNetwork()(MagicCodeInputWithRef); +export default forwardRef(MagicCodeInput); diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index a250e21c0021..c1f4df8d4c99 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -2,6 +2,7 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react' import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; +import useKeyboardState from '@hooks/useKeyboardState'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -47,6 +48,7 @@ function BaseModal( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets(); @@ -163,7 +165,7 @@ function BaseModal( safeAreaPaddingRight, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding, + shouldAddBottomSafeAreaPadding: !keyboardStateContextValue?.isKeyboardShown && shouldAddBottomSafeAreaPadding, shouldAddTopSafeAreaPadding, modalContainerStyleMarginTop: modalContainerStyle.marginTop, modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, @@ -177,7 +179,7 @@ function BaseModal( onBackdropPress={handleBackdropPress} // Note: Escape key on web/desktop will trigger onBackButtonPress callback // eslint-disable-next-line react/jsx-props-no-multi-spaces - onBackButtonPress={onClose} + onBackButtonPress={Modal.closeTop} onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 7043173b3641..4b4e3915f969 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -52,7 +52,7 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); - const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport); + const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = policy?.type; @@ -66,8 +66,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); const shouldShowPayButton = useMemo( - () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport) && !isAutoReimbursable, - [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport, isAutoReimbursable], + () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !ReportUtils.isArchivedRoom(chatReport) && !isAutoReimbursable, + [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableSpend, chatReport, isAutoReimbursable], ); const shouldShowApproveButton = useMemo(() => { if (!isPaidGroupPolicy) { @@ -76,12 +76,12 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt return isManager && !isDraft && !isApproved && !isSettled; }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableTotal, moneyRequestReport.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth); // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on diff --git a/src/components/MultiGestureCanvas/constants.ts b/src/components/MultiGestureCanvas/constants.ts new file mode 100644 index 000000000000..58ad6997bbeb --- /dev/null +++ b/src/components/MultiGestureCanvas/constants.ts @@ -0,0 +1,28 @@ +import type {WithSpringConfig} from 'react-native-reanimated'; +import type {ZoomRange} from './types'; + +const DOUBLE_TAP_SCALE = 3; + +// The spring config is used to determine the physics of the spring animation +// Details and a playground for testing different configs can be found at +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring +const SPRING_CONFIG: WithSpringConfig = { + mass: 1, + stiffness: 1000, + damping: 500, +}; + +// The default zoom range within the user can pinch to zoom the content inside the canvas +const DEFAULT_ZOOM_RANGE: Required = { + min: 1, + max: 20, +}; + +// The zoom range bounce factors are used to determine the amount of bounce +// that is allowed when the user zooms more than the min or max zoom levels +const ZOOM_RANGE_BOUNCE_FACTORS: Required = { + min: 0.7, + max: 1.5, +}; + +export {DOUBLE_TAP_SCALE, SPRING_CONFIG, DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; diff --git a/src/components/MultiGestureCanvas/getCanvasFitScale.ts b/src/components/MultiGestureCanvas/getCanvasFitScale.ts deleted file mode 100644 index e3e402fb066b..000000000000 --- a/src/components/MultiGestureCanvas/getCanvasFitScale.ts +++ /dev/null @@ -1,22 +0,0 @@ -type GetCanvasFitScale = (props: { - canvasSize: { - width: number; - height: number; - }; - contentSize: { - width: number; - height: number; - }; -}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; - -const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { - const scaleX = canvasSize.width / contentSize.width; - const scaleY = canvasSize.height / contentSize.height; - - const minScale = Math.min(scaleX, scaleY); - const maxScale = Math.max(scaleX, scaleY); - - return {scaleX, scaleY, minScale, maxScale}; -}; - -export default getCanvasFitScale; diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js deleted file mode 100644 index bbfb7768c461..000000000000 --- a/src/components/MultiGestureCanvas/index.js +++ /dev/null @@ -1,617 +0,0 @@ -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - runOnJS, - runOnUI, - useAnimatedReaction, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - useWorkletCallback, - withDecay, - withSpring, -} from 'react-native-reanimated'; -import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import getCanvasFitScale from './getCanvasFitScale'; -import {defaultZoomRange, multiGestureCanvasDefaultProps, multiGestureCanvasPropTypes} from './propTypes'; - -const DOUBLE_TAP_SCALE = 3; - -const zoomScaleBounceFactors = { - min: 0.7, - max: 1.5, -}; - -const SPRING_CONFIG = { - mass: 1, - stiffness: 1000, - damping: 500, -}; - -function clamp(value, lowerBound, upperBound) { - 'worklet'; - - return Math.min(Math.max(lowerBound, value), upperBound); -} - -function getDeepDefaultProps({contentSize: contentSizeProp = {}, zoomRange: zoomRangeProp = {}}) { - const contentSize = { - width: contentSizeProp.width == null ? 1 : contentSizeProp.width, - height: contentSizeProp.height == null ? 1 : contentSizeProp.height, - }; - - const zoomRange = { - min: zoomRangeProp.min == null ? defaultZoomRange.min : zoomRangeProp.min, - max: zoomRangeProp.max == null ? defaultZoomRange.max : zoomRangeProp.max, - }; - - return {contentSize, zoomRange}; -} - -function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, children, ...props}) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {contentSize, zoomRange} = getDeepDefaultProps(props); - - const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - - const pagerRefFallback = useRef(null); - const {onTap, onSwipe, onSwipeSuccess, pagerRef, shouldPagerScroll, isScrolling, onPinchGestureChange} = attachmentCarouselPagerContext || { - onTap: () => undefined, - onSwipe: () => undefined, - onSwipeSuccess: () => undefined, - onPinchGestureChange: () => undefined, - pagerRef: pagerRefFallback, - shouldPagerScroll: false, - isScrolling: false, - ...props, - }; - - const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); - const scaledWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); - const scaledHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); - - // On double tap zoom to fill, but at least 3x zoom - const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); - - const zoomScale = useSharedValue(1); - // Adding together the pinch zoom scale and the initial scale to fit the content into the canvas - // Using the smaller content scale, so that the immage is not bigger than the canvas - // and not smaller than needed to fit - const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); - - const zoomScaledContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); - const zoomScaledContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); - - // used for pan gesture - const translateY = useSharedValue(0); - const translateX = useSharedValue(0); - const offsetX = useSharedValue(0); - const offsetY = useSharedValue(0); - const isSwiping = useSharedValue(false); - - // used for moving fingers when pinching - const pinchTranslateX = useSharedValue(0); - const pinchTranslateY = useSharedValue(0); - const pinchBounceTranslateX = useSharedValue(0); - const pinchBounceTranslateY = useSharedValue(0); - - // storage for the the origin of the gesture - const origin = { - x: useSharedValue(0), - y: useSharedValue(0), - }; - - // storage for the pan velocity to calculate the decay - const panVelocityX = useSharedValue(0); - const panVelocityY = useSharedValue(0); - - // store scale in between gestures - const pinchScaleOffset = useSharedValue(1); - - // disable pan vertically when content is smaller than screen - const canPanVertically = useDerivedValue(() => canvasSize.height < zoomScaledContentHeight.value, [canvasSize.height]); - - // calculates bounds of the scaled content - // can we pan left/right/up/down - // can be used to limit gesture or implementing tension effect - const getBounds = useWorkletCallback(() => { - let rightBoundary = 0; - let topBoundary = 0; - - if (canvasSize.width < zoomScaledContentWidth.value) { - rightBoundary = Math.abs(canvasSize.width - zoomScaledContentWidth.value) / 2; - } - - if (canvasSize.height < zoomScaledContentHeight.value) { - topBoundary = Math.abs(zoomScaledContentHeight.value - canvasSize.height) / 2; - } - - const maxVector = {x: rightBoundary, y: topBoundary}; - const minVector = {x: -rightBoundary, y: -topBoundary}; - - const target = { - x: clamp(offsetX.value, minVector.x, maxVector.x), - y: clamp(offsetY.value, minVector.y, maxVector.y), - }; - - const isInBoundaryX = target.x === offsetX.value; - const isInBoundaryY = target.y === offsetY.value; - - return { - target, - isInBoundaryX, - isInBoundaryY, - minVector, - maxVector, - canPanLeft: target.x < maxVector.x, - canPanRight: target.x > minVector.x, - }; - }, [canvasSize.width, canvasSize.height]); - - const afterPanGesture = useWorkletCallback(() => { - const {target, isInBoundaryX, isInBoundaryY, minVector, maxVector} = getBounds(); - - if (!canPanVertically.value) { - offsetY.value = withSpring(target.y, SPRING_CONFIG); - } - - if (zoomScale.value === 1 && offsetX.value === 0 && offsetY.value === 0 && translateX.value === 0 && translateY.value === 0) { - // we don't need to run any animations - return; - } - - if (zoomScale.value <= 1) { - // just center it - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - return; - } - - const deceleration = 0.9915; - - if (isInBoundaryX) { - if (Math.abs(panVelocityX.value) > 0 && zoomScale.value <= zoomRange.max) { - offsetX.value = withDecay({ - velocity: panVelocityX.value, - clamp: [minVector.x, maxVector.x], - deceleration, - rubberBandEffect: false, - }); - } - } else { - offsetX.value = withSpring(target.x, SPRING_CONFIG); - } - - if (isInBoundaryY) { - if ( - Math.abs(panVelocityY.value) > 0 && - zoomScale.value <= zoomRange.max && - // limit vertical pan only when content is smaller than screen - offsetY.value !== minVector.y && - offsetY.value !== maxVector.y - ) { - offsetY.value = withDecay({ - velocity: panVelocityY.value, - clamp: [minVector.y, maxVector.y], - deceleration, - }); - } - } else { - offsetY.value = withSpring(target.y, SPRING_CONFIG, () => { - isSwiping.value = false; - }); - } - }); - - const stopAnimation = useWorkletCallback(() => { - cancelAnimation(offsetX); - cancelAnimation(offsetY); - }); - - const zoomToCoordinates = useWorkletCallback( - (canvasFocalX, canvasFocalY) => { - 'worklet'; - - stopAnimation(); - - const canvasOffsetX = Math.max(0, (canvasSize.width - scaledWidth) / 2); - const canvasOffsetY = Math.max(0, (canvasSize.height - scaledHeight) / 2); - - const contentFocal = { - x: clamp(canvasFocalX - canvasOffsetX, 0, scaledWidth), - y: clamp(canvasFocalY - canvasOffsetY, 0, scaledHeight), - }; - - const canvasCenter = { - x: canvasSize.width / 2, - y: canvasSize.height / 2, - }; - - const originContentCenter = { - x: scaledWidth / 2, - y: scaledHeight / 2, - }; - - const targetContentSize = { - width: scaledWidth * doubleTapScale, - height: scaledHeight * doubleTapScale, - }; - - const targetContentCenter = { - x: targetContentSize.width / 2, - y: targetContentSize.height / 2, - }; - - const currentOrigin = { - x: (targetContentCenter.x - canvasCenter.x) * -1, - y: (targetContentCenter.y - canvasCenter.y) * -1, - }; - - const koef = { - x: (1 / originContentCenter.x) * contentFocal.x - 1, - y: (1 / originContentCenter.y) * contentFocal.y - 1, - }; - - const target = { - x: currentOrigin.x * koef.x, - y: currentOrigin.y * koef.y, - }; - - if (targetContentSize.height < canvasSize.height) { - target.y = 0; - } - - offsetX.value = withSpring(target.x, SPRING_CONFIG); - offsetY.value = withSpring(target.y, SPRING_CONFIG); - zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG); - pinchScaleOffset.value = doubleTapScale; - }, - [scaledWidth, scaledHeight, canvasSize, doubleTapScale], - ); - - const reset = useWorkletCallback((animated) => { - pinchScaleOffset.value = 1; - - stopAnimation(); - - if (animated) { - offsetX.value = withSpring(0, SPRING_CONFIG); - offsetY.value = withSpring(0, SPRING_CONFIG); - zoomScale.value = withSpring(1, SPRING_CONFIG); - } else { - zoomScale.value = 1; - translateX.value = 0; - translateY.value = 0; - offsetX.value = 0; - offsetY.value = 0; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - } - }); - - const doubleTap = Gesture.Tap() - .numberOfTaps(2) - .maxDelay(150) - .maxDistance(20) - .onEnd((evt) => { - if (zoomScale.value > 1) { - reset(true); - } else { - zoomToCoordinates(evt.x, evt.y); - } - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - - const panGestureRef = useRef(Gesture.Pan()); - - const singleTap = Gesture.Tap() - .numberOfTaps(1) - .maxDuration(50) - .requireExternalGestureToFail(doubleTap, panGestureRef) - .onBegin(() => { - stopAnimation(); - }) - .onFinalize((evt, success) => { - if (!success || !onTap) { - return; - } - - runOnJS(onTap)(); - }); - - const previousTouch = useSharedValue(null); - - const panGesture = Gesture.Pan() - .manualActivation(true) - .averageTouches(true) - .onTouchesMove((evt, state) => { - if (zoomScale.value > 1) { - state.activate(); - } - - // TODO: Swipe down to close carousel gesture - // this needs fine tuning to work properly - // if (!isScrolling.value && scale.value === 1 && previousTouch.value != null) { - // const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x); - // const velocityY = evt.allTouches[0].y - previousTouch.value.y; - - // // TODO: this needs tuning - // if (Math.abs(velocityY) > velocityX && velocityY > 20) { - // state.activate(); - - // isSwiping.value = true; - // previousTouch.value = null; - - // runOnJS(onSwipeDown)(); - // return; - // } - // } - - if (previousTouch.value == null) { - previousTouch.value = { - x: evt.allTouches[0].x, - y: evt.allTouches[0].y, - }; - } - }) - .simultaneousWithExternalGesture(pagerRef, doubleTap, singleTap) - .onBegin(() => { - stopAnimation(); - }) - .onChange((evt) => { - // since we running both pinch and pan gesture handlers simultaneously - // we need to make sure that we don't pan when we pinch and move fingers - // since we track it as pinch focal gesture - if (evt.numberOfPointers > 1 || isScrolling.value) { - return; - } - - panVelocityX.value = evt.velocityX; - - panVelocityY.value = evt.velocityY; - - if (!isSwiping.value) { - translateX.value += evt.changeX; - } - - if (canPanVertically.value || isSwiping.value) { - translateY.value += evt.changeY; - } - }) - .onEnd((evt) => { - previousTouch.value = null; - - if (isScrolling.value) { - return; - } - - offsetX.value += translateX.value; - offsetY.value += translateY.value; - translateX.value = 0; - translateY.value = 0; - - if (isSwiping.value) { - const enoughVelocity = Math.abs(evt.velocityY) > 300 && Math.abs(evt.velocityX) < Math.abs(evt.velocityY); - const rightDirection = (evt.translationY > 0 && evt.velocityY > 0) || (evt.translationY < 0 && evt.velocityY < 0); - - if (enoughVelocity && rightDirection) { - const maybeInvert = (v) => { - const invert = evt.velocityY < 0; - return invert ? -v : v; - }; - - offsetY.value = withSpring( - maybeInvert(contentSize.height * 2), - { - stiffness: 50, - damping: 30, - mass: 1, - overshootClamping: true, - restDisplacementThreshold: 300, - restSpeedThreshold: 300, - velocity: Math.abs(evt.velocityY) < 1200 ? maybeInvert(1200) : evt.velocityY, - }, - () => { - runOnJS(onSwipeSuccess)(); - }, - ); - return; - } - } - - afterPanGesture(); - - panVelocityX.value = 0; - panVelocityY.value = 0; - }) - .withRef(panGestureRef); - - const getAdjustedFocal = useWorkletCallback( - (focalX, focalY) => ({ - x: focalX - (canvasSize.width / 2 + offsetX.value), - y: focalY - (canvasSize.height / 2 + offsetY.value), - }), - [canvasSize.width, canvasSize.height], - ); - - // used to store event scale value when we limit scale - const pinchGestureScale = useSharedValue(1); - const pinchGestureRunning = useSharedValue(false); - - const [pinchEnabled, setPinchEnabled] = useState(true); - useEffect(() => { - if (pinchEnabled) { - return; - } - setPinchEnabled(true); - }, [pinchEnabled]); - - const pinchGesture = Gesture.Pinch() - .enabled(pinchEnabled) - .onTouchesDown((evt, state) => { - // we don't want to activate pinch gesture when we are scrolling pager - if (!isScrolling.value) { - return; - } - - state.fail(); - }) - .simultaneousWithExternalGesture(panGesture, doubleTap) - .onStart((evt) => { - pinchGestureRunning.value = true; - - stopAnimation(); - - const adjustFocal = getAdjustedFocal(evt.focalX, evt.focalY); - - origin.x.value = adjustFocal.x; - origin.y.value = adjustFocal.y; - }) - .onChange((evt) => { - if (evt.numberOfPointers !== 2) { - runOnJS(setPinchEnabled)(false); - return; - } - - const newZoomScale = pinchScaleOffset.value * evt.scale; - - if (zoomScale.value >= zoomRange.min * zoomScaleBounceFactors.min && zoomScale.value <= zoomRange.max * zoomScaleBounceFactors.max) { - zoomScale.value = newZoomScale; - pinchGestureScale.value = evt.scale; - } - - const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); - const newPinchTranslateX = adjustedFocal.x + pinchGestureScale.value * origin.x.value * -1; - const newPinchTranslateY = adjustedFocal.y + pinchGestureScale.value * origin.y.value * -1; - - if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { - pinchTranslateX.value = newPinchTranslateX; - pinchTranslateY.value = newPinchTranslateY; - } else { - pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; - pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; - } - }) - .onEnd(() => { - offsetX.value += pinchTranslateX.value; - offsetY.value += pinchTranslateY.value; - pinchTranslateX.value = 0; - pinchTranslateY.value = 0; - pinchScaleOffset.value = zoomScale.value; - pinchGestureScale.value = 1; - - if (pinchScaleOffset.value < zoomRange.min) { - pinchScaleOffset.value = zoomRange.min; - zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG); - } else if (pinchScaleOffset.value > zoomRange.max) { - pinchScaleOffset.value = zoomRange.max; - zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG); - } - - if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { - pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); - pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); - } - - pinchGestureRunning.value = false; - - if (onScaleChanged != null) { - runOnJS(onScaleChanged)(zoomScale.value); - } - }); - - const [isPinchGestureInUse, setIsPinchGestureInUse] = useState(false); - useAnimatedReaction( - () => [zoomScale.value, pinchGestureRunning.value], - ([zoom, running]) => { - const newIsPinchGestureInUse = zoom !== 1 || running; - if (isPinchGestureInUse !== newIsPinchGestureInUse) { - runOnJS(setIsPinchGestureInUse)(newIsPinchGestureInUse); - } - }, - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => onPinchGestureChange(isPinchGestureInUse), [isPinchGestureInUse]); - - const animatedStyles = useAnimatedStyle(() => { - const x = pinchTranslateX.value + pinchBounceTranslateX.value + translateX.value + offsetX.value; - const y = pinchTranslateY.value + pinchBounceTranslateY.value + translateY.value + offsetY.value; - - if (isSwiping.value) { - onSwipe(y); - } - - return { - transform: [ - { - translateX: x, - }, - { - translateY: y, - }, - {scale: totalScale.value}, - ], - }; - }); - - // reacts to scale change and enables/disables pager scroll - useAnimatedReaction( - () => zoomScale.value, - () => { - shouldPagerScroll.value = zoomScale.value === 1; - }, - ); - - const mounted = useRef(false); - useEffect(() => { - if (!mounted.current) { - mounted.current = true; - return; - } - - if (!isActive) { - runOnUI(reset)(false); - } - }, [isActive, mounted, reset]); - - return ( - - - - - {children} - - - - - ); -} -MultiGestureCanvas.propTypes = multiGestureCanvasPropTypes; -MultiGestureCanvas.defaultProps = multiGestureCanvasDefaultProps; -MultiGestureCanvas.displayName = 'MultiGestureCanvas'; - -export default MultiGestureCanvas; -export {defaultZoomRange, zoomScaleBounceFactors}; diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx new file mode 100644 index 000000000000..a7eca6baa18f --- /dev/null +++ b/src/components/MultiGestureCanvas/index.tsx @@ -0,0 +1,266 @@ +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; +import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; +import Animated, {cancelAnimation, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import {DEFAULT_ZOOM_RANGE, SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; +import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from './types'; +import usePanGesture from './usePanGesture'; +import usePinchGesture from './usePinchGesture'; +import useTapGestures from './useTapGestures'; +import * as MultiGestureCanvasUtils from './utils'; + +type MultiGestureCanvasProps = ChildrenProps & { + /** + * Wheter the canvas is currently active (in the screen) or not. + * Disables certain gestures and functionality + */ + isActive?: boolean; + + /** The width and height of the canvas. + * This is needed in order to properly scale the content in the canvas + */ + canvasSize: CanvasSize; + + /** The width and height of the content. + * This is needed in order to properly scale the content in the canvas + */ + contentSize?: ContentSize; + + /** Range of zoom that can be applied to the content by pinching or double tapping. */ + zoomRange?: Partial; + + /** Handles scale changed event */ + onScaleChanged?: OnScaleChangedCallback; +}; + +function MultiGestureCanvas({ + canvasSize, + contentSize = {width: 1, height: 1}, + zoomRange: zoomRangeProp, + isActive = true, + children, + onScaleChanged: onScaleChangedProp, +}: MultiGestureCanvasProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + + const isSwipingInPagerFallback = useSharedValue(false); + + // If the MultiGestureCanvas used inside a AttachmentCarouselPager, we need to adapt the behaviour based on the pager state + const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); + const { + onTap, + onScaleChanged: onScaleChangedContext, + isPagerScrolling: isPagerSwiping, + pagerRef, + } = useMemo( + () => + attachmentCarouselPagerContext ?? { + onTap: () => {}, + onScaleChanged: () => {}, + pagerRef: undefined, + isPagerScrolling: isSwipingInPagerFallback, + }, + [attachmentCarouselPagerContext, isSwipingInPagerFallback], + ); + + /** + * Calls the onScaleChanged callback from the both props and the pager context + */ + const onScaleChanged = useCallback( + (newScale: number) => { + onScaleChangedProp?.(newScale); + onScaleChangedContext(newScale); + }, + [onScaleChangedContext, onScaleChangedProp], + ); + + const zoomRange = useMemo( + () => ({ + min: zoomRangeProp?.min ?? DEFAULT_ZOOM_RANGE.min, + max: zoomRangeProp?.max ?? DEFAULT_ZOOM_RANGE.max, + }), + [zoomRangeProp?.max, zoomRangeProp?.min], + ); + + // Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors + // to fit the content inside the canvas + // We later use the lower of the two scale factors to fit the content inside the canvas + const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]); + + const zoomScale = useSharedValue(1); + + // Adding together zoom scale and the initial scale to fit the content into the canvas + // Using the minimum content scale, so that the image is not bigger than the canvas + // and not smaller than needed to fit + const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]); + + const panTranslateX = useSharedValue(0); + const panTranslateY = useSharedValue(0); + const panGestureRef = useRef(Gesture.Pan()); + + const pinchScale = useSharedValue(1); + const pinchTranslateX = useSharedValue(0); + const pinchTranslateY = useSharedValue(0); + + // Total offset of the content including previous translations from panning and pinching gestures + const offsetX = useSharedValue(0); + const offsetY = useSharedValue(0); + + /** + * Stops any currently running decay animation from panning + */ + const stopAnimation = useWorkletCallback(() => { + cancelAnimation(offsetX); + cancelAnimation(offsetY); + }); + + /** + * Resets the canvas to the initial state and animates back smoothly + */ + const reset = useWorkletCallback((animated: boolean, callback?: () => void) => { + stopAnimation(); + + pinchScale.value = 1; + + if (animated) { + offsetX.value = withSpring(0, SPRING_CONFIG); + offsetY.value = withSpring(0, SPRING_CONFIG); + panTranslateX.value = withSpring(0, SPRING_CONFIG); + panTranslateY.value = withSpring(0, SPRING_CONFIG); + pinchTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchTranslateY.value = withSpring(0, SPRING_CONFIG); + zoomScale.value = withSpring(1, SPRING_CONFIG, callback); + + return; + } + + offsetX.value = 0; + offsetY.value = 0; + panTranslateX.value = 0; + panTranslateY.value = 0; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + zoomScale.value = 1; + + if (callback === undefined) { + return; + } + + callback(); + }); + + const {singleTapGesture: basicSingleTapGesture, doubleTapGesture} = useTapGestures({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + offsetX, + offsetY, + pinchScale, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, + }); + const singleTapGesture = basicSingleTapGesture.requireExternalGestureToFail(doubleTapGesture, panGestureRef); + + const panGestureSimultaneousList = useMemo( + () => (pagerRef === undefined ? [singleTapGesture, doubleTapGesture] : [pagerRef as unknown as Exclude, singleTapGesture, doubleTapGesture]), + [doubleTapGesture, pagerRef, singleTapGesture], + ); + + const panGesture = usePanGesture({ + canvasSize, + contentSize, + zoomScale, + totalScale, + offsetX, + offsetY, + panTranslateX, + panTranslateY, + isPagerSwiping, + stopAnimation, + }) + .simultaneousWithExternalGesture(...panGestureSimultaneousList) + .withRef(panGestureRef); + + const pinchGesture = usePinchGesture({ + canvasSize, + zoomScale, + zoomRange, + offsetX, + offsetY, + pinchTranslateX, + pinchTranslateY, + pinchScale, + isPagerSwiping, + stopAnimation, + onScaleChanged, + }).simultaneousWithExternalGesture(panGesture, singleTapGesture, doubleTapGesture); + + // Trigger a reset when the canvas gets inactive, but only if it was already mounted before + const mounted = useRef(false); + useEffect(() => { + if (!mounted.current) { + mounted.current = true; + return; + } + + if (!isActive) { + runOnUI(reset)(false); + } + }, [isActive, mounted, reset]); + + // Animate the x and y position of the content within the canvas based on all of the gestures + const animatedStyles = useAnimatedStyle(() => { + const x = pinchTranslateX.value + panTranslateX.value + offsetX.value; + const y = pinchTranslateY.value + panTranslateY.value + offsetY.value; + + return { + transform: [ + { + translateX: x, + }, + { + translateY: y, + }, + {scale: totalScale.value}, + ], + }; + }); + + const containerStyles = useMemo(() => [styles.flex1, StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width)], [StyleUtils, canvasSize.width, styles.flex1]); + + return ( + + + + + {children} + + + + + ); +} +MultiGestureCanvas.displayName = 'MultiGestureCanvas'; + +export default MultiGestureCanvas; +export {DEFAULT_ZOOM_RANGE, ZOOM_RANGE_BOUNCE_FACTORS}; +export type {MultiGestureCanvasProps}; diff --git a/src/components/MultiGestureCanvas/propTypes.js b/src/components/MultiGestureCanvas/propTypes.js deleted file mode 100644 index f1961ec0e156..000000000000 --- a/src/components/MultiGestureCanvas/propTypes.js +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types'; - -const defaultZoomRange = { - min: 1, - max: 20, -}; - -const zoomRangePropTypes = { - /** Range of zoom that can be applied to the content by pinching or double tapping. */ - zoomRange: PropTypes.shape({ - min: PropTypes.number, - max: PropTypes.number, - }), -}; - -const zoomRangeDefaultProps = { - zoomRange: { - min: defaultZoomRange.min, - max: defaultZoomRange.max, - }, -}; - -const multiGestureCanvasPropTypes = { - ...zoomRangePropTypes, - - /** - * Wheter the canvas is currently active (in the screen) or not. - * Disables certain gestures and functionality - */ - isActive: PropTypes.bool, - - /** Handles scale changed event */ - onScaleChanged: PropTypes.func, - - /** The width and height of the canvas. - * This is needed in order to properly scale the content in the canvas - */ - canvasSize: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - - /** The width and height of the content. - * This is needed in order to properly scale the content in the canvas - */ - contentSize: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number, - }), - - /** The scale factors (scaleX, scaleY) that are used to scale the content (width/height) to the canvas size. - * `scaledWidth` and `scaledHeight` reflect the actual size of the content after scaling. - */ - contentScaling: PropTypes.shape({ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - scaledWidth: PropTypes.number, - scaledHeight: PropTypes.number, - }), - - /** Content that should be transformed inside the canvas (images, pdf, ...) */ - children: PropTypes.node.isRequired, -}; - -const multiGestureCanvasDefaultProps = { - isActive: true, - onScaleChanged: () => undefined, - contentSize: undefined, - contentScaling: undefined, - zoomRange: undefined, -}; - -export {defaultZoomRange, zoomRangePropTypes, zoomRangeDefaultProps, multiGestureCanvasPropTypes, multiGestureCanvasDefaultProps}; diff --git a/src/components/MultiGestureCanvas/types.ts b/src/components/MultiGestureCanvas/types.ts index 0242f045feef..bbd8f69e6947 100644 --- a/src/components/MultiGestureCanvas/types.ts +++ b/src/components/MultiGestureCanvas/types.ts @@ -1,6 +1,50 @@ +import type {SharedValue} from 'react-native-reanimated'; + +/** Dimensions of the canvas rendered by the MultiGestureCanvas */ +type CanvasSize = { + width: number; + height: number; +}; + +/** Dimensions of the content passed to the MultiGestureCanvas */ +type ContentSize = { + width: number; + height: number; +}; + +/** Range of zoom that can be applied to the content by pinching or double tapping. */ type ZoomRange = { min: number; max: number; }; -export default ZoomRange; +/** Triggered whenever the scale of the MultiGestureCanvas changes */ +type OnScaleChangedCallback = (zoomScale: number) => void; + +/** Triggered when the canvas is tapped (single tap) */ +type OnTapCallback = () => void; + +/** Types used of variables used within the MultiGestureCanvas component and it's hooks */ +type MultiGestureCanvasVariables = { + canvasSize: CanvasSize; + contentSize: ContentSize; + zoomRange: ZoomRange; + minContentScale: number; + maxContentScale: number; + isPagerSwiping: SharedValue; + zoomScale: SharedValue; + totalScale: SharedValue; + pinchScale: SharedValue; + offsetX: SharedValue; + offsetY: SharedValue; + panTranslateX: SharedValue; + panTranslateY: SharedValue; + pinchTranslateX: SharedValue; + pinchTranslateY: SharedValue; + stopAnimation: () => void; + reset: (animated: boolean, callback: () => void) => void; + onTap: OnTapCallback; + onScaleChanged: OnScaleChangedCallback | undefined; +}; + +export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, MultiGestureCanvasVariables}; diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts new file mode 100644 index 000000000000..8a646446fad4 --- /dev/null +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -0,0 +1,160 @@ +/* eslint-disable no-param-reassign */ +import type {PanGesture} from 'react-native-gesture-handler'; +import {Gesture} from 'react-native-gesture-handler'; +import {useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG} from './constants'; +import type {MultiGestureCanvasVariables} from './types'; +import * as MultiGestureCanvasUtils from './utils'; + +// This value determines how fast the pan animation should phase out +// We're using a "withDecay" animation to smoothly phase out the pan animation +// https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/ +const PAN_DECAY_DECELARATION = 0.9915; + +type UsePanGestureProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'isPagerSwiping' | 'stopAnimation' +>; + +const usePanGesture = ({canvasSize, contentSize, zoomScale, totalScale, offsetX, offsetY, panTranslateX, panTranslateY, isPagerSwiping, stopAnimation}: UsePanGestureProps): PanGesture => { + // The content size after fitting it to the canvas and zooming + const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]); + const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]); + + // Velocity of the pan gesture + // We need to keep track of the velocity to properly phase out/decay the pan animation + const panVelocityX = useSharedValue(0); + const panVelocityY = useSharedValue(0); + + // Calculates bounds of the scaled content + // Can we pan left/right/up/down + // Can be used to limit gesture or implementing tension effect + const getBounds = useWorkletCallback(() => { + let horizontalBoundary = 0; + let verticalBoundary = 0; + + if (canvasSize.width < zoomedContentWidth.value) { + horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2; + } + + if (canvasSize.height < zoomedContentHeight.value) { + verticalBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2; + } + + const horizontalBoundaries = {min: -horizontalBoundary, max: horizontalBoundary}; + const verticalBoundaries = {min: -verticalBoundary, max: verticalBoundary}; + + const clampedOffset = { + x: MultiGestureCanvasUtils.clamp(offsetX.value, horizontalBoundaries.min, horizontalBoundaries.max), + y: MultiGestureCanvasUtils.clamp(offsetY.value, verticalBoundaries.min, verticalBoundaries.max), + }; + + // If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries + const isInHoriztontalBoundary = clampedOffset.x === offsetX.value; + const isInVerticalBoundary = clampedOffset.y === offsetY.value; + + return { + horizontalBoundaries, + verticalBoundaries, + clampedOffset, + isInHoriztontalBoundary, + isInVerticalBoundary, + }; + }, [canvasSize.width, canvasSize.height]); + + // We want to smoothly decay/end the gesture by phasing out the pan animation + // In case the content is outside of the boundaries of the canvas, + // we need to move the content back into the boundaries + const finishPanGesture = useWorkletCallback(() => { + // If the content is centered within the canvas, we don't need to run any animations + if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) { + return; + } + + const {clampedOffset, isInHoriztontalBoundary, isInVerticalBoundary, horizontalBoundaries, verticalBoundaries} = getBounds(); + + // If the content is within the horizontal/vertical boundaries of the canvas, we can smoothly phase out the animation + // If not, we need to snap back to the boundaries + if (isInHoriztontalBoundary) { + // If the (absolute) velocity is 0, we don't need to run an animation + if (Math.abs(panVelocityX.value) !== 0) { + // Phase out the pan animation + offsetX.value = withDecay({ + velocity: panVelocityX.value, + clamp: [horizontalBoundaries.min, horizontalBoundaries.max], + deceleration: PAN_DECAY_DECELARATION, + rubberBandEffect: false, + }); + } + } else { + // Animated back to the boundary + offsetX.value = withSpring(clampedOffset.x, SPRING_CONFIG); + } + + if (isInVerticalBoundary) { + // If the (absolute) velocity is 0, we don't need to run an animation + if (Math.abs(panVelocityY.value) !== 0) { + // Phase out the pan animation + offsetY.value = withDecay({ + velocity: panVelocityY.value, + clamp: [verticalBoundaries.min, verticalBoundaries.max], + deceleration: PAN_DECAY_DECELARATION, + }); + } + } else { + // Animated back to the boundary + offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG); + } + + // Reset velocity variables after we finished the pan gesture + panVelocityX.value = 0; + panVelocityY.value = 0; + }); + + const panGesture = Gesture.Pan() + .manualActivation(true) + .averageTouches(true) + // eslint-disable-next-line @typescript-eslint/naming-convention + .onTouchesMove((_evt, state) => { + // We only allow panning when the content is zoomed in + if (zoomScale.value <= 1 || isPagerSwiping.value) { + return; + } + + state.activate(); + }) + .onStart(() => { + stopAnimation(); + }) + .onChange((evt) => { + // Since we're running both pinch and pan gesture handlers simultaneously, + // we need to make sure that we don't pan when we pinch since we track it as pinch focal gesture. + if (evt.numberOfPointers > 1) { + return; + } + + panVelocityX.value = evt.velocityX; + panVelocityY.value = evt.velocityY; + + panTranslateX.value += evt.changeX; + panTranslateY.value += evt.changeY; + }) + .onEnd(() => { + // Add pan translation to total offset and reset gesture variables + offsetX.value += panTranslateX.value; + offsetY.value += panTranslateY.value; + panTranslateX.value = 0; + panTranslateY.value = 0; + + // If we are swiping (in the pager), we don't want to return to boundaries + if (isPagerSwiping.value) { + return; + } + + finishPanGesture(); + }); + + return panGesture; +}; + +export default usePanGesture; diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts new file mode 100644 index 000000000000..2ff375dc7edd --- /dev/null +++ b/src/components/MultiGestureCanvas/usePinchGesture.ts @@ -0,0 +1,174 @@ +/* eslint-disable no-param-reassign */ +import {useEffect, useState} from 'react'; +import type {PinchGesture} from 'react-native-gesture-handler'; +import {Gesture} from 'react-native-gesture-handler'; +import {runOnJS, useAnimatedReaction, useSharedValue, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants'; +import type {MultiGestureCanvasVariables} from './types'; + +type UsePinchGestureProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'zoomScale' | 'zoomRange' | 'offsetX' | 'offsetY' | 'pinchTranslateX' | 'pinchTranslateY' | 'pinchScale' | 'isPagerSwiping' | 'stopAnimation' | 'onScaleChanged' +>; + +const usePinchGesture = ({ + canvasSize, + zoomScale, + zoomRange, + offsetX, + offsetY, + pinchTranslateX: totalPinchTranslateX, + pinchTranslateY: totalPinchTranslateY, + pinchScale, + isPagerSwiping, + stopAnimation, + onScaleChanged, +}: UsePinchGestureProps): PinchGesture => { + // The current pinch gesture event scale + const currentPinchScale = useSharedValue(1); + + // Origin of the pinch gesture + const pinchOrigin = { + x: useSharedValue(0), + y: useSharedValue(0), + }; + + // How much the content is translated during the pinch gesture + // While the pinch gesture is running, the pan gesture is disabled + // Therefore we need to add the translation separately + const pinchTranslateX = useSharedValue(0); + const pinchTranslateY = useSharedValue(0); + + // In order to keep track of the "bounce" effect when "overzooming"/"underzooming", + // we need to have extra "bounce" translation variables + const pinchBounceTranslateX = useSharedValue(0); + const pinchBounceTranslateY = useSharedValue(0); + + const triggerScaleChangedEvent = () => { + 'worklet'; + + if (onScaleChanged === undefined) { + return; + } + + runOnJS(onScaleChanged)(zoomScale.value); + }; + + // Update the total (pinch) translation based on the regular pinch + bounce + useAnimatedReaction( + () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value], + ([translateX, translateY, bounceX, bounceY]) => { + totalPinchTranslateX.value = translateX + bounceX; + totalPinchTranslateY.value = translateY + bounceY; + }, + ); + + /** + * Calculates the adjusted focal point of the pinch gesture, + * based on the canvas size and the current offset + */ + const getAdjustedFocal = useWorkletCallback( + (focalX: number, focalY: number) => ({ + x: focalX - (canvasSize.width / 2 + offsetX.value), + y: focalY - (canvasSize.height / 2 + offsetY.value), + }), + [canvasSize.width, canvasSize.height], + ); + + // The pinch gesture is disabled when we release one of the fingers + // On the next render, we need to re-enable the pinch gesture + const [pinchEnabled, setPinchEnabled] = useState(true); + useEffect(() => { + if (pinchEnabled) { + return; + } + setPinchEnabled(true); + }, [pinchEnabled]); + + const pinchGesture = Gesture.Pinch() + .enabled(pinchEnabled) + // eslint-disable-next-line @typescript-eslint/naming-convention + .onTouchesDown((_evt, state) => { + // We don't want to activate pinch gesture when we are swiping in the pager + if (!isPagerSwiping.value) { + return; + } + + state.fail(); + }) + .onStart((evt) => { + stopAnimation(); + + // Set the origin focal point of the pinch gesture at the start of the gesture + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); + pinchOrigin.x.value = adjustedFocal.x; + pinchOrigin.y.value = adjustedFocal.y; + }) + .onChange((evt) => { + // Disable the pinch gesture if one finger is released, + // to prevent the content from shaking/jumping + if (evt.numberOfPointers !== 2) { + runOnJS(setPinchEnabled)(false); + return; + } + + const newZoomScale = pinchScale.value * evt.scale; + + // Limit the zoom scale to zoom range including bounce range + if (zoomScale.value >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScale.value <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) { + zoomScale.value = newZoomScale; + currentPinchScale.value = evt.scale; + + triggerScaleChangedEvent(); + } + + // Calculate new pinch translation + const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY); + const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1; + const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1; + + // If the zoom scale is within the zoom range, we perform the regular pinch translation + // Otherwise it means that we are "overzoomed" or "underzoomed", so we need to bounce back + if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) { + pinchTranslateX.value = newPinchTranslateX; + pinchTranslateY.value = newPinchTranslateY; + } else { + // Store x and y translation that is produced while bouncing + // so we can revert the bounce once pinch gesture is released + pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value; + pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value; + } + }) + .onEnd(() => { + // Add pinch translation to total offset and reset gesture variables + offsetX.value += pinchTranslateX.value; + offsetY.value += pinchTranslateY.value; + pinchTranslateX.value = 0; + pinchTranslateY.value = 0; + currentPinchScale.value = 1; + + // If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation + if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) { + pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG); + pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG); + } + + if (zoomScale.value < zoomRange.min) { + // If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum + pinchScale.value = zoomRange.min; + zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent); + } else if (zoomScale.value > zoomRange.max) { + // If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum + pinchScale.value = zoomRange.max; + zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent); + } else { + // Otherwise, we just update the pinch scale offset + pinchScale.value = zoomScale.value; + triggerScaleChangedEvent(); + } + }); + + return pinchGesture; +}; + +export default usePinchGesture; diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts new file mode 100644 index 000000000000..ce67f11a91c8 --- /dev/null +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -0,0 +1,149 @@ +/* eslint-disable no-param-reassign */ +import {useMemo} from 'react'; +import type {TapGesture} from 'react-native-gesture-handler'; +import {Gesture} from 'react-native-gesture-handler'; +import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; +import type {MultiGestureCanvasVariables} from './types'; +import * as MultiGestureCanvasUtils from './utils'; + +type UseTapGesturesProps = Pick< + MultiGestureCanvasVariables, + 'canvasSize' | 'contentSize' | 'minContentScale' | 'maxContentScale' | 'offsetX' | 'offsetY' | 'pinchScale' | 'zoomScale' | 'reset' | 'stopAnimation' | 'onScaleChanged' | 'onTap' +>; + +const useTapGestures = ({ + canvasSize, + contentSize, + minContentScale, + maxContentScale, + offsetX, + offsetY, + pinchScale, + zoomScale, + reset, + stopAnimation, + onScaleChanged, + onTap, +}: UseTapGesturesProps): {singleTapGesture: TapGesture; doubleTapGesture: TapGesture} => { + // The content size after scaling it with minimum scale to fit the content into the canvas + const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]); + const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]); + + // On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE + const doubleTapScale = useMemo(() => Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale), [maxContentScale, minContentScale]); + + const zoomToCoordinates = useWorkletCallback( + (focalX: number, focalY: number, callback: () => void) => { + 'worklet'; + + stopAnimation(); + + // By how much the canvas is bigger than the content horizontally and vertically per side + const horizontalCanvasOffset = Math.max(0, (canvasSize.width - scaledContentWidth) / 2); + const verticalCanvasOffset = Math.max(0, (canvasSize.height - scaledContentHeight) / 2); + + // We need to adjust the focal point to take into account the canvas offset + // The focal point cannot be outside of the content's bounds + const adjustedFocalPoint = { + x: MultiGestureCanvasUtils.clamp(focalX - horizontalCanvasOffset, 0, scaledContentWidth), + y: MultiGestureCanvasUtils.clamp(focalY - verticalCanvasOffset, 0, scaledContentHeight), + }; + + // The center of the canvas + const canvasCenter = { + x: canvasSize.width / 2, + y: canvasSize.height / 2, + }; + + // The center of the content before zooming + const originalContentCenter = { + x: scaledContentWidth / 2, + y: scaledContentHeight / 2, + }; + + // The size of the content after zooming + const zoomedContentSize = { + width: scaledContentWidth * doubleTapScale, + height: scaledContentHeight * doubleTapScale, + }; + + // The center of the zoomed content + const zoomedContentCenter = { + x: zoomedContentSize.width / 2, + y: zoomedContentSize.height / 2, + }; + + // By how much the zoomed content is bigger/smaller than the canvas. + const zoomedContentOffset = { + x: zoomedContentCenter.x - canvasCenter.x, + y: zoomedContentCenter.y - canvasCenter.y, + }; + + // How much the content needs to be shifted based on the focal point + const shiftingFactor = { + x: adjustedFocalPoint.x / originalContentCenter.x - 1, + y: adjustedFocalPoint.y / originalContentCenter.y - 1, + }; + + // The offset after applying the focal point adjusted shift. + // We need to invert the shift, because the content is moving in the opposite direction (* -1) + const offsetAfterZooming = { + x: zoomedContentOffset.x * (shiftingFactor.x * -1), + y: zoomedContentOffset.y * (shiftingFactor.y * -1), + }; + + // If the zoomed content is less tall than the canvas, we need to reset the vertical offset + if (zoomedContentSize.height < canvasSize.height) { + offsetAfterZooming.y = 0; + } + + offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG); + offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG); + zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback); + pinchScale.value = doubleTapScale; + }, + [scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale], + ); + + const doubleTapGesture = Gesture.Tap() + .numberOfTaps(2) + .maxDelay(150) + .maxDistance(20) + .onEnd((evt) => { + const triggerScaleChangedEvent = () => { + 'worklet'; + + if (onScaleChanged != null) { + runOnJS(onScaleChanged)(zoomScale.value); + } + }; + + // If the content is already zoomed, we want to reset the zoom, + // otherwise we want to zoom in + if (zoomScale.value > 1) { + reset(true, triggerScaleChangedEvent); + } else { + zoomToCoordinates(evt.x, evt.y, triggerScaleChangedEvent); + } + }); + + const singleTapGesture = Gesture.Tap() + .numberOfTaps(1) + .maxDuration(125) + .onBegin(() => { + stopAnimation(); + }) + // eslint-disable-next-line @typescript-eslint/naming-convention + .onFinalize((_evt, success) => { + if (!success || onTap === undefined) { + return; + } + + runOnJS(onTap)(); + }); + + return {singleTapGesture, doubleTapGesture}; +}; + +export default useTapGestures; diff --git a/src/components/MultiGestureCanvas/utils.ts b/src/components/MultiGestureCanvas/utils.ts new file mode 100644 index 000000000000..e5688489c048 --- /dev/null +++ b/src/components/MultiGestureCanvas/utils.ts @@ -0,0 +1,22 @@ +import type {CanvasSize, ContentSize} from './types'; + +type GetCanvasFitScale = (props: {canvasSize: CanvasSize; contentSize: ContentSize}) => {scaleX: number; scaleY: number; minScale: number; maxScale: number}; + +const getCanvasFitScale: GetCanvasFitScale = ({canvasSize, contentSize}) => { + const scaleX = canvasSize.width / contentSize.width; + const scaleY = canvasSize.height / contentSize.height; + + const minScale = Math.min(scaleX, scaleY); + const maxScale = Math.max(scaleX, scaleY); + + return {scaleX, scaleY, minScale, maxScale}; +}; + +/** Clamps a value between a lower and upper bound */ +function clamp(value: number, lowerBound: number, upperBound: number) { + 'worklet'; + + return Math.min(Math.max(lowerBound, value), upperBound); +} + +export {getCanvasFitScale, clamp}; diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index dd8cd115e13f..97e85cacf42d 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -195,7 +195,7 @@ function OptionRow({ shouldHaveOptionSeparator && styles.borderTop, !onSelectRow && !isOptionDisabled ? styles.cursorDefault : null, ]} - accessibilityLabel={option.text} + accessibilityLabel={option.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={!optionIsFocused ? hoverStyle ?? styles.sidebarLinkHover : undefined} diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 8cac059436b5..8a39c8e1f029 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -35,7 +35,7 @@ function BaseOptionsList( optionHoveredStyle, contentContainerStyles, sectionHeaderStyle, - showScrollIndicator = false, + showScrollIndicator = true, listContainerStyles: listContainerStylesProp, shouldDisableRowInnerPadding = false, shouldPreventDefaultFocusOnSelectRow = false, diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index bbcce6fff9a6..a466dd2fc77b 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -433,17 +433,7 @@ class BaseOptionsSelector extends Component { return; } - // Note: react-native's SectionList automatically strips out any empty sections. - // So we need to reduce the sectionIndex to remove any empty sections in front of the one we're trying to scroll to. - // Otherwise, it will cause an index-out-of-bounds error and crash the app. - let adjustedSectionIndex = sectionIndex; - for (let i = 0; i < sectionIndex; i++) { - if (_.isEmpty(lodashGet(this.state.sections, `[${i}].data`))) { - adjustedSectionIndex--; - } - } - - this.list.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated}); + this.list.scrollToLocation({sectionIndex, itemIndex, animated}); } /** diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx index a8cab171ffca..c02815d74153 100644 --- a/src/components/PurposeForUsingExpensifyModal.tsx +++ b/src/components/PurposeForUsingExpensifyModal.tsx @@ -60,14 +60,14 @@ const messageCopy = { [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Hi there, to split an expense such as with a friend, please:\n' + '\n' + - 'Press the big green + button\n' + - 'Choose *Request money*\n' + - 'Indicate how much was spent, either manually, by scanning a receipt, or by tracking distance\n' + - 'Enter the email address or phone number of your friend\n' + - 'Press *Split* next to their name\n' + - 'Repeat as many times as you like for each of your friends\n' + - 'Press *Add to split* when done adding friends\n' + - 'Press Split to split the bill\n' + + '1. Press the big green + button\n' + + '2. Choose *Request money*\n' + + '3. Indicate how much was spent, either manually, by scanning a receipt, or by tracking distance\n' + + '4. Enter the email address or phone number of your friend\n' + + '5. Press *Split* next to their name\n' + + '6. Repeat as many times as you like for each of your friends\n' + + '7. Press *Add to split* when done adding friends\n' + + '8. Press Split to split the bill\n' + '\n' + "This will send a money request to each of your friends for however much they owe you, and we'll take care of getting you paid back. Thanks for asking, and let me know how it goes!", }; diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index f7917a852704..e523e4cf4091 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,17 +1,16 @@ -import type {Component, ForwardedRef} from 'react'; +import type {ForwardedRef} from 'react'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports import type {TextInputProps} from 'react-native'; import {TextInput} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; import Animated from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; -type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput & HTMLInputElement; + +function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef) { const theme = useTheme(); return ( @@ -23,7 +22,7 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef { - showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive); + showContextMenuForReport(event, props.contextMenuAnchor, props.reportID, props.action, props.checkIfContextMenuActive); }; const getPreviewHeaderText = () => { diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index aa5d0513f0d7..710adeb1589e 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -2,6 +2,7 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; import type {ReactElement} from 'react'; import {View} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; import AttachmentModal from '@components/AttachmentModal'; import EReceiptThumbnail from '@components/EReceiptThumbnail'; import Image from '@components/Image'; @@ -17,10 +18,10 @@ import type {Transaction} from '@src/types/onyx'; type ReportActionItemImageProps = { /** thumbnail URI for the image */ - thumbnail?: string | number; + thumbnail?: string | ImageSourcePropType | null; /** URI for the image or local numeric reference for the image */ - image: string | number; + image: string | ImageSourcePropType; /** whether or not to enable the image preview modal */ enablePreviewModal?: boolean; diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index c24defb8ac08..00b91bf4f862 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -6,20 +6,13 @@ import Text from '@components/Text'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils'; import variables from '@styles/variables'; -import type {Transaction} from '@src/types/onyx'; import ReportActionItemImage from './ReportActionItemImage'; -type Image = { - thumbnail: string | number; - image: string | number; - transaction: Transaction; - isLocalFile: boolean; -}; - type ReportActionItemImagesProps = { /** array of image and thumbnail URIs */ - images: Image[]; + images: ThumbnailAndImageURI[]; // We're not providing default values for size and total and disabling the ESLint rule // because we want them to default to the length of images, but we can't set default props @@ -79,7 +72,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; return ( ; - /** The role of the current user in the policy */ - role: PropTypes.string, + /** ChatReport associated with iouReport */ + chatReport: OnyxEntry; - /** Whether Scheduled Submit is turned on for this policy */ - isHarvestingEnabled: PropTypes.bool, - }), + /** Active IOU Report for current report */ + iouReport: OnyxEntry; - /* Onyx Props */ - /** chatReport associated with iouReport */ - chatReport: reportPropTypes, + /** Session info for the currently logged in user. */ + session: OnyxEntry; - /** Extra styles to pass to View wrapper */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), + /** All the transactions, used to update ReportPreview label and status */ + transactions: OnyxCollection; - /** Active IOU Report for current report */ - iouReport: PropTypes.shape({ - /** AccountID of the manager in this iou report */ - managerID: PropTypes.number, + /** All of the transaction violations */ + transactionViolations: OnyxCollection; +}; - /** AccountID of the creator of this iou report */ - ownerAccountID: PropTypes.number, +type ReportPreviewProps = ReportPreviewOnyxProps & { + /** All the data of the action */ + action: ReportAction; - /** Outstanding amount in cents of this transaction */ - total: PropTypes.number, + /** The associated chatReport */ + chatReportID: string; - /** Currency of outstanding amount of this transaction */ - currency: PropTypes.string, + /** The active IOUReport, used for Onyx subscription */ + iouReportID: string; - /** Is the iouReport waiting for the submitter to add a credit bank account? */ - isWaitingOnBankAccount: PropTypes.bool, - }), + /** The report's policyID, used for Onyx subscription */ + policyID: string; - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: refPropTypes, + contextMenuAnchor?: ContextMenuAnchor; /** Callback for updating context menu active state, used for showing context menu */ - checkIfContextMenuActive: PropTypes.func, + checkIfContextMenuActive?: () => void; /** Whether a message is a whisper */ - isWhisper: PropTypes.bool, - - /** All the transactions, used to update ReportPreview label and status */ - transactions: PropTypes.objectOf(transactionPropTypes), + isWhisper?: boolean; - /** All of the transaction violations */ - transactionViolations: transactionViolationsPropType, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - contextMenuAnchor: null, - chatReport: {}, - containerStyles: [], - iouReport: {}, - checkIfContextMenuActive: () => {}, - session: { - accountID: null, - }, - isWhisper: false, - transactionViolations: { - violations: [], - }, - policy: { - isHarvestingEnabled: false, - }, - transactions: {}, + /** Whether the corresponding report action item is hovered */ + isHovered?: boolean; }; -function ReportPreview(props) { +function ReportPreview({ + iouReport, + session, + policy, + iouReportID, + policyID, + chatReportID, + chatReport, + action, + containerStyles, + contextMenuAnchor, + transactions, + transactionViolations, + isHovered = false, + isWhisper = false, + checkIfContextMenuActive = () => {}, +}: ReportPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -143,49 +106,51 @@ function ReportPreview(props) { const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo( () => ({ - hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(props.iouReportID), - areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action), - hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID), - hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(props.iouReportID), + hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(iouReportID), + areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(iouReportID, action), + hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(iouReportID), + hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(iouReportID), }), // When transactions get updated these status may have changed, so that is a case where we also want to run this. // eslint-disable-next-line react-hooks/exhaustive-deps - [props.transactions, props.iouReportID, props.action], + [transactions, iouReportID, action], ); - const managerID = props.iouReport.managerID || 0; - const isCurrentUserManager = managerID === lodashGet(props.session, 'accountID'); - const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.iouReport); - const policyType = lodashGet(props.policy, 'type'); - const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(props.iouReport, props.policy); - - const iouSettled = ReportUtils.isSettled(props.iouReportID); - const iouCanceled = ReportUtils.isArchivedRoom(props.chatReport); - const numberOfRequests = ReportActionUtils.getNumberOfMoneyRequests(props.action); - const moneyRequestComment = lodashGet(props.action, 'childLastMoneyRequestComment', ''); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.chatReport); - const isDraftExpenseReport = isPolicyExpenseChat && ReportUtils.isDraftExpenseReport(props.iouReport); - - const isApproved = ReportUtils.isReportApproved(props.iouReport); - const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(props.iouReport); - const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(props.iouReportID); - const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; + const managerID = iouReport?.managerID ?? 0; + const isCurrentUserManager = managerID === session?.accountID; + const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); + const policyType = policy?.type; + const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(iouReport, policy); + + const iouSettled = ReportUtils.isSettled(iouReportID); + const iouCanceled = ReportUtils.isArchivedRoom(chatReport); + const numberOfRequests = ReportActionUtils.getNumberOfMoneyRequests(action); + const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); + const isDraftExpenseReport = isPolicyExpenseChat && ReportUtils.isDraftExpenseReport(iouReport); + + const isApproved = ReportUtils.isReportApproved(iouReport); + const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(iouReport); + const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); + const numberOfScanningReceipts = transactionsWithReceipts.filter((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; const hasReceipts = transactionsWithReceipts.length > 0; const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; - const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); + const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); - const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); + const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - if (TransactionUtils.isPartialMerchant(formattedMerchant)) { + if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; } - const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null)); - if (hasPendingWaypoints) { - formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')); + const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && transactionsWithReceipts.every((transaction) => transaction.pendingFields?.waypoints); + if (formattedMerchant && hasPendingWaypoints) { + formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, translate('common.tbd')); } const previewSubtitle = + // Formatted merchant can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing formattedMerchant || - props.translate('iou.requestCount', { + translate('iou.requestCount', { count: numberOfRequests - numberOfScanningReceipts, scanningReceipts: numberOfScanningReceipts, }); @@ -194,63 +159,67 @@ function ReportPreview(props) { // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => props.chatReport.isOwnPolicyExpenseChat && !props.policy.isHarvestingEnabled, - [props.chatReport.isOwnPolicyExpenseChat, props.policy.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !policy?.isHarvestingEnabled, + [chatReport?.isOwnPolicyExpenseChat, policy?.isHarvestingEnabled], ); - const getDisplayAmount = () => { + const getDisplayAmount = (): string => { if (hasPendingWaypoints) { - return props.translate('common.tbd'); + return translate('common.tbd'); } if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); } if (isScanning) { - return props.translate('iou.receiptScanning'); + return translate('iou.receiptScanning'); } if (hasOnlyDistanceRequests) { - return props.translate('common.tbd'); + return translate('common.tbd'); } // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; - const actionMessage = lodashGet(props.action, ['message', 0, 'text'], ''); + const actionMessage = action.message?.[0]?.text ?? ''; const splits = actionMessage.split(' '); - for (let i = 0; i < splits.length; i++) { - if (/\d/.test(splits[i])) { - displayAmount = splits[i]; + + splits.forEach((split) => { + if (!/\d/.test(split)) { + return; } - } + + displayAmount = split; + }); + return displayAmount; }; const getPreviewMessage = () => { if (isScanning) { - return props.translate('common.receipt'); + return translate('common.receipt'); } - const payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + const payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); if (isApproved) { - return props.translate('iou.managerApproved', {manager: payerOrApproverName}); + return translate('iou.managerApproved', {manager: payerOrApproverName}); } - const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); - let paymentVerb = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes'; - if (iouSettled || props.iouReport.isWaitingOnBankAccount) { + const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + let paymentVerb: TranslationPaths = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes'; + if (iouSettled || iouReport?.isWaitingOnBankAccount) { paymentVerb = 'iou.payerPaid'; } - return props.translate(paymentVerb, {payer: managerName}); + return translate(paymentVerb, {payer: managerName}); }; - const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); + const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(props.chatReport); - const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(props.policy, 'role') === CONST.POLICY.ROLE.ADMIN; + const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(chatReport); + const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN; const isPayer = isPaidGroupPolicy ? // In a paid group policy, the admin approver can pay the report directly by skipping the approval step isPolicyAdmin && (isApproved || isCurrentUserManager) : isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager); const shouldShowPayButton = useMemo( - () => isPayer && !isDraftExpenseReport && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled && !isAutoReimbursable, - [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, props.iouReport], + () => isPayer && !isDraftExpenseReport && !iouSettled && !iouReport?.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled && !isAutoReimbursable, + [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport], ); const shouldShowApproveButton = useMemo(() => { if (!isPaidGroupPolicy) { @@ -260,25 +229,25 @@ function ReportPreview(props) { }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; return ( - - + + { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.iouReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); }} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} - onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)} + onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]} role="button" - accessibilityLabel={props.translate('iou.viewDetails')} + accessibilityLabel={translate('iou.viewDetails')} > - + {hasReceipts && ( )} @@ -297,7 +266,7 @@ function ReportPreview(props) { {getDisplayAmount()} - {ReportUtils.isSettled(props.iouReportID) && ( + {ReportUtils.isSettled(iouReportID) && ( IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)} + // @ts-expect-error TODO: Remove this once SettlementButton (https://github.com/Expensify/App/issues/25100) is migrated to TypeScript. + currency={iouReport?.currency} + policyID={policyID} + chatReportID={chatReportID} + iouReport={iouReport} + onPress={(paymentType: PaymentMethodType) => chatReport && iouReport && IOU.payMoneyRequest(paymentType, chatReport, iouReport)} enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} addBankAccountRoute={bankAccountRoute} shouldHidePaymentOptions={!shouldShowPayButton} @@ -342,7 +312,7 @@ function ReportPreview(props) { success={isWaitingForSubmissionFromCurrentUser} text={translate('common.submit')} style={styles.mt3} - onPress={() => IOU.submitReport(props.iouReport)} + onPress={() => iouReport && IOU.submitReport(iouReport)} /> )} @@ -353,30 +323,25 @@ function ReportPreview(props) { ); } -ReportPreview.propTypes = propTypes; -ReportPreview.defaultProps = defaultProps; ReportPreview.displayName = 'ReportPreview'; -export default compose( - withLocalize, - withOnyx({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - }, - session: { - key: ONYXKEYS.SESSION, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, - }), -)(ReportPreview); +export default withOnyx({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + chatReport: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, + }, + iouReport: { + key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + }, + session: { + key: ONYXKEYS.SESSION, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, +})(ReportPreview); diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index cbd166d79d3a..16af68d9677c 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -1,7 +1,5 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import type {Text as RNText} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -24,6 +22,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as TaskUtils from '@libs/TaskUtils'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; @@ -65,7 +64,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & chatReportID: string; /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: RNText | null; + contextMenuAnchor: ContextMenuAnchor; /** Callback for updating context menu active state, used for showing context menu */ checkIfContextMenuActive: () => void; diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index b337c3581213..7c7364cc58f0 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -36,11 +36,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const isDefault = !(isChatRoom || isPolicyExpenseChat); const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs); diff --git a/src/components/SafeAreaConsumer/types.ts b/src/components/SafeAreaConsumer/types.ts index 432bf3f25ca1..d7b115983434 100644 --- a/src/components/SafeAreaConsumer/types.ts +++ b/src/components/SafeAreaConsumer/types.ts @@ -1,7 +1,7 @@ import type {DimensionValue} from 'react-native'; import type {EdgeInsets} from 'react-native-safe-area-context'; -type ChildrenProps = { +type SafeAreaChildrenProps = { paddingTop?: DimensionValue; paddingBottom?: DimensionValue; insets?: EdgeInsets; @@ -11,7 +11,9 @@ type ChildrenProps = { }; type SafeAreaConsumerProps = { - children: React.FC; + children: React.FC; }; export default SafeAreaConsumerProps; + +export type {SafeAreaChildrenProps}; diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx index 4d465ed64a74..1ac53651a542 100644 --- a/src/components/ScrollViewWithContext.tsx +++ b/src/components/ScrollViewWithContext.tsx @@ -1,5 +1,5 @@ -import type {ForwardedRef} from 'react'; -import React, {useMemo, useRef, useState} from 'react'; +import type {ForwardedRef, ReactNode} from 'react'; +import React, {createContext, forwardRef, useMemo, useRef, useState} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps} from 'react-native'; import {ScrollView} from 'react-native'; @@ -10,16 +10,16 @@ type ScrollContextValue = { scrollViewRef: ForwardedRef; }; -const ScrollContext = React.createContext({ +const ScrollContext = createContext({ contentOffsetY: 0, scrollViewRef: null, }); -type ScrollViewWithContextProps = { +type ScrollViewWithContextProps = Partial & { onScroll?: (event: NativeSyntheticEvent) => void; - children?: React.ReactNode; + children?: ReactNode; scrollEventThrottle?: number; -} & Partial; +}; /* * is a wrapper around that provides a ref to the . @@ -28,7 +28,7 @@ type ScrollViewWithContextProps = { * Using this wrapper will automatically handle scrolling to the picker's * when the picker modal is opened */ -function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { +function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ...restProps}: ScrollViewWithContextProps, ref: ForwardedRef) { const [contentOffsetY, setContentOffsetY] = useState(0); const defaultScrollViewRef = useRef(null); const scrollViewRef = ref ?? defaultScrollViewRef; @@ -54,15 +54,17 @@ function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children, {...restProps} ref={scrollViewRef} onScroll={setContextScrollPosition} - scrollEventThrottle={scrollEventThrottle ?? MIN_SMOOTH_SCROLL_EVENT_THROTTLE} + // It's possible for scrollEventThrottle to be 0, so we must use "||" to fallback to MIN_SMOOTH_SCROLL_EVENT_THROTTLE. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + scrollEventThrottle={scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE} > {children} ); } -ScrollViewWithContextWithRef.displayName = 'ScrollViewWithContextWithRef'; +ScrollViewWithContext.displayName = 'ScrollViewWithContext'; -export default React.forwardRef(ScrollViewWithContextWithRef); +export default forwardRef(ScrollViewWithContext); export {ScrollContext}; export type {ScrollContextValue}; diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index d97c47c84ee7..815b80aaa50e 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -43,10 +43,10 @@ function BaseSelectionList( onScrollBeginDrag, headerMessage = '', confirmButtonText = '', - onConfirm = () => {}, + onConfirm, headerContent, footerContent, - showScrollIndicator = false, + showScrollIndicator = true, showLoadingPlaceholder = false, showConfirmButton = false, shouldPreventDefaultFocusOnSelectRow = false, @@ -167,17 +167,7 @@ function BaseSelectionList( const itemIndex = item.index ?? -1; const sectionIndex = item.sectionIndex ?? -1; - // Note: react-native's SectionList automatically strips out any empty sections. - // So we need to reduce the sectionIndex to remove any empty sections in front of the one we're trying to scroll to. - // Otherwise, it will cause an index-out-of-bounds error and crash the app. - let adjustedSectionIndex = sectionIndex; - for (let i = 0; i < sectionIndex; i++) { - if (sections[i].data) { - adjustedSectionIndex--; - } - } - - listRef.current.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight}); + listRef.current.scrollToLocation({sectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight}); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -363,6 +353,11 @@ function BaseSelectionList( return; } + // scroll is unnecessary if multiple options cannot be selected + if (!canSelectMultiple) { + return; + } + // set the focus on the first item when the sections list is changed if (sections.length > 0) { updateAndScrollToFocusedIndex(0); @@ -379,10 +374,10 @@ function BaseSelectionList( }); /** Calls confirm action when pressing CTRL (CMD) + Enter */ - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm ?? selectFocusedOption, { captureOnInputs: true, shouldBubble: !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && !!onConfirm && isFocused, + isActive: !disableKeyboardShortcuts && isFocused, }); return ( diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 17557051bef9..374ca8a2f1e5 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -1,15 +1,16 @@ import {createContext} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {Report, ReportAction} from '@src/types/onyx'; type ShowContextMenuContextProps = { - anchor: RNText | null; + anchor: ContextMenuAnchor; report: OnyxEntry; action: OnyxEntry; checkIfContextMenuActive: () => void; @@ -36,7 +37,7 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; */ function showContextMenuForReport( event: GestureResponderEvent | MouseEvent, - anchor: RNText | null, + anchor: ContextMenuAnchor, reportID: string, action: OnyxEntry, checkIfContextMenuActive: () => void, diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 99b3e98588ac..4b764fc5baad 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,14 +1,14 @@ import Str from 'expensify-common/lib/str'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RNTextInput from '@components/RNTextInput'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; @@ -58,7 +58,7 @@ function BaseTextInput( inputID, ...props }: BaseTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); @@ -81,7 +81,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -323,7 +323,7 @@ function BaseTextInput( ref.current = element; } - (input.current as AnimatedTextInputRef | null) = element; + input.current = element; }} // eslint-disable-next-line {...inputProps} @@ -425,6 +425,9 @@ function BaseTextInput( styles.visibilityHidden, ]} onLayout={(e) => { + if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { + return; + } setTextInputWidth(e.nativeEvent.layout.width); setTextInputHeight(e.nativeEvent.layout.height); }} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 9c3899979aaa..8717da2def0a 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,13 +1,13 @@ import Str from 'expensify-common/lib/str'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; -import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; @@ -58,7 +58,7 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: BaseTextInputRef, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -79,7 +79,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -337,7 +337,7 @@ function BaseTextInput( ref.current = element; } - (input.current as AnimatedTextInputRef | null) = element; + input.current = element as HTMLInputElement | null; }} // eslint-disable-next-line {...inputProps} @@ -435,8 +435,9 @@ function BaseTextInput( */} {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value - // https://github.com/Expensify/App/issues/8158 - // https://github.com/Expensify/App/issues/26628 + // Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628 + // For mobile Chrome, ensure proper display of the text selection handle (blue bubble down). + // Reference: https://github.com/Expensify/App/issues/34921 { + if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { + return; + } let additionalWidth = 0; - if (Browser.isMobileSafari() || Browser.isSafari()) { + if (Browser.isMobileSafari() || Browser.isSafari() || Browser.isMobileChrome()) { additionalWidth = 2; } setTextInputWidth(e.nativeEvent.layout.width + additionalWidth); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 21875d4dcc64..95e90867b0eb 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,5 @@ -import type {Component, ForwardedRef} from 'react'; import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {MaybePhraseKey} from '@libs/Localize'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -111,8 +110,8 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; -type BaseTextInputRef = ForwardedRef>>; +type BaseTextInputRef = HTMLFormElement | AnimatedTextInputRef; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; -export type {CustomBaseTextInputProps, BaseTextInputRef, BaseTextInputProps}; +export type {BaseTextInputProps, BaseTextInputRef, CustomBaseTextInputProps}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 656f0657dd26..acc40295d575 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,10 +1,11 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseTextInput from './BaseTextInput'; import type {BaseTextInputProps, BaseTextInputRef} from './BaseTextInput/types'; -function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { const styles = useThemeStyles(); useEffect(() => { diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 3043edbd26a5..75c4d52e0f86 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,3 +1,4 @@ +import type {ForwardedRef} from 'react'; import React, {useEffect, useRef} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,9 +11,9 @@ import * as styleConst from './styleConst'; type RemoveVisibilityListener = () => void; -function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef(null); useEffect(() => { @@ -57,7 +58,7 @@ function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={(element) => { - textInputRef.current = element as HTMLElement; + textInputRef.current = element as HTMLFormElement; if (!ref) { return; diff --git a/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js b/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js index ee7abde8c554..bb1bb9c9bfa1 100644 --- a/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js +++ b/src/components/TextInputWithCurrencySymbol/BaseTextInputWithCurrencySymbol.js @@ -2,6 +2,7 @@ import React from 'react'; import AmountTextInput from '@components/AmountTextInput'; import CurrencySymbolButton from '@components/CurrencySymbolButton'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import * as textInputWithCurrencySymbolPropTypes from './textInputWithCurrencySymbolPropTypes'; @@ -10,6 +11,7 @@ function BaseTextInputWithCurrencySymbol(props) { const {fromLocaleDigit} = useLocalize(); const currencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(props.selectedCurrencyCode); const isCurrencySymbolLTR = CurrencyUtils.isCurrencySymbolLTR(props.selectedCurrencyCode); + const styles = useThemeStyles(); const currencySymbolButton = ( ); diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 76371bbbc9e1..72172365df13 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -1,7 +1,8 @@ /* eslint-disable react/jsx-props-no-spreading */ import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import useThemePreferenceWithStaticOverride from '@hooks/useThemePreferenceWithStaticOverride'; +import DomUtils from '@libs/DomUtils'; // eslint-disable-next-line no-restricted-imports import themes from '@styles/theme'; import ThemeContext from '@styles/theme/context/ThemeContext'; @@ -21,6 +22,10 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr const theme = useMemo(() => themes[themePreference], [themePreference]); + useEffect(() => { + DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input'); + }, [theme.text]); + return {children}; } diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index 920b8f9f4130..7384874a2746 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -71,7 +71,7 @@ function ThreeDotsMenu({ const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); - const buttonRef = useRef(null); + const buttonRef = useRef(null); const {translate} = useLocalize(); const showPopoverMenu = () => { @@ -92,6 +92,7 @@ function ThreeDotsMenu({ hidePopoverMenu(); return; } + buttonRef.current?.blur(); showPopoverMenu(); if (onIconPress) { onIconPress(); diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index a1778e2feaee..5950bae5205c 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -1,6 +1,6 @@ import lodashClamp from 'lodash/clamp'; import React, {useCallback, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {Dimensions, View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,7 +10,7 @@ import ImageWithSizeCalculation from './ImageWithSizeCalculation'; type ThumbnailImageProps = { /** Source URL for the preview image */ - previewSourceURL: string | number; + previewSourceURL: string | ImageSourcePropType; /** Any additional styles to apply */ style?: StyleProp; diff --git a/src/components/WalletStatementModal/types.ts b/src/components/WalletStatementModal/types.ts index f6989f37f49b..567202730b7d 100644 --- a/src/components/WalletStatementModal/types.ts +++ b/src/components/WalletStatementModal/types.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Session} from '@src/types/onyx'; type WalletStatementOnyxProps = { diff --git a/src/hooks/useResponsiveLayout.ts b/src/hooks/useResponsiveLayout.ts index dd782a9dbba5..f00890116d47 100644 --- a/src/hooks/useResponsiveLayout.ts +++ b/src/hooks/useResponsiveLayout.ts @@ -1,25 +1,21 @@ -import type {ParamListBase, RouteProp} from '@react-navigation/native'; -import {useRoute} from '@react-navigation/native'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import NAVIGATORS from '@src/NAVIGATORS'; import useWindowDimensions from './useWindowDimensions'; -type RouteParams = ParamListBase & { - params: {isInRHP?: boolean}; -}; type ResponsiveLayoutResult = { shouldUseNarrowLayout: boolean; + isSmallScreenWidth: boolean; + isInModal: boolean; }; /** - * Hook to determine if we are on mobile devices or in the RHP + * Hook to determine if we are on mobile devices or in the Modal Navigator */ export default function useResponsiveLayout(): ResponsiveLayoutResult { const {isSmallScreenWidth} = useWindowDimensions(); - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - const {params} = useRoute>(); - return {shouldUseNarrowLayout: isSmallScreenWidth || (params?.isInRHP ?? false)}; - } catch (error) { - return { - shouldUseNarrowLayout: isSmallScreenWidth, - }; - } + const state = navigationRef?.getRootState(); + const lastRoute = state?.routes?.at(-1); + const lastRouteName = lastRoute?.name; + const isInModal = lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; + const shouldUseNarrowLayout = isSmallScreenWidth || isInModal; + return {shouldUseNarrowLayout, isSmallScreenWidth, isInModal}; } diff --git a/src/hooks/useThemePreference.ts b/src/hooks/useThemePreference.ts index ca585065c4e6..fdee7e147882 100644 --- a/src/hooks/useThemePreference.ts +++ b/src/hooks/useThemePreference.ts @@ -1,23 +1,17 @@ -import {useContext, useEffect, useState} from 'react'; +import {useContext, useMemo} from 'react'; import {useColorScheme} from 'react-native'; import {PreferredThemeContext} from '@components/OnyxProvider'; -import type {ThemePreferenceWithoutSystem} from '@styles/theme/types'; import CONST from '@src/CONST'; function useThemePreference() { - const [themePreference, setThemePreference] = useState(CONST.THEME.FALLBACK); const preferredThemeFromStorage = useContext(PreferredThemeContext); const systemTheme = useColorScheme(); - useEffect(() => { + const themePreference = useMemo(() => { const theme = preferredThemeFromStorage ?? CONST.THEME.DEFAULT; - // If the user chooses to use the device theme settings, we need to set the theme preference to the system theme - if (theme === CONST.THEME.SYSTEM) { - setThemePreference(systemTheme ?? CONST.THEME.FALLBACK); - } else { - setThemePreference(theme); - } + // If the user chooses to use the device theme settings, set the theme preference to the system theme + return theme === CONST.THEME.SYSTEM ? systemTheme ?? CONST.THEME.FALLBACK : theme; }, [preferredThemeFromStorage, systemTheme]); return themePreference; diff --git a/src/languages/en.ts b/src/languages/en.ts index 8a959b5da550..98d00089637a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -301,6 +301,7 @@ export default { showing: 'Showing', of: 'of', default: 'Default', + update: 'Update', }, location: { useCurrent: 'Use current location', @@ -774,6 +775,11 @@ export default { isShownOnProfile: 'Your timezone is shown on your profile.', getLocationAutomatically: 'Automatically determine your location.', }, + updateRequiredView: { + updateRequired: 'Update required', + pleaseInstall: 'Please update to the latest version of New Expensify', + toGetLatestChanges: 'For mobile or desktop, download and install the latest version. For web, refresh your browser.', + }, initialSettingsPage: { about: 'About', aboutPage: { @@ -1288,8 +1294,8 @@ export default { dob: 'Please select a valid date of birth', age: 'Must be over 18 years old', ssnLast4: 'Please enter valid last 4 digits of SSN', - firstName: 'Please enter valid first name', - lastName: 'Please enter valid last name', + firstName: 'Please enter a valid first name', + lastName: 'Please enter a valid last name', noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit bank account or debit card', validationAmounts: 'The validation amounts you entered are incorrect. Please double-check your bank statement and try again.', }, @@ -1764,6 +1770,8 @@ export default { markAsIncomplete: 'Mark as incomplete', assigneeError: 'There was an error assigning this task, please try another assignee.', genericCreateTaskFailureMessage: 'Unexpected error create task, please try again later.', + deleteTask: 'Delete task', + deleteConfirmation: 'Are you sure that you want to delete this task?', }, statementPage: { title: (year, monthName) => `${monthName} ${year} statement`, @@ -2000,7 +2008,7 @@ export default { }, cardTransactions: { notActivated: 'Not activated', - outOfPocket: 'Out of pocket', + outOfPocket: 'Out-of-pocket spend', companySpend: 'Company spend', }, distance: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 271e564c9b1f..427097d5d16b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -290,6 +290,7 @@ export default { showing: 'Mostrando', of: 'de', default: 'Predeterminado', + update: 'Actualizar', }, location: { useCurrent: 'Usar ubicación actual', @@ -768,6 +769,11 @@ export default { isShownOnProfile: 'Tu zona horaria se muestra en tu perfil.', getLocationAutomatically: 'Detecta tu ubicación automáticamente.', }, + updateRequiredView: { + updateRequired: 'Actualización requerida', + pleaseInstall: 'Por favor, actualiza a la última versión de Nuevo Expensify', + toGetLatestChanges: 'Para móvil o escritorio, descarga e instala la última versión. Para la web, actualiza tu navegador.', + }, initialSettingsPage: { about: 'Acerca de', aboutPage: { @@ -1789,6 +1795,8 @@ export default { markAsIncomplete: 'Marcar como incompleta', assigneeError: 'Hubo un error al asignar esta tarea, inténtalo con otro usuario.', genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea, por favor, inténtalo más tarde.', + deleteTask: 'Eliminar tarea', + deleteConfirmation: '¿Estás seguro de que quieres eliminar esta tarea?', }, statementPage: { title: (year, monthName) => `Estado de cuenta de ${monthName} ${year}`, @@ -2487,7 +2495,7 @@ export default { }, cardTransactions: { notActivated: 'No activado', - outOfPocket: 'Por cuenta propia', + outOfPocket: 'Gastos por cuenta propia', companySpend: 'Gastos de empresa', }, distance: { diff --git a/src/libs/API.ts b/src/libs/API/index.ts similarity index 89% rename from src/libs/API.ts rename to src/libs/API/index.ts index 4305469eafd5..dbbcf790edf0 100644 --- a/src/libs/API.ts +++ b/src/libs/API/index.ts @@ -1,15 +1,15 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import Log from '@libs/Log'; +import * as Middleware from '@libs/Middleware'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import * as Pusher from '@libs/Pusher/pusher'; +import * as Request from '@libs/Request'; import CONST from '@src/CONST'; import type OnyxRequest from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; -import pkg from '../../package.json'; -import Log from './Log'; -import * as Middleware from './Middleware'; -import * as SequentialQueue from './Network/SequentialQueue'; -import * as Pusher from './Pusher/pusher'; -import * as Request from './Request'; +import pkg from '../../../package.json'; +import type {ApiRequest, ApiRequestCommandParameters, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types'; // Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next). // Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next. @@ -38,8 +38,6 @@ type OnyxData = { finallyData?: OnyxUpdate[]; }; -type ApiRequestType = ValueOf; - /** * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values). * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later. @@ -54,7 +52,7 @@ type ApiRequestType = ValueOf; * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. */ -function write(command: string, apiCommandParameters: Record = {}, onyxData: OnyxData = {}) { +function write(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) { Log.info('Called API write', false, {command, ...apiCommandParameters}); const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; @@ -112,11 +110,11 @@ function write(command: string, apiCommandParameters: Record = * response back to the caller or to trigger reconnection callbacks when re-authentication is required. * @returns */ -function makeRequestWithSideEffects( - command: string, - apiCommandParameters = {}, +function makeRequestWithSideEffects( + command: TCommand, + apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}, - apiRequestType: ApiRequestType = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, + apiRequestType: ApiRequest = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, ): Promise { Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters}); const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData; @@ -157,7 +155,7 @@ function makeRequestWithSideEffects( * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. */ -function read(command: string, apiCommandParameters: Record, onyxData: OnyxData = {}) { +function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) { // Ensure all write requests on the sequential queue have finished responding before running read requests. // Responses from read requests can overwrite the optimistic data inserted by // write requests that use the same Onyx keys and haven't responded yet. diff --git a/src/libs/API/parameters/AcceptWalletTermsParams.ts b/src/libs/API/parameters/AcceptWalletTermsParams.ts new file mode 100644 index 000000000000..897f002eb77a --- /dev/null +++ b/src/libs/API/parameters/AcceptWalletTermsParams.ts @@ -0,0 +1,6 @@ +type AcceptWalletTermsParams = { + hasAcceptedTerms: boolean; + reportID: string; +}; + +export default AcceptWalletTermsParams; diff --git a/src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts b/src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts new file mode 100644 index 000000000000..98d7f9f4ae32 --- /dev/null +++ b/src/libs/API/parameters/ActivatePhysicalExpensifyCardParams.ts @@ -0,0 +1,5 @@ +type ActivatePhysicalExpensifyCardParams = { + cardLastFourDigits: string; + cardID: number; +}; +export default ActivatePhysicalExpensifyCardParams; diff --git a/src/libs/API/parameters/AddCommentOrAttachementParams.ts b/src/libs/API/parameters/AddCommentOrAttachementParams.ts new file mode 100644 index 000000000000..58faf9fdfc9c --- /dev/null +++ b/src/libs/API/parameters/AddCommentOrAttachementParams.ts @@ -0,0 +1,13 @@ +type AddCommentOrAttachementParams = { + reportID: string; + reportActionID?: string; + commentReportActionID?: string | null; + reportComment?: string; + file?: File; + timezone?: string; + shouldAllowActionableMentionWhispers?: boolean; + clientCreatedTime?: string; + isOldDotConciergeChat?: boolean; +}; + +export default AddCommentOrAttachementParams; diff --git a/src/libs/API/parameters/AddEmojiReactionParams.ts b/src/libs/API/parameters/AddEmojiReactionParams.ts new file mode 100644 index 000000000000..fa31da9538ad --- /dev/null +++ b/src/libs/API/parameters/AddEmojiReactionParams.ts @@ -0,0 +1,10 @@ +type AddEmojiReactionParams = { + reportID: string; + skinTone: string | number; + emojiCode: string; + reportActionID: string; + createdAt: string; + useEmojiReactions: boolean; +}; + +export default AddEmojiReactionParams; diff --git a/src/libs/API/parameters/AddMembersToWorkspaceParams.ts b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts new file mode 100644 index 000000000000..4e96fd07d301 --- /dev/null +++ b/src/libs/API/parameters/AddMembersToWorkspaceParams.ts @@ -0,0 +1,8 @@ +type AddMembersToWorkspaceParams = { + employees: string; + welcomeNote: string; + policyID: string; + reportCreationData?: string; +}; + +export default AddMembersToWorkspaceParams; diff --git a/src/libs/API/parameters/AddNewContactMethodParams.ts b/src/libs/API/parameters/AddNewContactMethodParams.ts new file mode 100644 index 000000000000..f5cd7824c191 --- /dev/null +++ b/src/libs/API/parameters/AddNewContactMethodParams.ts @@ -0,0 +1,3 @@ +type AddNewContactMethodParams = {partnerUserID: string}; + +export default AddNewContactMethodParams; diff --git a/src/libs/API/parameters/AddPaymentCardParams.ts b/src/libs/API/parameters/AddPaymentCardParams.ts new file mode 100644 index 000000000000..1c9b1fc4fa30 --- /dev/null +++ b/src/libs/API/parameters/AddPaymentCardParams.ts @@ -0,0 +1,14 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type AddPaymentCardParams = { + cardNumber: string; + cardYear: string; + cardMonth: string; + cardCVV: string; + addressName: string; + addressZip: string; + currency: ValueOf; + isP2PDebitCard: boolean; +}; +export default AddPaymentCardParams; diff --git a/src/libs/API/parameters/AddPersonalBankAccountParams.ts b/src/libs/API/parameters/AddPersonalBankAccountParams.ts new file mode 100644 index 000000000000..1fa8fc0eb48d --- /dev/null +++ b/src/libs/API/parameters/AddPersonalBankAccountParams.ts @@ -0,0 +1,12 @@ +type AddPersonalBankAccountParams = { + addressName: string; + routingNumber: string; + accountNumber: string; + isSavings: boolean; + setupType: string; + bank?: string; + plaidAccountID: string; + plaidAccessToken: string; +}; + +export default AddPersonalBankAccountParams; diff --git a/src/libs/API/parameters/AddSchoolPrincipalParams.ts b/src/libs/API/parameters/AddSchoolPrincipalParams.ts new file mode 100644 index 000000000000..5602dd22973c --- /dev/null +++ b/src/libs/API/parameters/AddSchoolPrincipalParams.ts @@ -0,0 +1,9 @@ +type AddSchoolPrincipalParams = { + firstName: string; + lastName: string; + partnerUserID: string; + policyID: string; + reportCreationData: string; +}; + +export default AddSchoolPrincipalParams; diff --git a/src/libs/API/parameters/AddWorkspaceRoomParams.ts b/src/libs/API/parameters/AddWorkspaceRoomParams.ts new file mode 100644 index 000000000000..f7cbff9565ef --- /dev/null +++ b/src/libs/API/parameters/AddWorkspaceRoomParams.ts @@ -0,0 +1,15 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {WriteCapability} from '@src/types/onyx/Report'; + +type AddWorkspaceRoomParams = { + reportID: string; + createdReportActionID: string; + policyID?: string; + reportName?: string; + visibility?: ValueOf; + writeCapability?: WriteCapability; + welcomeMessage?: string; +}; + +export default AddWorkspaceRoomParams; diff --git a/src/libs/API/parameters/AnswerQuestionsForWalletParams.ts b/src/libs/API/parameters/AnswerQuestionsForWalletParams.ts new file mode 100644 index 000000000000..34a08d7c54ee --- /dev/null +++ b/src/libs/API/parameters/AnswerQuestionsForWalletParams.ts @@ -0,0 +1,6 @@ +type AnswerQuestionsForWalletParams = { + idologyAnswers: string; + idNumber: string; +}; + +export default AnswerQuestionsForWalletParams; diff --git a/src/libs/API/parameters/AuthenticatePusherParams.ts b/src/libs/API/parameters/AuthenticatePusherParams.ts new file mode 100644 index 000000000000..95e930431ccd --- /dev/null +++ b/src/libs/API/parameters/AuthenticatePusherParams.ts @@ -0,0 +1,10 @@ +type AuthenticatePusherParams = { + // eslint-disable-next-line @typescript-eslint/naming-convention + socket_id: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + channel_name: string; + shouldRetry: boolean; + forceNetworkRequest: boolean; +}; + +export default AuthenticatePusherParams; diff --git a/src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts b/src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts new file mode 100644 index 000000000000..02ee6cd75219 --- /dev/null +++ b/src/libs/API/parameters/BankAccountHandlePlaidErrorParams.ts @@ -0,0 +1,7 @@ +type BankAccountHandlePlaidErrorParams = { + bankAccountID: number; + error: string; + errorDescription: string; + plaidRequestID: string; +}; +export default BankAccountHandlePlaidErrorParams; diff --git a/src/libs/API/parameters/BeginAppleSignInParams.ts b/src/libs/API/parameters/BeginAppleSignInParams.ts new file mode 100644 index 000000000000..c427d99fcef9 --- /dev/null +++ b/src/libs/API/parameters/BeginAppleSignInParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type BeginAppleSignInParams = { + idToken: string | undefined | null; + preferredLocale: ValueOf | null; +}; + +export default BeginAppleSignInParams; diff --git a/src/libs/API/parameters/BeginGoogleSignInParams.ts b/src/libs/API/parameters/BeginGoogleSignInParams.ts new file mode 100644 index 000000000000..fae84d76b0d9 --- /dev/null +++ b/src/libs/API/parameters/BeginGoogleSignInParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type BeginGoogleSignInParams = { + token: string | null; + preferredLocale: ValueOf | null; +}; + +export default BeginGoogleSignInParams; diff --git a/src/libs/API/parameters/BeginSignInParams.ts b/src/libs/API/parameters/BeginSignInParams.ts new file mode 100644 index 000000000000..2f85a3335c62 --- /dev/null +++ b/src/libs/API/parameters/BeginSignInParams.ts @@ -0,0 +1,5 @@ +type BeginSignInParams = { + email: string; +}; + +export default BeginSignInParams; diff --git a/src/libs/API/parameters/CancelTaskParams.ts b/src/libs/API/parameters/CancelTaskParams.ts new file mode 100644 index 000000000000..fc753cd2ea5b --- /dev/null +++ b/src/libs/API/parameters/CancelTaskParams.ts @@ -0,0 +1,6 @@ +type CancelTaskParams = { + cancelledTaskReportActionID?: string; + taskReportID?: string; +}; + +export default CancelTaskParams; diff --git a/src/libs/API/parameters/ChronosRemoveOOOEventParams.ts b/src/libs/API/parameters/ChronosRemoveOOOEventParams.ts new file mode 100644 index 000000000000..4a4f8fe6008a --- /dev/null +++ b/src/libs/API/parameters/ChronosRemoveOOOEventParams.ts @@ -0,0 +1,6 @@ +type ChronosRemoveOOOEventParams = { + googleEventID: string; + reportActionID: string; +}; + +export default ChronosRemoveOOOEventParams; diff --git a/src/libs/API/parameters/CloseAccountParams.ts b/src/libs/API/parameters/CloseAccountParams.ts new file mode 100644 index 000000000000..643d5468778f --- /dev/null +++ b/src/libs/API/parameters/CloseAccountParams.ts @@ -0,0 +1,3 @@ +type CloseAccountParams = {message: string}; + +export default CloseAccountParams; diff --git a/src/libs/API/parameters/CompleteEngagementModalParams.ts b/src/libs/API/parameters/CompleteEngagementModalParams.ts new file mode 100644 index 000000000000..cffbc0a5ba66 --- /dev/null +++ b/src/libs/API/parameters/CompleteEngagementModalParams.ts @@ -0,0 +1,10 @@ +type CompleteEngagementModalParams = { + reportID: string; + reportActionID?: string; + commentReportActionID?: string | null; + reportComment?: string; + engagementChoice: string; + timezone?: string; +}; + +export default CompleteEngagementModalParams; diff --git a/src/libs/API/parameters/CompleteTaskParams.ts b/src/libs/API/parameters/CompleteTaskParams.ts new file mode 100644 index 000000000000..2312588a6b83 --- /dev/null +++ b/src/libs/API/parameters/CompleteTaskParams.ts @@ -0,0 +1,6 @@ +type CompleteTaskParams = { + taskReportID?: string; + completedTaskReportActionID?: string; +}; + +export default CompleteTaskParams; diff --git a/src/libs/API/parameters/ConnectBankAccountManuallyParams.ts b/src/libs/API/parameters/ConnectBankAccountManuallyParams.ts new file mode 100644 index 000000000000..4f166cfd3aa9 --- /dev/null +++ b/src/libs/API/parameters/ConnectBankAccountManuallyParams.ts @@ -0,0 +1,7 @@ +type ConnectBankAccountManuallyParams = { + bankAccountID: number; + accountNumber?: string; + routingNumber?: string; + plaidMask?: string; +}; +export default ConnectBankAccountManuallyParams; diff --git a/src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts b/src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts new file mode 100644 index 000000000000..63df9d280412 --- /dev/null +++ b/src/libs/API/parameters/ConnectBankAccountWithPlaidParams.ts @@ -0,0 +1,10 @@ +type ConnectBankAccountWithPlaidParams = { + bankAccountID: number; + routingNumber: string; + accountNumber: string; + bank?: string; + plaidAccountID: string; + plaidAccessToken: string; +}; + +export default ConnectBankAccountWithPlaidParams; diff --git a/src/libs/API/parameters/CreateTaskParams.ts b/src/libs/API/parameters/CreateTaskParams.ts new file mode 100644 index 000000000000..0ead163c623b --- /dev/null +++ b/src/libs/API/parameters/CreateTaskParams.ts @@ -0,0 +1,15 @@ +type CreateTaskParams = { + parentReportActionID?: string; + parentReportID?: string; + taskReportID?: string; + createdTaskReportActionID?: string; + title?: string; + description?: string; + assignee?: string; + assigneeAccountID?: number; + assigneeChatReportID?: string; + assigneeChatReportActionID?: string; + assigneeChatCreatedReportActionID?: string; +}; + +export default CreateTaskParams; diff --git a/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts new file mode 100644 index 000000000000..761a6c2f5008 --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceFromIOUPaymentParams.ts @@ -0,0 +1,20 @@ +type CreateWorkspaceFromIOUPaymentParams = { + policyID: string; + announceChatReportID: string; + adminsChatReportID: string; + expenseChatReportID: string; + ownerEmail: string; + makeMeAdmin: boolean; + policyName: string; + type: string; + announceCreatedReportActionID: string; + adminsCreatedReportActionID: string; + expenseCreatedReportActionID: string; + customUnitID: string; + customUnitRateID: string; + iouReportID: string; + memberData: string; + reportActionID: string; +}; + +export default CreateWorkspaceFromIOUPaymentParams; diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts new file mode 100644 index 000000000000..c86598b48953 --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceParams.ts @@ -0,0 +1,17 @@ +type CreateWorkspaceParams = { + policyID: string; + announceChatReportID: string; + adminsChatReportID: string; + expenseChatReportID: string; + ownerEmail: string; + makeMeAdmin: boolean; + policyName: string; + type: string; + announceCreatedReportActionID: string; + adminsCreatedReportActionID: string; + expenseCreatedReportActionID: string; + customUnitID: string; + customUnitRateID: string; +}; + +export default CreateWorkspaceParams; diff --git a/src/libs/API/parameters/DeleteCommentParams.ts b/src/libs/API/parameters/DeleteCommentParams.ts new file mode 100644 index 000000000000..d51546eec86f --- /dev/null +++ b/src/libs/API/parameters/DeleteCommentParams.ts @@ -0,0 +1,6 @@ +type DeleteCommentParams = { + reportID: string; + reportActionID: string; +}; + +export default DeleteCommentParams; diff --git a/src/libs/API/parameters/DeleteContactMethodParams.ts b/src/libs/API/parameters/DeleteContactMethodParams.ts new file mode 100644 index 000000000000..274c3ba73512 --- /dev/null +++ b/src/libs/API/parameters/DeleteContactMethodParams.ts @@ -0,0 +1,3 @@ +type DeleteContactMethodParams = {partnerUserID: string}; + +export default DeleteContactMethodParams; diff --git a/src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts b/src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts new file mode 100644 index 000000000000..6566d2b917b4 --- /dev/null +++ b/src/libs/API/parameters/DeleteMembersFromWorkspaceParams.ts @@ -0,0 +1,6 @@ +type DeleteMembersFromWorkspaceParams = { + emailList: string; + policyID: string; +}; + +export default DeleteMembersFromWorkspaceParams; diff --git a/src/libs/API/parameters/DeletePaymentBankAccountParams.ts b/src/libs/API/parameters/DeletePaymentBankAccountParams.ts new file mode 100644 index 000000000000..737a61ccc16b --- /dev/null +++ b/src/libs/API/parameters/DeletePaymentBankAccountParams.ts @@ -0,0 +1,3 @@ +type DeletePaymentBankAccountParams = {bankAccountID: number}; + +export default DeletePaymentBankAccountParams; diff --git a/src/libs/API/parameters/DeletePaymentCardParams.ts b/src/libs/API/parameters/DeletePaymentCardParams.ts new file mode 100644 index 000000000000..e82edfbf525a --- /dev/null +++ b/src/libs/API/parameters/DeletePaymentCardParams.ts @@ -0,0 +1,4 @@ +type DeletePaymentCardParams = { + fundID: number; +}; +export default DeletePaymentCardParams; diff --git a/src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts b/src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts new file mode 100644 index 000000000000..1e0c26fbb49c --- /dev/null +++ b/src/libs/API/parameters/DeleteWorkspaceAvatarParams.ts @@ -0,0 +1,5 @@ +type DeleteWorkspaceAvatarParams = { + policyID: string; +}; + +export default DeleteWorkspaceAvatarParams; diff --git a/src/libs/API/parameters/DeleteWorkspaceParams.ts b/src/libs/API/parameters/DeleteWorkspaceParams.ts new file mode 100644 index 000000000000..c535e8123d25 --- /dev/null +++ b/src/libs/API/parameters/DeleteWorkspaceParams.ts @@ -0,0 +1,5 @@ +type DeleteWorkspaceParams = { + policyID: string; +}; + +export default DeleteWorkspaceParams; diff --git a/src/libs/API/parameters/EditTaskAssigneeParams.ts b/src/libs/API/parameters/EditTaskAssigneeParams.ts new file mode 100644 index 000000000000..cfac73067587 --- /dev/null +++ b/src/libs/API/parameters/EditTaskAssigneeParams.ts @@ -0,0 +1,10 @@ +type EditTaskAssigneeParams = { + taskReportID?: string; + assignee?: string; + editedTaskReportActionID?: string; + assigneeChatReportID?: string; + assigneeChatReportActionID?: string; + assigneeChatCreatedReportActionID?: string; +}; + +export default EditTaskAssigneeParams; diff --git a/src/libs/API/parameters/EditTaskParams.ts b/src/libs/API/parameters/EditTaskParams.ts new file mode 100644 index 000000000000..01595b7928c5 --- /dev/null +++ b/src/libs/API/parameters/EditTaskParams.ts @@ -0,0 +1,8 @@ +type EditTaskParams = { + taskReportID?: string; + title?: string; + description?: string; + editedTaskReportActionID?: string; +}; + +export default EditTaskParams; diff --git a/src/libs/API/parameters/ExpandURLPreviewParams.ts b/src/libs/API/parameters/ExpandURLPreviewParams.ts new file mode 100644 index 000000000000..1b0e7e6fc78d --- /dev/null +++ b/src/libs/API/parameters/ExpandURLPreviewParams.ts @@ -0,0 +1,6 @@ +type ExpandURLPreviewParams = { + reportID: string; + reportActionID: string; +}; + +export default ExpandURLPreviewParams; diff --git a/src/libs/API/parameters/FlagCommentParams.ts b/src/libs/API/parameters/FlagCommentParams.ts new file mode 100644 index 000000000000..1789ffb16c8c --- /dev/null +++ b/src/libs/API/parameters/FlagCommentParams.ts @@ -0,0 +1,7 @@ +type FlagCommentParams = { + severity: string; + reportActionID: string; + isDevRequest: boolean; +}; + +export default FlagCommentParams; diff --git a/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts b/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts new file mode 100644 index 000000000000..38df5a37ba97 --- /dev/null +++ b/src/libs/API/parameters/GetMissingOnyxMessagesParams.ts @@ -0,0 +1,6 @@ +type GetMissingOnyxMessagesParams = { + updateIDFrom: number; + updateIDTo: number | string; +}; + +export default GetMissingOnyxMessagesParams; diff --git a/src/libs/API/parameters/GetNewerActionsParams.ts b/src/libs/API/parameters/GetNewerActionsParams.ts new file mode 100644 index 000000000000..76ab1938b640 --- /dev/null +++ b/src/libs/API/parameters/GetNewerActionsParams.ts @@ -0,0 +1,6 @@ +type GetNewerActionsParams = { + reportID: string; + reportActionID: string; +}; + +export default GetNewerActionsParams; diff --git a/src/libs/API/parameters/GetOlderActionsParams.ts b/src/libs/API/parameters/GetOlderActionsParams.ts new file mode 100644 index 000000000000..4e585ba8afdd --- /dev/null +++ b/src/libs/API/parameters/GetOlderActionsParams.ts @@ -0,0 +1,6 @@ +type GetOlderActionsParams = { + reportID: string; + reportActionID: string; +}; + +export default GetOlderActionsParams; diff --git a/src/libs/API/parameters/GetReportPrivateNoteParams.ts b/src/libs/API/parameters/GetReportPrivateNoteParams.ts new file mode 100644 index 000000000000..3e52119c164f --- /dev/null +++ b/src/libs/API/parameters/GetReportPrivateNoteParams.ts @@ -0,0 +1,5 @@ +type GetReportPrivateNoteParams = { + reportID: string; +}; + +export default GetReportPrivateNoteParams; diff --git a/src/libs/API/parameters/GetRouteForDraftParams.ts b/src/libs/API/parameters/GetRouteForDraftParams.ts new file mode 100644 index 000000000000..5a213c3f2d49 --- /dev/null +++ b/src/libs/API/parameters/GetRouteForDraftParams.ts @@ -0,0 +1,6 @@ +type GetRouteForDraftParams = { + transactionID: string; + waypoints: string; +}; + +export default GetRouteForDraftParams; diff --git a/src/libs/API/parameters/GetRouteParams.ts b/src/libs/API/parameters/GetRouteParams.ts new file mode 100644 index 000000000000..d6ff7b972e0d --- /dev/null +++ b/src/libs/API/parameters/GetRouteParams.ts @@ -0,0 +1,6 @@ +type GetRouteParams = { + transactionID: string; + waypoints: string; +}; + +export default GetRouteParams; diff --git a/src/libs/API/parameters/GetStatementPDFParams.ts b/src/libs/API/parameters/GetStatementPDFParams.ts new file mode 100644 index 000000000000..3fa8bb732531 --- /dev/null +++ b/src/libs/API/parameters/GetStatementPDFParams.ts @@ -0,0 +1,5 @@ +type GetStatementPDFParams = { + period: string; +}; + +export default GetStatementPDFParams; diff --git a/src/libs/API/parameters/HandleRestrictedEventParams.ts b/src/libs/API/parameters/HandleRestrictedEventParams.ts new file mode 100644 index 000000000000..808220f07747 --- /dev/null +++ b/src/libs/API/parameters/HandleRestrictedEventParams.ts @@ -0,0 +1,5 @@ +type HandleRestrictedEventParams = { + eventName: string; +}; + +export default HandleRestrictedEventParams; diff --git a/src/libs/API/parameters/InviteToRoomParams.ts b/src/libs/API/parameters/InviteToRoomParams.ts new file mode 100644 index 000000000000..b1af3a4fc3df --- /dev/null +++ b/src/libs/API/parameters/InviteToRoomParams.ts @@ -0,0 +1,6 @@ +type InviteToRoomParams = { + reportID: string; + inviteeEmails: string[]; +}; + +export default InviteToRoomParams; diff --git a/src/libs/API/parameters/LeaveRoomParams.ts b/src/libs/API/parameters/LeaveRoomParams.ts new file mode 100644 index 000000000000..0d0483eca88a --- /dev/null +++ b/src/libs/API/parameters/LeaveRoomParams.ts @@ -0,0 +1,5 @@ +type LeaveRoomParams = { + reportID: string; +}; + +export default LeaveRoomParams; diff --git a/src/libs/API/parameters/LogOutParams.ts b/src/libs/API/parameters/LogOutParams.ts new file mode 100644 index 000000000000..7cb81080b19f --- /dev/null +++ b/src/libs/API/parameters/LogOutParams.ts @@ -0,0 +1,9 @@ +type LogOutParams = { + authToken: string | null; + partnerUserID: string; + partnerName: string; + partnerPassword: string; + shouldRetry: boolean; +}; + +export default LogOutParams; diff --git a/src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts b/src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts new file mode 100644 index 000000000000..9cc07214845c --- /dev/null +++ b/src/libs/API/parameters/MakeDefaultPaymentMethodParams.ts @@ -0,0 +1,5 @@ +type MakeDefaultPaymentMethodParams = { + bankAccountID: number; + fundID: number; +}; +export default MakeDefaultPaymentMethodParams; diff --git a/src/libs/API/parameters/MarkAsUnreadParams.ts b/src/libs/API/parameters/MarkAsUnreadParams.ts new file mode 100644 index 000000000000..0a4a0d98c18c --- /dev/null +++ b/src/libs/API/parameters/MarkAsUnreadParams.ts @@ -0,0 +1,6 @@ +type MarkAsUnreadParams = { + reportID: string; + lastReadTime: string; +}; + +export default MarkAsUnreadParams; diff --git a/src/libs/API/parameters/OpenAppParams.ts b/src/libs/API/parameters/OpenAppParams.ts new file mode 100644 index 000000000000..ac0100109c51 --- /dev/null +++ b/src/libs/API/parameters/OpenAppParams.ts @@ -0,0 +1,6 @@ +type OpenAppParams = { + policyIDList: string[]; + enablePriorityModeFilter: boolean; +}; + +export default OpenAppParams; diff --git a/src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts b/src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts new file mode 100644 index 000000000000..b9f098da9dae --- /dev/null +++ b/src/libs/API/parameters/OpenDraftWorkspaceRequestParams.ts @@ -0,0 +1,5 @@ +type OpenDraftWorkspaceRequestParams = { + policyID: string; +}; + +export default OpenDraftWorkspaceRequestParams; diff --git a/src/libs/API/parameters/OpenOldDotLinkParams.ts b/src/libs/API/parameters/OpenOldDotLinkParams.ts new file mode 100644 index 000000000000..873b1550368f --- /dev/null +++ b/src/libs/API/parameters/OpenOldDotLinkParams.ts @@ -0,0 +1,5 @@ +type OpenOldDotLinkParams = { + shouldRetry?: boolean; +}; + +export default OpenOldDotLinkParams; diff --git a/src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts b/src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts new file mode 100644 index 000000000000..c92d72460fa9 --- /dev/null +++ b/src/libs/API/parameters/OpenPlaidBankAccountSelectorParams.ts @@ -0,0 +1,8 @@ +type OpenPlaidBankAccountSelectorParams = { + publicToken: string; + allowDebit: boolean; + bank: string; + bankAccountID: number; +}; + +export default OpenPlaidBankAccountSelectorParams; diff --git a/src/libs/API/parameters/OpenPlaidBankLoginParams.ts b/src/libs/API/parameters/OpenPlaidBankLoginParams.ts new file mode 100644 index 000000000000..f76e05423d03 --- /dev/null +++ b/src/libs/API/parameters/OpenPlaidBankLoginParams.ts @@ -0,0 +1,7 @@ +type OpenPlaidBankLoginParams = { + redirectURI: string | undefined; + allowDebit: boolean; + bankAccountID: number; +}; + +export default OpenPlaidBankLoginParams; diff --git a/src/libs/API/parameters/OpenProfileParams.ts b/src/libs/API/parameters/OpenProfileParams.ts new file mode 100644 index 000000000000..f42ea8234fc8 --- /dev/null +++ b/src/libs/API/parameters/OpenProfileParams.ts @@ -0,0 +1,5 @@ +type OpenProfileParams = { + timezone: string; +}; + +export default OpenProfileParams; diff --git a/src/libs/API/parameters/OpenPublicProfilePageParams.ts b/src/libs/API/parameters/OpenPublicProfilePageParams.ts new file mode 100644 index 000000000000..3bb50c563e28 --- /dev/null +++ b/src/libs/API/parameters/OpenPublicProfilePageParams.ts @@ -0,0 +1,5 @@ +type OpenPublicProfilePageParams = { + accountID: number; +}; + +export default OpenPublicProfilePageParams; diff --git a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts new file mode 100644 index 000000000000..d831609b2e0a --- /dev/null +++ b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts @@ -0,0 +1,12 @@ +import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; + +type ReimbursementAccountStep = BankAccountStep | ''; +type ReimbursementAccountSubStep = BankAccountSubStep | ''; + +type OpenReimbursementAccountPageParams = { + stepToOpen: ReimbursementAccountStep; + subStep: ReimbursementAccountSubStep; + localCurrentStep: ReimbursementAccountStep; +}; + +export default OpenReimbursementAccountPageParams; diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts new file mode 100644 index 000000000000..477a002516de --- /dev/null +++ b/src/libs/API/parameters/OpenReportParams.ts @@ -0,0 +1,12 @@ +type OpenReportParams = { + reportID: string; + emailList?: string; + accountIDList?: string; + parentReportActionID?: string; + shouldRetry?: boolean; + createdReportActionID?: string; + clientLastReadTime?: string; + idempotencyKey?: string; +}; + +export default OpenReportParams; diff --git a/src/libs/API/parameters/OpenRoomMembersPageParams.ts b/src/libs/API/parameters/OpenRoomMembersPageParams.ts new file mode 100644 index 000000000000..7ea1afb9bdb9 --- /dev/null +++ b/src/libs/API/parameters/OpenRoomMembersPageParams.ts @@ -0,0 +1,5 @@ +type OpenRoomMembersPageParams = { + reportID: string; +}; + +export default OpenRoomMembersPageParams; diff --git a/src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts b/src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts new file mode 100644 index 000000000000..0b622bee75b7 --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceInvitePageParams.ts @@ -0,0 +1,6 @@ +type OpenWorkspaceInvitePageParams = { + policyID: string; + clientMemberEmails: string; +}; + +export default OpenWorkspaceInvitePageParams; diff --git a/src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts b/src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts new file mode 100644 index 000000000000..2dab31ac356b --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceMembersPageParams.ts @@ -0,0 +1,6 @@ +type OpenWorkspaceMembersPageParams = { + policyID: string; + clientMemberEmails: string; +}; + +export default OpenWorkspaceMembersPageParams; diff --git a/src/libs/API/parameters/OpenWorkspaceParams.ts b/src/libs/API/parameters/OpenWorkspaceParams.ts new file mode 100644 index 000000000000..3ea0d4b3dabe --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceParams.ts @@ -0,0 +1,6 @@ +type OpenWorkspaceParams = { + policyID: string; + clientMemberAccountIDs: string; +}; + +export default OpenWorkspaceParams; diff --git a/src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts b/src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts new file mode 100644 index 000000000000..317241c8842f --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspaceReimburseViewParams.ts @@ -0,0 +1,5 @@ +type OpenWorkspaceReimburseViewParams = { + policyID: string; +}; + +export default OpenWorkspaceReimburseViewParams; diff --git a/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts b/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts new file mode 100644 index 000000000000..758152abc2af --- /dev/null +++ b/src/libs/API/parameters/OptInOutToPushNotificationsParams.ts @@ -0,0 +1,5 @@ +type OptInOutToPushNotificationsParams = { + deviceID: string | null; +}; + +export default OptInOutToPushNotificationsParams; diff --git a/src/libs/API/parameters/PaymentCardParams.ts b/src/libs/API/parameters/PaymentCardParams.ts new file mode 100644 index 000000000000..3e705994e9fa --- /dev/null +++ b/src/libs/API/parameters/PaymentCardParams.ts @@ -0,0 +1,3 @@ +type PaymentCardParams = {expirationDate: string; cardNumber: string; securityCode: string; nameOnCard: string; addressZipCode: string}; + +export default PaymentCardParams; diff --git a/src/libs/API/parameters/ReadNewestActionParams.ts b/src/libs/API/parameters/ReadNewestActionParams.ts new file mode 100644 index 000000000000..590dfce25a4e --- /dev/null +++ b/src/libs/API/parameters/ReadNewestActionParams.ts @@ -0,0 +1,6 @@ +type ReadNewestActionParams = { + reportID: string; + lastReadTime: string; +}; + +export default ReadNewestActionParams; diff --git a/src/libs/API/parameters/ReconnectAppParams.ts b/src/libs/API/parameters/ReconnectAppParams.ts new file mode 100644 index 000000000000..8c5b7d6c0da9 --- /dev/null +++ b/src/libs/API/parameters/ReconnectAppParams.ts @@ -0,0 +1,7 @@ +type ReconnectAppParams = { + mostRecentReportActionLastModified?: string; + updateIDFrom?: number; + policyIDList: string[]; +}; + +export default ReconnectAppParams; diff --git a/src/libs/API/parameters/ReconnectToReportParams.ts b/src/libs/API/parameters/ReconnectToReportParams.ts new file mode 100644 index 000000000000..e7701cd36ca9 --- /dev/null +++ b/src/libs/API/parameters/ReconnectToReportParams.ts @@ -0,0 +1,5 @@ +type ReconnectToReportParams = { + reportID: string; +}; + +export default ReconnectToReportParams; diff --git a/src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts b/src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts new file mode 100644 index 000000000000..0eb31c47865d --- /dev/null +++ b/src/libs/API/parameters/ReferTeachersUniteVolunteerParams.ts @@ -0,0 +1,8 @@ +type ReferTeachersUniteVolunteerParams = { + reportID: string; + firstName: string; + lastName: string; + partnerUserID: string; +}; + +export default ReferTeachersUniteVolunteerParams; diff --git a/src/libs/API/parameters/RemoveEmojiReactionParams.ts b/src/libs/API/parameters/RemoveEmojiReactionParams.ts new file mode 100644 index 000000000000..5d474dff713b --- /dev/null +++ b/src/libs/API/parameters/RemoveEmojiReactionParams.ts @@ -0,0 +1,8 @@ +type RemoveEmojiReactionParams = { + reportID: string; + reportActionID: string; + emojiCode: string; + useEmojiReactions: boolean; +}; + +export default RemoveEmojiReactionParams; diff --git a/src/libs/API/parameters/RemoveFromRoomParams.ts b/src/libs/API/parameters/RemoveFromRoomParams.ts new file mode 100644 index 000000000000..6bf94a534dbd --- /dev/null +++ b/src/libs/API/parameters/RemoveFromRoomParams.ts @@ -0,0 +1,6 @@ +type RemoveFromRoomParams = { + reportID: string; + targetAccountIDs: number[]; +}; + +export default RemoveFromRoomParams; diff --git a/src/libs/API/parameters/ReopenTaskParams.ts b/src/libs/API/parameters/ReopenTaskParams.ts new file mode 100644 index 000000000000..ecdff74504f7 --- /dev/null +++ b/src/libs/API/parameters/ReopenTaskParams.ts @@ -0,0 +1,6 @@ +type ReopenTaskParams = { + taskReportID?: string; + reopenedTaskReportActionID?: string; +}; + +export default ReopenTaskParams; diff --git a/src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts b/src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts new file mode 100644 index 000000000000..350795d46355 --- /dev/null +++ b/src/libs/API/parameters/ReportVirtualExpensifyCardFraudParams.ts @@ -0,0 +1,4 @@ +type ReportVirtualExpensifyCardFraudParams = { + cardID: number; +}; +export default ReportVirtualExpensifyCardFraudParams; diff --git a/src/libs/API/parameters/RequestAccountValidationLinkParams.ts b/src/libs/API/parameters/RequestAccountValidationLinkParams.ts new file mode 100644 index 000000000000..be33c5648685 --- /dev/null +++ b/src/libs/API/parameters/RequestAccountValidationLinkParams.ts @@ -0,0 +1,5 @@ +type RequestAccountValidationLinkParams = { + email?: string; +}; + +export default RequestAccountValidationLinkParams; diff --git a/src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts b/src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts new file mode 100644 index 000000000000..13a26b717619 --- /dev/null +++ b/src/libs/API/parameters/RequestContactMethodValidateCodeParams.ts @@ -0,0 +1,3 @@ +type RequestContactMethodValidateCodeParams = {email: string}; + +export default RequestContactMethodValidateCodeParams; diff --git a/src/libs/API/parameters/RequestNewValidateCodeParams.ts b/src/libs/API/parameters/RequestNewValidateCodeParams.ts new file mode 100644 index 000000000000..329b234023d0 --- /dev/null +++ b/src/libs/API/parameters/RequestNewValidateCodeParams.ts @@ -0,0 +1,5 @@ +type RequestNewValidateCodeParams = { + email?: string; +}; + +export default RequestNewValidateCodeParams; diff --git a/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts new file mode 100644 index 000000000000..91995b6e37aa --- /dev/null +++ b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts @@ -0,0 +1,13 @@ +type RequestPhysicalExpensifyCardParams = { + authToken: string; + legalFirstName: string; + legalLastName: string; + phoneNumber: string; + addressCity: string; + addressCountry: string; + addressState: string; + addressStreet: string; + addressZip: string; +}; + +export default RequestPhysicalExpensifyCardParams; diff --git a/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts b/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts new file mode 100644 index 000000000000..bc86923a83a4 --- /dev/null +++ b/src/libs/API/parameters/RequestReplacementExpensifyCardParams.ts @@ -0,0 +1,6 @@ +type RequestReplacementExpensifyCardParams = { + cardID: number; + reason: string; +}; + +export default RequestReplacementExpensifyCardParams; diff --git a/src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts b/src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts new file mode 100644 index 000000000000..2a37f7119304 --- /dev/null +++ b/src/libs/API/parameters/RequestUnlinkValidationLinkParams.ts @@ -0,0 +1,5 @@ +type RequestUnlinkValidationLinkParams = { + email?: string; +}; + +export default RequestUnlinkValidationLinkParams; diff --git a/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts b/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts new file mode 100644 index 000000000000..87dfc934eb5f --- /dev/null +++ b/src/libs/API/parameters/ResolveActionableMentionWhisperParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type ResolveActionableMentionWhisperParams = { + reportActionID: string; + resolution: ValueOf; +}; + +export default ResolveActionableMentionWhisperParams; diff --git a/src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts b/src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts new file mode 100644 index 000000000000..ec698fc85269 --- /dev/null +++ b/src/libs/API/parameters/RevealExpensifyCardDetailsParams.ts @@ -0,0 +1,3 @@ +type RevealExpensifyCardDetailsParams = {cardID: number}; + +export default RevealExpensifyCardDetailsParams; diff --git a/src/libs/API/parameters/SearchForReportsParams.ts b/src/libs/API/parameters/SearchForReportsParams.ts new file mode 100644 index 000000000000..b6d1bbadb1dc --- /dev/null +++ b/src/libs/API/parameters/SearchForReportsParams.ts @@ -0,0 +1,5 @@ +type SearchForReportsParams = { + searchInput: string; +}; + +export default SearchForReportsParams; diff --git a/src/libs/API/parameters/SendPerformanceTimingParams.ts b/src/libs/API/parameters/SendPerformanceTimingParams.ts new file mode 100644 index 000000000000..aebdaa40c8bb --- /dev/null +++ b/src/libs/API/parameters/SendPerformanceTimingParams.ts @@ -0,0 +1,7 @@ +type SendPerformanceTimingParams = { + name: string; + value: number; + platform: string; +}; + +export default SendPerformanceTimingParams; diff --git a/src/libs/API/parameters/SetContactMethodAsDefaultParams.ts b/src/libs/API/parameters/SetContactMethodAsDefaultParams.ts new file mode 100644 index 000000000000..03a33eb81c36 --- /dev/null +++ b/src/libs/API/parameters/SetContactMethodAsDefaultParams.ts @@ -0,0 +1,5 @@ +type SetContactMethodAsDefaultParams = { + partnerUserID: string; +}; + +export default SetContactMethodAsDefaultParams; diff --git a/src/libs/API/parameters/SetNameValuePairParams.ts b/src/libs/API/parameters/SetNameValuePairParams.ts new file mode 100644 index 000000000000..bc83d431224b --- /dev/null +++ b/src/libs/API/parameters/SetNameValuePairParams.ts @@ -0,0 +1,6 @@ +type SetNameValuePairParams = { + name: string; + value: boolean; +}; + +export default SetNameValuePairParams; diff --git a/src/libs/API/parameters/SignInUserParams.ts b/src/libs/API/parameters/SignInUserParams.ts new file mode 100644 index 000000000000..9fe973c42862 --- /dev/null +++ b/src/libs/API/parameters/SignInUserParams.ts @@ -0,0 +1,12 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type SignInUserParams = { + twoFactorAuthCode?: string; + email?: string; + preferredLocale: ValueOf | null; + validateCode?: string; + deviceInfo: string; +}; + +export default SignInUserParams; diff --git a/src/libs/API/parameters/SignInUserWithLinkParams.ts b/src/libs/API/parameters/SignInUserWithLinkParams.ts new file mode 100644 index 000000000000..ae3589d4e305 --- /dev/null +++ b/src/libs/API/parameters/SignInUserWithLinkParams.ts @@ -0,0 +1,12 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type SignInUserWithLinkParams = { + accountID: number; + validateCode?: string; + twoFactorAuthCode?: string; + preferredLocale: ValueOf | null; + deviceInfo: string; +}; + +export default SignInUserWithLinkParams; diff --git a/src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts b/src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts new file mode 100644 index 000000000000..447c0ede0399 --- /dev/null +++ b/src/libs/API/parameters/SignInWithShortLivedAuthTokenParams.ts @@ -0,0 +1,7 @@ +type SignInWithShortLivedAuthTokenParams = { + authToken: string; + oldPartnerUserID: string; + skipReauthentication: boolean; +}; + +export default SignInWithShortLivedAuthTokenParams; diff --git a/src/libs/API/parameters/TogglePinnedChatParams.ts b/src/libs/API/parameters/TogglePinnedChatParams.ts new file mode 100644 index 000000000000..338a77172dd6 --- /dev/null +++ b/src/libs/API/parameters/TogglePinnedChatParams.ts @@ -0,0 +1,6 @@ +type TogglePinnedChatParams = { + reportID: string; + pinnedValue: boolean; +}; + +export default TogglePinnedChatParams; diff --git a/src/libs/API/parameters/TransferWalletBalanceParams.ts b/src/libs/API/parameters/TransferWalletBalanceParams.ts new file mode 100644 index 000000000000..c25268d92bf3 --- /dev/null +++ b/src/libs/API/parameters/TransferWalletBalanceParams.ts @@ -0,0 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type TransferWalletBalanceParams = Partial, number | undefined>>; + +export default TransferWalletBalanceParams; diff --git a/src/libs/API/parameters/UnlinkLoginParams.ts b/src/libs/API/parameters/UnlinkLoginParams.ts new file mode 100644 index 000000000000..1a60e480bb18 --- /dev/null +++ b/src/libs/API/parameters/UnlinkLoginParams.ts @@ -0,0 +1,6 @@ +type UnlinkLoginParams = { + accountID: number; + validateCode: string; +}; + +export default UnlinkLoginParams; diff --git a/src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts b/src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts new file mode 100644 index 000000000000..07c13e0cf55e --- /dev/null +++ b/src/libs/API/parameters/UpdateAutomaticTimezoneParams.ts @@ -0,0 +1,4 @@ +type UpdateAutomaticTimezoneParams = { + timezone: string; +}; +export default UpdateAutomaticTimezoneParams; diff --git a/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts new file mode 100644 index 000000000000..414c87ee8989 --- /dev/null +++ b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts @@ -0,0 +1,5 @@ +import type {ACHContractStepProps} from '@src/types/onyx/ReimbursementAccountDraft'; + +type UpdateBeneficialOwnersForBankAccountParams = ACHContractStepProps; + +export default UpdateBeneficialOwnersForBankAccountParams; diff --git a/src/libs/API/parameters/UpdateChatPriorityModeParams.ts b/src/libs/API/parameters/UpdateChatPriorityModeParams.ts new file mode 100644 index 000000000000..8bbb7bf6943c --- /dev/null +++ b/src/libs/API/parameters/UpdateChatPriorityModeParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type UpdateChatPriorityModeParams = { + value: ValueOf; + automatic: boolean; +}; + +export default UpdateChatPriorityModeParams; diff --git a/src/libs/API/parameters/UpdateCommentParams.ts b/src/libs/API/parameters/UpdateCommentParams.ts new file mode 100644 index 000000000000..e4ba9391ccd4 --- /dev/null +++ b/src/libs/API/parameters/UpdateCommentParams.ts @@ -0,0 +1,7 @@ +type UpdateCommentParams = { + reportID: string; + reportComment: string; + reportActionID: string; +}; + +export default UpdateCommentParams; diff --git a/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts new file mode 100644 index 000000000000..7588039a9abf --- /dev/null +++ b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts @@ -0,0 +1,8 @@ +import type {BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps} from '@src/types/onyx/ReimbursementAccountDraft'; + +type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps; + +type UpdateCompanyInformationForBankAccountParams = BankAccountCompanyInformation & {policyID: string}; + +export default UpdateCompanyInformationForBankAccountParams; +export type {BankAccountCompanyInformation}; diff --git a/src/libs/API/parameters/UpdateDateOfBirthParams.ts b/src/libs/API/parameters/UpdateDateOfBirthParams.ts new file mode 100644 index 000000000000..e98336d16bff --- /dev/null +++ b/src/libs/API/parameters/UpdateDateOfBirthParams.ts @@ -0,0 +1,4 @@ +type UpdateDateOfBirthParams = { + dob?: string; +}; +export default UpdateDateOfBirthParams; diff --git a/src/libs/API/parameters/UpdateDisplayNameParams.ts b/src/libs/API/parameters/UpdateDisplayNameParams.ts new file mode 100644 index 000000000000..0febd6765fc0 --- /dev/null +++ b/src/libs/API/parameters/UpdateDisplayNameParams.ts @@ -0,0 +1,5 @@ +type UpdateDisplayNameParams = { + firstName: string; + lastName: string; +}; +export default UpdateDisplayNameParams; diff --git a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts new file mode 100644 index 000000000000..f790ada3aad9 --- /dev/null +++ b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts @@ -0,0 +1,3 @@ +type UpdateFrequentlyUsedEmojisParams = {value: string}; + +export default UpdateFrequentlyUsedEmojisParams; diff --git a/src/libs/API/parameters/UpdateHomeAddressParams.ts b/src/libs/API/parameters/UpdateHomeAddressParams.ts new file mode 100644 index 000000000000..7de71fe67c58 --- /dev/null +++ b/src/libs/API/parameters/UpdateHomeAddressParams.ts @@ -0,0 +1,11 @@ +type UpdateHomeAddressParams = { + homeAddressStreet: string; + addressStreet2: string; + homeAddressCity: string; + addressState: string; + addressZipCode: string; + addressCountry: string; + addressStateLong?: string; +}; + +export default UpdateHomeAddressParams; diff --git a/src/libs/API/parameters/UpdateLegalNameParams.ts b/src/libs/API/parameters/UpdateLegalNameParams.ts new file mode 100644 index 000000000000..2c55cec13cc4 --- /dev/null +++ b/src/libs/API/parameters/UpdateLegalNameParams.ts @@ -0,0 +1,6 @@ +type UpdateLegalNameParams = { + legalFirstName: string; + legalLastName: string; +}; + +export default UpdateLegalNameParams; diff --git a/src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts b/src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts new file mode 100644 index 000000000000..311d3a5518df --- /dev/null +++ b/src/libs/API/parameters/UpdateNewsletterSubscriptionParams.ts @@ -0,0 +1,3 @@ +type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; + +export default UpdateNewsletterSubscriptionParams; diff --git a/src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts b/src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts new file mode 100644 index 000000000000..d874dced4a92 --- /dev/null +++ b/src/libs/API/parameters/UpdatePersonalDetailsForWalletParams.ts @@ -0,0 +1,13 @@ +type UpdatePersonalDetailsForWalletParams = { + phoneNumber: string; + legalFirstName: string; + legalLastName: string; + addressStreet: string; + addressCity: string; + addressState: string; + addressZip: string; + dob: string; + ssn: string; +}; + +export default UpdatePersonalDetailsForWalletParams; diff --git a/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts new file mode 100644 index 000000000000..4de2e462fc7a --- /dev/null +++ b/src/libs/API/parameters/UpdatePersonalInformationForBankAccountParams.ts @@ -0,0 +1,5 @@ +import type {RequestorStepProps} from '@src/types/onyx/ReimbursementAccountDraft'; + +type UpdatePersonalInformationForBankAccountParams = RequestorStepProps; + +export default UpdatePersonalInformationForBankAccountParams; diff --git a/src/libs/API/parameters/UpdatePolicyRoomNameParams.ts b/src/libs/API/parameters/UpdatePolicyRoomNameParams.ts new file mode 100644 index 000000000000..65b858b7c20f --- /dev/null +++ b/src/libs/API/parameters/UpdatePolicyRoomNameParams.ts @@ -0,0 +1,6 @@ +type UpdatePolicyRoomNameParams = { + reportID: string; + policyRoomName: string; +}; + +export default UpdatePolicyRoomNameParams; diff --git a/src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts b/src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts new file mode 100644 index 000000000000..a769e3635bfa --- /dev/null +++ b/src/libs/API/parameters/UpdatePreferredEmojiSkinToneParams.ts @@ -0,0 +1,5 @@ +type UpdatePreferredEmojiSkinToneParams = { + value: number; +}; + +export default UpdatePreferredEmojiSkinToneParams; diff --git a/src/libs/API/parameters/UpdatePreferredLocaleParams.ts b/src/libs/API/parameters/UpdatePreferredLocaleParams.ts new file mode 100644 index 000000000000..5dd991dea3b5 --- /dev/null +++ b/src/libs/API/parameters/UpdatePreferredLocaleParams.ts @@ -0,0 +1,10 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type Locale = ValueOf; + +type UpdatePreferredLocaleParams = { + value: Locale; +}; + +export default UpdatePreferredLocaleParams; diff --git a/src/libs/API/parameters/UpdatePronounsParams.ts b/src/libs/API/parameters/UpdatePronounsParams.ts new file mode 100644 index 000000000000..f7ac30a5b2ef --- /dev/null +++ b/src/libs/API/parameters/UpdatePronounsParams.ts @@ -0,0 +1,5 @@ +type UpdatePronounsParams = { + pronouns: string; +}; + +export default UpdatePronounsParams; diff --git a/src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts b/src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts new file mode 100644 index 000000000000..c58746b316fe --- /dev/null +++ b/src/libs/API/parameters/UpdateReportNotificationPreferenceParams.ts @@ -0,0 +1,8 @@ +import type {NotificationPreference} from '@src/types/onyx/Report'; + +type UpdateReportNotificationPreferenceParams = { + reportID: string; + notificationPreference: NotificationPreference; +}; + +export default UpdateReportNotificationPreferenceParams; diff --git a/src/libs/API/parameters/UpdateReportPrivateNoteParams.ts b/src/libs/API/parameters/UpdateReportPrivateNoteParams.ts new file mode 100644 index 000000000000..30fad3bec3ab --- /dev/null +++ b/src/libs/API/parameters/UpdateReportPrivateNoteParams.ts @@ -0,0 +1,6 @@ +type UpdateReportPrivateNoteParams = { + reportID: string; + privateNotes: string; +}; + +export default UpdateReportPrivateNoteParams; diff --git a/src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts b/src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts new file mode 100644 index 000000000000..30b85b15aa3b --- /dev/null +++ b/src/libs/API/parameters/UpdateReportWriteCapabilityParams.ts @@ -0,0 +1,8 @@ +import type {WriteCapability} from '@src/types/onyx/Report'; + +type UpdateReportWriteCapabilityParams = { + reportID: string; + writeCapability: WriteCapability; +}; + +export default UpdateReportWriteCapabilityParams; diff --git a/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts b/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts new file mode 100644 index 000000000000..595e14f7c54c --- /dev/null +++ b/src/libs/API/parameters/UpdateSelectedTimezoneParams.ts @@ -0,0 +1,5 @@ +type UpdateSelectedTimezoneParams = { + timezone: string; +}; + +export default UpdateSelectedTimezoneParams; diff --git a/src/libs/API/parameters/UpdateStatusParams.ts b/src/libs/API/parameters/UpdateStatusParams.ts new file mode 100644 index 000000000000..ba812e554cd7 --- /dev/null +++ b/src/libs/API/parameters/UpdateStatusParams.ts @@ -0,0 +1,7 @@ +type UpdateStatusParams = { + text?: string; + emojiCode: string; + clearAfter?: string; +}; + +export default UpdateStatusParams; diff --git a/src/libs/API/parameters/UpdateThemeParams.ts b/src/libs/API/parameters/UpdateThemeParams.ts new file mode 100644 index 000000000000..10a8c243d6e4 --- /dev/null +++ b/src/libs/API/parameters/UpdateThemeParams.ts @@ -0,0 +1,5 @@ +type UpdateThemeParams = { + value: string; +}; + +export default UpdateThemeParams; diff --git a/src/libs/API/parameters/UpdateUserAvatarParams.ts b/src/libs/API/parameters/UpdateUserAvatarParams.ts new file mode 100644 index 000000000000..2dce38e8763c --- /dev/null +++ b/src/libs/API/parameters/UpdateUserAvatarParams.ts @@ -0,0 +1,7 @@ +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; + +type UpdateUserAvatarParams = { + file: File | CustomRNImageManipulatorResult; +}; + +export default UpdateUserAvatarParams; diff --git a/src/libs/API/parameters/UpdateWelcomeMessageParams.ts b/src/libs/API/parameters/UpdateWelcomeMessageParams.ts new file mode 100644 index 000000000000..a2d3b59fe3fa --- /dev/null +++ b/src/libs/API/parameters/UpdateWelcomeMessageParams.ts @@ -0,0 +1,6 @@ +type UpdateWelcomeMessageParams = { + reportID: string; + welcomeMessage: string; +}; + +export default UpdateWelcomeMessageParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts b/src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts new file mode 100644 index 000000000000..a4c1edf83dab --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceAvatarParams.ts @@ -0,0 +1,6 @@ +type UpdateWorkspaceAvatarParams = { + policyID: string; + file: File; +}; + +export default UpdateWorkspaceAvatarParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts new file mode 100644 index 000000000000..22bbd20c7308 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceCustomUnitAndRateParams.ts @@ -0,0 +1,8 @@ +type UpdateWorkspaceCustomUnitAndRateParams = { + policyID: string; + lastModified: number; + customUnit: string; + customUnitRate: string; +}; + +export default UpdateWorkspaceCustomUnitAndRateParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts b/src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts new file mode 100644 index 000000000000..9aeb4be97a43 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceGeneralSettingsParams.ts @@ -0,0 +1,7 @@ +type UpdateWorkspaceGeneralSettingsParams = { + policyID: string; + workspaceName: string; + currency: string; +}; + +export default UpdateWorkspaceGeneralSettingsParams; diff --git a/src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts b/src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts new file mode 100644 index 000000000000..546889b7a68e --- /dev/null +++ b/src/libs/API/parameters/ValidateBankAccountWithTransactionsParams.ts @@ -0,0 +1,6 @@ +type ValidateBankAccountWithTransactionsParams = { + bankAccountID: number; + validateCode: string; +}; + +export default ValidateBankAccountWithTransactionsParams; diff --git a/src/libs/API/parameters/ValidateLoginParams.ts b/src/libs/API/parameters/ValidateLoginParams.ts new file mode 100644 index 000000000000..361c374e4e32 --- /dev/null +++ b/src/libs/API/parameters/ValidateLoginParams.ts @@ -0,0 +1,6 @@ +type ValidateLoginParams = { + accountID: number; + validateCode: string; +}; + +export default ValidateLoginParams; diff --git a/src/libs/API/parameters/ValidateSecondaryLoginParams.ts b/src/libs/API/parameters/ValidateSecondaryLoginParams.ts new file mode 100644 index 000000000000..870a756da524 --- /dev/null +++ b/src/libs/API/parameters/ValidateSecondaryLoginParams.ts @@ -0,0 +1,3 @@ +type ValidateSecondaryLoginParams = {partnerUserID: string; validateCode: string}; + +export default ValidateSecondaryLoginParams; diff --git a/src/libs/API/parameters/ValidateTwoFactorAuthParams.ts b/src/libs/API/parameters/ValidateTwoFactorAuthParams.ts new file mode 100644 index 000000000000..dad8f53089dd --- /dev/null +++ b/src/libs/API/parameters/ValidateTwoFactorAuthParams.ts @@ -0,0 +1,5 @@ +type ValidateTwoFactorAuthParams = { + twoFactorAuthCode: string; +}; + +export default ValidateTwoFactorAuthParams; diff --git a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts new file mode 100644 index 000000000000..424cef92c08f --- /dev/null +++ b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts @@ -0,0 +1,5 @@ +type VerifyIdentityForBankAccountParams = { + bankAccountID: number; + onfidoData: string; +}; +export default VerifyIdentityForBankAccountParams; diff --git a/src/libs/API/parameters/VerifyIdentityParams.ts b/src/libs/API/parameters/VerifyIdentityParams.ts new file mode 100644 index 000000000000..04ddf17bed87 --- /dev/null +++ b/src/libs/API/parameters/VerifyIdentityParams.ts @@ -0,0 +1,5 @@ +type VerifyIdentityParams = { + onfidoData: string; +}; + +export default VerifyIdentityParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts new file mode 100644 index 000000000000..039398c0fbf6 --- /dev/null +++ b/src/libs/API/parameters/index.ts @@ -0,0 +1,124 @@ +export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhysicalExpensifyCardParams'; +export type {default as AddNewContactMethodParams} from './AddNewContactMethodParams'; +export type {default as AddPaymentCardParams} from './AddPaymentCardParams'; +export type {default as AddPersonalBankAccountParams} from './AddPersonalBankAccountParams'; +export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams'; +export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams'; +export type {default as BankAccountHandlePlaidErrorParams} from './BankAccountHandlePlaidErrorParams'; +export type {default as BeginAppleSignInParams} from './BeginAppleSignInParams'; +export type {default as BeginGoogleSignInParams} from './BeginGoogleSignInParams'; +export type {default as BeginSignInParams} from './BeginSignInParams'; +export type {default as CloseAccountParams} from './CloseAccountParams'; +export type {default as ConnectBankAccountManuallyParams} from './ConnectBankAccountManuallyParams'; +export type {default as ConnectBankAccountWithPlaidParams} from './ConnectBankAccountWithPlaidParams'; +export type {default as DeleteContactMethodParams} from './DeleteContactMethodParams'; +export type {default as DeletePaymentBankAccountParams} from './DeletePaymentBankAccountParams'; +export type {default as DeletePaymentCardParams} from './DeletePaymentCardParams'; +export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams'; +export type {default as GetMissingOnyxMessagesParams} from './GetMissingOnyxMessagesParams'; +export type {default as GetNewerActionsParams} from './GetNewerActionsParams'; +export type {default as GetOlderActionsParams} from './GetOlderActionsParams'; +export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams'; +export type {default as GetRouteForDraftParams} from './GetRouteForDraftParams'; +export type {default as GetRouteParams} from './GetRouteParams'; +export type {default as GetStatementPDFParams} from './GetStatementPDFParams'; +export type {default as HandleRestrictedEventParams} from './HandleRestrictedEventParams'; +export type {default as LogOutParams} from './LogOutParams'; +export type {default as MakeDefaultPaymentMethodParams} from './MakeDefaultPaymentMethodParams'; +export type {default as OpenAppParams} from './OpenAppParams'; +export type {default as OpenOldDotLinkParams} from './OpenOldDotLinkParams'; +export type {default as OpenPlaidBankAccountSelectorParams} from './OpenPlaidBankAccountSelectorParams'; +export type {default as OpenPlaidBankLoginParams} from './OpenPlaidBankLoginParams'; +export type {default as OpenProfileParams} from './OpenProfileParams'; +export type {default as OpenPublicProfilePageParams} from './OpenPublicProfilePageParams'; +export type {default as OpenReimbursementAccountPageParams} from './OpenReimbursementAccountPageParams'; +export type {default as OpenReportParams} from './OpenReportParams'; +export type {default as OpenRoomMembersPageParams} from './OpenRoomMembersPageParams'; +export type {default as PaymentCardParams} from './PaymentCardParams'; +export type {default as ReconnectAppParams} from './ReconnectAppParams'; +export type {default as ReferTeachersUniteVolunteerParams} from './ReferTeachersUniteVolunteerParams'; +export type {default as ReportVirtualExpensifyCardFraudParams} from './ReportVirtualExpensifyCardFraudParams'; +export type {default as RequestContactMethodValidateCodeParams} from './RequestContactMethodValidateCodeParams'; +export type {default as RequestNewValidateCodeParams} from './RequestNewValidateCodeParams'; +export type {default as RequestPhysicalExpensifyCardParams} from './RequestPhysicalExpensifyCardParams'; +export type {default as RequestReplacementExpensifyCardParams} from './RequestReplacementExpensifyCardParams'; +export type {default as RequestUnlinkValidationLinkParams} from './RequestUnlinkValidationLinkParams'; +export type {default as RequestAccountValidationLinkParams} from './RequestAccountValidationLinkParams'; +export type {default as ResolveActionableMentionWhisperParams} from './ResolveActionableMentionWhisperParams'; +export type {default as RevealExpensifyCardDetailsParams} from './RevealExpensifyCardDetailsParams'; +export type {default as SearchForReportsParams} from './SearchForReportsParams'; +export type {default as SendPerformanceTimingParams} from './SendPerformanceTimingParams'; +export type {default as SetContactMethodAsDefaultParams} from './SetContactMethodAsDefaultParams'; +export type {default as SignInUserWithLinkParams} from './SignInUserWithLinkParams'; +export type {default as SignInWithShortLivedAuthTokenParams} from './SignInWithShortLivedAuthTokenParams'; +export type {default as UnlinkLoginParams} from './UnlinkLoginParams'; +export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTimezoneParams'; +export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams'; +export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams'; +export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams'; +export type {default as UpdateFrequentlyUsedEmojisParams} from './UpdateFrequentlyUsedEmojisParams'; +export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams'; +export type {default as UpdateLegalNameParams} from './UpdateLegalNameParams'; +export type {default as UpdateNewsletterSubscriptionParams} from './UpdateNewsletterSubscriptionParams'; +export type {default as UpdatePersonalInformationForBankAccountParams} from './UpdatePersonalInformationForBankAccountParams'; +export type {default as UpdatePreferredEmojiSkinToneParams} from './UpdatePreferredEmojiSkinToneParams'; +export type {default as UpdatePreferredLocaleParams} from './UpdatePreferredLocaleParams'; +export type {default as UpdatePronounsParams} from './UpdatePronounsParams'; +export type {default as UpdateSelectedTimezoneParams} from './UpdateSelectedTimezoneParams'; +export type {default as UpdateStatusParams} from './UpdateStatusParams'; +export type {default as UpdateThemeParams} from './UpdateThemeParams'; +export type {default as UpdateUserAvatarParams} from './UpdateUserAvatarParams'; +export type {default as ValidateBankAccountWithTransactionsParams} from './ValidateBankAccountWithTransactionsParams'; +export type {default as ValidateLoginParams} from './ValidateLoginParams'; +export type {default as ValidateSecondaryLoginParams} from './ValidateSecondaryLoginParams'; +export type {default as ValidateTwoFactorAuthParams} from './ValidateTwoFactorAuthParams'; +export type {default as VerifyIdentityForBankAccountParams} from './VerifyIdentityForBankAccountParams'; +export type {default as AnswerQuestionsForWalletParams} from './AnswerQuestionsForWalletParams'; +export type {default as AddCommentOrAttachementParams} from './AddCommentOrAttachementParams'; +export type {default as OptInOutToPushNotificationsParams} from './OptInOutToPushNotificationsParams'; +export type {default as ReconnectToReportParams} from './ReconnectToReportParams'; +export type {default as ReadNewestActionParams} from './ReadNewestActionParams'; +export type {default as MarkAsUnreadParams} from './MarkAsUnreadParams'; +export type {default as TogglePinnedChatParams} from './TogglePinnedChatParams'; +export type {default as DeleteCommentParams} from './DeleteCommentParams'; +export type {default as UpdateCommentParams} from './UpdateCommentParams'; +export type {default as UpdateReportNotificationPreferenceParams} from './UpdateReportNotificationPreferenceParams'; +export type {default as UpdateWelcomeMessageParams} from './UpdateWelcomeMessageParams'; +export type {default as UpdateReportWriteCapabilityParams} from './UpdateReportWriteCapabilityParams'; +export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; +export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; +export type {default as AddEmojiReactionParams} from './AddEmojiReactionParams'; +export type {default as RemoveEmojiReactionParams} from './RemoveEmojiReactionParams'; +export type {default as LeaveRoomParams} from './LeaveRoomParams'; +export type {default as InviteToRoomParams} from './InviteToRoomParams'; +export type {default as RemoveFromRoomParams} from './RemoveFromRoomParams'; +export type {default as FlagCommentParams} from './FlagCommentParams'; +export type {default as UpdateReportPrivateNoteParams} from './UpdateReportPrivateNoteParams'; +export type {default as UpdateCompanyInformationForBankAccountParams} from './UpdateCompanyInformationForBankAccountParams'; +export type {default as UpdatePersonalDetailsForWalletParams} from './UpdatePersonalDetailsForWalletParams'; +export type {default as VerifyIdentityParams} from './VerifyIdentityParams'; +export type {default as AcceptWalletTermsParams} from './AcceptWalletTermsParams'; +export type {default as ChronosRemoveOOOEventParams} from './ChronosRemoveOOOEventParams'; +export type {default as TransferWalletBalanceParams} from './TransferWalletBalanceParams'; +export type {default as DeleteWorkspaceParams} from './DeleteWorkspaceParams'; +export type {default as CreateWorkspaceParams} from './CreateWorkspaceParams'; +export type {default as UpdateWorkspaceGeneralSettingsParams} from './UpdateWorkspaceGeneralSettingsParams'; +export type {default as DeleteWorkspaceAvatarParams} from './DeleteWorkspaceAvatarParams'; +export type {default as UpdateWorkspaceAvatarParams} from './UpdateWorkspaceAvatarParams'; +export type {default as AddMembersToWorkspaceParams} from './AddMembersToWorkspaceParams'; +export type {default as DeleteMembersFromWorkspaceParams} from './DeleteMembersFromWorkspaceParams'; +export type {default as OpenWorkspaceParams} from './OpenWorkspaceParams'; +export type {default as OpenWorkspaceReimburseViewParams} from './OpenWorkspaceReimburseViewParams'; +export type {default as OpenWorkspaceInvitePageParams} from './OpenWorkspaceInvitePageParams'; +export type {default as OpenWorkspaceMembersPageParams} from './OpenWorkspaceMembersPageParams'; +export type {default as OpenDraftWorkspaceRequestParams} from './OpenDraftWorkspaceRequestParams'; +export type {default as UpdateWorkspaceCustomUnitAndRateParams} from './UpdateWorkspaceCustomUnitAndRateParams'; +export type {default as CreateWorkspaceFromIOUPaymentParams} from './CreateWorkspaceFromIOUPaymentParams'; +export type {default as CreateTaskParams} from './CreateTaskParams'; +export type {default as CancelTaskParams} from './CancelTaskParams'; +export type {default as EditTaskAssigneeParams} from './EditTaskAssigneeParams'; +export type {default as EditTaskParams} from './EditTaskParams'; +export type {default as ReopenTaskParams} from './ReopenTaskParams'; +export type {default as CompleteTaskParams} from './CompleteTaskParams'; +export type {default as CompleteEngagementModalParams} from './CompleteEngagementModalParams'; +export type {default as SetNameValuePairParams} from './SetNameValuePairParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts new file mode 100644 index 000000000000..f58ebc30b4a2 --- /dev/null +++ b/src/libs/API/types.ts @@ -0,0 +1,318 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import type * as Parameters from './parameters'; +import type SignInUserParams from './parameters/SignInUserParams'; +import type UpdateBeneficialOwnersForBankAccountParams from './parameters/UpdateBeneficialOwnersForBankAccountParams'; + +type ApiRequest = ValueOf; + +const WRITE_COMMANDS = { + UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', + RECONNECT_APP: 'ReconnectApp', + OPEN_PROFILE: 'OpenProfile', + HANDLE_RESTRICTED_EVENT: 'HandleRestrictedEvent', + OPEN_REPORT: 'OpenReport', + DELETE_PAYMENT_BANK_ACCOUNT: 'DeletePaymentBankAccount', + UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT: 'UpdatePersonalInformationForBankAccount', + VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS: 'ValidateBankAccountWithTransactions', + UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT: 'UpdateCompanyInformationForBankAccount', + UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT: 'UpdateBeneficialOwnersForBankAccount', + CONNECT_BANK_ACCOUNT_MANUALLY: 'ConnectBankAccountManually', + VERIFY_IDENTITY_FOR_BANK_ACCOUNT: 'VerifyIdentityForBankAccount', + BANK_ACCOUNT_HANDLE_PLAID_ERROR: 'BankAccount_HandlePlaidError', + REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD: 'ReportVirtualExpensifyCardFraud', + REQUEST_REPLACEMENT_EXPENSIFY_CARD: 'RequestReplacementExpensifyCard', + ACTIVATE_PHYSICAL_EXPENSIFY_CARD: 'ActivatePhysicalExpensifyCard', + CHRONOS_REMOVE_OOO_EVENT: 'Chronos_RemoveOOOEvent', + MAKE_DEFAULT_PAYMENT_METHOD: 'MakeDefaultPaymentMethod', + ADD_PAYMENT_CARD: 'AddPaymentCard', + TRANSFER_WALLET_BALANCE: 'TransferWalletBalance', + DELETE_PAYMENT_CARD: 'DeletePaymentCard', + UPDATE_PRONOUNS: 'UpdatePronouns', + UPDATE_DISPLAY_NAME: 'UpdateDisplayName', + UPDATE_LEGAL_NAME: 'UpdateLegalName', + UPDATE_DATE_OF_BIRTH: 'UpdateDateOfBirth', + UPDATE_HOME_ADDRESS: 'UpdateHomeAddress', + UPDATE_AUTOMATIC_TIMEZONE: 'UpdateAutomaticTimezone', + UPDATE_SELECTED_TIMEZONE: 'UpdateSelectedTimezone', + UPDATE_USER_AVATAR: 'UpdateUserAvatar', + DELETE_USER_AVATAR: 'DeleteUserAvatar', + REFER_TEACHERS_UNITE_VOLUNTEER: 'ReferTeachersUniteVolunteer', + ADD_SCHOOL_PRINCIPAL: 'AddSchoolPrincipal', + CLOSE_ACCOUNT: 'CloseAccount', + REQUEST_CONTACT_METHOD_VALIDATE_CODE: 'RequestContactMethodValidateCode', + UPDATE_NEWSLETTER_SUBSCRIPTION: 'UpdateNewsletterSubscription', + DELETE_CONTACT_METHOD: 'DeleteContactMethod', + ADD_NEW_CONTACT_METHOD: 'AddNewContactMethod', + VALIDATE_LOGIN: 'ValidateLogin', + VALIDATE_SECONDARY_LOGIN: 'ValidateSecondaryLogin', + UPDATE_PREFERRED_EMOJI_SKIN_TONE: 'UpdatePreferredEmojiSkinTone', + UPDATE_FREQUENTLY_USED_EMOJIS: 'UpdateFrequentlyUsedEmojis', + UPDATE_CHAT_PRIORITY_MODE: 'UpdateChatPriorityMode', + SET_CONTACT_METHOD_AS_DEFAULT: 'SetContactMethodAsDefault', + UPDATE_THEME: 'UpdateTheme', + UPDATE_STATUS: 'UpdateStatus', + CLEAR_STATUS: 'ClearStatus', + UPDATE_PERSONAL_DETAILS_FOR_WALLET: 'UpdatePersonalDetailsForWallet', + VERIFY_IDENTITY: 'VerifyIdentity', + ACCEPT_WALLET_TERMS: 'AcceptWalletTerms', + ANSWER_QUESTIONS_FOR_WALLET: 'AnswerQuestionsForWallet', + REQUEST_PHYSICAL_EXPENSIFY_CARD: 'RequestPhysicalExpensifyCard', + LOG_OUT: 'LogOut', + REQUEST_ACCOUNT_VALIDATION_LINK: 'RequestAccountValidationLink', + REQUEST_NEW_VALIDATE_CODE: 'RequestNewValidateCode', + SIGN_IN_WITH_APPLE: 'SignInWithApple', + SIGN_IN_WITH_GOOGLE: 'SignInWithGoogle', + SIGN_IN_USER: 'SigninUser', + SIGN_IN_USER_WITH_LINK: 'SigninUserWithLink', + REQUEST_UNLINK_VALIDATION_LINK: 'RequestUnlinkValidationLink', + UNLINK_LOGIN: 'UnlinkLogin', + ENABLE_TWO_FACTOR_AUTH: 'EnableTwoFactorAuth', + DISABLE_TWO_FACTOR_AUTH: 'DisableTwoFactorAuth', + TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate', + ADD_COMMENT: 'AddComment', + ADD_ATTACHMENT: 'AddAttachment', + CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid', + ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications', + OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications', + RECONNECT_TO_REPORT: 'ReconnectToReport', + READ_NEWEST_ACTION: 'ReadNewestAction', + MARK_AS_UNREAD: 'MarkAsUnread', + TOGGLE_PINNED_CHAT: 'TogglePinnedChat', + DELETE_COMMENT: 'DeleteComment', + UPDATE_COMMENT: 'UpdateComment', + UPDATE_REPORT_NOTIFICATION_PREFERENCE: 'UpdateReportNotificationPreference', + UPDATE_WELCOME_MESSAGE: 'UpdateWelcomeMessage', + UPDATE_REPORT_WRITE_CAPABILITY: 'UpdateReportWriteCapability', + ADD_WORKSPACE_ROOM: 'AddWorkspaceRoom', + UPDATE_POLICY_ROOM_NAME: 'UpdatePolicyRoomName', + ADD_EMOJI_REACTION: 'AddEmojiReaction', + REMOVE_EMOJI_REACTION: 'RemoveEmojiReaction', + LEAVE_ROOM: 'LeaveRoom', + INVITE_TO_ROOM: 'InviteToRoom', + REMOVE_FROM_ROOM: 'RemoveFromRoom', + FLAG_COMMENT: 'FlagComment', + UPDATE_REPORT_PRIVATE_NOTE: 'UpdateReportPrivateNote', + RESOLVE_ACTIONABLE_MENTION_WHISPER: 'ResolveActionableMentionWhisper', + DELETE_WORKSPACE: 'DeleteWorkspace', + DELETE_MEMBERS_FROM_WORKSPACE: 'DeleteMembersFromWorkspace', + ADD_MEMBERS_TO_WORKSPACE: 'AddMembersToWorkspace', + UPDATE_WORKSPACE_AVATAR: 'UpdateWorkspaceAvatar', + DELETE_WORKSPACE_AVATAR: 'DeleteWorkspaceAvatar', + UPDATE_WORKSPACE_GENERAL_SETTINGS: 'UpdateWorkspaceGeneralSettings', + UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE: 'UpdateWorkspaceCustomUnitAndRate', + CREATE_WORKSPACE: 'CreateWorkspace', + CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', + CREATE_TASK: 'CreateTask', + CANCEL_TASK: 'CancelTask', + EDIT_TASK_ASSIGNEE: 'EditTaskAssignee', + EDIT_TASK: 'EditTask', + REOPEN_TASK: 'ReopenTask', + COMPLETE_TASK: 'CompleteTask', + COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal', + SET_NAME_VALUE_PAIR: 'SetNameValuePair', +} as const; + +type WriteCommand = ValueOf; + +type WriteCommandParameters = { + [WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE]: Parameters.UpdatePreferredLocaleParams; + [WRITE_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; + [WRITE_COMMANDS.OPEN_PROFILE]: Parameters.OpenProfileParams; + [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: Parameters.HandleRestrictedEventParams; + [WRITE_COMMANDS.OPEN_REPORT]: Parameters.OpenReportParams; + [WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT]: Parameters.DeletePaymentBankAccountParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT]: Parameters.UpdatePersonalInformationForBankAccountParams; + [WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS]: Parameters.ValidateBankAccountWithTransactionsParams; + [WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT]: Parameters.UpdateCompanyInformationForBankAccountParams; + [WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT]: UpdateBeneficialOwnersForBankAccountParams; + [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY]: Parameters.ConnectBankAccountManuallyParams; + [WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT]: Parameters.VerifyIdentityForBankAccountParams; + [WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR]: Parameters.BankAccountHandlePlaidErrorParams; + [WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD]: Parameters.ReportVirtualExpensifyCardFraudParams; + [WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD]: Parameters.RequestReplacementExpensifyCardParams; + [WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD]: Parameters.ActivatePhysicalExpensifyCardParams; + [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: Parameters.MakeDefaultPaymentMethodParams; + [WRITE_COMMANDS.ADD_PAYMENT_CARD]: Parameters.AddPaymentCardParams; + [WRITE_COMMANDS.DELETE_PAYMENT_CARD]: Parameters.DeletePaymentCardParams; + [WRITE_COMMANDS.UPDATE_PRONOUNS]: Parameters.UpdatePronounsParams; + [WRITE_COMMANDS.UPDATE_DISPLAY_NAME]: Parameters.UpdateDisplayNameParams; + [WRITE_COMMANDS.UPDATE_LEGAL_NAME]: Parameters.UpdateLegalNameParams; + [WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH]: Parameters.UpdateDateOfBirthParams; + [WRITE_COMMANDS.UPDATE_HOME_ADDRESS]: Parameters.UpdateHomeAddressParams; + [WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: Parameters.UpdateAutomaticTimezoneParams; + [WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE]: Parameters.UpdateSelectedTimezoneParams; + [WRITE_COMMANDS.UPDATE_USER_AVATAR]: Parameters.UpdateUserAvatarParams; + [WRITE_COMMANDS.DELETE_USER_AVATAR]: EmptyObject; + [WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER]: Parameters.ReferTeachersUniteVolunteerParams; + [WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL]: Parameters.AddSchoolPrincipalParams; + [WRITE_COMMANDS.CLOSE_ACCOUNT]: Parameters.CloseAccountParams; + [WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE]: Parameters.RequestContactMethodValidateCodeParams; + [WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION]: Parameters.UpdateNewsletterSubscriptionParams; + [WRITE_COMMANDS.DELETE_CONTACT_METHOD]: Parameters.DeleteContactMethodParams; + [WRITE_COMMANDS.ADD_NEW_CONTACT_METHOD]: Parameters.AddNewContactMethodParams; + [WRITE_COMMANDS.VALIDATE_LOGIN]: Parameters.ValidateLoginParams; + [WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: Parameters.ValidateSecondaryLoginParams; + [WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: Parameters.UpdatePreferredEmojiSkinToneParams; + [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: Parameters.UpdateFrequentlyUsedEmojisParams; + [WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: Parameters.UpdateChatPriorityModeParams; + [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams; + [WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams; + [WRITE_COMMANDS.UPDATE_STATUS]: Parameters.UpdateStatusParams; + [WRITE_COMMANDS.CLEAR_STATUS]: EmptyObject; + [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: Parameters.UpdatePersonalDetailsForWalletParams; + [WRITE_COMMANDS.VERIFY_IDENTITY]: Parameters.VerifyIdentityParams; + [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: Parameters.AcceptWalletTermsParams; + [WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET]: Parameters.AnswerQuestionsForWalletParams; + [WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD]: Parameters.RequestPhysicalExpensifyCardParams; + [WRITE_COMMANDS.LOG_OUT]: Parameters.LogOutParams; + [WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK]: Parameters.RequestAccountValidationLinkParams; + [WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE]: Parameters.RequestNewValidateCodeParams; + [WRITE_COMMANDS.SIGN_IN_WITH_APPLE]: Parameters.BeginAppleSignInParams; + [WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE]: Parameters.BeginGoogleSignInParams; + [WRITE_COMMANDS.SIGN_IN_USER]: SignInUserParams; + [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: Parameters.SignInUserWithLinkParams; + [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: Parameters.RequestUnlinkValidationLinkParams; + [WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams; + [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: EmptyObject; + [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: EmptyObject; + [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams; + [WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams; + [WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams; + [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountWithPlaidParams; + [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; + [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; + [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; + [WRITE_COMMANDS.RECONNECT_TO_REPORT]: Parameters.ReconnectToReportParams; + [WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams; + [WRITE_COMMANDS.MARK_AS_UNREAD]: Parameters.MarkAsUnreadParams; + [WRITE_COMMANDS.TOGGLE_PINNED_CHAT]: Parameters.TogglePinnedChatParams; + [WRITE_COMMANDS.DELETE_COMMENT]: Parameters.DeleteCommentParams; + [WRITE_COMMANDS.UPDATE_COMMENT]: Parameters.UpdateCommentParams; + [WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE]: Parameters.UpdateReportNotificationPreferenceParams; + [WRITE_COMMANDS.UPDATE_WELCOME_MESSAGE]: Parameters.UpdateWelcomeMessageParams; + [WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY]: Parameters.UpdateReportWriteCapabilityParams; + [WRITE_COMMANDS.ADD_WORKSPACE_ROOM]: Parameters.AddWorkspaceRoomParams; + [WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME]: Parameters.UpdatePolicyRoomNameParams; + [WRITE_COMMANDS.ADD_EMOJI_REACTION]: Parameters.AddEmojiReactionParams; + [WRITE_COMMANDS.REMOVE_EMOJI_REACTION]: Parameters.RemoveEmojiReactionParams; + [WRITE_COMMANDS.LEAVE_ROOM]: Parameters.LeaveRoomParams; + [WRITE_COMMANDS.INVITE_TO_ROOM]: Parameters.InviteToRoomParams; + [WRITE_COMMANDS.REMOVE_FROM_ROOM]: Parameters.RemoveFromRoomParams; + [WRITE_COMMANDS.FLAG_COMMENT]: Parameters.FlagCommentParams; + [WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE]: Parameters.UpdateReportPrivateNoteParams; + [WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER]: Parameters.ResolveActionableMentionWhisperParams; + [WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT]: Parameters.ChronosRemoveOOOEventParams; + [WRITE_COMMANDS.TRANSFER_WALLET_BALANCE]: Parameters.TransferWalletBalanceParams; + [WRITE_COMMANDS.DELETE_WORKSPACE]: Parameters.DeleteWorkspaceParams; + [WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE]: Parameters.DeleteMembersFromWorkspaceParams; + [WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE]: Parameters.AddMembersToWorkspaceParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_AVATAR]: Parameters.UpdateWorkspaceAvatarParams; + [WRITE_COMMANDS.DELETE_WORKSPACE_AVATAR]: Parameters.DeleteWorkspaceAvatarParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_GENERAL_SETTINGS]: Parameters.UpdateWorkspaceGeneralSettingsParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE]: Parameters.UpdateWorkspaceCustomUnitAndRateParams; + [WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams; + [WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams; + [WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams; + [WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams; + [WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams; + [WRITE_COMMANDS.EDIT_TASK]: Parameters.EditTaskParams; + [WRITE_COMMANDS.REOPEN_TASK]: Parameters.ReopenTaskParams; + [WRITE_COMMANDS.COMPLETE_TASK]: Parameters.CompleteTaskParams; + [WRITE_COMMANDS.COMPLETE_ENGAGEMENT_MODAL]: Parameters.CompleteEngagementModalParams; + [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; +}; + +const READ_COMMANDS = { + OPEN_APP: 'OpenApp', + OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage', + OPEN_WORKSPACE_VIEW: 'OpenWorkspaceView', + GET_MAPBOX_ACCESS_TOKEN: 'GetMapboxAccessToken', + OPEN_PAYMENTS_PAGE: 'OpenPaymentsPage', + OPEN_PERSONAL_DETAILS_PAGE: 'OpenPersonalDetailsPage', + OPEN_PUBLIC_PROFILE_PAGE: 'OpenPublicProfilePage', + OPEN_PLAID_BANK_LOGIN: 'OpenPlaidBankLogin', + OPEN_PLAID_BANK_ACCOUNT_SELECTOR: 'OpenPlaidBankAccountSelector', + GET_OLDER_ACTIONS: 'GetOlderActions', + GET_NEWER_ACTIONS: 'GetNewerActions', + EXPAND_URL_PREVIEW: 'ExpandURLPreview', + GET_REPORT_PRIVATE_NOTE: 'GetReportPrivateNote', + OPEN_ROOM_MEMBERS_PAGE: 'OpenRoomMembersPage', + SEARCH_FOR_REPORTS: 'SearchForReports', + SEND_PERFORMANCE_TIMING: 'SendPerformanceTiming', + GET_ROUTE: 'GetRoute', + GET_ROUTE_FOR_DRAFT: 'GetRouteForDraft', + GET_STATEMENT_PDF: 'GetStatementPDF', + OPEN_ONFIDO_FLOW: 'OpenOnfidoFlow', + OPEN_INITIAL_SETTINGS_PAGE: 'OpenInitialSettingsPage', + OPEN_ENABLE_PAYMENTS_PAGE: 'OpenEnablePaymentsPage', + BEGIN_SIGNIN: 'BeginSignIn', + SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN: 'SignInWithShortLivedAuthToken', + OPEN_WORKSPACE_REIMBURSE_VIEW: 'OpenWorkspaceReimburseView', + OPEN_WORKSPACE: 'OpenWorkspace', + OPEN_WORKSPACE_MEMBERS_PAGE: 'OpenWorkspaceMembersPage', + OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', + OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', +} as const; + +type ReadCommand = ValueOf; + +type ReadCommandParameters = { + [READ_COMMANDS.OPEN_APP]: Parameters.OpenAppParams; + [READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams; + [READ_COMMANDS.OPEN_WORKSPACE_VIEW]: EmptyObject; + [READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: EmptyObject; + [READ_COMMANDS.OPEN_PAYMENTS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_PERSONAL_DETAILS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE]: Parameters.OpenPublicProfilePageParams; + [READ_COMMANDS.OPEN_PLAID_BANK_LOGIN]: Parameters.OpenPlaidBankLoginParams; + [READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR]: Parameters.OpenPlaidBankAccountSelectorParams; + [READ_COMMANDS.GET_OLDER_ACTIONS]: Parameters.GetOlderActionsParams; + [READ_COMMANDS.GET_NEWER_ACTIONS]: Parameters.GetNewerActionsParams; + [READ_COMMANDS.EXPAND_URL_PREVIEW]: Parameters.ExpandURLPreviewParams; + [READ_COMMANDS.GET_REPORT_PRIVATE_NOTE]: Parameters.GetReportPrivateNoteParams; + [READ_COMMANDS.OPEN_ROOM_MEMBERS_PAGE]: Parameters.OpenRoomMembersPageParams; + [READ_COMMANDS.SEARCH_FOR_REPORTS]: Parameters.SearchForReportsParams; + [READ_COMMANDS.SEND_PERFORMANCE_TIMING]: Parameters.SendPerformanceTimingParams; + [READ_COMMANDS.GET_ROUTE]: Parameters.GetRouteParams; + [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: Parameters.GetRouteForDraftParams; + [READ_COMMANDS.GET_STATEMENT_PDF]: Parameters.GetStatementPDFParams; + [READ_COMMANDS.OPEN_ONFIDO_FLOW]: EmptyObject; + [READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: EmptyObject; + [READ_COMMANDS.BEGIN_SIGNIN]: Parameters.BeginSignInParams; + [READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: Parameters.SignInWithShortLivedAuthTokenParams; + [READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW]: Parameters.OpenWorkspaceReimburseViewParams; + [READ_COMMANDS.OPEN_WORKSPACE]: Parameters.OpenWorkspaceParams; + [READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE]: Parameters.OpenWorkspaceMembersPageParams; + [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; + [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; +}; + +const SIDE_EFFECT_REQUEST_COMMANDS = { + AUTHENTICATE_PUSHER: 'AuthenticatePusher', + OPEN_REPORT: 'OpenReport', + OPEN_OLD_DOT_LINK: 'OpenOldDotLink', + REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails', + GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages', + RECONNECT_APP: 'ReconnectApp', +} as const; + +type SideEffectRequestCommand = ValueOf; + +type SideEffectRequestCommandParameters = { + [SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER]: Parameters.AuthenticatePusherParams; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT]: Parameters.OpenReportParams; + [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK]: Parameters.OpenOldDotLinkParams; + [SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS]: Parameters.RevealExpensifyCardDetailsParams; + [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams; + [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; +}; + +type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; + +export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS}; + +export type {ApiRequest, ApiRequestCommandParameters, WriteCommand, ReadCommand, SideEffectRequestCommand}; diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 32ebca9afee8..94bba5d0d00c 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -14,6 +14,14 @@ function insertText(text: string, selection: Selection, textToInsert: string): s return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length); } +/** + * Insert a white space at given index of text + * @param text - text that needs whitespace to be appended to + */ +function insertWhiteSpaceAtIndex(text: string, index: number) { + return `${text.slice(0, index)} ${text.slice(index)}`; +} + /** * Check whether we can skip trigger hotkeys on some specific devices. */ @@ -23,4 +31,22 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown; } -export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys}; +/** + * Finds the length of common suffix between two texts + */ +function findCommonSuffixLength(str1: string, str2: string, cursorPosition: number) { + let commonSuffixLength = 0; + const minLength = Math.min(str1.length - cursorPosition, str2.length); + + for (let i = 1; i <= minLength; i++) { + if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) { + commonSuffixLength++; + } else { + break; + } + } + + return commonSuffixLength; +} + +export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength}; diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 526769723531..dde9cf28148a 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -360,12 +360,18 @@ function getMicroseconds(): number { return Date.now() * CONST.MICROSECONDS_PER_MS; } +function getDBTimeFromDate(date: Date): string { + return date.toISOString().replace('T', ' ').replace('Z', ''); +} + /** - * Returns the current time in milliseconds in the format expected by the database + * Convert the given timestamp to the "yyyy-MM-dd HH:mm:ss" format, as expected by the database + * + * @param [timestamp] the given timestamp (if omitted, defaults to the current time) */ function getDBTime(timestamp: string | number = ''): string { const datetime = timestamp ? new Date(timestamp) : new Date(); - return datetime.toISOString().replace('T', ' ').replace('Z', ''); + return getDBTimeFromDate(datetime); } /** @@ -733,6 +739,15 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { }; } +/** + * Return the date with full format if the created date is the current date. + * Otherwise return the created date. + */ +function enrichMoneyRequestTimestamp(created: string): string { + const now = new Date(); + const createdDate = parse(created, CONST.DATE.FNS_FORMAT_STRING, now); + return isSameDay(createdDate, now) ? getDBTimeFromDate(now) : created; +} /** * Returns the last business day of given date month * @@ -796,6 +811,7 @@ const DateUtils = { getWeekEndsOn, isTimeAtLeastOneMinuteInFuture, formatToSupportedTimezone, + enrichMoneyRequestTimestamp, getLastBusinessDayOfMonth, }; diff --git a/src/libs/DomUtils/index.native.ts b/src/libs/DomUtils/index.native.ts index 7117f5942f9a..eac988e55f1b 100644 --- a/src/libs/DomUtils/index.native.ts +++ b/src/libs/DomUtils/index.native.ts @@ -2,6 +2,10 @@ import type GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => null; +const addCSS = () => {}; + +const getAutofilledInputStyle = () => null; + const requestAnimationFrame = (callback: () => void) => { if (!callback) { return; @@ -11,6 +15,8 @@ const requestAnimationFrame = (callback: () => void) => { }; export default { + addCSS, + getAutofilledInputStyle, getActiveElement, requestAnimationFrame, }; diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 068eb0c9fe7e..330123833c1f 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -2,7 +2,68 @@ import type GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => document.activeElement; +const addCSS = (css: string, styleId: string) => { + const existingStyle = document.getElementById(styleId); + + if (existingStyle) { + if ('styleSheet' in existingStyle) { + // Supports IE8 and below + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (existingStyle.styleSheet as any).cssText = css; + } else { + existingStyle.innerHTML = css; + } + } else { + const styleElement = document.createElement('style'); + styleElement.setAttribute('id', styleId); + styleElement.setAttribute('type', 'text/css'); + + if ('styleSheet' in styleElement) { + // Supports IE8 and below + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (styleElement.styleSheet as any).cssText = css; + } else { + styleElement.appendChild(document.createTextNode(css)); + } + + const head = document.getElementsByTagName('head')[0]; + head.appendChild(styleElement); + } +}; + +/** + * Customizes the background and text colors for autofill inputs in Chrome + * Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. + * We should rely on the chrome-autofilled property being added to the input when users use auto-fill + */ +const getAutofilledInputStyle = (inputTextColor: string) => ` + input[chrome-autofilled], + input[chrome-autofilled]:hover, + input[chrome-autofilled]:focus, + textarea[chrome-autofilled], + textarea[chrome-autofilled]:hover, + textarea[chrome-autofilled]:focus, + select[chrome-autofilled], + select[chrome-autofilled]:hover, + select[chrome-autofilled]:focus, + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + textarea:-webkit-autofill, + textarea:-webkit-autofill:hover, + textarea:-webkit-autofill:focus, + select:-webkit-autofill, + select:-webkit-autofill:hover, + select:-webkit-autofill:focus { + -webkit-background-clip: text; + -webkit-text-fill-color: ${inputTextColor}; + caret-color: ${inputTextColor}; + } +`; + export default { + addCSS, + getAutofilledInputStyle, getActiveElement, requestAnimationFrame: window.requestAnimationFrame.bind(window), }; diff --git a/src/libs/E2E/API.mock.ts b/src/libs/E2E/API.mock.ts deleted file mode 100644 index 83b7cb218977..000000000000 --- a/src/libs/E2E/API.mock.ts +++ /dev/null @@ -1,82 +0,0 @@ -import Onyx from 'react-native-onyx'; -import Log from '@libs/Log'; -import type Response from '@src/types/onyx/Response'; -// mock functions -import mockAuthenticatePusher from './apiMocks/authenticatePusher'; -import mockBeginSignin from './apiMocks/beginSignin'; -import mockOpenApp from './apiMocks/openApp'; -import mockOpenReport from './apiMocks/openReport'; -import mockReadNewestAction from './apiMocks/readNewestAction'; -import mockSigninUser from './apiMocks/signinUser'; - -type ApiCommandParameters = Record; - -type Mocks = Record Response>; - -/** - * A dictionary which has the name of a API command as key, and a function which - * receives the api command parameters as value and is expected to return a response - * object. - */ -const mocks: Mocks = { - BeginSignIn: mockBeginSignin, - SigninUser: mockSigninUser, - OpenApp: mockOpenApp, - ReconnectApp: mockOpenApp, - OpenReport: mockOpenReport, - ReconnectToReport: mockOpenReport, - AuthenticatePusher: mockAuthenticatePusher, - ReadNewestAction: mockReadNewestAction, -}; - -function mockCall(command: string, apiCommandParameters: ApiCommandParameters, tag: string): Promise | Promise | undefined { - const mockResponse = mocks[command]?.(apiCommandParameters); - if (!mockResponse) { - Log.warn(`[${tag}] for command ${command} is not mocked yet! ⚠️`); - return; - } - - if (Array.isArray(mockResponse.onyxData)) { - return Onyx.update(mockResponse.onyxData); - } - - return Promise.resolve(mockResponse); -} - -/** - * All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData. - * This is so that if the network is unavailable or the app is closed, we can send the WRITE request later. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - */ -function write(command: string, apiCommandParameters: ApiCommandParameters = {}): Promise | Promise | undefined { - return mockCall(command, apiCommandParameters, 'API.write'); -} - -/** - * For commands where the network response must be accessed directly or when there is functionality that can only - * happen once the request is finished (eg. calling third-party services like Onfido and Plaid, redirecting a user - * depending on the response data, etc.). - * It works just like API.read(), except that it will return a promise. - * Using this method is discouraged and will throw an ESLint error. Use it sparingly and only when all other alternatives have been exhausted. - * It is best to discuss it in Slack anytime you are tempted to use this method. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - */ -function makeRequestWithSideEffects(command: string, apiCommandParameters: ApiCommandParameters = {}): Promise | Promise | undefined { - return mockCall(command, apiCommandParameters, 'API.makeRequestWithSideEffects'); -} - -/** - * Requests made with this method are not be persisted to disk. If there is no network connectivity, the request is ignored and discarded. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - */ -function read(command: string, apiCommandParameters: ApiCommandParameters): Promise | Promise | undefined { - return mockCall(command, apiCommandParameters, 'API.read'); -} - -export {write, makeRequestWithSideEffects, read}; diff --git a/src/libs/E2E/actions/e2eLogin.ts b/src/libs/E2E/actions/e2eLogin.ts index 6a25705df755..f98eab5005e1 100644 --- a/src/libs/E2E/actions/e2eLogin.ts +++ b/src/libs/E2E/actions/e2eLogin.ts @@ -1,8 +1,20 @@ +/* eslint-disable rulesdir/prefer-actions-set-data */ + /* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ import Onyx from 'react-native-onyx'; -import * as Session from '@userActions/Session'; +import {Authenticate} from '@libs/Authentication'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; +import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; +const e2eUserCredentials = { + email: getConfigValueOrThrow('EXPENSIFY_PARTNER_PASSWORD_EMAIL'), + partnerUserID: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_ID'), + partnerUserSecret: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_SECRET'), + partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, + partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, +}; + /** * Command for e2e test to automatically sign in a user. * If the user is already logged in the function will simply @@ -10,7 +22,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; * * @return Resolved true when the user was actually signed in. Returns false if the user was already logged in. */ -export default function (email = 'fake@email.com', password = 'Password123'): Promise { +export default function (): Promise { const waitForBeginSignInToFinish = (): Promise => new Promise((resolve) => { const id = Onyx.connect({ @@ -30,7 +42,7 @@ export default function (email = 'fake@email.com', password = 'Password123'): Pr let neededLogin = false; // Subscribe to auth token, to check if we are authenticated - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const connectionId = Onyx.connect({ key: ONYXKEYS.SESSION, callback: (session) => { @@ -38,15 +50,24 @@ export default function (email = 'fake@email.com', password = 'Password123'): Pr neededLogin = true; // authenticate with a predefined user - Session.beginSignIn(email); - waitForBeginSignInToFinish().then(() => { - Session.signIn(password); - }); - } else { - // signal that auth was completed - resolve(neededLogin); - Onyx.disconnect(connectionId); + console.debug('[E2E] Signing in…'); + Authenticate(e2eUserCredentials) + .then((response) => { + Onyx.merge(ONYXKEYS.SESSION, { + authToken: response.authToken, + email: e2eUserCredentials.email, + }); + console.debug('[E2E] Signed in finished!'); + return waitForBeginSignInToFinish(); + }) + .catch((error) => { + console.error('[E2E] Error while signing in', error); + reject(error); + }); } + // signal that auth was completed + resolve(neededLogin); + Onyx.disconnect(connectionId); }, }); }); diff --git a/src/libs/E2E/actions/waitForAppLoaded.ts b/src/libs/E2E/actions/waitForAppLoaded.ts new file mode 100644 index 000000000000..bea739a1b4c7 --- /dev/null +++ b/src/libs/E2E/actions/waitForAppLoaded.ts @@ -0,0 +1,19 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +// Once we get the sidebar loaded end mark we know that the app is ready to be used: +export default function waitForAppLoaded(): Promise { + return new Promise((resolve) => { + const connectionId = Onyx.connect({ + key: ONYXKEYS.IS_SIDEBAR_LOADED, + callback: (isSidebarLoaded) => { + if (!isSidebarLoaded) { + return; + } + + resolve(); + Onyx.disconnect(connectionId); + }, + }); + }); +} diff --git a/src/libs/E2E/apiMocks/authenticatePusher.ts b/src/libs/E2E/apiMocks/authenticatePusher.ts deleted file mode 100644 index 28f9ebbbee88..000000000000 --- a/src/libs/E2E/apiMocks/authenticatePusher.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type Response from '@src/types/onyx/Response'; - -const authenticatePusher = (): Response => ({ - auth: 'auth', - // eslint-disable-next-line @typescript-eslint/naming-convention - shared_secret: 'secret', - jsonCode: 200, - requestID: '783ef7fc3991969a-SJC', -}); - -export default authenticatePusher; diff --git a/src/libs/E2E/apiMocks/beginSignin.ts b/src/libs/E2E/apiMocks/beginSignin.ts deleted file mode 100644 index a578f935c2aa..000000000000 --- a/src/libs/E2E/apiMocks/beginSignin.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {SigninParams} from '@libs/E2E/types'; -import type Response from '@src/types/onyx/Response'; - -const beginSignin = ({email}: SigninParams): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'credentials', - value: { - login: email, - }, - }, - { - onyxMethod: 'merge', - key: 'account', - value: { - validated: true, - }, - }, - ], - jsonCode: 200, - requestID: '783e54ef4b38cff5-SJC', -}); - -export default beginSignin; diff --git a/src/libs/E2E/apiMocks/openApp.ts b/src/libs/E2E/apiMocks/openApp.ts deleted file mode 100644 index d6dd4a8f8003..000000000000 --- a/src/libs/E2E/apiMocks/openApp.ts +++ /dev/null @@ -1,2069 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type Response from '@src/types/onyx/Response'; - -const openApp = (): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'user', - value: { - isFromPublicDomain: false, - }, - }, - { - onyxMethod: 'merge', - key: 'currencyList', - value: { - AED: { - symbol: 'Dhs', - name: 'UAE Dirham', - ISO4217: '784', - }, - AFN: { - symbol: 'Af', - name: 'Afghan Afghani', - ISO4217: '971', - }, - ALL: { - symbol: 'ALL', - name: 'Albanian Lek', - ISO4217: '008', - }, - AMD: { - symbol: '\u0564\u0580', - name: 'Armenian Dram', - ISO4217: '051', - }, - ANG: { - symbol: 'NA\u0192', - name: 'Neth Antilles Guilder', - ISO4217: '532', - }, - AOA: { - symbol: 'Kz', - name: 'Angolan Kwanza', - ISO4217: '973', - }, - ARS: { - symbol: 'AR$', - name: 'Argentine Peso', - ISO4217: '032', - }, - AUD: { - symbol: 'A$', - name: 'Australian Dollar', - ISO4217: '036', - }, - AWG: { - symbol: '\u0192', - name: 'Aruba Florin', - ISO4217: '533', - }, - AZN: { - symbol: 'man', - name: 'Azerbaijani Manat', - ISO4217: '944', - }, - BAM: { - symbol: 'KM', - name: 'Bosnia And Herzegovina Convertible Mark', - ISO4217: '977', - }, - BBD: { - symbol: 'Bds$', - name: 'Barbados Dollar', - ISO4217: '052', - }, - BDT: { - symbol: 'Tk', - name: 'Bangladesh Taka', - ISO4217: '050', - }, - BGN: { - symbol: '\u043b\u0432', - name: 'Bulgarian Lev', - ISO4217: '975', - }, - BHD: { - symbol: 'BHD', - name: 'Bahraini Dinar', - ISO4217: '048', - }, - BIF: { - symbol: 'FBu', - name: 'Burundi Franc', - decimals: 0, - ISO4217: '108', - }, - BMD: { - symbol: 'BD$', - name: 'Bermuda Dollar', - ISO4217: '060', - }, - BND: { - symbol: 'BN$', - name: 'Brunei Dollar', - ISO4217: '096', - }, - BOB: { - symbol: 'Bs', - name: 'Bolivian Boliviano', - ISO4217: '068', - }, - BRL: { - symbol: 'R$', - name: 'Brazilian Real', - ISO4217: '986', - }, - BSD: { - symbol: 'BS$', - name: 'Bahamian Dollar', - ISO4217: '044', - }, - BTN: { - symbol: 'Nu.', - name: 'Bhutan Ngultrum', - ISO4217: '064', - }, - BWP: { - symbol: 'P', - name: 'Botswana Pula', - ISO4217: '072', - }, - BYN: { - symbol: 'BR', - name: 'Belarus Ruble', - ISO4217: '933', - }, - BYR: { - symbol: 'BR', - name: 'Belarus Ruble', - retired: true, - retirementDate: '2016-07-01', - ISO4217: '974', - }, - BZD: { - symbol: 'BZ$', - name: 'Belize Dollar', - ISO4217: '084', - }, - CAD: { - symbol: 'C$', - name: 'Canadian Dollar', - ISO4217: '124', - }, - CDF: { - symbol: 'CDF', - name: 'Congolese Franc', - ISO4217: '976', - }, - CHF: { - symbol: 'CHF', - name: 'Swiss Franc', - ISO4217: '756', - }, - CLP: { - symbol: 'Ch$', - name: 'Chilean Peso', - decimals: 0, - ISO4217: '152', - }, - CNY: { - symbol: '\u00a5', - name: 'Chinese Yuan', - ISO4217: '156', - }, - COP: { - symbol: 'Col$', - name: 'Colombian Peso', - decimals: 0, - ISO4217: '170', - }, - CRC: { - symbol: 'CR\u20a1', - name: 'Costa Rica Colon', - ISO4217: '188', - }, - CUC: { - symbol: 'CUC', - name: 'Cuban Convertible Peso', - ISO4217: '931', - }, - CUP: { - symbol: '$MN', - name: 'Cuban Peso', - ISO4217: '192', - }, - CVE: { - symbol: 'Esc', - name: 'Cape Verde Escudo', - ISO4217: '132', - }, - CZK: { - symbol: 'K\u010d', - name: 'Czech Koruna', - ISO4217: '203', - }, - DJF: { - symbol: 'Fdj', - name: 'Dijibouti Franc', - decimals: 0, - ISO4217: '262', - }, - DKK: { - symbol: 'Dkr', - name: 'Danish Krone', - ISO4217: '208', - }, - DOP: { - symbol: 'RD$', - name: 'Dominican Peso', - ISO4217: '214', - }, - DZD: { - symbol: 'DZD', - name: 'Algerian Dinar', - ISO4217: '012', - }, - EEK: { - symbol: 'KR', - name: 'Estonian Kroon', - ISO4217: '', - retired: true, - }, - EGP: { - symbol: 'EGP', - name: 'Egyptian Pound', - ISO4217: '818', - }, - ERN: { - symbol: 'Nfk', - name: 'Eritrea Nakfa', - ISO4217: '232', - }, - ETB: { - symbol: 'Br', - name: 'Ethiopian Birr', - ISO4217: '230', - }, - EUR: { - symbol: '\u20ac', - name: 'Euro', - ISO4217: '978', - }, - FJD: { - symbol: 'FJ$', - name: 'Fiji Dollar', - ISO4217: '242', - }, - FKP: { - symbol: 'FK\u00a3', - name: 'Falkland Islands Pound', - ISO4217: '238', - }, - GBP: { - symbol: '\u00a3', - name: 'British Pound', - ISO4217: '826', - }, - GEL: { - symbol: '\u10da', - name: 'Georgian Lari', - ISO4217: '981', - }, - GHS: { - symbol: '\u20b5', - name: 'Ghanaian Cedi', - ISO4217: '936', - }, - GIP: { - symbol: '\u00a3G', - name: 'Gibraltar Pound', - ISO4217: '292', - }, - GMD: { - symbol: 'D', - name: 'Gambian Dalasi', - ISO4217: '270', - }, - GNF: { - symbol: 'FG', - name: 'Guinea Franc', - decimals: 0, - ISO4217: '324', - }, - GTQ: { - symbol: 'Q', - name: 'Guatemala Quetzal', - ISO4217: '320', - }, - GYD: { - symbol: 'GY$', - name: 'Guyana Dollar', - ISO4217: '328', - }, - HKD: { - symbol: 'HK$', - name: 'Hong Kong Dollar', - ISO4217: '344', - }, - HNL: { - symbol: 'HNL', - name: 'Honduras Lempira', - ISO4217: '340', - }, - HRK: { - symbol: 'kn', - name: 'Croatian Kuna', - ISO4217: '191', - }, - HTG: { - symbol: 'G', - name: 'Haiti Gourde', - ISO4217: '332', - }, - HUF: { - symbol: 'Ft', - name: 'Hungarian Forint', - ISO4217: '348', - }, - IDR: { - symbol: 'Rp', - name: 'Indonesian Rupiah', - ISO4217: '360', - }, - ILS: { - symbol: '\u20aa', - name: 'Israeli Shekel', - ISO4217: '376', - }, - INR: { - symbol: '\u20b9', - name: 'Indian Rupee', - ISO4217: '356', - }, - IQD: { - symbol: 'IQD', - name: 'Iraqi Dinar', - ISO4217: '368', - }, - IRR: { - symbol: '\ufdfc', - name: 'Iran Rial', - ISO4217: '364', - }, - ISK: { - symbol: 'kr', - name: 'Iceland Krona', - decimals: 0, - ISO4217: '352', - }, - JMD: { - symbol: 'J$', - name: 'Jamaican Dollar', - ISO4217: '388', - }, - JOD: { - symbol: 'JOD', - name: 'Jordanian Dinar', - ISO4217: '400', - }, - JPY: { - symbol: '\u00a5', - name: 'Japanese Yen', - decimals: 0, - ISO4217: '392', - }, - KES: { - symbol: 'KSh', - name: 'Kenyan Shilling', - ISO4217: '404', - }, - KGS: { - symbol: 'KGS', - name: 'Kyrgyzstani Som', - ISO4217: '417', - }, - KHR: { - symbol: 'KHR', - name: 'Cambodia Riel', - ISO4217: '116', - }, - KMF: { - symbol: 'CF', - name: 'Comoros Franc', - ISO4217: '174', - }, - KPW: { - symbol: 'KP\u20a9', - name: 'North Korean Won', - ISO4217: '408', - }, - KRW: { - symbol: '\u20a9', - name: 'Korean Won', - ISO4217: '410', - }, - KWD: { - symbol: 'KWD', - name: 'Kuwaiti Dinar', - ISO4217: '414', - }, - KYD: { - symbol: 'CI$', - name: 'Cayman Islands Dollar', - ISO4217: '136', - }, - KZT: { - symbol: '\u3012', - name: 'Kazakhstan Tenge', - ISO4217: '398', - }, - LAK: { - symbol: '\u20ad', - name: 'Lao Kip', - ISO4217: '418', - }, - LBP: { - symbol: 'LBP', - name: 'Lebanese Pound', - ISO4217: '422', - }, - LKR: { - symbol: 'SL\u20a8', - name: 'Sri Lanka Rupee', - ISO4217: '144', - }, - LRD: { - symbol: 'L$', - name: 'Liberian Dollar', - ISO4217: '430', - }, - LSL: { - symbol: 'M', - name: 'Lesotho Loti', - ISO4217: '426', - }, - LTL: { - symbol: 'Lt', - name: 'Lithuanian Lita', - retirementDate: '2015-08-22', - retired: true, - ISO4217: '440', - }, - LVL: { - symbol: 'Ls', - name: 'Latvian Lat', - ISO4217: '428', - retired: true, - }, - LYD: { - symbol: 'LYD', - name: 'Libyan Dinar', - ISO4217: '434', - }, - MAD: { - symbol: 'MAD', - name: 'Moroccan Dirham', - ISO4217: '504', - }, - MDL: { - symbol: 'MDL', - name: 'Moldovan Leu', - ISO4217: '498', - }, - MGA: { - symbol: 'MGA', - name: 'Malagasy Ariary', - ISO4217: '969', - }, - MKD: { - symbol: '\u0434\u0435\u043d', - name: 'Macedonian Denar', - ISO4217: '807', - }, - MMK: { - symbol: 'Ks', - name: 'Myanmar Kyat', - ISO4217: '104', - }, - MNT: { - symbol: '\u20ae', - name: 'Mongolian Tugrik', - ISO4217: '496', - }, - MOP: { - symbol: 'MOP$', - name: 'Macau Pataca', - ISO4217: '446', - }, - MRO: { - symbol: 'UM', - name: 'Mauritania Ougulya', - decimals: 0, - retired: true, - retirementDate: '2018-07-11', - ISO4217: '478', - }, - MRU: { - symbol: 'UM', - name: 'Mauritania Ougulya', - decimals: 0, - ISO4217: '', - }, - MUR: { - symbol: 'Rs', - name: 'Mauritius Rupee', - ISO4217: '480', - }, - MVR: { - symbol: 'Rf', - name: 'Maldives Rufiyaa', - ISO4217: '462', - }, - MWK: { - symbol: 'MK', - name: 'Malawi Kwacha', - ISO4217: '454', - }, - MXN: { - symbol: 'Mex$', - name: 'Mexican Peso', - ISO4217: '484', - }, - MYR: { - symbol: 'RM', - name: 'Malaysian Ringgit', - ISO4217: '458', - }, - MZN: { - symbol: 'MTn', - name: 'Mozambican Metical', - ISO4217: '943', - }, - NAD: { - symbol: 'N$', - name: 'Namibian Dollar', - ISO4217: '516', - }, - NGN: { - symbol: '\u20a6', - name: 'Nigerian Naira', - ISO4217: '566', - }, - NIO: { - symbol: 'NIO', - name: 'Nicaragua Cordoba', - ISO4217: '558', - }, - NOK: { - symbol: 'Nkr', - name: 'Norwegian Krone', - ISO4217: '578', - }, - NPR: { - symbol: '\u20a8', - name: 'Nepalese Rupee', - ISO4217: '524', - }, - NZD: { - symbol: 'NZ$', - name: 'New Zealand Dollar', - ISO4217: '554', - }, - OMR: { - symbol: 'OMR', - name: 'Omani Rial', - ISO4217: '512', - }, - PAB: { - symbol: 'B', - name: 'Panama Balboa', - ISO4217: '590', - }, - PEN: { - symbol: 'S/.', - name: 'Peruvian Nuevo Sol', - ISO4217: '604', - }, - PGK: { - symbol: 'K', - name: 'Papua New Guinea Kina', - ISO4217: '598', - }, - PHP: { - symbol: '\u20b1', - name: 'Philippine Peso', - ISO4217: '608', - }, - PKR: { - symbol: 'Rs', - name: 'Pakistani Rupee', - ISO4217: '586', - }, - PLN: { - symbol: 'z\u0142', - name: 'Polish Zloty', - ISO4217: '985', - }, - PYG: { - symbol: '\u20b2', - name: 'Paraguayan Guarani', - ISO4217: '600', - }, - QAR: { - symbol: 'QAR', - name: 'Qatar Rial', - ISO4217: '634', - }, - RON: { - symbol: 'RON', - name: 'Romanian New Leu', - ISO4217: '946', - }, - RSD: { - symbol: '\u0420\u0421\u0414', - name: 'Serbian Dinar', - ISO4217: '941', - }, - RUB: { - symbol: '\u20bd', - name: 'Russian Rouble', - ISO4217: '643', - }, - RWF: { - symbol: 'RF', - name: 'Rwanda Franc', - decimals: 0, - ISO4217: '646', - }, - SAR: { - symbol: 'SAR', - name: 'Saudi Arabian Riyal', - ISO4217: '682', - }, - SBD: { - symbol: 'SI$', - name: 'Solomon Islands Dollar', - ISO4217: '090', - }, - SCR: { - symbol: 'SR', - name: 'Seychelles Rupee', - ISO4217: '690', - }, - SDG: { - symbol: 'SDG', - name: 'Sudanese Pound', - ISO4217: '938', - }, - SEK: { - symbol: 'Skr', - name: 'Swedish Krona', - ISO4217: '752', - }, - SGD: { - symbol: 'S$', - name: 'Singapore Dollar', - ISO4217: '702', - }, - SHP: { - symbol: '\u00a3S', - name: 'St Helena Pound', - ISO4217: '654', - }, - SLL: { - symbol: 'Le', - name: 'Sierra Leone Leone', - ISO4217: '694', - }, - SOS: { - symbol: 'So.', - name: 'Somali Shilling', - ISO4217: '706', - }, - SRD: { - symbol: 'SRD', - name: 'Surinamese Dollar', - ISO4217: '968', - }, - STD: { - symbol: 'Db', - name: 'Sao Tome Dobra', - retired: true, - retirementDate: '2018-07-11', - ISO4217: '678', - }, - STN: { - symbol: 'Db', - name: 'Sao Tome Dobra', - ISO4217: '', - }, - SVC: { - symbol: 'SVC', - name: 'El Salvador Colon', - ISO4217: '222', - }, - SYP: { - symbol: 'SYP', - name: 'Syrian Pound', - ISO4217: '760', - }, - SZL: { - symbol: 'E', - name: 'Swaziland Lilageni', - ISO4217: '748', - }, - THB: { - symbol: '\u0e3f', - name: 'Thai Baht', - ISO4217: '764', - }, - TJS: { - symbol: 'TJS', - name: 'Tajikistani Somoni', - ISO4217: '972', - }, - TMT: { - symbol: 'm', - name: 'Turkmenistani Manat', - ISO4217: '934', - }, - TND: { - symbol: 'TND', - name: 'Tunisian Dinar', - ISO4217: '788', - }, - TOP: { - symbol: 'T$', - name: "Tonga Pa'ang", - ISO4217: '776', - }, - TRY: { - symbol: 'TL', - name: 'Turkish Lira', - ISO4217: '949', - }, - TTD: { - symbol: 'TT$', - name: 'Trinidad & Tobago Dollar', - ISO4217: '780', - }, - TWD: { - symbol: 'NT$', - name: 'Taiwan Dollar', - ISO4217: '901', - }, - TZS: { - symbol: 'TZS', - name: 'Tanzanian Shilling', - ISO4217: '834', - }, - UAH: { - symbol: '\u20b4', - name: 'Ukraine Hryvnia', - ISO4217: '980', - }, - UGX: { - symbol: 'USh', - name: 'Ugandan Shilling', - decimals: 0, - ISO4217: '800', - }, - USD: { - symbol: '$', - name: 'United States Dollar', - ISO4217: '840', - }, - UYU: { - symbol: '$U', - name: 'Uruguayan New Peso', - ISO4217: '858', - }, - UZS: { - symbol: 'UZS', - name: 'Uzbekistani Som', - ISO4217: '860', - }, - VEB: { - symbol: 'Bs.', - name: 'Venezuelan Bolivar', - retired: true, - retirementDate: '2008-02-01', - ISO4217: '', - }, - VEF: { - symbol: 'Bs.F', - name: 'Venezuelan Bolivar Fuerte', - retired: true, - retirementDate: '2018-08-20', - ISO4217: '937', - }, - VES: { - symbol: 'Bs.S', - name: 'Venezuelan Bolivar Soberano', - ISO4217: '928', - }, - VND: { - symbol: '\u20ab', - name: 'Vietnam Dong', - decimals: 0, - ISO4217: '704', - }, - VUV: { - symbol: 'Vt', - name: 'Vanuatu Vatu', - ISO4217: '548', - }, - WST: { - symbol: 'WS$', - name: 'Samoa Tala', - ISO4217: '882', - }, - XAF: { - symbol: 'FCFA', - name: 'CFA Franc (BEAC)', - decimals: 0, - ISO4217: '950', - }, - XCD: { - symbol: 'EC$', - name: 'East Caribbean Dollar', - ISO4217: '951', - }, - XOF: { - symbol: 'CFA', - name: 'CFA Franc (BCEAO)', - decimals: 0, - ISO4217: '952', - }, - XPF: { - symbol: 'XPF', - name: 'Pacific Franc', - decimals: 0, - ISO4217: '953', - }, - YER: { - symbol: 'YER', - name: 'Yemen Riyal', - ISO4217: '886', - }, - ZAR: { - symbol: 'R', - name: 'South African Rand', - ISO4217: '710', - }, - ZMK: { - symbol: 'ZK', - name: 'Zambian Kwacha', - retired: true, - retirementDate: '2013-01-01', - ISO4217: '894', - }, - ZMW: { - symbol: 'ZMW', - name: 'Zambian Kwacha', - cacheBurst: 1, - ISO4217: '967', - }, - }, - }, - { - onyxMethod: 'merge', - key: 'nvp_priorityMode', - value: 'default', - }, - { - onyxMethod: 'merge', - key: 'isFirstTimeNewExpensifyUser', - value: false, - }, - { - onyxMethod: 'merge', - key: 'preferredLocale', - value: 'en', - }, - { - onyxMethod: 'merge', - key: 'preferredEmojiSkinTone', - value: -1, - }, - { - onyxMethod: 'set', - key: 'frequentlyUsedEmojis', - value: [ - { - code: '\ud83e\udd11', - count: 155, - keywords: ['rich', 'money_mouth_face', 'face', 'money', 'mouth'], - lastUpdatedAt: 1669657594, - name: 'money_mouth_face', - }, - { - code: '\ud83e\udd17', - count: 91, - keywords: ['hugs', 'face', 'hug', 'hugging'], - lastUpdatedAt: 1669660894, - name: 'hugs', - }, - { - code: '\ud83d\ude0d', - count: 68, - keywords: ['love', 'crush', 'heart_eyes', 'eye', 'face', 'heart', 'smile'], - lastUpdatedAt: 1669659126, - name: 'heart_eyes', - }, - { - code: '\ud83e\udd14', - count: 56, - keywords: ['thinking', 'face'], - lastUpdatedAt: 1669661008, - name: 'thinking', - }, - { - code: '\ud83d\ude02', - count: 55, - keywords: ['tears', 'joy', 'face', 'laugh', 'tear'], - lastUpdatedAt: 1670346435, - name: 'joy', - }, - { - code: '\ud83d\ude05', - count: 41, - keywords: ['hot', 'sweat_smile', 'cold', 'face', 'open', 'smile', 'sweat'], - lastUpdatedAt: 1670346845, - name: 'sweat_smile', - }, - { - code: '\ud83d\ude04', - count: 37, - keywords: ['happy', 'joy', 'laugh', 'pleased', 'smile', 'eye', 'face', 'mouth', 'open'], - lastUpdatedAt: 1669659306, - name: 'smile', - }, - { - code: '\ud83d\ude18', - count: 27, - keywords: ['face', 'heart', 'kiss'], - lastUpdatedAt: 1670346848, - name: 'kissing_heart', - }, - { - code: '\ud83e\udd23', - count: 25, - keywords: ['lol', 'laughing', 'rofl', 'face', 'floor', 'laugh', 'rolling'], - lastUpdatedAt: 1669659311, - name: 'rofl', - }, - { - code: '\ud83d\ude0b', - count: 18, - keywords: ['tongue', 'lick', 'yum', 'delicious', 'face', 'savouring', 'smile', 'um'], - lastUpdatedAt: 1669658204, - name: 'yum', - }, - { - code: '\ud83d\ude0a', - count: 17, - keywords: ['proud', 'blush', 'eye', 'face', 'smile'], - lastUpdatedAt: 1669661018, - name: 'blush', - }, - { - code: '\ud83d\ude06', - count: 17, - keywords: ['happy', 'haha', 'laughing', 'satisfied', 'face', 'laugh', 'mouth', 'open', 'smile'], - lastUpdatedAt: 1669659070, - name: 'laughing', - }, - { - code: '\ud83d\ude10', - count: 17, - keywords: ['deadpan', 'face', 'neutral'], - lastUpdatedAt: 1669658922, - name: 'neutral_face', - }, - { - code: '\ud83d\ude03', - count: 17, - keywords: ['happy', 'joy', 'haha', 'smiley', 'face', 'mouth', 'open', 'smile'], - lastUpdatedAt: 1669636981, - name: 'smiley', - }, - { - code: '\ud83d\ude17', - count: 15, - keywords: ['face', 'kiss'], - lastUpdatedAt: 1669639079, - name: 'kissing', - }, - { - code: '\ud83d\ude1a', - count: 14, - keywords: ['kissing_closed_eyes', 'closed', 'eye', 'face', 'kiss'], - lastUpdatedAt: 1669660248, - name: 'kissing_closed_eyes', - }, - { - code: '\ud83d\ude19', - count: 12, - keywords: ['kissing_smiling_eyes', 'eye', 'face', 'kiss', 'smile'], - lastUpdatedAt: 1669658208, - name: 'kissing_smiling_eyes', - }, - { - code: '\ud83e\udd10', - count: 11, - keywords: ['face', 'mouth', 'zipper'], - lastUpdatedAt: 1670346432, - name: 'zipper_mouth_face', - }, - { - code: '\ud83d\ude25', - count: 11, - keywords: ['disappointed', 'face', 'relieved', 'whew'], - lastUpdatedAt: 1669660257, - name: 'disappointed_relieved', - }, - { - code: '\ud83d\ude0e', - count: 11, - keywords: ['bright', 'cool', 'eye', 'eyewear', 'face', 'glasses', 'smile', 'sun', 'sunglasses', 'weather'], - lastUpdatedAt: 1669660252, - name: 'sunglasses', - }, - { - code: '\ud83d\ude36', - count: 11, - keywords: ['face', 'mouth', 'quiet', 'silent'], - lastUpdatedAt: 1669659075, - name: 'no_mouth', - }, - { - code: '\ud83d\ude11', - count: 11, - keywords: ['expressionless', 'face', 'inexpressive', 'unexpressive'], - lastUpdatedAt: 1669640332, - name: 'expressionless', - }, - { - code: '\ud83d\ude0f', - count: 11, - keywords: ['face', 'smirk'], - lastUpdatedAt: 1666207075, - name: 'smirk', - }, - { - code: '\ud83e\udd70', - count: 1, - keywords: ['love', 'smiling_face_with_three_hearts'], - lastUpdatedAt: 1670581230, - name: 'smiling_face_with_three_hearts', - }, - ], - }, - { - onyxMethod: 'merge', - key: 'private_blockedFromConcierge', - value: {}, - }, - { - onyxMethod: 'merge', - key: 'user', - value: { - isSubscribedToNewsletter: true, - validated: true, - isUsingExpensifyCard: true, - }, - }, - { - onyxMethod: 'set', - key: 'loginList', - value: { - 'applausetester+perf2@applause.expensifail.com': { - partnerName: 'expensify.com', - partnerUserID: 'applausetester+perf2@applause.expensifail.com', - validatedDate: '2022-08-01 05:00:48', - }, - }, - }, - { - onyxMethod: 'merge', - key: 'personalDetailsList', - value: { - 1: { - accountID: 1, - login: 'fake2@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/7a1fd3cdd41564cf04f4305140372b59d1dcd495_128.jpeg', - displayName: 'fake2@gmail.com', - pronouns: '__predefined_zeHirHirs', - timezone: { - automatic: false, - selected: 'Europe/Monaco', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 2: { - accountID: 2, - login: 'fake1@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/d76dfb6912a0095cbfd2a02f64f4d9d2d9c33c29_128.jpeg', - displayName: '"Chat N Laz"', - pronouns: '__predefined_theyThemTheirs', - timezone: { - automatic: true, - selected: 'Europe/Athens', - }, - firstName: '"Chat N', - lastName: 'Laz"', - phoneNumber: '', - validated: true, - }, - 3: { - accountID: 3, - login: 'fake4@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/e769e0edf5fd0bc11cfa7c39ec2605c5310d26de_128.jpeg', - displayName: 'fake4@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 4: { - accountID: 4, - login: 'fake3@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/301e37631eca9e3127d6b668822e3a53771551f6_128.jpeg', - displayName: '123 Ios', - pronouns: '__predefined_perPers', - timezone: { - automatic: false, - selected: 'Europe/Helsinki', - }, - firstName: '123', - lastName: 'Ios', - phoneNumber: '', - validated: true, - }, - 5: { - accountID: 5, - login: 'fake5@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/2810a38b66d9a60fe41a9cf39c9fd6ecbe2cb35f_128.jpeg', - displayName: 'Qqq Qqq', - pronouns: '__predefined_sheHerHers', - timezone: { - automatic: false, - selected: 'Europe/Lisbon', - }, - firstName: 'Qqq', - lastName: 'Qqq', - phoneNumber: '', - validated: true, - }, - 6: { - accountID: 6, - login: 'andreylazutkinutest@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/2af13161ffcc95fc807769bb22c013c32280f338_128.jpeg', - displayName: 'Main Ios🏴󠁧󠁢󠁳󠁣󠁴󠁿ios', - pronouns: '__predefined_heHimHis', - timezone: { - automatic: false, - selected: 'Europe/London', - }, - firstName: 'Main', - lastName: 'Ios🏴󠁧󠁢󠁳󠁣󠁴󠁿ios', - phoneNumber: '', - validated: true, - }, - 7: { - accountID: 7, - login: 'applausetester+0604lsn@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/ad20184a011ed54383d69e4fe68658522583cbb8_128.jpeg', - displayName: '0604 Lsn', - pronouns: '__predefined_zeHirHirs', - timezone: { - automatic: false, - selected: 'America/Costa_Rica', - }, - firstName: '0604', - lastName: 'Lsn', - phoneNumber: '', - validated: true, - }, - 8: { - accountID: 8, - login: 'applausetester+0704sveta@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/63cc4a392cc64ba1c8f6a1b90d5f1441a23270d1_128.jpeg', - displayName: '07 04 0704 Lsn lsn', - pronouns: '__predefined_callMeByMyName', - timezone: { - automatic: false, - selected: 'Africa/Freetown', - }, - firstName: '07 04 0704', - lastName: 'Lsn lsn', - phoneNumber: '', - validated: true, - }, - 9: { - accountID: 9, - login: 'applausetester+0707abb@applause.expensifail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', - displayName: 'Katya Becciv', - pronouns: '__predefined_sheHerHers', - timezone: { - automatic: false, - selected: 'America/New_York', - }, - firstName: 'Katya', - lastName: 'Becciv', - phoneNumber: '', - validated: true, - }, - 10: { - accountID: 10, - login: 'applausetester+0901abb@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/0f6e999ba61695599f092b7652c1e159aee62c65_128.jpeg', - displayName: 'Katie Becciv', - pronouns: '__predefined_faeFaer', - timezone: { - automatic: false, - selected: 'Africa/Accra', - }, - firstName: 'Katie', - lastName: 'Becciv', - phoneNumber: '', - validated: true, - }, - 11: { - accountID: 11, - login: 'applausetester+1904lsn@applause.expensifail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', - displayName: '11 11', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Athens', - }, - firstName: '11', - lastName: '11', - phoneNumber: '', - validated: true, - }, - 12: { - accountID: 12, - login: 'applausetester+42222abb@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/d166c112f300a6e30bc70752cd394c3fde099e4f_128.jpeg', - displayName: '"First"', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/New_York', - }, - firstName: '"First"', - lastName: '', - phoneNumber: '', - validated: true, - }, - 13: { - accountID: 13, - login: 'applausetester+bernardo@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/803733b7038bbd5e543315fa9c6c0118eda227af_128.jpeg', - displayName: 'bernardo utest', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/Los_Angeles', - }, - firstName: 'bernardo', - lastName: 'utest', - phoneNumber: '', - validated: false, - }, - 14: { - accountID: 14, - login: 'applausetester+ihchat4@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/1008dcaadc12badbddf4720dcb7ad99b7384c613_128.jpeg', - displayName: 'Chat HT', - pronouns: '__predefined_callMeByMyName', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: 'Chat', - lastName: 'HT', - phoneNumber: '', - validated: true, - }, - 15: { - accountID: 15, - login: 'applausetester+pd1005@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/86c9b7dce35aea83b69c6e825a4b3d00a87389b7_128.jpeg', - displayName: 'applausetester+pd1005@applause.expensifail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Lisbon', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 16: { - accountID: 16, - login: 'fake6@gmail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_7.png', - displayName: 'fake6@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Warsaw', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 17: { - accountID: 17, - login: 'applausetester+perf2@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/1486f9cc6367d8c399ee453ad5b686d157bb4dda_128.jpeg', - displayName: 'applausetester+perf2@applause.expensifail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/Los_Angeles', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - localCurrencyCode: 'USD', - }, - 18: { - accountID: 18, - login: 'fake7@gmail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_2.png', - displayName: 'fake7@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'America/Toronto', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 19: { - accountID: 19, - login: 'fake8@gmail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/7b0a9cf9c93987053be9d6cc707cb1f091a1ef46_128.jpeg', - displayName: 'fake8@gmail.com', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Paris', - }, - firstName: '', - lastName: '', - phoneNumber: '', - validated: true, - }, - 20: { - accountID: 20, - login: 'applausetester@applause.expensifail.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/8ddbb1a4675883ea12b3021f698a8b2dcfc18d42_128.jpeg', - displayName: 'Applause Main Account', - pronouns: '__predefined_coCos', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: 'Applause', - lastName: 'Main Account', - phoneNumber: '', - validated: true, - }, - 21: { - accountID: 21, - login: 'christoph+hightraffic@margelo.io', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_1.png', - displayName: 'Christoph Pader', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Vienna', - }, - firstName: 'Christoph', - lastName: 'Pader', - phoneNumber: '', - validated: true, - }, - 22: { - accountID: 22, - login: 'concierge@expensify.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/icons/concierge_2022.png', - displayName: 'Concierge', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Moscow', - }, - firstName: 'Concierge', - lastName: '', - phoneNumber: '', - validated: true, - }, - 23: { - accountID: 23, - login: 'svetlanalazutkinautest+0211@gmail.com', - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_6.png', - displayName: 'Chat S', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - firstName: 'Chat S', - lastName: '', - phoneNumber: '', - validated: true, - }, - 24: { - accountID: 24, - login: 'tayla.lay@team.expensify.com', - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/d3196c27ed6bdb2df741af29a3ccfdb0f9919c41_128.jpeg', - displayName: 'Tayla Simmons', - pronouns: '__predefined_sheHerHers', - timezone: { - automatic: true, - selected: 'America/Chicago', - }, - firstName: 'Tayla', - lastName: 'Simmons', - phoneNumber: '', - validated: true, - }, - }, - }, - { - onyxMethod: 'set', - key: 'betas', - value: ['all'], - }, - { - onyxMethod: 'merge', - key: 'countryCode', - value: 1, - }, - { - onyxMethod: 'merge', - key: 'account', - value: { - requiresTwoFactorAuth: false, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'policy_', - value: { - policy_28493C792FA01DAE: { - isFromFullPolicy: false, - id: '28493C792FA01DAE', - name: "applausetester+perf2's Expenses", - role: 'admin', - type: 'personal', - owner: 'applausetester+perf2@applause.expensifail.com', - outputCurrency: 'USD', - avatar: '', - employeeList: [], - }, - policy_A6511FF8D2EE7661: { - isFromFullPolicy: false, - id: 'A6511FF8D2EE7661', - name: "Applause's Workspace", - role: 'admin', - type: 'free', - owner: 'applausetester+perf2@applause.expensifail.com', - outputCurrency: 'INR', - avatar: '', - employeeList: [], - }, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'report_', - value: { - report_98258097: { - reportID: '98258097', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22], - isPinned: true, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-03 06:45:00', - lastMessageTimestamp: 1659509100000, - lastMessageText: 'You can easily track, approve, and pay bills in Expensify with your custom compa', - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: - 'You can easily track, approve, and pay bills in Expensify with your custom company bill pay email address: ' + - 'applause.expensifail.com@expensify.cash. Learn more ' + - 'here.' + - ' For questions, just reply to this message.', - }, - report_98258458: { - reportID: '98258458', - reportName: '', - chatType: 'policyExpenseChat', - ownerAccountID: 17, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [20, 17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-03 20:30:55.599', - lastMessageTimestamp: 1667507455599, - lastMessageText: '', - lastActorAccountID: 20, - notificationPreference: 'always', - stateNum: 2, - statusNum: 2, - oldPolicyName: 'Crowded Policy - Definitive Edition', - visibility: null, - isOwnPolicyExpenseChat: true, - lastMessageHtml: '', - }, - report_98344717: { - reportID: '98344717', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [14], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-02 20:03:42', - lastMessageTimestamp: 1659470622000, - lastMessageText: 'Requested \u20b41.67 from applausetester+perf2@applause.expensifail.com', - lastActorAccountID: 14, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Requested \u20b41.67 from applausetester+perf2@applause.expensifail.com', - }, - report_98345050: { - reportID: '98345050', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [4], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-04 21:18:00.038', - lastMessageTimestamp: 1667596680038, - lastMessageText: 'Cancelled the \u20b440.00 request', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Cancelled the \u20b440.00 request', - }, - report_98345315: { - reportID: '98345315', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [4, 16, 18, 19], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-01 20:48:16', - lastMessageTimestamp: 1659386896000, - lastMessageText: 'applausetester+perf2@applause.expensifail.com', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'applausetester+perf2@applause.expensifail.com', - }, - report_98345625: { - reportID: '98345625', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [2, 1, 4, 3, 5, 16, 18, 19], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-01 20:49:11', - lastMessageTimestamp: 1659386951000, - lastMessageText: 'Say hello\ud83d\ude10', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Say hello\ud83d\ude10', - }, - report_98345679: { - reportID: '98345679', - reportName: '', - chatType: 'policyExpenseChat', - ownerAccountID: 17, - policyID: '1CE001C4B9F3CA54', - participantAccountIDs: [4, 17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-16 12:30:57', - lastMessageTimestamp: 1660653057000, - lastMessageText: '', - lastActorAccountID: 4, - notificationPreference: 'always', - stateNum: 2, - statusNum: 2, - oldPolicyName: "Andreylazutkinutest+123's workspace", - visibility: null, - isOwnPolicyExpenseChat: true, - lastMessageHtml: '', - }, - report_98414813: { - reportID: '98414813', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [14, 16], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-08-02 20:03:41', - lastMessageTimestamp: 1659470621000, - lastMessageText: 'Split \u20b45.00 with applausetester+perf2@applause.expensifail.com and applauseteste', - lastActorAccountID: 14, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Split \u20b45.00 with applausetester+perf2@applause.expensifail.com and fake6@gmail.com', - }, - report_98817646: { - reportID: '98817646', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [16], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-12-09 10:17:18.362', - lastMessageTimestamp: 1670581038362, - lastMessageText: 'RR', - lastActorAccountID: 17, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'RR', - iouReportID: '2543745284790730', - }, - report_358751490033727: { - reportID: '358751490033727', - reportName: '#digimobileroom', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-10-12 17:47:45.228', - lastMessageTimestamp: 1665596865228, - lastMessageText: 'STAGING_CHAT_MESSAGE_A2C534B7-3509-416E-A0AD-8463831C29DD', - lastActorAccountID: 25, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'STAGING_CHAT_MESSAGE_A2C534B7-3509-416E-A0AD-8463831C29DD', - }, - report_663424408122117: { - reportID: '663424408122117', - reportName: '#announce', - chatType: 'policyAnnounce', - ownerAccountID: 0, - policyID: 'A6511FF8D2EE7661', - participantAccountIDs: [17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_944123936554214: { - reportID: '944123936554214', - reportName: '', - chatType: 'policyExpenseChat', - ownerAccountID: 17, - policyID: 'A6511FF8D2EE7661', - participantAccountIDs: [17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: true, - lastMessageHtml: '', - }, - report_2242399088152511: { - reportID: '2242399088152511', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22, 10, 6, 8, 4], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-03 20:48:58.815', - lastMessageTimestamp: 1667508538815, - lastMessageText: 'Hi there, thanks for reaching out! How may I help?', - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '

Hi there, thanks for reaching out! How may I help?

', - }, - report_2576922422943214: { - reportID: '2576922422943214', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [12], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-12-01 08:05:11.009', - lastMessageTimestamp: 1669881911009, - lastMessageText: 'Test', - lastActorAccountID: 17, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Test', - }, - report_2752461403207161: { - reportID: '2752461403207161', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [2], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_3785654888638968: { - reportID: '3785654888638968', - reportName: '#jack', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-10-12 12:20:00.668', - lastMessageTimestamp: 1665577200668, - lastMessageText: 'Room renamed to #jack', - lastActorAccountID: 15, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Room renamed to #jack', - }, - report_4867098979334014: { - reportID: '4867098979334014', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [21], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-12-16 18:14:00.208', - lastMessageTimestamp: 1671214440208, - lastMessageText: 'Requested \u20ac200.00 from Christoph for Essen mit Kunden', - lastActorAccountID: 17, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Requested \u20ac200.00 from Christoph for Essen mit Kunden', - iouReportID: '4249286573496381', - }, - report_5277760851229035: { - reportID: '5277760851229035', - reportName: '#kasper_tha_cat', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-29 12:38:15.985', - lastMessageTimestamp: 1669725495985, - lastMessageText: 'fff', - lastActorAccountID: 16, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: - 'fff
f
f
f
f
f
f
f
f

f
f
f
f

f
' + - 'f
f
f
f
f

f
f
f
f
f
ff', - }, - report_5324367938904284: { - reportID: '5324367938904284', - reportName: '#applause.expensifail.com', - chatType: 'domainAll', - ownerAccountID: 99, - policyID: '_FAKE_', - participantAccountIDs: [13], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-11-29 21:08:00.793', - lastMessageTimestamp: 1669756080793, - lastMessageText: 'Iviviviv8b', - lastActorAccountID: 10, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Iviviviv8b', - }, - report_5654270288238256: { - reportID: '5654270288238256', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [6, 2, 9, 4, 5, 7, 100, 11], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_6194900075541844: { - reportID: '6194900075541844', - reportName: '#admins', - chatType: 'policyAdmins', - ownerAccountID: 0, - policyID: 'A6511FF8D2EE7661', - participantAccountIDs: [17], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_6801643744224146: { - reportID: '6801643744224146', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22, 6, 2, 23, 9, 4, 5, 7], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-09-15 12:57:59.526', - lastMessageTimestamp: 1663246679526, - lastMessageText: "\ud83d\udc4b Welcome to Expensify! I'm Concierge. Is there anything I can help with? Click ", - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: - "\ud83d\udc4b Welcome to Expensify! I'm Concierge. Is there anything I can help with? Click the + icon on the homescreen to explore the features you can use.", - }, - report_7658708888047100: { - reportID: '7658708888047100', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [22, 6, 4, 5, 24, 101], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-09-16 11:12:46.739', - lastMessageTimestamp: 1663326766739, - lastMessageText: 'Hi there! How can I help?\u00a0', - lastActorAccountID: 22, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Hi there! How can I help?\u00a0', - }, - report_7756405299640824: { - reportID: '7756405299640824', - reportName: '#jackd23', - chatType: 'policyRoom', - ownerAccountID: 0, - policyID: 'C28C2634DD7226B8', - participantAccountIDs: [15], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '2022-10-12 12:46:43.577', - lastMessageTimestamp: 1665578803577, - lastMessageText: 'Room renamed to #jackd23', - lastActorAccountID: 15, - notificationPreference: 'daily', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: 'restricted', - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'Room renamed to #jackd23', - }, - report_7819732651025410: { - reportID: '7819732651025410', - reportName: 'Chat Report', - chatType: null, - ownerAccountID: 0, - policyID: '_FAKE_', - participantAccountIDs: [5], - isPinned: false, - lastReadCreated: '1980-01-01 00:00:00.000', - lastVisibleActionCreated: '', - lastMessageTimestamp: 0, - lastMessageText: '', - lastActorAccountID: 0, - notificationPreference: 'always', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: '', - }, - report_2543745284790730: { - reportID: '2543745284790730', - ownerAccountID: 17, - managerID: 16, - currency: 'USD', - chatReportID: '98817646', - cachedTotal: '($1,473.11)', - total: 147311, - stateNum: 1, - statusNum: 1, - }, - report_4249286573496381: { - reportID: '4249286573496381', - ownerAccountID: 17, - managerID: 21, - currency: 'USD', - chatReportID: '4867098979334014', - cachedTotal: '($212.78)', - total: 21278, - stateNum: 1, - statusNum: 1, - }, - }, - }, - ], - jsonCode: 200, - requestID: '783ef7fac81f969a-SJC', -}); - -export default openApp; diff --git a/src/libs/E2E/apiMocks/openReport.ts b/src/libs/E2E/apiMocks/openReport.ts deleted file mode 100644 index 49d44605592d..000000000000 --- a/src/libs/E2E/apiMocks/openReport.ts +++ /dev/null @@ -1,1972 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import type Response from '@src/types/onyx/Response'; - -export default (): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'report_98345625', - value: { - reportID: '98345625', - reportName: 'Chat Report', - type: 'chat', - chatType: null, - ownerAccountID: 0, - managerID: 0, - policyID: '_FAKE_', - participantAccountIDs: [14567013], - isPinned: false, - lastReadTime: '2023-09-14 11:50:21.768', - lastMentionedTime: '2023-07-27 07:37:43.100', - lastReadSequenceNumber: 0, - lastVisibleActionCreated: '2023-08-29 12:38:16.070', - lastVisibleActionLastModified: '2023-08-29 12:38:16.070', - lastMessageText: 'terry+hightraffic@margelo.io owes \u20ac12.00', - lastActorAccountID: 14567013, - notificationPreference: 'always', - welcomeMessage: '', - stateNum: 0, - statusNum: 0, - oldPolicyName: '', - visibility: null, - isOwnPolicyExpenseChat: false, - lastMessageHtml: 'terry+hightraffic@margelo.io owes \u20ac12.00', - iouReportID: '206636935813547', - hasOutstandingChildRequest: false, - policyName: null, - hasParentAccess: null, - parentReportID: null, - parentReportActionID: null, - writeCapability: 'all', - description: null, - isDeletedParentAction: null, - total: 0, - currency: 'USD', - chatReportID: null, - isWaitingOnBankAccount: false, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'transactions_', - value: { - transactions_5509240412000765850: { - amount: 1200, - billable: false, - cardID: 15467728, - category: '', - comment: { - comment: '', - }, - created: '2023-08-29', - currency: 'EUR', - filename: '', - merchant: 'Request', - modifiedAmount: 0, - modifiedCreated: '', - modifiedCurrency: '', - modifiedMerchant: '', - originalAmount: 0, - originalCurrency: '', - parentTransactionID: '', - receipt: {}, - reimbursable: true, - reportID: '206636935813547', - status: 'Pending', - tag: '', - transactionID: '5509240412000765850', - hasEReceipt: false, - }, - }, - }, - { - onyxMethod: 'merge', - key: 'reportActions_98345625', - value: { - '885570376575240776': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '', - text: '', - isEdited: true, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - edits: [], - html: '', - lastModified: '2023-09-01 07:43:29.374', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-31 07:23:52.892', - timestamp: 1693466632, - reportActionTimestamp: 1693466632892, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '885570376575240776', - previousReportActionID: '6576518341807837187', - lastModified: '2023-09-01 07:43:29.374', - whisperedToAccountIDs: [], - }, - '6576518341807837187': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'terry+hightraffic@margelo.io owes \u20ac12.00', - text: 'terry+hightraffic@margelo.io owes \u20ac12.00', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - lastModified: '2023-08-29 12:38:16.070', - linkedReportID: '206636935813547', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-08-29 12:38:16.070', - timestamp: 1693312696, - reportActionTimestamp: 1693312696070, - automatic: false, - actionName: 'REPORTPREVIEW', - shouldShow: true, - reportActionID: '6576518341807837187', - previousReportActionID: '2658221912430757962', - lastModified: '2023-08-29 12:38:16.070', - childReportID: '206636935813547', - childType: 'iou', - childStatusNum: 1, - childStateNum: 1, - childMoneyRequestCount: 1, - whisperedToAccountIDs: [], - }, - '2658221912430757962': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'Hshshdhdhejje
Cuududdke

F
D
R
D
R
Jfj c
D

D
D
R
D
R', - text: 'Hshshdhdhejje\nCuududdke\n\nF\nD\nR\nD\nR\nJfj c\nD\n\nD\nD\nR\nD\nR', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [ - { - emoji: 'heart', - users: [ - { - accountID: 12883048, - skinTone: -1, - }, - ], - }, - ], - }, - ], - originalMessage: { - html: 'Hshshdhdhejje
Cuududdke

F
D
R
D
R
Jfj c
D

D
D
R
D
R', - lastModified: '2023-08-25 12:39:48.121', - reactions: [ - { - emoji: 'heart', - users: [ - { - accountID: 12883048, - skinTone: -1, - }, - ], - }, - ], - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:54:06.972', - timestamp: 1692953646, - reportActionTimestamp: 1692953646972, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2658221912430757962', - previousReportActionID: '6551789403725495383', - lastModified: '2023-08-25 12:39:48.121', - childReportID: '1411015346900020', - childType: 'chat', - childOldestFourAccountIDs: '12883048', - childCommenterCount: 1, - childLastVisibleActionCreated: '2023-08-29 06:08:59.247', - childVisibleActionCount: 1, - whisperedToAccountIDs: [], - }, - '6551789403725495383': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'Typing with the composer is now also reasonably fast again', - text: 'Typing with the composer is now also reasonably fast again', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'Typing with the composer is now also reasonably fast again', - lastModified: '2023-08-25 08:53:57.490', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:53:57.490', - timestamp: 1692953637, - reportActionTimestamp: 1692953637490, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6551789403725495383', - previousReportActionID: '6184477005811241106', - lastModified: '2023-08-25 08:53:57.490', - whisperedToAccountIDs: [], - }, - '6184477005811241106': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '\ud83d\ude3a', - text: '\ud83d\ude3a', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '\ud83d\ude3a', - lastModified: '2023-08-25 08:53:41.689', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:53:41.689', - timestamp: 1692953621, - reportActionTimestamp: 1692953621689, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6184477005811241106', - previousReportActionID: '7473953427765241164', - lastModified: '2023-08-25 08:53:41.689', - whisperedToAccountIDs: [], - }, - '7473953427765241164': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'Skkkkkkrrrrrrrr', - text: 'Skkkkkkrrrrrrrr', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'Skkkkkkrrrrrrrr', - lastModified: '2023-08-25 08:53:31.900', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-25 08:53:31.900', - timestamp: 1692953611, - reportActionTimestamp: 1692953611900, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7473953427765241164', - previousReportActionID: '872421684593496491', - lastModified: '2023-08-25 08:53:31.900', - whisperedToAccountIDs: [], - }, - '872421684593496491': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hello this is a new test will my version sync though? i doubt it lolasdasdasdaoe f t asdasd okay und das ging jetzt eh oder? ja schaut ganz gut aus okay geht das immer noch ? schaut gut aus ja true ghw test test 2 test 4 tse 3 oida', - text: 'hello this is a new test will my version sync though? i doubt it lolasdasdasdaoe f t asdasd okay und das ging jetzt eh oder? ja schaut ganz gut aus okay geht das immer noch ? schaut gut aus ja true ghw test test 2 test 4 tse 3 oida', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hello this is a new test will my version sync though? i doubt it lolasdasdasdaoe f t asdasd okay und das ging jetzt eh oder? ja schaut ganz gut aus okay geht das immer noch ? schaut gut aus ja true ghw test test 2 test 4 tse 3 oida', - lastModified: '2023-08-11 13:35:03.962', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-11 13:35:03.962', - timestamp: 1691760903, - reportActionTimestamp: 1691760903962, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '872421684593496491', - previousReportActionID: '175680146540578558', - lastModified: '2023-08-11 13:35:03.962', - whisperedToAccountIDs: [], - }, - '175680146540578558': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '', - text: '[Attachment]', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '', - lastModified: '2023-08-10 06:59:21.381', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-10 06:59:21.381', - timestamp: 1691650761, - reportActionTimestamp: 1691650761381, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '175680146540578558', - previousReportActionID: '1264289784533901723', - lastModified: '2023-08-10 06:59:21.381', - whisperedToAccountIDs: [], - }, - '1264289784533901723': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '', - text: '[Attachment]', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '', - lastModified: '2023-08-10 06:59:16.922', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-10 06:59:16.922', - timestamp: 1691650756, - reportActionTimestamp: 1691650756922, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1264289784533901723', - previousReportActionID: '4870277010164688289', - lastModified: '2023-08-10 06:59:16.922', - whisperedToAccountIDs: [], - }, - '4870277010164688289': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'send test', - text: 'send test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'send test', - lastModified: '2023-08-09 06:43:25.209', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-09 06:43:25.209', - timestamp: 1691563405, - reportActionTimestamp: 1691563405209, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4870277010164688289', - previousReportActionID: '7931783095143103530', - lastModified: '2023-08-09 06:43:25.209', - whisperedToAccountIDs: [], - }, - '7931783095143103530': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hello terry \ud83d\ude04 this is a test @terry+hightraffic@margelo.io', - text: 'hello terry \ud83d\ude04 this is a test @terry+hightraffic@margelo.io', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hello terry \ud83d\ude04 this is a test @terry+hightraffic@margelo.io', - lastModified: '2023-08-08 14:38:45.035', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-08 14:38:45.035', - timestamp: 1691505525, - reportActionTimestamp: 1691505525035, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7931783095143103530', - previousReportActionID: '4598496324774172433', - lastModified: '2023-08-08 14:38:45.035', - whisperedToAccountIDs: [], - }, - '4598496324774172433': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: '\ud83d\uddff', - text: '\ud83d\uddff', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '\ud83d\uddff', - lastModified: '2023-08-08 13:21:42.102', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-08 13:21:42.102', - timestamp: 1691500902, - reportActionTimestamp: 1691500902102, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4598496324774172433', - previousReportActionID: '3324110555952451144', - lastModified: '2023-08-08 13:21:42.102', - whisperedToAccountIDs: [], - }, - '3324110555952451144': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test \ud83d\uddff', - text: 'test \ud83d\uddff', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test \ud83d\uddff', - lastModified: '2023-08-08 13:21:32.101', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-08 13:21:32.101', - timestamp: 1691500892, - reportActionTimestamp: 1691500892101, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '3324110555952451144', - previousReportActionID: '5389364980227777980', - lastModified: '2023-08-08 13:21:32.101', - whisperedToAccountIDs: [], - }, - '5389364980227777980': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'okay now it will work again y \ud83d\udc42', - text: 'okay now it will work again y \ud83d\udc42', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'okay now it will work again y \ud83d\udc42', - lastModified: '2023-08-07 10:54:38.141', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-08-07 10:54:38.141', - timestamp: 1691405678, - reportActionTimestamp: 1691405678141, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5389364980227777980', - previousReportActionID: '4717622390560689493', - lastModified: '2023-08-07 10:54:38.141', - whisperedToAccountIDs: [], - }, - '4717622390560689493': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hmmmm', - text: 'hmmmm', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hmmmm', - lastModified: '2023-07-27 18:13:45.322', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 18:13:45.322', - timestamp: 1690481625, - reportActionTimestamp: 1690481625322, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4717622390560689493', - previousReportActionID: '745721424446883075', - lastModified: '2023-07-27 18:13:45.322', - whisperedToAccountIDs: [], - }, - '745721424446883075': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test', - text: 'test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test', - lastModified: '2023-07-27 18:13:32.595', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 18:13:32.595', - timestamp: 1690481612, - reportActionTimestamp: 1690481612595, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '745721424446883075', - previousReportActionID: '3986429677777110818', - lastModified: '2023-07-27 18:13:32.595', - whisperedToAccountIDs: [], - }, - '3986429677777110818': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'I will', - text: 'I will', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'I will', - lastModified: '2023-07-27 17:03:11.250', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 17:03:11.250', - timestamp: 1690477391, - reportActionTimestamp: 1690477391250, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '3986429677777110818', - previousReportActionID: '7317910228472011573', - lastModified: '2023-07-27 17:03:11.250', - childReportID: '3338245207149134', - childType: 'chat', - whisperedToAccountIDs: [], - }, - '7317910228472011573': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'will you>', - text: 'will you>', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'will you>', - lastModified: '2023-07-27 16:46:58.988', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 16:46:58.988', - timestamp: 1690476418, - reportActionTimestamp: 1690476418988, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7317910228472011573', - previousReportActionID: '6779343397958390319', - lastModified: '2023-07-27 16:46:58.988', - whisperedToAccountIDs: [], - }, - '6779343397958390319': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'i will always send :#', - text: 'i will always send :#', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'i will always send :#', - lastModified: '2023-07-27 07:55:33.468', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:33.468', - timestamp: 1690444533, - reportActionTimestamp: 1690444533468, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6779343397958390319', - previousReportActionID: '5084145419388195535', - lastModified: '2023-07-27 07:55:33.468', - whisperedToAccountIDs: [], - }, - '5084145419388195535': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new test', - text: 'new test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new test', - lastModified: '2023-07-27 07:55:22.309', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:22.309', - timestamp: 1690444522, - reportActionTimestamp: 1690444522309, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5084145419388195535', - previousReportActionID: '6742067600980190659', - lastModified: '2023-07-27 07:55:22.309', - whisperedToAccountIDs: [], - }, - '6742067600980190659': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'okay good', - text: 'okay good', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'okay good', - lastModified: '2023-07-27 07:55:15.362', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:15.362', - timestamp: 1690444515, - reportActionTimestamp: 1690444515362, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6742067600980190659', - previousReportActionID: '7811212427986810247', - lastModified: '2023-07-27 07:55:15.362', - whisperedToAccountIDs: [], - }, - '7811212427986810247': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test 2', - text: 'test 2', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 2', - lastModified: '2023-07-27 07:55:10.629', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:55:10.629', - timestamp: 1690444510, - reportActionTimestamp: 1690444510629, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7811212427986810247', - previousReportActionID: '4544757211729131829', - lastModified: '2023-07-27 07:55:10.629', - whisperedToAccountIDs: [], - }, - '4544757211729131829': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new test', - text: 'new test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new test', - lastModified: '2023-07-27 07:53:41.960', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:41.960', - timestamp: 1690444421, - reportActionTimestamp: 1690444421960, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4544757211729131829', - previousReportActionID: '8290114634148431001', - lastModified: '2023-07-27 07:53:41.960', - whisperedToAccountIDs: [], - }, - '8290114634148431001': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'something was real', - text: 'something was real', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'something was real', - lastModified: '2023-07-27 07:53:27.836', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:27.836', - timestamp: 1690444407, - reportActionTimestamp: 1690444407836, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '8290114634148431001', - previousReportActionID: '5597494166918965742', - lastModified: '2023-07-27 07:53:27.836', - whisperedToAccountIDs: [], - }, - '5597494166918965742': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'oida', - text: 'oida', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'oida', - lastModified: '2023-07-27 07:53:20.783', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:20.783', - timestamp: 1690444400, - reportActionTimestamp: 1690444400783, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5597494166918965742', - previousReportActionID: '7445709165354739065', - lastModified: '2023-07-27 07:53:20.783', - whisperedToAccountIDs: [], - }, - '7445709165354739065': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test 12', - text: 'test 12', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 12', - lastModified: '2023-07-27 07:53:17.393', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:17.393', - timestamp: 1690444397, - reportActionTimestamp: 1690444397393, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '7445709165354739065', - previousReportActionID: '1985264407541504554', - lastModified: '2023-07-27 07:53:17.393', - whisperedToAccountIDs: [], - }, - '1985264407541504554': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new test', - text: 'new test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new test', - lastModified: '2023-07-27 07:53:07.894', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:53:07.894', - timestamp: 1690444387, - reportActionTimestamp: 1690444387894, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1985264407541504554', - previousReportActionID: '6101278009725036288', - lastModified: '2023-07-27 07:53:07.894', - whisperedToAccountIDs: [], - }, - '6101278009725036288': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'grrr', - text: 'grrr', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'grrr', - lastModified: '2023-07-27 07:52:56.421', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:56.421', - timestamp: 1690444376, - reportActionTimestamp: 1690444376421, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6101278009725036288', - previousReportActionID: '6913024396112106680', - lastModified: '2023-07-27 07:52:56.421', - whisperedToAccountIDs: [], - }, - '6913024396112106680': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'ne w test', - text: 'ne w test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'ne w test', - lastModified: '2023-07-27 07:52:53.352', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:53.352', - timestamp: 1690444373, - reportActionTimestamp: 1690444373352, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6913024396112106680', - previousReportActionID: '3663318486255461038', - lastModified: '2023-07-27 07:52:53.352', - whisperedToAccountIDs: [], - }, - '3663318486255461038': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'well', - text: 'well', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'well', - lastModified: '2023-07-27 07:52:47.044', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:47.044', - timestamp: 1690444367, - reportActionTimestamp: 1690444367044, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '3663318486255461038', - previousReportActionID: '6652909175804277965', - lastModified: '2023-07-27 07:52:47.044', - whisperedToAccountIDs: [], - }, - '6652909175804277965': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'hu', - text: 'hu', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hu', - lastModified: '2023-07-27 07:52:43.489', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:43.489', - timestamp: 1690444363, - reportActionTimestamp: 1690444363489, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6652909175804277965', - previousReportActionID: '4738491624635492834', - lastModified: '2023-07-27 07:52:43.489', - whisperedToAccountIDs: [], - }, - '4738491624635492834': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test', - text: 'test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test', - lastModified: '2023-07-27 07:52:40.145', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:52:40.145', - timestamp: 1690444360, - reportActionTimestamp: 1690444360145, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4738491624635492834', - previousReportActionID: '1621235410433805703', - lastModified: '2023-07-27 07:52:40.145', - whisperedToAccountIDs: [], - }, - '1621235410433805703': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test 4', - text: 'test 4', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 4', - lastModified: '2023-07-27 07:48:36.809', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:48:36.809', - timestamp: 1690444116, - reportActionTimestamp: 1690444116809, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1621235410433805703', - previousReportActionID: '1024550225871474566', - lastModified: '2023-07-27 07:48:36.809', - whisperedToAccountIDs: [], - }, - '1024550225871474566': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test 3', - text: 'test 3', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test 3', - lastModified: '2023-07-27 07:48:24.183', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:48:24.183', - timestamp: 1690444104, - reportActionTimestamp: 1690444104183, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1024550225871474566', - previousReportActionID: '5598482410513625723', - lastModified: '2023-07-27 07:48:24.183', - whisperedToAccountIDs: [], - }, - '5598482410513625723': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'test2', - text: 'test2', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test2', - lastModified: '2023-07-27 07:42:25.340', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:25.340', - timestamp: 1690443745, - reportActionTimestamp: 1690443745340, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5598482410513625723', - previousReportActionID: '115121137377026405', - lastModified: '2023-07-27 07:42:25.340', - whisperedToAccountIDs: [], - }, - '115121137377026405': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'test', - text: 'test', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'test', - lastModified: '2023-07-27 07:42:22.583', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:42:22.583', - timestamp: 1690443742, - reportActionTimestamp: 1690443742583, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '115121137377026405', - previousReportActionID: '2167420855737359171', - lastModified: '2023-07-27 07:42:22.583', - whisperedToAccountIDs: [], - }, - '2167420855737359171': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'new message', - text: 'new message', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'new message', - lastModified: '2023-07-27 07:42:09.177', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:09.177', - timestamp: 1690443729, - reportActionTimestamp: 1690443729177, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2167420855737359171', - previousReportActionID: '6106926938128802897', - lastModified: '2023-07-27 07:42:09.177', - whisperedToAccountIDs: [], - }, - '6106926938128802897': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'oh', - text: 'oh', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'oh', - lastModified: '2023-07-27 07:42:03.902', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:03.902', - timestamp: 1690443723, - reportActionTimestamp: 1690443723902, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '6106926938128802897', - previousReportActionID: '4366704007455141347', - lastModified: '2023-07-27 07:42:03.902', - whisperedToAccountIDs: [], - }, - '4366704007455141347': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'hm lol', - text: 'hm lol', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hm lol', - lastModified: '2023-07-27 07:42:00.734', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:42:00.734', - timestamp: 1690443720, - reportActionTimestamp: 1690443720734, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4366704007455141347', - previousReportActionID: '2078794664797360607', - lastModified: '2023-07-27 07:42:00.734', - whisperedToAccountIDs: [], - }, - '2078794664797360607': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'hi?', - text: 'hi?', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hi?', - lastModified: '2023-07-27 07:41:49.724', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:41:49.724', - timestamp: 1690443709, - reportActionTimestamp: 1690443709724, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2078794664797360607', - previousReportActionID: '2030060194258527427', - lastModified: '2023-07-27 07:41:49.724', - whisperedToAccountIDs: [], - }, - '2030060194258527427': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'lets have a thread about it, will ya?', - text: 'lets have a thread about it, will ya?', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'lets have a thread about it, will ya?', - lastModified: '2023-07-27 07:40:49.146', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:40:49.146', - timestamp: 1690443649, - reportActionTimestamp: 1690443649146, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '2030060194258527427', - previousReportActionID: '5540483153987237906', - lastModified: '2023-07-27 07:40:49.146', - childReportID: '5860710623453234', - childType: 'chat', - childOldestFourAccountIDs: '14567013,12883048', - childCommenterCount: 2, - childLastVisibleActionCreated: '2023-07-27 07:41:03.550', - childVisibleActionCount: 2, - whisperedToAccountIDs: [], - }, - '5540483153987237906': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: '@hanno@margelo.io i mention you lasagna :)', - text: '@hanno@margelo.io i mention you lasagna :)', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '@hanno@margelo.io i mention you lasagna :)', - lastModified: '2023-07-27 07:37:43.100', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:37:43.100', - timestamp: 1690443463, - reportActionTimestamp: 1690443463100, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '5540483153987237906', - previousReportActionID: '8050559753491913991', - lastModified: '2023-07-27 07:37:43.100', - whisperedToAccountIDs: [], - }, - '8050559753491913991': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: '@terry+hightraffic@margelo.io', - text: '@terry+hightraffic@margelo.io', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: '@terry+hightraffic@margelo.io', - lastModified: '2023-07-27 07:36:41.708', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:36:41.708', - timestamp: 1690443401, - reportActionTimestamp: 1690443401708, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '8050559753491913991', - previousReportActionID: '881015235172878574', - lastModified: '2023-07-27 07:36:41.708', - whisperedToAccountIDs: [], - }, - '881015235172878574': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'yeah lets see', - text: 'yeah lets see', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'yeah lets see', - lastModified: '2023-07-27 07:25:15.997', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-27 07:25:15.997', - timestamp: 1690442715, - reportActionTimestamp: 1690442715997, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '881015235172878574', - previousReportActionID: '4800357767877651330', - lastModified: '2023-07-27 07:25:15.997', - whisperedToAccountIDs: [], - }, - '4800357767877651330': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'asdasdasd', - text: 'asdasdasd', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'asdasdasd', - lastModified: '2023-07-27 07:25:03.093', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-27 07:25:03.093', - timestamp: 1690442703, - reportActionTimestamp: 1690442703093, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '4800357767877651330', - previousReportActionID: '9012557872554910346', - lastModified: '2023-07-27 07:25:03.093', - whisperedToAccountIDs: [], - }, - '9012557872554910346': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'yeah', - text: 'yeah', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'yeah', - lastModified: '2023-07-26 19:49:40.471', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-26 19:49:40.471', - timestamp: 1690400980, - reportActionTimestamp: 1690400980471, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '9012557872554910346', - previousReportActionID: '8440677969068645500', - lastModified: '2023-07-26 19:49:40.471', - whisperedToAccountIDs: [], - }, - '8440677969068645500': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'hello motor', - text: 'hello motor', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'hello motor', - lastModified: '2023-07-26 19:49:36.262', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-26 19:49:36.262', - timestamp: 1690400976, - reportActionTimestamp: 1690400976262, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '8440677969068645500', - previousReportActionID: '306887996337608775', - lastModified: '2023-07-26 19:49:36.262', - whisperedToAccountIDs: [], - }, - '306887996337608775': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'a new messagfe', - text: 'a new messagfe', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'a new messagfe', - lastModified: '2023-07-26 19:49:29.512', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-26 19:49:29.512', - timestamp: 1690400969, - reportActionTimestamp: 1690400969512, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '306887996337608775', - previousReportActionID: '587892433077506227', - lastModified: '2023-07-26 19:49:29.512', - whisperedToAccountIDs: [], - }, - '587892433077506227': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Hanno J. G\u00f6decke', - }, - ], - actorAccountID: 12883048, - message: [ - { - type: 'COMMENT', - html: 'good', - text: 'good', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'good', - lastModified: '2023-07-26 19:49:20.473', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/fc1b8880216a5a76c8fd9998aaa33c080dacda5d_128.jpeg', - created: '2023-07-26 19:49:20.473', - timestamp: 1690400960, - reportActionTimestamp: 1690400960473, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '587892433077506227', - previousReportActionID: '1433103421804347060', - lastModified: '2023-07-26 19:49:20.473', - whisperedToAccountIDs: [], - }, - '1433103421804347060': { - person: [ - { - type: 'TEXT', - style: 'strong', - text: 'Terry Hightraffic1337', - }, - ], - actorAccountID: 14567013, - message: [ - { - type: 'COMMENT', - html: 'ah', - text: 'ah', - isEdited: false, - whisperedTo: [], - isDeletedParentAction: false, - reactions: [], - }, - ], - originalMessage: { - html: 'ah', - lastModified: '2023-07-26 19:49:12.762', - }, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - created: '2023-07-26 19:49:12.762', - timestamp: 1690400952, - reportActionTimestamp: 1690400952762, - automatic: false, - actionName: 'ADDCOMMENT', - shouldShow: true, - reportActionID: '1433103421804347060', - previousReportActionID: '8774157052628183778', - lastModified: '2023-07-26 19:49:12.762', - whisperedToAccountIDs: [], - }, - }, - }, - { - onyxMethod: 'mergecollection', - key: 'reportActionsReactions_', - value: { - reportActionsReactions_2658221912430757962: { - heart: { - createdAt: '2023-08-25 12:37:45', - users: { - 12883048: { - skinTones: { - '-1': '2023-08-25 12:37:45', - }, - }, - }, - }, - }, - }, - }, - { - onyxMethod: 'merge', - key: 'personalDetailsList', - value: { - 14567013: { - accountID: 14567013, - avatar: 'https://d1wpcgnaa73g0y.cloudfront.net/49a4c96c366f9a32905b30462f91ea39e5eee5e8_128.jpeg', - displayName: 'Terry Hightraffic1337', - firstName: 'Terry', - lastName: 'Hightraffic1337', - status: null, - login: 'terry+hightraffic@margelo.io', - pronouns: '', - timezone: { - automatic: true, - selected: 'Europe/Kyiv', - }, - phoneNumber: '', - validated: true, - }, - }, - }, - ], - jsonCode: 200, - requestID: '81b8b8509a7f5b54-VIE', -}); diff --git a/src/libs/E2E/apiMocks/readNewestAction.ts b/src/libs/E2E/apiMocks/readNewestAction.ts deleted file mode 100644 index eb3800a98b81..000000000000 --- a/src/libs/E2E/apiMocks/readNewestAction.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type Response from '@src/types/onyx/Response'; - -export default (): Response => ({ - jsonCode: 200, - requestID: '81b8c48e3bfe5a84-VIE', - onyxData: [ - { - onyxMethod: 'merge', - key: 'report_98345625', - value: { - lastReadTime: '2023-10-25 07:32:48.915', - }, - }, - ], -}); diff --git a/src/libs/E2E/apiMocks/signinUser.ts b/src/libs/E2E/apiMocks/signinUser.ts deleted file mode 100644 index 7063e56f94be..000000000000 --- a/src/libs/E2E/apiMocks/signinUser.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type {SigninParams} from '@libs/E2E/types'; -import type Response from '@src/types/onyx/Response'; - -const signinUser = ({email}: SigninParams): Response => ({ - onyxData: [ - { - onyxMethod: 'merge', - key: 'session', - value: { - authToken: 'fakeAuthToken', - accountID: 12313081, - email, - encryptedAuthToken: 'fakeEncryptedAuthToken', - }, - }, - { - onyxMethod: 'set', - key: 'shouldShowComposeInput', - value: true, - }, - { - onyxMethod: 'merge', - key: 'credentials', - value: { - autoGeneratedLogin: 'fake', - autoGeneratedPassword: 'fake', - }, - }, - { - onyxMethod: 'merge', - key: 'user', - value: { - isUsingExpensifyCard: false, - }, - }, - { - onyxMethod: 'set', - key: 'betas', - value: ['all'], - }, - { - onyxMethod: 'merge', - key: 'account', - value: { - requiresTwoFactorAuth: false, - }, - }, - ], - jsonCode: 200, - requestID: '783e5f3cadfbcfc0-SJC', -}); - -export default signinUser; diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 472567cc6c1d..265c55c4a230 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -1,5 +1,6 @@ import Config from '../../../tests/e2e/config'; import Routes from '../../../tests/e2e/server/routes'; +import type {NetworkCacheMap, TestConfig} from './types'; type TestResult = { name: string; @@ -9,10 +10,6 @@ type TestResult = { renderCount?: number; }; -type TestConfig = { - name: string; -}; - type NativeCommandPayload = { text: string; }; @@ -24,26 +21,31 @@ type NativeCommand = { const SERVER_ADDRESS = `http://localhost:${Config.SERVER_PORT}`; -/** - * Submits a test result to the server. - * Note: a test can have multiple test results. - */ -const submitTestResults = (testResult: TestResult): Promise => { - console.debug(`[E2E] Submitting test result '${testResult.name}'…`); - return fetch(`${SERVER_ADDRESS}${Routes.testResults}`, { +const defaultHeaders = { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'X-E2E-Server-Request': 'true', +}; + +const defaultRequestInit: RequestInit = { + headers: defaultHeaders, +}; + +const sendRequest = (url: string, data: Record): Promise => + fetch(url, { method: 'POST', headers: { // eslint-disable-next-line @typescript-eslint/naming-convention 'Content-Type': 'application/json', + ...defaultHeaders, }, - body: JSON.stringify(testResult), + body: JSON.stringify(data), }).then((res) => { if (res.status === 200) { - console.debug(`[E2E] Test result '${testResult.name}' submitted successfully`); - return; + return res; } - const errorMsg = `Test result submission failed with status code ${res.status}`; - res.json() + const errorMsg = `[E2E] Client failed to send request to "${url}". Returned status: ${res.status}`; + return res + .json() .then((responseText) => { throw new Error(`${errorMsg}: ${responseText}`); }) @@ -51,14 +53,24 @@ const submitTestResults = (testResult: TestResult): Promise => { throw new Error(errorMsg); }); }); + +/** + * Submits a test result to the server. + * Note: a test can have multiple test results. + */ +const submitTestResults = (testResult: TestResult): Promise => { + console.debug(`[E2E] Submitting test result '${testResult.name}'…`); + return sendRequest(`${SERVER_ADDRESS}${Routes.testResults}`, testResult).then(() => { + console.debug(`[E2E] Test result '${testResult.name}' submitted successfully`); + }); }; -const submitTestDone = () => fetch(`${SERVER_ADDRESS}${Routes.testDone}`); +const submitTestDone = () => fetch(`${SERVER_ADDRESS}${Routes.testDone}`, defaultRequestInit); let currentActiveTestConfig: TestConfig | null = null; const getTestConfig = (): Promise => - fetch(`${SERVER_ADDRESS}${Routes.testConfig}`) + fetch(`${SERVER_ADDRESS}${Routes.testConfig}`, defaultRequestInit) .then((res: Response): Promise => res.json()) .then((config: TestConfig) => { currentActiveTestConfig = config; @@ -67,27 +79,30 @@ const getTestConfig = (): Promise => const getCurrentActiveTestConfig = () => currentActiveTestConfig; -const sendNativeCommand = (payload: NativeCommand) => - fetch(`${SERVER_ADDRESS}${Routes.testNativeCommand}`, { - method: 'POST', - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }).then((res) => { - if (res.status === 200) { - return true; - } - const errorMsg = `Sending native command failed with status code ${res.status}`; - res.json() - .then((responseText) => { - throw new Error(`${errorMsg}: ${responseText}`); - }) - .catch(() => { - throw new Error(errorMsg); - }); +const sendNativeCommand = (payload: NativeCommand) => { + console.debug(`[E2E] Sending native command '${payload.actionName}'…`); + return sendRequest(`${SERVER_ADDRESS}${Routes.testNativeCommand}`, payload).then(() => { + console.debug(`[E2E] Native command '${payload.actionName}' sent successfully`); + }); +}; + +const updateNetworkCache = (appInstanceId: string, networkCache: NetworkCacheMap) => { + console.debug('[E2E] Updating network cache…'); + return sendRequest(`${SERVER_ADDRESS}${Routes.testUpdateNetworkCache}`, { + appInstanceId, + cache: networkCache, + }).then(() => { + console.debug('[E2E] Network cache updated successfully'); }); +}; + +const getNetworkCache = (appInstanceId: string): Promise => + sendRequest(`${SERVER_ADDRESS}${Routes.testGetNetworkCache}`, {appInstanceId}) + .then((res): Promise => res.json()) + .then((networkCache: NetworkCacheMap) => { + console.debug('[E2E] Network cache fetched successfully'); + return networkCache; + }); export default { submitTestResults, @@ -95,4 +110,6 @@ export default { getTestConfig, getCurrentActiveTestConfig, sendNativeCommand, + updateNetworkCache, + getNetworkCache, }; diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index cbd63270e736..79276e7a5d75 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -8,10 +8,14 @@ import type {ValueOf} from 'type-fest'; import * as Metrics from '@libs/Metrics'; import Performance from '@libs/Performance'; +import Config from 'react-native-config'; import E2EConfig from '../../../tests/e2e/config'; import E2EClient from './client'; +import installNetworkInterceptor from './utils/NetworkInterceptor'; +import LaunchArgs from './utils/LaunchArgs'; +import type { TestConfig } from './types'; -type Tests = Record, () => void>; +type Tests = Record, (config: TestConfig) => void>; console.debug('=========================='); console.debug('==== Running e2e test ===='); @@ -22,6 +26,12 @@ if (!Metrics.canCapturePerformanceMetrics()) { throw new Error('Performance module not available! Please set CAPTURE_METRICS=true in your environment file!'); } +const appInstanceId = Config.E2E_BRANCH +if (!appInstanceId) { + throw new Error('E2E_BRANCH not set in environment file!'); +} + + // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, @@ -41,10 +51,18 @@ const appReady = new Promise((resolve) => { }); }); +// Install the network interceptor +installNetworkInterceptor( + () => E2EClient.getNetworkCache(appInstanceId), + (networkCache) => E2EClient.updateNetworkCache(appInstanceId, networkCache), + LaunchArgs.mockNetwork ?? false +) + E2EClient.getTestConfig() .then((config): Promise | undefined => { const test = tests[config.name]; if (!test) { + console.error(`[E2E] Test '${config.name}' not found`); // instead of throwing, report the error to the server, which is better for DX return E2EClient.submitTestResults({ name: config.name, @@ -57,7 +75,7 @@ E2EClient.getTestConfig() .then(() => { console.debug('[E2E] App is ready, running test…'); Performance.measureFailSafe('appStartedToReady', 'regularAppStart'); - test(); + test(config); }) .catch((error) => { console.error('[E2E] Error while waiting for app to become ready', error); diff --git a/src/libs/E2E/tests/appStartTimeTest.e2e.ts b/src/libs/E2E/tests/appStartTimeTest.e2e.ts index 6589e594dac6..5720af8b3641 100644 --- a/src/libs/E2E/tests/appStartTimeTest.e2e.ts +++ b/src/libs/E2E/tests/appStartTimeTest.e2e.ts @@ -1,6 +1,7 @@ import Config from 'react-native-config'; import type {PerformanceEntry} from 'react-native-performance'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import Performance from '@libs/Performance'; @@ -8,8 +9,10 @@ const test = () => { // check for login (if already logged in the action will simply resolve) E2ELogin().then((neededLogin) => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting metrics and submitting them…'); diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index ff948c298b4a..ef380f847c3f 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -1,34 +1,25 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import mockReport from '@libs/E2E/apiMocks/openReport'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; +import type {TestConfig} from '@libs/E2E/types'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -type ReportValue = { - reportID: string; -}; - -type OnyxData = { - value: ReportValue; -}; - -type MockReportResponse = { - onyxData: OnyxData[]; -}; - -const test = () => { +const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const report = mockReport() as MockReportResponse; - const {reportID} = report.onyxData[0].value; + const reportID = getConfigValueOrThrow('reportID', config); E2ELogin().then((neededLogin) => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting chat opening metrics and submitting them…'); diff --git a/src/libs/E2E/tests/openSearchPageTest.e2e.ts b/src/libs/E2E/tests/openSearchPageTest.e2e.ts index c68553d6de8a..86da851396f6 100644 --- a/src/libs/E2E/tests/openSearchPageTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchPageTest.e2e.ts @@ -1,5 +1,6 @@ import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; @@ -12,8 +13,10 @@ const test = () => { E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting search metrics and submitting them…'); diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 90d0dc9e0bb6..4e0678aeb020 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -1,7 +1,10 @@ import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; +import type {TestConfig} from '@libs/E2E/types'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import {getRerenderCount, resetRerenderCount} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e'; @@ -9,14 +12,18 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import * as NativeCommands from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; -const test = () => { +const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for typing'); + const reportID = getConfigValueOrThrow('reportID', config); + E2ELogin().then((neededLogin) => { if (neededLogin) { - // we don't want to submit the first login to the results - return E2EClient.submitTestDone(); + return waitForAppLoaded().then(() => + // we don't want to submit the first login to the results + E2EClient.submitTestDone(), + ); } console.debug('[E2E] Logged in, getting typing metrics and submitting them…'); @@ -27,7 +34,8 @@ const test = () => { } console.debug(`[E2E] Sidebar loaded, navigating to a report…`); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute('98345625')); + // Crowded Policy (Do Not Delete) Report, has a input bar available: + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); // Wait until keyboard is visible (so we are focused on the input): waitForKeyboard().then(() => { diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index fcdfa01d7132..2d48813fa115 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -4,4 +4,23 @@ type SigninParams = { type IsE2ETestSession = () => boolean; -export type {SigninParams, IsE2ETestSession}; +type NetworkCacheEntry = { + url: string; + options: RequestInit; + status: number; + statusText: string; + headers: Record; + body: string; +}; + +type NetworkCacheMap = Record< + string, // hash + NetworkCacheEntry +>; + +type TestConfig = { + name: string; + [key: string]: string; +}; + +export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig}; diff --git a/src/libs/E2E/utils/LaunchArgs.ts b/src/libs/E2E/utils/LaunchArgs.ts new file mode 100644 index 000000000000..4e452d766eff --- /dev/null +++ b/src/libs/E2E/utils/LaunchArgs.ts @@ -0,0 +1,8 @@ +import {LaunchArguments} from 'react-native-launch-arguments'; + +type ExpectedArgs = { + mockNetwork?: boolean; +}; +const LaunchArgs = LaunchArguments.value(); + +export default LaunchArgs; diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts new file mode 100644 index 000000000000..3a4a48f7db53 --- /dev/null +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -0,0 +1,171 @@ +/* eslint-disable @lwc/lwc/no-async-await */ +import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types'; + +const LOG_TAG = `[E2E][NetworkInterceptor]`; +// Requests with these headers will be ignored: +const IGNORE_REQUEST_HEADERS = ['X-E2E-Server-Request']; + +let globalResolveIsNetworkInterceptorInstalled: () => void; +let globalRejectIsNetworkInterceptorInstalled: (error: Error) => void; +const globalIsNetworkInterceptorInstalledPromise = new Promise((resolve, reject) => { + globalResolveIsNetworkInterceptorInstalled = resolve; + globalRejectIsNetworkInterceptorInstalled = reject; +}); +let networkCache: NetworkCacheMap | null = null; + +/** + * The headers of a fetch request can be passed as an array of tuples or as an object. + * This function converts the headers to an object. + */ +function getFetchRequestHeadersAsObject(fetchRequest: RequestInit): Record { + const headers: Record = {}; + if (Array.isArray(fetchRequest.headers)) { + fetchRequest.headers.forEach(([key, value]) => { + headers[key] = value; + }); + } else if (typeof fetchRequest.headers === 'object') { + Object.entries(fetchRequest.headers).forEach(([key, value]) => { + headers[key] = value; + }); + } + return headers; +} + +/** + * This function extracts the RequestInit from the arguments of fetch. + * It is needed because the arguments can be passed in different ways. + */ +function fetchArgsGetRequestInit(args: Parameters): RequestInit { + const [firstArg, secondArg] = args; + if (typeof firstArg === 'string' || (typeof firstArg === 'object' && firstArg instanceof URL)) { + if (secondArg == null) { + return {}; + } + return secondArg; + } + return firstArg; +} + +/** + * This function extracts the url from the arguments of fetch. + */ +function fetchArgsGetUrl(args: Parameters): string { + const [firstArg] = args; + if (typeof firstArg === 'string') { + return firstArg; + } + if (typeof firstArg === 'object' && firstArg instanceof URL) { + return firstArg.href; + } + if (typeof firstArg === 'object' && firstArg instanceof Request) { + return firstArg.url; + } + throw new Error('Could not get url from fetch args'); +} + +/** + * This function transforms a NetworkCacheEntry (internal representation) to a (fetch) Response. + */ +function networkCacheEntryToResponse({headers, status, statusText, body}: NetworkCacheEntry): Response { + // Transform headers to Headers object: + const newHeaders = new Headers(); + Object.entries(headers).forEach(([key, value]) => { + newHeaders.append(key, value); + }); + + return new Response(body, { + status, + statusText, + headers: newHeaders, + }); +} + +/** + * This function hashes the arguments of fetch. + */ +function hashFetchArgs(args: Parameters) { + const url = fetchArgsGetUrl(args); + const options = fetchArgsGetRequestInit(args); + const headers = getFetchRequestHeadersAsObject(options); + // Note: earlier we were using the body value as well, however + // the body for the same request might be different due to including + // times or app versions. + return `${url}${JSON.stringify(headers)}`; +} + +/** + * Install a network interceptor by overwriting the global fetch function: + * - Overwrites fetch globally with a custom implementation + * - For each fetch request we cache the request and the response + * - The cache is send to the test runner server to persist the network cache in between sessions + * - On e2e test start the network cache is requested and loaded + * - If a fetch request is already in the NetworkInterceptors cache instead of making a real API request the value from the cache is used. + */ +export default function installNetworkInterceptor( + getNetworkCache: () => Promise, + updateNetworkCache: (networkCache: NetworkCacheMap) => Promise, + shouldReturnRecordedResponse: boolean, +) { + console.debug(LOG_TAG, 'installing with shouldReturnRecordedResponse:', shouldReturnRecordedResponse); + const originalFetch = global.fetch; + + if (networkCache == null && shouldReturnRecordedResponse) { + console.debug(LOG_TAG, 'fetching network cache …'); + getNetworkCache() + .then((newCache) => { + networkCache = newCache; + globalResolveIsNetworkInterceptorInstalled(); + console.debug(LOG_TAG, 'network cache fetched!'); + }, globalRejectIsNetworkInterceptorInstalled) + .catch(globalRejectIsNetworkInterceptorInstalled); + } else { + networkCache = {}; + globalResolveIsNetworkInterceptorInstalled(); + } + + global.fetch = async (...args: Parameters) => { + const options = fetchArgsGetRequestInit(args); + const headers = getFetchRequestHeadersAsObject(options); + const url = fetchArgsGetUrl(args); + // Check if headers contain any of the ignored headers, or if react native metro server: + if (IGNORE_REQUEST_HEADERS.some((header) => headers[header] != null) || url.includes('8081')) { + return originalFetch(...args); + } + + await globalIsNetworkInterceptorInstalledPromise; + + const hash = hashFetchArgs(args); + const cachedResponse = networkCache?.[hash]; + if (shouldReturnRecordedResponse && cachedResponse != null) { + const response = networkCacheEntryToResponse(cachedResponse); + console.debug(LOG_TAG, 'Returning recorded response for url:', url); + return Promise.resolve(response); + } + if (shouldReturnRecordedResponse) { + console.debug('!!! Missed cache hit for url:', url); + } + + return originalFetch(...args) + .then(async (res) => { + if (networkCache != null) { + const body = await res.clone().text(); + networkCache[hash] = { + url, + options, + body, + headers: getFetchRequestHeadersAsObject(options), + status: res.status, + statusText: res.statusText, + }; + console.debug(LOG_TAG, 'Updating network cache for url:', url); + // Send the network cache to the test server: + return updateNetworkCache(networkCache).then(() => res); + } + return res; + }) + .then((res) => { + console.debug(LOG_TAG, 'Network cache updated!'); + return res; + }); + }; +} diff --git a/src/libs/E2E/utils/getConfigValueOrThrow.ts b/src/libs/E2E/utils/getConfigValueOrThrow.ts new file mode 100644 index 000000000000..a694d6709ed6 --- /dev/null +++ b/src/libs/E2E/utils/getConfigValueOrThrow.ts @@ -0,0 +1,12 @@ +import Config from 'react-native-config'; + +/** + * Gets a config value or throws an error if the value is not defined. + */ +export default function getConfigValueOrThrow(key: string, config = Config): string { + const value = config[key]; + if (value == null) { + throw new Error(`Missing config value for ${key}`); + } + return value; +} diff --git a/src/libs/Environment/betaChecker/index.android.ts b/src/libs/Environment/betaChecker/index.android.ts index aeb1527457f7..4b912e0daaa5 100644 --- a/src/libs/Environment/betaChecker/index.android.ts +++ b/src/libs/Environment/betaChecker/index.android.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import semver from 'semver'; -import * as AppUpdate from '@userActions/AppUpdate'; +import * as AppUpdate from '@libs/actions/AppUpdate'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import pkg from '../../../../package.json'; diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 159a5817189b..2466a262b4b9 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -38,7 +38,7 @@ function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatO * Method used to get an error object with microsecond as the key. * @param error - error key or message to be saved */ -function getMicroSecondOnyxError(error: string): Record { +function getMicroSecondOnyxError(error: string): Errors { return {[DateUtils.getMicroseconds()]: error}; } @@ -51,7 +51,7 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: TOnyxData): string { @@ -98,7 +98,7 @@ type ErrorsList = Record; /** * Method used to generate error message for given inputID - * @param errorList - An object containing current errors in the form + * @param errors - An object containing current errors in the form * @param message - Message to assign to the inputID errors */ function addErrorMessage(errors: ErrorsList, inputID?: string, message?: TKey) { diff --git a/src/libs/FormUtils.ts b/src/libs/FormUtils.ts index 5366e149728e..37241df49af7 100644 --- a/src/libs/FormUtils.ts +++ b/src/libs/FormUtils.ts @@ -1,7 +1,4 @@ -import type {OnyxFormKey} from '@src/ONYXKEYS'; - -type ExcludeDraft = T extends `${string}Draft` ? never : T; -type OnyxFormKeyWithoutDraft = ExcludeDraft; +import type {OnyxFormKeyWithoutDraft} from '@components/Form/types'; function getDraftKey(formID: OnyxFormKeyWithoutDraft): `${OnyxFormKeyWithoutDraft}Draft` { return `${formID}Draft`; diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 22e342ac847b..cf35cb6c4d29 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -6,6 +6,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RequestType} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import * as NetworkActions from './actions/Network'; +import * as UpdateRequired from './actions/UpdateRequired'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from './API/types'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; @@ -29,7 +31,7 @@ let cancellationController = new AbortController(); /** * The API commands that require the skew calculation */ -const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; +const addSkewList: string[] = [SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, READ_COMMANDS.OPEN_APP]; /** * Regex to get API command from the command @@ -128,6 +130,10 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form alert('Too many auth writes', message); } } + if (response.jsonCode === CONST.JSON_CODE.UPDATE_REQUIRED) { + // Trigger a modal and disable the app as the user needs to upgrade to the latest minimum version to continue + UpdateRequired.alertUser(); + } return response as Promise; }); } diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 11dd0f5badda..c0fb4c6195b1 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -3,9 +3,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, Transaction} from '@src/types/onyx'; -import * as IOU from './actions/IOU'; import * as CurrencyUtils from './CurrencyUtils'; -import * as FileUtils from './fileDownload/FileUtils'; import Navigation from './Navigation/Navigation'; import * as TransactionUtils from './TransactionUtils'; @@ -24,32 +22,6 @@ function navigateToStartMoneyRequestStep(requestType: ValueOf void; -// eslint-disable-next-line rulesdir/no-negated-variables -function navigateToStartStepIfScanFileCannotBeRead( - receiptFilename: string, - receiptPath: string, - onSuccess: SuccessCallback, - requestType: ValueOf, - iouType: ValueOf, - transactionID: string, - reportID: string, -) { - if (!receiptFilename || !receiptPath) { - return; - } - - const onFailure = () => { - IOU.setMoneyRequestReceipt(transactionID, '', '', true); - if (requestType === CONST.IOU.REQUEST_TYPE.MANUAL) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); - return; - } - navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); - }; - FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure); -} - /** * Calculates the amount per user given a list of participants * @@ -127,4 +99,4 @@ function isValidMoneyRequestType(iouType: string): boolean { return moneyRequestType.includes(iouType); } -export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; +export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep}; diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index dca84b9b11e0..3781890013eb 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -59,4 +59,14 @@ function getPhoneLogin(partnerUserID: string): string { return appendCountryCode(getPhoneNumberWithoutSpecialChars(partnerUserID)); } -export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin}; +/** + * Check whether 2 emails have the same private domain + */ +function areEmailsFromSamePrivateDomain(email1: string, email2: string): boolean { + if (isEmailPublicDomain(email1) || isEmailPublicDomain(email2)) { + return false; + } + return Str.extractEmailDomain(email1).toLowerCase() === Str.extractEmailDomain(email2).toLowerCase(); +} + +export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain}; diff --git a/src/libs/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts index 27a904f692ed..97f4a21866c5 100644 --- a/src/libs/Middleware/Logging.ts +++ b/src/libs/Middleware/Logging.ts @@ -1,3 +1,4 @@ +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import type Request from '@src/types/onyx/Request'; @@ -87,7 +88,7 @@ const Logging: Middleware = (response, request) => { // This error seems to only throw on dev when localhost:8080 tries to access the production web server. It's unclear whether this can happen on production or if // it's a sign that the web server is down. Log.hmmm('[Network] API request error: Gateway Timeout error', logParams); - } else if (request.command === 'AuthenticatePusher') { + } else if (request.command === SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER) { // AuthenticatePusher requests can return with fetch errors and no message. It happens because we return a non 200 header like 403 Forbidden. // This is common to see if we are subscribing to a bad channel related to something the user shouldn't be able to access. There's no additional information // we can get about these requests. diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index a9182745098b..8e357b0f2251 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -1,3 +1,4 @@ +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as OnyxUpdates from '@userActions/OnyxUpdates'; import CONST from '@src/CONST'; @@ -6,7 +7,7 @@ import type Middleware from './types'; // If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of // date because all these requests are updating the app to the most current state. -const requestsToIgnoreLastUpdateID = ['OpenApp', 'ReconnectApp', 'GetMissingOnyxMessages']; +const requestsToIgnoreLastUpdateID: string[] = [READ_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]; const SaveResponseInOnyx: Middleware = (requestResponse, request) => requestResponse.then((response = {}) => { diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 548751cbd8d1..dd6de59b6175 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTags, ReportAction} from '@src/types/onyx'; @@ -95,12 +96,12 @@ function getForDistanceRequest(newDistance: string, oldDistance: string, newAmou * ModifiedExpense::getNewDotComment in Web-Expensify should match this. * If we change this function be sure to update the backend as well. */ -function getForReportAction(reportAction: ReportAction): string { - if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { +function getForReportAction(reportID: string | undefined, reportAction: OnyxEntry): string { + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { return ''; } - const reportActionOriginalMessage = reportAction.originalMessage as ExpenseOriginalMessage | undefined; - const policyID = ReportUtils.getReportPolicyID(reportAction.reportID) ?? ''; + const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; + const policyID = ReportUtils.getReportPolicyID(reportID) ?? ''; const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts index 020719e2dc36..0ca5417e9f6e 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts @@ -1,6 +1,7 @@ import type {NavigationState, PartialState, RouterConfigOptions, StackNavigationState} from '@react-navigation/native'; import {StackRouter} from '@react-navigation/native'; import type {ParamListBase} from '@react-navigation/routers'; +import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import type {ResponsiveStackNavigatorRouterOptions} from './types'; @@ -22,6 +23,17 @@ const getTopMostReportIDFromRHP = (state: State): string => { const topmostRoute = state.routes.at(-1); + // In the case of money requests, send money and split bill, + // we want to ignore the associated report and fall back to the default navigation behavior + if ( + topmostRoute?.params && + 'iouType' in topmostRoute.params && + typeof topmostRoute.params.iouType === 'string' && + (topmostRoute.params.iouType === CONST.IOU.TYPE.REQUEST || topmostRoute.params.iouType === CONST.IOU.TYPE.SEND || topmostRoute.params.iouType === CONST.IOU.TYPE.SPLIT) + ) { + return ''; + } + if (topmostRoute?.state) { return getTopMostReportIDFromRHP(topmostRoute.state); } diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 91285821fe9f..83890848b8f7 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -184,7 +184,7 @@ function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopT } const isCentralPaneFocused = findFocusedRoute(navigationRef.current.getState())?.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR; - const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute); + const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? ''); // Allow CentralPane to use UP with fallback route if the path is not found in root navigator. if (isCentralPaneFocused && fallbackRoute && distanceFromPathInRootNavigator === -1) { diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index b5466a9bbc2f..2ae3414956a8 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -3,7 +3,7 @@ import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs import type {EventMapCore, NavigationState, ScreenListeners} from '@react-navigation/native'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; diff --git a/src/libs/Network/NetworkStore.ts b/src/libs/Network/NetworkStore.ts index 59a52dfd01c4..5b93c9adc11a 100644 --- a/src/libs/Network/NetworkStore.ts +++ b/src/libs/Network/NetworkStore.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type Credentials from '@src/types/onyx/Credentials'; @@ -95,7 +96,7 @@ function getAuthToken(): string | null { } function isSupportRequest(command: string): boolean { - return ['OpenApp', 'ReconnectApp', 'OpenReport'].includes(command); + return [READ_COMMANDS.OPEN_APP, SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT].some((cmd) => cmd === command); } function getSupportAuthToken(): string | null { diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index e65bd3d0021f..911e665f3ff4 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -2,9 +2,9 @@ import Str from 'expensify-common/lib/str'; import type {ImageSourcePropType} from 'react-native'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; +import * as AppUpdate from '@libs/actions/AppUpdate'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import * as ReportUtils from '@libs/ReportUtils'; -import * as AppUpdate from '@userActions/AppUpdate'; import type {Report, ReportAction} from '@src/types/onyx'; import focusApp from './focusApp'; import type {LocalNotificationClickHandler, LocalNotificationData} from './types'; @@ -109,7 +109,7 @@ export default { pushModifiedExpenseNotification(report: Report, reportAction: ReportAction, onClick: LocalNotificationClickHandler, usesIcon = false) { const title = reportAction.person?.map((f) => f.text).join(', ') ?? ''; - const body = ModifiedExpenseMessage.getForReportAction(reportAction); + const body = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); const icon = usesIcon ? EXPENSIFY_ICON_URL : ''; const data = { reportID: report.reportID, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.ts similarity index 61% rename from src/libs/OptionsListUtils.js rename to src/libs/OptionsListUtils.ts index d44df3c6c39c..812ebb051624 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.ts @@ -1,12 +1,23 @@ /* eslint-disable no-continue */ import Str from 'expensify-common/lib/str'; +// eslint-disable-next-line you-dont-need-lodash-underscore/get import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; +import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyCategories, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import times from '@src/utils/times'; import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; @@ -26,41 +37,139 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +type Tag = { + enabled: boolean; + name: string; + accountID: number | null; +}; + +type Option = Partial; + +type PayeePersonalDetails = { + text: string; + alternateText: string; + icons: OnyxCommon.Icon[]; + descriptiveText: string; + login: string; + accountID: number; +}; + +type CategorySection = { + title: string | undefined; + shouldShow: boolean; + indexOffset: number; + data: Option[]; +}; + +type Category = { + name: string; + enabled: boolean; +}; + +type Hierarchy = Record; + +type GetOptionsConfig = { + reportActions?: ReportActions; + betas?: Beta[]; + selectedOptions?: Option[]; + maxRecentReportsToShow?: number; + excludeLogins?: string[]; + includeMultipleParticipantReports?: boolean; + includePersonalDetails?: boolean; + includeRecentReports?: boolean; + sortByReportTypeInSearch?: boolean; + searchInputValue?: string; + showChatPreviewLine?: boolean; + sortPersonalDetailsByAlphaAsc?: boolean; + forcePolicyNamePreview?: boolean; + includeOwnedWorkspaceChats?: boolean; + includeThreads?: boolean; + includeTasks?: boolean; + includeMoneyRequests?: boolean; + excludeUnknownUsers?: boolean; + includeP2P?: boolean; + includeCategories?: boolean; + categories?: PolicyCategories; + recentlyUsedCategories?: string[]; + includeTags?: boolean; + tags?: Record; + recentlyUsedTags?: string[]; + canInviteUser?: boolean; + includeSelectedOptions?: boolean; + includePolicyTaxRates?: boolean; + policyTaxRates?: PolicyTaxRateWithDefault; + transactionViolations?: OnyxCollection; +}; + +type MemberForList = { + text: string; + alternateText: string | null; + keyForList: string | null; + isSelected: boolean; + isDisabled: boolean | null; + accountID?: number | null; + login: string | null; + rightElement: React.ReactNode | null; + icons?: OnyxCommon.Icon[]; + pendingAction?: OnyxCommon.PendingAction; +}; + +type SectionForSearchTerm = { + section: CategorySection; + newIndexOffset: number; +}; + +type GetOptions = { + recentReports: ReportUtils.OptionData[]; + personalDetails: ReportUtils.OptionData[]; + userToInvite: ReportUtils.OptionData | null; + currentUserOption: ReportUtils.OptionData | null | undefined; + categoryOptions: CategorySection[]; + tagOptions: CategorySection[]; + policyTaxRatesOptions: CategorySection[]; +}; + +type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}; + /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can * be configured to display different results based on the options passed to the private getOptions() method. Public * methods should be named for the views they build options for and then exported for use in a component. */ - -let currentUserLogin; -let currentUserAccountID; +let currentUserLogin: string | undefined; +let currentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserLogin = val && val.email; - currentUserAccountID = val && val.accountID; + callback: (value) => { + currentUserLogin = value?.email; + currentUserAccountID = value?.accountID; }, }); -let loginList; +let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, - callback: (val) => (loginList = _.isEmpty(val) ? {} : val), + callback: (value) => (loginList = isEmptyObject(value) ? {} : value), }); -let allPersonalDetails; +let allPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = _.isEmpty(val) ? {} : val), + callback: (value) => (allPersonalDetails = isEmptyObject(value) ? {} : value), }); -let preferredLocale; +let preferredLocale: DeepValueOf = CONST.LOCALES.DEFAULT; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (val) => (preferredLocale = val || CONST.LOCALES.DEFAULT), + callback: (value) => { + if (!value) { + return; + } + preferredLocale = value; + }, }); -const policies = {}; +const policies: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, callback: (policy, key) => { @@ -72,10 +181,10 @@ Onyx.connect({ }, }); -const lastReportActions = {}; -const allSortedReportActions = {}; -const allReportActions = {}; -const visibleReportActionItems = {}; +const lastReportActions: ReportActions = {}; +const allSortedReportActions: Record = {}; +const allReportActions: Record = {}; +const visibleReportActionItems: ReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { @@ -84,14 +193,13 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); allReportActions[reportID] = actions; - const sortedReportActions = ReportActionUtils.getSortedReportActions(_.toArray(actions), true); + const sortedReportActions = ReportActionUtils.getSortedReportActions(Object.values(actions), true); allSortedReportActions[reportID] = sortedReportActions; - lastReportActions[reportID] = _.first(sortedReportActions); + lastReportActions[reportID] = sortedReportActions[0]; // The report is only visible if it is the last action not deleted that // does not match a closed or created state. - const reportActionsForDisplay = _.filter( - sortedReportActions, + const reportActionsForDisplay = sortedReportActions.filter( (reportAction, actionKey) => ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey) && !ReportActionUtils.isWhisperAction(reportAction) && @@ -102,7 +210,7 @@ Onyx.connect({ }, }); -const policyExpenseReports = {}; +const policyExpenseReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { @@ -113,81 +221,80 @@ Onyx.connect({ }, }); -let allTransactions = {}; +let allTransactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, - callback: (val) => { - if (!val) { + callback: (value) => { + if (!value) { return; } - allTransactions = _.pick(val, (transaction) => transaction); + + allTransactions = Object.keys(value) + .filter((key) => !!value[key]) + .reduce((result: OnyxCollection, key) => { + if (result) { + // eslint-disable-next-line no-param-reassign + result[key] = value[key]; + } + return result; + }, {}); }, }); /** * Adds expensify SMS domain (@expensify.sms) if login is a phone number and if it's not included yet - * - * @param {String} login - * @return {String} */ -function addSMSDomainIfPhoneNumber(login) { +function addSMSDomainIfPhoneNumber(login: string): string { const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(login); if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) { - return parsedPhoneNumber.number.e164 + CONST.SMS.DOMAIN; + return parsedPhoneNumber.number?.e164 + CONST.SMS.DOMAIN; } return login; } /** - * Returns avatar data for a list of user accountIDs - * - * @param {Array} accountIDs - * @param {Object} personalDetails - * @param {Object} defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in - * @returns {Object} + * @param defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in + * @returns Returns avatar data for a list of user accountIDs */ -function getAvatarsForAccountIDs(accountIDs, personalDetails, defaultValues = {}) { - const reversedDefaultValues = {}; - _.map(Object.entries(defaultValues), (item) => { +function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntry, defaultValues: Record = {}): OnyxCommon.Icon[] { + const reversedDefaultValues: Record = {}; + + Object.entries(defaultValues).forEach((item) => { reversedDefaultValues[item[1]] = item[0]; }); - - return _.map(accountIDs, (accountID) => { - const login = lodashGet(reversedDefaultValues, accountID, ''); - const userPersonalDetail = lodashGet(personalDetails, accountID, {login, accountID, avatar: ''}); + return accountIDs.map((accountID) => { + const login = reversedDefaultValues[accountID] ?? ''; + const userPersonalDetail = personalDetails?.[accountID] ?? {login, accountID, avatar: ''}; return { id: accountID, source: UserUtils.getAvatar(userPersonalDetail.avatar, userPersonalDetail.accountID), type: CONST.ICON_TYPE_AVATAR, - name: userPersonalDetail.login, + name: userPersonalDetail.login ?? '', }; }); } /** * Returns the personal details for an array of accountIDs - * - * @param {Array} accountIDs - * @param {Object | null} personalDetails - * @returns {Object} – keys of the object are emails, values are PersonalDetails objects. + * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { - const personalDetailsForAccountIDs = {}; +function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, personalDetails: OnyxEntry): PersonalDetailsList { + const personalDetailsForAccountIDs: PersonalDetailsList = {}; if (!personalDetails) { return personalDetailsForAccountIDs; } - _.each(accountIDs, (accountID) => { + accountIDs?.forEach((accountID) => { const cleanAccountID = Number(accountID); if (!cleanAccountID) { return; } - let personalDetail = personalDetails[accountID]; + let personalDetail: OnyxEntry = personalDetails[accountID]; if (!personalDetail) { personalDetail = { avatar: UserUtils.getDefaultAvatar(cleanAccountID), - }; + } as PersonalDetails; } if (cleanAccountID === CONST.ACCOUNT_ID.CONCIERGE) { @@ -202,58 +309,52 @@ function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { /** * Return true if personal details data is ready, i.e. report list options can be created. - * @param {Object} personalDetails - * @returns {Boolean} */ -function isPersonalDetailsReady(personalDetails) { - return !_.isEmpty(personalDetails) && _.some(_.keys(personalDetails), (key) => personalDetails[key].accountID); +function isPersonalDetailsReady(personalDetails: OnyxEntry): boolean { + const personalDetailsKeys = Object.keys(personalDetails ?? {}); + return personalDetailsKeys.some((key) => personalDetails?.[key]?.accountID); } /** * Get the participant option for a report. - * @param {Object} participant - * @param {Array} personalDetails - * @returns {Object} */ -function getParticipantsOption(participant, personalDetails) { - const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; - const login = detail.login || participant.login; +function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { + const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const login = detail?.login || participant.login || ''; const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login)); return { - keyForList: String(detail.accountID), + keyForList: String(detail?.accountID), login, - accountID: detail.accountID, + accountID: detail?.accountID ?? -1, text: displayName, - firstName: lodashGet(detail, 'firstName', ''), - lastName: lodashGet(detail, 'lastName', ''), + firstName: detail?.firstName ?? '', + lastName: detail?.lastName ?? '', alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, icons: [ { - source: UserUtils.getAvatar(detail.avatar, detail.accountID), + source: UserUtils.getAvatar(detail?.avatar ?? '', detail?.accountID ?? -1), name: login, type: CONST.ICON_TYPE_AVATAR, - id: detail.accountID, + id: detail?.accountID, }, ], - phoneNumber: lodashGet(detail, 'phoneNumber', ''), + phoneNumber: detail?.phoneNumber ?? '', selected: participant.selected, isSelected: participant.selected, - searchText: participant.searchText, + searchText: participant.searchText ?? undefined, }; } /** * Constructs a Set with all possible names (displayName, firstName, lastName, email) for all participants in a report, * to be used in isSearchStringMatch. - * - * @param {Array} personalDetailList - * @return {Set} */ -function getParticipantNames(personalDetailList) { +function getParticipantNames(personalDetailList?: Array> | null): Set { // We use a Set because `Set.has(value)` on a Set of with n entries is up to n (or log(n)) times faster than // `_.contains(Array, value)` for an Array with n members. - const participantNames = new Set(); - _.each(personalDetailList, (participant) => { + const participantNames = new Set(); + personalDetailList?.forEach((participant) => { if (participant.login) { participantNames.add(participant.login.toLowerCase()); } @@ -273,21 +374,19 @@ function getParticipantNames(personalDetailList) { /** * A very optimized method to remove duplicates from an array. * Taken from https://stackoverflow.com/a/9229821/9114791 - * - * @param {Array} items - * @returns {Array} */ -function uniqFast(items) { - const seenItems = {}; - const result = []; +function uniqFast(items: string[]): string[] { + const seenItems: Record = {}; + const result: string[] = []; let j = 0; - for (let i = 0; i < items.length; i++) { - const item = items[i]; + + for (const item of items) { if (seenItems[item] !== 1) { seenItems[item] = 1; result[j++] = item; } } + return result; } @@ -298,21 +397,19 @@ function uniqFast(items) { * This method must be incredibly performant. It was found to be a big performance bottleneck * when dealing with accounts that have thousands of reports. For loops are more efficient than _.each * Array.prototype.push.apply is faster than using the spread operator, and concat() is faster than push(). - * - * @param {Object} report - * @param {String} reportName - * @param {Array} personalDetailList - * @param {Boolean} isChatRoomOrPolicyExpenseChat - * @param {Boolean} isThread - * @return {String} + */ -function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolicyExpenseChat, isThread) { - let searchTerms = []; +function getSearchText( + report: OnyxEntry, + reportName: string, + personalDetailList: Array>, + isChatRoomOrPolicyExpenseChat: boolean, + isThread: boolean, +): string { + let searchTerms: string[] = []; if (!isChatRoomOrPolicyExpenseChat) { - for (let i = 0; i < personalDetailList.length; i++) { - const personalDetail = personalDetailList[i]; - + for (const personalDetail of personalDetailList) { if (personalDetail.login) { // The regex below is used to remove dots only from the local part of the user email (local-part@domain) // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) @@ -333,18 +430,19 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic const chatRoomSubtitle = ReportUtils.getChatRoomSubtitle(report); Array.prototype.push.apply(searchTerms, title.split(/[,\s]/)); - Array.prototype.push.apply(searchTerms, chatRoomSubtitle.split(/[,\s]/)); + Array.prototype.push.apply(searchTerms, chatRoomSubtitle?.split(/[,\s]/) ?? ['']); } else if (isChatRoomOrPolicyExpenseChat) { const chatRoomSubtitle = ReportUtils.getChatRoomSubtitle(report); - Array.prototype.push.apply(searchTerms, chatRoomSubtitle.split(/[,\s]/)); + Array.prototype.push.apply(searchTerms, chatRoomSubtitle?.split(/[,\s]/) ?? ['']); } else { - const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs || []; - for (let i = 0; i < visibleChatMemberAccountIDs.length; i++) { - const accountID = visibleChatMemberAccountIDs[i]; - - if (allPersonalDetails[accountID] && allPersonalDetails[accountID].login) { - searchTerms = searchTerms.concat(allPersonalDetails[accountID].login); + const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; + if (allPersonalDetails) { + for (const accountID of visibleChatMemberAccountIDs) { + const login = allPersonalDetails[accountID]?.login; + if (login) { + searchTerms = searchTerms.concat(login); + } } } } @@ -355,122 +453,118 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. - * @param {Object} report - * @param {Object} reportActions - * @returns {Object} */ -function getAllReportErrors(report, reportActions) { - const reportErrors = report.errors || {}; - const reportErrorFields = report.errorFields || {}; - const reportActionErrors = _.reduce( - reportActions, - (prevReportActionErrors, action) => (!action || _.isEmpty(action.errors) ? prevReportActionErrors : _.extend(prevReportActionErrors, action.errors)), +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry): OnyxCommon.Errors { + const reportErrors = report?.errors ?? {}; + const reportErrorFields = report?.errorFields ?? {}; + const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce( + (prevReportActionErrors, action) => (!action || isEmptyObject(action.errors) ? prevReportActionErrors : {...prevReportActionErrors, ...action.errors}), {}, ); - - const parentReportAction = !report.parentReportID || !report.parentReportActionID ? {} : lodashGet(allReportActions, [report.parentReportID, report.parentReportActionID], {}); - - if (parentReportAction.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { - const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], ''); - const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {}; - if (TransactionUtils.hasMissingSmartscanFields(transaction) && !ReportUtils.isSettled(transaction.reportID)) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + const parentReportAction: OnyxEntry = + !report?.parentReportID || !report?.parentReportActionID ? null : allReportActions?.[report.parentReportID ?? '']?.[report.parentReportActionID ?? ''] ?? null; + + if (parentReportAction?.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { + const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); } - } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report.ownerAccountID === currentUserAccountID) { - if (ReportUtils.hasMissingSmartscanFields(report.reportID) && !ReportUtils.isSettled(report.reportID)) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { + if (ReportUtils.hasMissingSmartscanFields(report?.reportID ?? '') && !ReportUtils.isSettled(report?.reportID)) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); } - } else if (ReportUtils.hasSmartscanError(_.values(reportActions))) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + } else if (ReportUtils.hasSmartscanError(Object.values(reportActions ?? {}))) { + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); } - // All error objects related to the report. Each object in the sources contains error messages keyed by microtime const errorSources = { reportErrors, ...reportErrorFields, - reportActionErrors, + ...reportActionErrors, }; - // Combine all error messages keyed by microtime into one object - const allReportErrors = _.reduce(errorSources, (prevReportErrors, errors) => (_.isEmpty(errors) ? prevReportErrors : _.extend(prevReportErrors, errors)), {}); + const allReportErrors = Object.values(errorSources)?.reduce((prevReportErrors, errors) => (isEmptyObject(errors) ? prevReportErrors : {...prevReportErrors, ...errors}), {}); return allReportErrors; } /** * Get the last message text from the report directly or from other sources for special cases. - * @param {Object} report - * @returns {String} */ -function getLastMessageTextForReport(report) { - const lastReportAction = _.find(allSortedReportActions[report.reportID], (reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); +function getLastMessageTextForReport(report: OnyxEntry): string { + const lastReportAction = allSortedReportActions[report?.reportID ?? '']?.find((reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)) ?? null; let lastMessageTextFromReport = ''; - const lastActionName = lodashGet(lastReportAction, 'actionName', ''); + const lastActionName = lastReportAction?.actionName ?? ''; if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) { const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true, false, null, true); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage); } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) { const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); - const lastIOUMoneyReportAction = _.find( - allSortedReportActions[iouReport.reportID], + const lastIOUMoneyReportAction = allSortedReportActions[iouReport?.reportID ?? '']?.find( (reportAction, key) => ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); - lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(iouReport, lastIOUMoneyReportAction, true, ReportUtils.isChatReport(report), null, true); + lastMessageTextFromReport = ReportUtils.getReportPreviewMessage( + !isEmptyObject(iouReport) ? iouReport : null, + lastIOUMoneyReportAction, + true, + ReportUtils.isChatReport(report), + null, + true, + ); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report); } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); - } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { - lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; + } else if (ReportUtils.isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, translationKey: report?.lastMessageTranslationKey, type: ''})) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + lastMessageTextFromReport = `[${Localize.translateLocal((report?.lastMessageTranslationKey || 'common.attachment') as TranslationPaths)}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(report?.reportID, lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED || lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ) { - lastMessageTextFromReport = lodashGet(lastReportAction, 'message[0].text', ''); + lastMessageTextFromReport = lastReportAction?.message?.[0].text ?? ''; } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) { lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction); } - return lastMessageTextFromReport || lodashGet(report, 'lastMessageText', ''); + return lastMessageTextFromReport || (report?.lastMessageText ?? ''); } /** * Creates a report list option - * - * @param {Array} accountIDs - * @param {Object} personalDetails - * @param {Object} report - * @param {Object} reportActions - * @param {Object} options - * @param {Boolean} [options.showChatPreviewLine] - * @param {Boolean} [options.forcePolicyNamePreview] - * @returns {Object} */ -function createOption(accountIDs, personalDetails, report, reportActions = {}, {showChatPreviewLine = false, forcePolicyNamePreview = false}) { - const result = { - text: null, +function createOption( + accountIDs: number[], + personalDetails: OnyxEntry, + report: OnyxEntry, + reportActions: ReportActions, + {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, +): ReportUtils.OptionData { + const result: ReportUtils.OptionData = { + text: undefined, alternateText: null, - pendingAction: null, - allReportErrors: null, + pendingAction: undefined, + allReportErrors: undefined, brickRoadIndicator: null, - icons: null, + icons: undefined, tooltipText: null, - ownerAccountID: null, + ownerAccountID: undefined, subtitle: null, - participantsList: null, + participantsList: undefined, accountID: 0, login: null, - reportID: null, + reportID: '', phoneNumber: null, hasDraftComment: false, keyForList: null, @@ -478,7 +572,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { isDefaultRoom: false, isPinned: false, isWaitingOnBankAccount: false, - iouReportID: null, + iouReportID: undefined, isIOUReportOwner: null, iouReportAmount: 0, isChatRoom: false, @@ -487,19 +581,19 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { isPolicyExpenseChat: false, isOwnPolicyExpenseChat: false, isExpenseReport: false, - policyID: null, + policyID: undefined, isOptimisticPersonalDetail: false, }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); - const personalDetailList = _.values(personalDetailMap); - const personalDetail = personalDetailList[0] || {}; + const personalDetailList = Object.values(personalDetailMap).filter((details): details is PersonalDetails => !!details); + const personalDetail = personalDetailList[0]; let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; result.participantsList = personalDetailList; - result.isOptimisticPersonalDetail = personalDetail.isOptimisticPersonalDetail; + result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail; if (report) { result.isChatRoom = ReportUtils.isChatRoom(report); @@ -511,10 +605,10 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.isTaskReport = ReportUtils.isTaskReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat || false; + result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat ?? false; result.allReportErrors = getAllReportErrors(report, reportActions); - result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; - result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; + result.brickRoadIndicator = !isEmptyObject(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom ?? report.pendingFields.createChat : undefined; result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); @@ -522,29 +616,32 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); - result.tooltipText = ReportUtils.getReportParticipantsTitle(report.visibleChatMemberAccountIDs || []); + result.tooltipText = ReportUtils.getReportParticipantsTitle(report.visibleChatMemberAccountIDs ?? []); result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; - hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); const lastMessageTextFromReport = getLastMessageTextForReport(report); - const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; + const lastActorDetails = personalDetailMap[report.lastActorAccountID ?? 0] ?? null; const lastActorDisplayName = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID - ? lastActorDetails.firstName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) + ? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + lastActorDetails.firstName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails) : ''; - let lastMessageText = lastMessageTextFromReport; - if (result.isArchivedRoom) { - const archiveReason = - (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || - CONST.REPORT.ARCHIVE_REASON.DEFAULT; - lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), - policyName: ReportUtils.getPolicyName(report), - }); + let lastMessageText = lastMessageTextFromReport; + const lastReportAction = lastReportActions[report.reportID ?? '']; + if (result.isArchivedRoom && lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { + const archiveReason = lastReportAction.originalMessage?.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + if (archiveReason === CONST.REPORT.ARCHIVE_REASON.DEFAULT || archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { + lastMessageText = Localize.translate(preferredLocale, 'reportArchiveReasons.default'); + } else { + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), + policyName: ReportUtils.getPolicyName(report), + }); + } } const lastAction = visibleReportActionItems[report.reportID]; @@ -561,27 +658,37 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } else if (result.isTaskReport) { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); } else { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login); + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); } reportName = ReportUtils.getReportName(report); } else { - reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail.login); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); result.keyForList = String(accountIDs[0]); - result.alternateText = LocalePhoneNumber.formatPhoneNumber(lodashGet(personalDetails, [accountIDs[0], 'login'], '')); + + result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails?.[accountIDs[0]]?.login ?? ''); } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestSpendBreakdown(result).totalDisplaySpend; if (!hasMultipleParticipants) { - result.login = personalDetail.login; - result.accountID = Number(personalDetail.accountID); - result.phoneNumber = personalDetail.phoneNumber; + result.login = personalDetail?.login; + result.accountID = Number(personalDetail?.accountID); + result.phoneNumber = personalDetail?.phoneNumber; } result.text = reportName; - result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); - result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), personalDetail.login, personalDetail.accountID); + // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + result.searchText = getSearchText(report, reportName, personalDetailList, !!result.isChatRoom || !!result.isPolicyExpenseChat, !!result.isThread); + result.icons = ReportUtils.getIcons( + report, + personalDetails, + UserUtils.getAvatar(personalDetail?.avatar ?? '', personalDetail?.accountID), + personalDetail?.login, + personalDetail?.accountID, + ); result.subtitle = subtitle; return result; @@ -589,16 +696,14 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { /** * Get the option for a policy expense report. - * @param {Object} report - * @returns {Object} */ -function getPolicyExpenseReportOption(report) { - const expenseReport = policyExpenseReports[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; +function getPolicyExpenseReportOption(report: Report): ReportUtils.OptionData { + const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; const option = createOption( - expenseReport.visibleChatMemberAccountIDs, - allPersonalDetails, - expenseReport, + expenseReport?.visibleChatMemberAccountIDs ?? [], + allPersonalDetails ?? {}, + expenseReport ?? null, {}, { showChatPreviewLine: false, @@ -616,16 +721,10 @@ function getPolicyExpenseReportOption(report) { /** * Searches for a match when provided with a value - * - * @param {String} searchValue - * @param {String} searchText - * @param {Set} [participantNames] - * @param {Boolean} isChatRoom - * @returns {Boolean} */ -function isSearchStringMatch(searchValue, searchText, participantNames = new Set(), isChatRoom = false) { +function isSearchStringMatch(searchValue: string, searchText?: string | null, participantNames = new Set(), isChatRoom = false): boolean { const searchWords = new Set(searchValue.replace(/,/g, ' ').split(' ')); - const valueToSearch = searchText && searchText.replace(new RegExp(/ /g), ''); + const valueToSearch = searchText?.replace(new RegExp(/ /g), ''); let matching = true; searchWords.forEach((word) => { // if one of the word is not matching, we don't need to check further @@ -633,7 +732,7 @@ function isSearchStringMatch(searchValue, searchText, participantNames = new Set return; } const matchRegex = new RegExp(Str.escapeForRegExp(word), 'i'); - matching = matchRegex.test(valueToSearch) || (!isChatRoom && participantNames.has(word)); + matching = matchRegex.test(valueToSearch ?? '') || (!isChatRoom && participantNames.has(word)); }); return matching; } @@ -642,68 +741,48 @@ function isSearchStringMatch(searchValue, searchText, participantNames = new Set * Checks if the given userDetails is currentUser or not. * Note: We can't migrate this off of using logins because this is used to check if you're trying to start a chat with * yourself or a different user, and people won't be starting new chats via accountID usually. - * - * @param {Object} userDetails - * @returns {Boolean} */ -function isCurrentUser(userDetails) { +function isCurrentUser(userDetails: PersonalDetails): boolean { if (!userDetails) { return false; } // If user login is a mobile number, append sms domain if not appended already. - const userDetailsLogin = addSMSDomainIfPhoneNumber(userDetails.login); + const userDetailsLogin = addSMSDomainIfPhoneNumber(userDetails.login ?? ''); - if (currentUserLogin.toLowerCase() === userDetailsLogin.toLowerCase()) { + if (currentUserLogin?.toLowerCase() === userDetailsLogin.toLowerCase()) { return true; } // Check if userDetails login exists in loginList - return _.some(_.keys(loginList), (login) => login.toLowerCase() === userDetailsLogin.toLowerCase()); + return Object.keys(loginList ?? {}).some((login) => login.toLowerCase() === userDetailsLogin.toLowerCase()); } /** * Calculates count of all enabled options - * - * @param {Object[]} options - an initial strings array - * @param {Boolean} options[].enabled - a flag to enable/disable option in a list - * @param {String} options[].name - a name of an option - * @returns {Number} */ -function getEnabledCategoriesCount(options) { - return _.filter(options, (option) => option.enabled).length; +function getEnabledCategoriesCount(options: PolicyCategories): number { + return Object.values(options).filter((option) => option.enabled).length; } /** * Verifies that there is at least one enabled option - * - * @param {Object[]} options - an initial strings array - * @param {Boolean} options[].enabled - a flag to enable/disable option in a list - * @param {String} options[].name - a name of an option - * @returns {Boolean} */ -function hasEnabledOptions(options) { - return _.some(options, (option) => option.enabled); +function hasEnabledOptions(options: PolicyCategories): boolean { + return Object.values(options).some((option) => option.enabled); } /** * Sorts categories using a simple object. * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. * Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. - * - * @param {Object} categories - * @returns {Array} */ -function sortCategories(categories) { +function sortCategories(categories: Record): Category[] { // Sorts categories alphabetically by name. - const sortedCategories = _.chain(categories) - .values() - .sortBy((category) => category.name) - .value(); + const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); // An object that respects nesting of categories. Also, can contain only uniq categories. const hierarchy = {}; - /** * Iterates over all categories to set each category in a proper place in hierarchy * It gets a path based on a category name e.g. "Parent: Child: Subcategory" -> "Parent.Child.Subcategory". @@ -719,10 +798,9 @@ function sortCategories(categories) { * } * } */ - _.each(sortedCategories, (category) => { + sortedCategories.forEach((category) => { const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); const existedValue = lodashGet(hierarchy, path, {}); - lodashSet(hierarchy, path, { ...existedValue, name: category.name, @@ -733,50 +811,42 @@ function sortCategories(categories) { * A recursive function to convert hierarchy into an array of category objects. * The category object contains base 2 properties: "name" and "enabled". * It iterates each key one by one. When a category has subcategories, goes deeper into them. Also, sorts subcategories alphabetically. - * - * @param {Object} initialHierarchy - * @returns {Array} */ - const flatHierarchy = (initialHierarchy) => - _.reduce( - initialHierarchy, - (acc, category) => { - const {name, ...subcategories} = category; - - if (!_.isEmpty(name)) { - const categoryObject = { - name, - enabled: lodashGet(categories, [name, 'enabled'], false), - }; - - acc.push(categoryObject); - } + const flatHierarchy = (initialHierarchy: Hierarchy) => + Object.values(initialHierarchy).reduce((acc: Category[], category) => { + const {name, ...subcategories} = category; + if (name) { + const categoryObject: Category = { + name, + enabled: categories[name].enabled ?? false, + }; + + acc.push(categoryObject); + } - if (!_.isEmpty(subcategories)) { - const nestedCategories = flatHierarchy(subcategories); + if (!isEmptyObject(subcategories)) { + const nestedCategories = flatHierarchy(subcategories); - acc.push(..._.sortBy(nestedCategories, 'name')); - } + acc.push(...nestedCategories.sort((a, b) => a.name.localeCompare(b.name))); + } - return acc; - }, - [], - ); + return acc; + }, []); return flatHierarchy(hierarchy); } /** * Sorts tags alphabetically by name. - * - * @param {Object} tags - * @returns {Array} */ -function sortTags(tags) { - const sortedTags = _.chain(tags) - .values() - .sortBy((tag) => tag.name) - .value(); +function sortTags(tags: Record | Tag[]) { + let sortedTags; + + if (Array.isArray(tags)) { + sortedTags = tags.sort((a, b) => a.name.localeCompare(b.name)); + } else { + sortedTags = Object.values(tags).sort((a, b) => a.name.localeCompare(b.name)); + } return sortedTags; } @@ -784,16 +854,14 @@ function sortTags(tags) { /** * Builds the options for the category tree hierarchy via indents * - * @param {Object[]} options - an initial object array - * @param {Boolean} options[].enabled - a flag to enable/disable option in a list - * @param {String} options[].name - a name of an option - * @param {Boolean} [isOneLine] - a flag to determine if text should be one line - * @returns {Array} + * @param options - an initial object array + * @param options[].enabled - a flag to enable/disable option in a list + * @param options[].name - a name of an option + * @param [isOneLine] - a flag to determine if text should be one line */ -function getCategoryOptionTree(options, isOneLine = false) { - const optionCollection = new Map(); - - _.each(options, (option) => { +function getCategoryOptionTree(options: Record | Category[], isOneLine = false): Option[] { + const optionCollection = new Map(); + Object.values(options).forEach((option) => { if (isOneLine) { if (optionCollection.has(option.name)) { return; @@ -811,7 +879,7 @@ function getCategoryOptionTree(options, isOneLine = false) { } option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { - const indents = _.times(index, () => CONST.INDENTS).join(''); + const indents = times(index, () => CONST.INDENTS).join(''); const isChild = array.length - 1 === index; const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); @@ -834,21 +902,19 @@ function getCategoryOptionTree(options, isOneLine = false) { /** * Builds the section list for categories - * - * @param {Object} categories - * @param {String[]} recentlyUsedCategories - * @param {Object[]} selectedOptions - * @param {String} selectedOptions[].name - * @param {String} searchInputValue - * @param {Number} maxRecentReportsToShow - * @returns {Array} */ -function getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow) { +function getCategoryListSections( + categories: PolicyCategories, + recentlyUsedCategories: string[], + selectedOptions: Category[], + searchInputValue: string, + maxRecentReportsToShow: number, +): CategorySection[] { const sortedCategories = sortCategories(categories); - const enabledCategories = _.filter(sortedCategories, (category) => category.enabled); + const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - const categorySections = []; - const numberOfCategories = _.size(enabledCategories); + const categorySections: CategorySection[] = []; + const numberOfCategories = enabledCategories.length; let indexOffset = 0; @@ -864,8 +930,8 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - if (!_.isEmpty(searchInputValue)) { - const searchCategories = _.filter(enabledCategories, (category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); + if (searchInputValue) { + const searchCategories = enabledCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); categorySections.push({ // "Search" section @@ -878,43 +944,42 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - if (!_.isEmpty(selectedOptions)) { + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const enabledAndSelectedCategories = sortedCategories.filter((category) => category.enabled || selectedOptionNames.includes(category.name)); + const numberOfVisibleCategories = enabledAndSelectedCategories.length; + + if (numberOfVisibleCategories < CONST.CATEGORY_LIST_THRESHOLD) { categorySections.push({ - // "Selected" section + // "All" section when items amount less than the threshold title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(selectedOptions, true), + data: getCategoryOptionTree(enabledAndSelectedCategories), }); - indexOffset += selectedOptions.length; + return categorySections; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredCategories = _.filter(enabledCategories, (category) => !_.includes(selectedOptionNames, category.name)); - const numberOfVisibleCategories = selectedOptions.length + filteredCategories.length; - - if (numberOfVisibleCategories < CONST.CATEGORY_LIST_THRESHOLD) { + if (selectedOptions.length > 0) { categorySections.push({ - // "All" section when items amount less than the threshold + // "Selected" section title: '', shouldShow: false, indexOffset, - data: getCategoryOptionTree(filteredCategories), + data: getCategoryOptionTree(selectedOptions, true), }); - return categorySections; + indexOffset += selectedOptions.length; } - const filteredRecentlyUsedCategories = _.chain(recentlyUsedCategories) - .filter((categoryName) => !_.includes(selectedOptionNames, categoryName) && lodashGet(categories, [categoryName, 'enabled'], false)) + const filteredRecentlyUsedCategories = recentlyUsedCategories + .filter((categoryName) => !selectedOptionNames.includes(categoryName) && categories[categoryName].enabled) .map((categoryName) => ({ name: categoryName, - enabled: lodashGet(categories, [categoryName, 'enabled'], false), - })) - .value(); + enabled: categories[categoryName].enabled ?? false, + })); - if (!_.isEmpty(filteredRecentlyUsedCategories)) { + if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); categorySections.push({ @@ -928,6 +993,8 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt indexOffset += filteredRecentlyUsedCategories.length; } + const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); + categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), @@ -940,15 +1007,12 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt } /** - * Transforms the provided tags into objects with a specific structure. + * Transforms the provided tags into option objects. * - * @param {Object[]} tags - an initial tag array - * @param {Boolean} tags[].enabled - a flag to enable/disable option in a list - * @param {String} tags[].name - a name of an option - * @returns {Array} + * @param tags - an initial tag array */ -function getTagsOptions(tags) { - return _.map(tags, (tag) => { +function getTagsOptions(tags: Category[]): Option[] { + return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); return { @@ -963,27 +1027,17 @@ function getTagsOptions(tags) { /** * Build the section list for tags - * - * @param {Object[]} tags - * @param {String} tags[].name - * @param {Boolean} tags[].enabled - * @param {String[]} recentlyUsedTags - * @param {Object[]} selectedOptions - * @param {String} selectedOptions[].name - * @param {String} searchInputValue - * @param {Number} maxRecentReportsToShow - * @returns {Array} */ -function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow) { +function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) { const tagSections = []; const sortedTags = sortTags(tags); - const enabledTags = _.filter(sortedTags, (tag) => tag.enabled); - const numberOfTags = _.size(enabledTags); + const enabledTags = sortedTags.filter((tag) => tag.enabled); + const numberOfTags = enabledTags.length; let indexOffset = 0; // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { - const selectedTagOptions = _.map(selectedOptions, (option) => ({ + const selectedTagOptions = selectedOptions.map((option) => ({ name: option.name, // Should be marked as enabled to be able to be de-selected enabled: true, @@ -999,8 +1053,8 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput return tagSections; } - if (!_.isEmpty(searchInputValue)) { - const searchTags = _.filter(enabledTags, (tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); + if (searchInputValue) { + const searchTags = enabledTags.filter((tag) => PolicyUtils.getCleanedTagName(tag.name.toLowerCase()).includes(searchInputValue.toLowerCase())); tagSections.push({ // "Search" section @@ -1025,22 +1079,21 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput return tagSections; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredRecentlyUsedTags = _.map( - _.filter(recentlyUsedTags, (recentlyUsedTag) => { - const tagObject = _.find(tags, (tag) => tag.name === recentlyUsedTag); - return Boolean(tagObject && tagObject.enabled) && !_.includes(selectedOptionNames, recentlyUsedTag); - }), - (tag) => ({name: tag, enabled: true}), - ); - const filteredTags = _.filter(enabledTags, (tag) => !_.includes(selectedOptionNames, tag.name)); - - if (!_.isEmpty(selectedOptions)) { - const selectedTagOptions = _.map(selectedOptions, (option) => { - const tagObject = _.find(tags, (tag) => tag.name === option.name); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredRecentlyUsedTags = recentlyUsedTags + .filter((recentlyUsedTag) => { + const tagObject = tags.find((tag) => tag.name === recentlyUsedTag); + return !!tagObject?.enabled && !selectedOptionNames.includes(recentlyUsedTag); + }) + .map((tag) => ({name: tag, enabled: true})); + const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); + + if (selectedOptions) { + const selectedTagOptions = selectedOptions.map((option) => { + const tagObject = tags.find((tag) => tag.name === option.name); return { name: option.name, - enabled: Boolean(tagObject && tagObject.enabled), + enabled: !!tagObject?.enabled, }; }); @@ -1055,7 +1108,7 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput indexOffset += selectedOptions.length; } - if (!_.isEmpty(filteredRecentlyUsedTags)) { + if (filteredRecentlyUsedTags.length > 0) { const cutRecentlyUsedTags = filteredRecentlyUsedTags.slice(0, maxRecentReportsToShow); tagSections.push({ @@ -1080,52 +1133,40 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput return tagSections; } -/** - * Represents the data for a single tax rate. - * - * @property {string} name - The name of the tax rate. - * @property {string} value - The value of the tax rate. - * @property {string} code - The code associated with the tax rate. - * @property {string} modifiedName - This contains the tax name and tax value as one name - * @property {boolean} [isDisabled] - Indicates if the tax rate is disabled. - */ +type PolicyTaxRateWithDefault = { + name: string; + defaultExternalID: string; + defaultValue: string; + foreignTaxDefault: string; + taxes: PolicyTaxRates; +}; /** * Transforms tax rates to a new object format - to add codes and new name with concatenated name and value. * - * @param {Object} policyTaxRates - The original tax rates object. - * @returns {Object.>} The transformed tax rates object. + * @param policyTaxRates - The original tax rates object. + * @returns The transformed tax rates object.g */ -function transformedTaxRates(policyTaxRates) { - const defaultTaxKey = policyTaxRates.defaultExternalID; - const getModifiedName = (data, code) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; - const taxes = Object.fromEntries(_.map(Object.entries(policyTaxRates.taxes), ([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); +function transformedTaxRates(policyTaxRates: PolicyTaxRateWithDefault | undefined): Record { + const defaultTaxKey = policyTaxRates?.defaultExternalID; + const getModifiedName = (data: PolicyTaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`; + const taxes = Object.fromEntries(Object.entries(policyTaxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}])); return taxes; } /** * Sorts tax rates alphabetically by name. - * - * @param {Object} taxRates - * @returns {Array} */ -function sortTaxRates(taxRates) { - const sortedtaxRates = _.chain(taxRates) - .values() - .sortBy((taxRate) => taxRate.name) - .value(); - +function sortTaxRates(taxRates: PolicyTaxRates): PolicyTaxRate[] { + const sortedtaxRates = lodashSortBy(taxRates, (taxRate) => taxRate.name); return sortedtaxRates; } /** * Builds the options for taxRates - * - * @param {Object[]} taxRates - an initial object array - * @returns {Array} */ -function getTaxRatesOptions(taxRates) { - return _.map(taxRates, (taxRate) => ({ +function getTaxRatesOptions(taxRates: Array>): Option[] { + return taxRates.map((taxRate) => ({ text: taxRate.modifiedName, keyForList: taxRate.code, searchText: taxRate.modifiedName, @@ -1137,32 +1178,27 @@ function getTaxRatesOptions(taxRates) { /** * Builds the section list for tax rates - * - * @param {Object} policyTaxRates - * @param {Object[]} selectedOptions - * @param {String} searchInputValue - * @returns {Array} */ -function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { +function getTaxRatesSection(policyTaxRates: PolicyTaxRateWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): CategorySection[] { const policyRatesSections = []; const taxes = transformedTaxRates(policyTaxRates); const sortedTaxRates = sortTaxRates(taxes); - const enabledTaxRates = _.filter(sortedTaxRates, (taxRate) => !taxRate.isDisabled); - const numberOfTaxRates = _.size(enabledTaxRates); + const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled); + const numberOfTaxRates = enabledTaxRates.length; let indexOffset = 0; // If all tax are disabled but there's a previously selected tag, show only the selected tag if (numberOfTaxRates === 0 && selectedOptions.length > 0) { - const selectedTaxRateOptions = _.map(selectedOptions, (option) => ({ + const selectedTaxRateOptions = selectedOptions.map((option) => ({ modifiedName: option.name, // Should be marked as enabled to be able to be de-selected isDisabled: false, })); policyRatesSections.push({ - // "Selected" section + // "Selected" sectiong title: '', shouldShow: false, indexOffset, @@ -1172,8 +1208,8 @@ function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { return policyRatesSections; } - if (!_.isEmpty(searchInputValue)) { - const searchTaxRates = _.filter(enabledTaxRates, (taxRate) => taxRate.modifiedName.toLowerCase().includes(searchInputValue.toLowerCase())); + if (searchInputValue) { + const searchTaxRates = enabledTaxRates.filter((taxRate) => taxRate.modifiedName.toLowerCase().includes(searchInputValue.toLowerCase())); policyRatesSections.push({ // "Search" section @@ -1198,16 +1234,16 @@ function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { return policyRatesSections; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredTaxRates = _.filter(enabledTaxRates, (taxRate) => !_.includes(selectedOptionNames, taxRate.modifiedName)); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredTaxRates = enabledTaxRates.filter((taxRate) => !selectedOptionNames.includes(taxRate.modifiedName)); - if (!_.isEmpty(selectedOptions)) { - const selectedTaxRatesOptions = _.map(selectedOptions, (option) => { - const taxRateObject = _.find(taxes, (taxRate) => taxRate.modifiedName === option.name); + if (selectedOptions.length > 0) { + const selectedTaxRatesOptions = selectedOptions.map((option) => { + const taxRateObject = Object.values(taxes).find((taxRate) => taxRate.modifiedName === option.name); return { modifiedName: option.name, - isDisabled: Boolean(taxRateObject && taxRateObject.isDisabled), + isDisabled: !!taxRateObject?.isDisabled, }; }); @@ -1236,34 +1272,25 @@ function getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue) { /** * Checks if a report option is selected based on matching accountID or reportID. * - * @param {Object} reportOption - The report option to be checked. - * @param {Object[]} selectedOptions - Array of selected options to compare with. - * @param {number} reportOption.accountID - The account ID of the report option. - * @param {number} reportOption.reportID - The report ID of the report option. - * @param {number} [selectedOptions[].accountID] - The account ID in the selected options. - * @param {number} [selectedOptions[].reportID] - The report ID in the selected options. - * @returns {boolean} True if the report option matches any of the selected options by accountID or reportID, false otherwise. + * @param reportOption - The report option to be checked. + * @param selectedOptions - Array of selected options to compare with. + * @returns true if the report option matches any of the selected options by accountID or reportID, false otherwise. */ -function isReportSelected(reportOption, selectedOptions) { +function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: Array>) { if (!selectedOptions || selectedOptions.length === 0) { return false; } - return _.some(selectedOptions, (option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return selectedOptions.some((option) => (option.accountID && option.accountID === reportOption.accountID) || (option.reportID && option.reportID === reportOption.reportID)); } /** * Build the options - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Object} options - * @returns {Object} - * @private */ function getOptions( - reports, - personalDetails, + reports: OnyxCollection, + personalDetails: OnyxEntry, { reportActions = {}, betas = [], @@ -1296,10 +1323,10 @@ function getOptions( transactionViolations = {}, includePolicyTaxRates, policyTaxRates, - }, -) { + }: GetOptionsConfig, +): GetOptions { if (includeCategories) { - const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow); + const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1313,7 +1340,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(_.values(tags), recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1327,7 +1354,7 @@ function getOptions( } if (includePolicyTaxRates) { - const policyTaxRatesOptions = getTaxRatesSection(policyTaxRates, selectedOptions, searchInputValue); + const policyTaxRatesOptions = getTaxRatesSection(policyTaxRates, selectedOptions as Category[], searchInputValue); return { recentReports: [], @@ -1353,25 +1380,26 @@ function getOptions( } let recentReportOptions = []; - let personalDetailsOptions = []; - const reportMapForAccountIDs = {}; + let personalDetailsOptions: ReportUtils.OptionData[] = []; + const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, (report) => { - const {parentReportID, parentReportActionID} = report || {}; + const filteredReports = Object.values(reports ?? {}).filter((report) => { + const {parentReportID, parentReportActionID} = report ?? {}; const canGetParentReport = parentReportID && parentReportActionID && allReportActions; - const parentReportAction = canGetParentReport ? lodashGet(allReportActions, [parentReportID, parentReportActionID], {}) : {}; + const parentReportAction = canGetParentReport ? allReportActions[parentReportID]?.[parentReportActionID] ?? null : null; const doesReportHaveViolations = betas.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, - currentReportId: Navigation.getTopmostReportId(), + currentReportId: Navigation.getTopmostReportId() ?? '', betas, policies, doesReportHaveViolations, isInGSDMode: false, + excludeEmptyChats: false, }); }); @@ -1379,17 +1407,17 @@ function getOptions( // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) // - All archived reports should remain at the bottom - const orderedReports = _.sortBy(filteredReports, (report) => { + const orderedReports = lodashSortBy(filteredReports, (report) => { if (ReportUtils.isArchivedRoom(report)) { return CONST.DATE.UNIX_EPOCH; } - return report.lastVisibleActionCreated; + return report?.lastVisibleActionCreated; }); orderedReports.reverse(); - const allReportOptions = []; - _.each(orderedReports, (report) => { + const allReportOptions: ReportUtils.OptionData[] = []; + orderedReports.forEach((report) => { if (!report) { return; } @@ -1399,7 +1427,7 @@ function getOptions( const isTaskReport = ReportUtils.isTaskReport(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const accountIDs = report.visibleChatMemberAccountIDs || []; + const accountIDs = report.visibleChatMemberAccountIDs ?? []; if (isPolicyExpenseChat && report.isOwnPolicyExpenseChat && !includeOwnedWorkspaceChats) { return; @@ -1446,16 +1474,15 @@ function getOptions( }), ); }); - - /* - We're only picking personal details that have logins and accountIDs set (sometimes the __fake__ account with `ID = 0` is present in the personal details collection) - This is a temporary fix for all the logic that's been breaking because of the new privacy changes - See https://github.com/Expensify/Expensify/issues/293465, https://github.com/Expensify/App/issues/33415 for more context - Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText - */ - const havingLoginPersonalDetails = !includeP2P ? {} : _.pick(personalDetails, (detail) => !!detail.login && !!detail.accountID && !detail.isOptimisticPersonalDetail); - let allPersonalDetailsOptions = _.map(havingLoginPersonalDetails, (personalDetail) => - createOption([personalDetail.accountID], personalDetails, reportMapForAccountIDs[personalDetail.accountID], reportActions, { + // We're only picking personal details that have logins set + // This is a temporary fix for all the logic that's been breaking because of the new privacy changes + // See https://github.com/Expensify/Expensify/issues/293465 for more context + // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText + const havingLoginPersonalDetails = !includeP2P + ? {} + : Object.fromEntries(Object.entries(personalDetails ?? {}).filter(([, detail]) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail)); + let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => + createOption([personalDetail?.accountID ?? -1], personalDetails, reportMapForAccountIDs[personalDetail?.accountID ?? -1], reportActions, { showChatPreviewLine, forcePolicyNamePreview, }), @@ -1463,11 +1490,11 @@ function getOptions( if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 - allPersonalDetailsOptions = lodashOrderBy(allPersonalDetailsOptions, [(personalDetail) => personalDetail.text && personalDetail.text.toLowerCase()], 'asc'); + allPersonalDetailsOptions = lodashOrderBy(allPersonalDetailsOptions, [(personalDetail) => personalDetail.text?.toLowerCase()], 'asc'); } // Exclude the current user from the personal details list - const optionsToExclude = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; + const optionsToExclude: Option[] = [{login: currentUserLogin}, {login: CONST.EMAIL.NOTIFICATIONS}]; // If we're including selected options from the search results, we only want to exclude them if the search input is empty // This is because on certain pages, we show the selected options at the top when the search input is empty @@ -1476,14 +1503,12 @@ function getOptions( optionsToExclude.push(...selectedOptions); } - _.each(excludeLogins, (login) => { + excludeLogins.forEach((login) => { optionsToExclude.push({login}); }); if (includeRecentReports) { - for (let i = 0; i < allReportOptions.length; i++) { - const reportOption = allReportOptions[i]; - + for (const reportOption of allReportOptions) { // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1505,8 +1530,8 @@ function getOptions( // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected if ( !includeThreads && - (reportOption.login || reportOption.reportID) && - _.some(optionsToExclude, (option) => (option.login && option.login === reportOption.login) || (option.reportID && option.reportID === reportOption.reportID)) + (!!reportOption.login || reportOption.reportID) && + optionsToExclude.some((option) => option.login === reportOption.login || option.reportID === reportOption.reportID) ) { continue; } @@ -1517,7 +1542,7 @@ function getOptions( if (searchValue) { // Determine if the search is happening within a chat room and starts with the report ID - const isReportIdSearch = isChatRoom && Str.startsWith(reportOption.reportID, searchValue); + const isReportIdSearch = isChatRoom && Str.startsWith(reportOption.reportID ?? '', searchValue); // Check if the search string matches the search text or participant names considering the type of the room const isSearchMatch = isSearchStringMatch(searchValue, searchText, participantNames, isChatRoom); @@ -1540,8 +1565,8 @@ function getOptions( if (includePersonalDetails) { // Next loop over all personal details removing any that are selectedUsers or recentChats - _.each(allPersonalDetailsOptions, (personalDetailOption) => { - if (_.some(optionsToExclude, (optionToExclude) => optionToExclude.login === personalDetailOption.login)) { + allPersonalDetailsOptions.forEach((personalDetailOption) => { + if (optionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) { return; } const {searchText, participantsList, isChatRoom} = personalDetailOption; @@ -1554,26 +1579,25 @@ function getOptions( }); } - let currentUserOption = _.find(allPersonalDetailsOptions, (personalDetailsOption) => personalDetailsOption.login === currentUserLogin); + let currentUserOption = allPersonalDetailsOptions.find((personalDetailsOption) => personalDetailsOption.login === currentUserLogin); if (searchValue && currentUserOption && !isSearchStringMatch(searchValue, currentUserOption.searchText)) { - currentUserOption = null; + currentUserOption = undefined; } - let userToInvite = null; + let userToInvite: ReportUtils.OptionData | null = null; const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; - const noOptionsMatchExactly = !_.find( - personalDetailsOptions.concat(recentReportOptions), - (option) => option.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase() || option.login === searchValue.toLowerCase(), - ); + const noOptionsMatchExactly = !personalDetailsOptions + .concat(recentReportOptions) + .find((option) => option.login === addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase()); if ( searchValue && (noOptions || noOptionsMatchExactly) && - !isCurrentUser({login: searchValue}) && - _.every(selectedOptions, (option) => option.login !== searchValue) && + !isCurrentUser({login: searchValue} as PersonalDetails) && + selectedOptions.every((option) => 'login' in option && option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number.input)))) && - !_.find(optionsToExclude, (optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && + (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + !optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers ) { @@ -1592,7 +1616,9 @@ function getOptions( }); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing userToInvite.text = userToInvite.text || searchValue; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing userToInvite.alternateText = userToInvite.alternateText || searchValue; // If user doesn't exist, use a default avatar @@ -1614,13 +1640,13 @@ function getOptions( recentReportOptions, [ (option) => { - if (option.isChatRoom || option.isArchivedRoom) { + if (!!option.isChatRoom || option.isArchivedRoom) { return 3; } if (!option.login) { return 2; } - if (option.login.toLowerCase() !== searchValue.toLowerCase()) { + if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { return 1; } @@ -1645,14 +1671,8 @@ function getOptions( /** * Build the options for the Search view - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {String} searchValue - * @param {Array} betas - * @returns {Object} */ -function getSearchOptions(reports, personalDetails, searchValue = '', betas) { +function getSearchOptions(reports: Record, personalDetails: OnyxEntry, searchValue = '', betas: Beta[] = []): GetOptions { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); const options = getOptions(reports, personalDetails, { @@ -1678,39 +1698,31 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { /** * Build the IOUConfirmation options for showing the payee personalDetail - * - * @param {Object} personalDetail - * @param {String} amountText - * @returns {Object} */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { - const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PayeePersonalDetails { + const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), - name: personalDetail.login, + name: personalDetail.login ?? '', type: CONST.ICON_TYPE_AVATAR, id: personalDetail.accountID, }, ], descriptiveText: amountText, - login: personalDetail.login, + login: personalDetail.login ?? '', accountID: personalDetail.accountID, }; } /** * Build the IOUConfirmationOptions for showing participants - * - * @param {Array} participants - * @param {String} amountText - * @returns {Array} */ -function getIOUConfirmationOptionsFromParticipants(participants, amountText) { - return _.map(participants, (participant) => ({ +function getIOUConfirmationOptionsFromParticipants(participants: Participant[], amountText: string): Participant[] { + return participants.map((participant) => ({ ...participant, descriptiveText: amountText, })); @@ -1718,46 +1730,26 @@ function getIOUConfirmationOptionsFromParticipants(participants, amountText) { /** * Build the options for the New Group view - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Array} [betas] - * @param {String} [searchValue] - * @param {Array} [selectedOptions] - * @param {Array} [excludeLogins] - * @param {Boolean} [includeOwnedWorkspaceChats] - * @param {boolean} [includeP2P] - * @param {boolean} [includeCategories] - * @param {Object} [categories] - * @param {Array} [recentlyUsedCategories] - * @param {boolean} [includeTags] - * @param {Object} [tags] - * @param {Array} [recentlyUsedTags] - * @param {boolean} [canInviteUser] - * @param {boolean} [includeSelectedOptions] - * @param {boolean} [includePolicyTaxRates] - * @param {Object} [policyTaxRates] - * @returns {Object} */ function getFilteredOptions( - reports, - personalDetails, - betas = [], + reports: Record, + personalDetails: OnyxEntry, + betas: Beta[] = [], searchValue = '', - selectedOptions = [], - excludeLogins = [], + selectedOptions: Array> = [], + excludeLogins: string[] = [], includeOwnedWorkspaceChats = false, includeP2P = true, includeCategories = false, - categories = {}, - recentlyUsedCategories = [], + categories: PolicyCategories = {}, + recentlyUsedCategories: string[] = [], includeTags = false, - tags = {}, - recentlyUsedTags = [], + tags: Record = {}, + recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, includePolicyTaxRates = false, - policyTaxRates = {}, + policyTaxRates: PolicyTaxRateWithDefault = {} as PolicyTaxRateWithDefault, ) { return getOptions(reports, personalDetails, { betas, @@ -1784,25 +1776,15 @@ function getFilteredOptions( /** * Build the options for the Share Destination for a Task - * * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Array} [betas] - * @param {String} [searchValue] - * @param {Array} [selectedOptions] - * @param {Array} [excludeLogins] - * @param {Boolean} [includeOwnedWorkspaceChats] - * @returns {Object} - * */ function getShareDestinationOptions( - reports, - personalDetails, - betas = [], + reports: Record, + personalDetails: OnyxEntry, + betas: Beta[] = [], searchValue = '', - selectedOptions = [], - excludeLogins = [], + selectedOptions: Array> = [], + excludeLogins: string[] = [], includeOwnedWorkspaceChats = true, excludeUnknownUsers = true, ) { @@ -1828,44 +1810,45 @@ function getShareDestinationOptions( /** * Format personalDetails or userToInvite to be shown in the list * - * @param {Object} member - personalDetails or userToInvite - * @param {Object} config - keys to overwrite the default values - * @returns {Object} + * @param member - personalDetails or userToInvite + * @param config - keys to overwrite the default values */ -function formatMemberForList(member, config = {}) { +function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { if (!member) { return undefined; } - const accountID = lodashGet(member, 'accountID', ''); + const accountID = member.accountID; return { - text: lodashGet(member, 'text', '') || lodashGet(member, 'displayName', ''), - alternateText: lodashGet(member, 'alternateText', '') || lodashGet(member, 'login', ''), - keyForList: lodashGet(member, 'keyForList', '') || String(accountID), + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + text: member.text || member.displayName || '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + alternateText: member.alternateText || member.login || '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + keyForList: member.keyForList || String(accountID ?? 0) || '', isSelected: false, isDisabled: false, accountID, - login: lodashGet(member, 'login', ''), + login: member.login ?? '', rightElement: null, - icons: lodashGet(member, 'icons'), - pendingAction: lodashGet(member, 'pendingAction'), + icons: member.icons, + pendingAction: member.pendingAction, ...config, }; } /** * Build the options for the Workspace Member Invite view - * - * @param {Object} personalDetails - * @param {Array} betas - * @param {String} searchValue - * @param {Array} excludeLogins - * @param {Boolean} includeSelectedOptions - * @returns {Object} */ -function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = [], includeSelectedOptions = false) { - return getOptions([], personalDetails, { +function getMemberInviteOptions( + personalDetails: OnyxEntry, + betas: Beta[] = [], + searchValue = '', + excludeLogins: string[] = [], + includeSelectedOptions = false, +): GetOptions { + return getOptions({}, personalDetails, { betas, searchInputValue: searchValue.trim(), includePersonalDetails: true, @@ -1877,15 +1860,8 @@ function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', e /** * Helper method that returns the text to be used for the header's message and title (if any) - * - * @param {Boolean} hasSelectableOptions - * @param {Boolean} hasUserToInvite - * @param {String} searchValue - * @param {Boolean} [maxParticipantsReached] - * @param {Boolean} [hasMatchedParticipant] - * @return {String} */ -function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, maxParticipantsReached = false, hasMatchedParticipant = false) { +function getHeaderMessage(hasSelectableOptions: boolean, hasUserToInvite: boolean, searchValue: string, maxParticipantsReached = false, hasMatchedParticipant = false): string { if (maxParticipantsReached) { return Localize.translate(preferredLocale, 'common.maxParticipantsReached', {count: CONST.REPORT.MAXIMUM_PARTICIPANTS}); } @@ -1918,12 +1894,8 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma /** * Helper method for non-user lists (eg. categories and tags) that returns the text to be used for the header's message and title (if any) - * - * @param {Boolean} hasSelectableOptions - * @param {String} searchValue - * @return {String} */ -function getHeaderMessageForNonUserList(hasSelectableOptions, searchValue) { +function getHeaderMessageForNonUserList(hasSelectableOptions: boolean, searchValue: string): string { if (searchValue && !hasSelectableOptions) { return Localize.translate(preferredLocale, 'common.noResultsFound'); } @@ -1932,25 +1904,23 @@ function getHeaderMessageForNonUserList(hasSelectableOptions, searchValue) { /** * Helper method to check whether an option can show tooltip or not - * @param {Object} option - * @returns {Boolean} */ -function shouldOptionShowTooltip(option) { - return (!option.isChatRoom || option.isThread) && !option.isArchivedRoom; +function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean { + return (!option.isChatRoom || !!option.isThread) && !option.isArchivedRoom; } /** * Handles the logic for displaying selected participants from the search term - * @param {String} searchTerm - * @param {Array} selectedOptions - * @param {Array} filteredRecentReports - * @param {Array} filteredPersonalDetails - * @param {Object} personalDetails - * @param {Boolean} shouldGetOptionDetails - * @param {Number} indexOffset - * @returns {Object} */ -function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, personalDetails = {}, shouldGetOptionDetails = false, indexOffset) { +function formatSectionsFromSearchTerm( + searchTerm: string, + selectedOptions: ReportUtils.OptionData[], + filteredRecentReports: ReportUtils.OptionData[], + filteredPersonalDetails: PersonalDetails[], + personalDetails: OnyxEntry = {}, + shouldGetOptionDetails = false, + indexOffset = 0, +): SectionForSearchTerm { // We show the selected participants at the top of the list when there is no search term // However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results // This clears up space on mobile views, where if you create a group with 4+ people you can't see the selected participants and the search results at the same time @@ -1959,12 +1929,12 @@ function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecen section: { title: undefined, data: shouldGetOptionDetails - ? _.map(selectedOptions, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + ? selectedOptions.map((participant) => { + const isPolicyExpenseChat = participant.isPolicyExpenseChat ?? false; return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails); }) : selectedOptions, - shouldShow: !_.isEmpty(selectedOptions), + shouldShow: selectedOptions.length > 0, indexOffset, }, newIndexOffset: indexOffset + selectedOptions.length, @@ -1973,11 +1943,11 @@ function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecen // If you select a new user you don't have a contact for, they won't get returned as part of a recent report or personal details // This will add them to the list of options, deduping them if they already exist in the other lists - const selectedParticipantsWithoutDetails = _.filter(selectedOptions, (participant) => { - const accountID = lodashGet(participant, 'accountID', null); - const isPartOfSearchTerm = participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase()); - const isReportInRecentReports = _.some(filteredRecentReports, (report) => report.accountID === accountID); - const isReportInPersonalDetails = _.some(filteredPersonalDetails, (personalDetail) => personalDetail.accountID === accountID); + const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => { + const accountID = participant.accountID ?? null; + const isPartOfSearchTerm = participant.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase()); + const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID); + const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID); return isPartOfSearchTerm && !isReportInRecentReports && !isReportInPersonalDetails; }); @@ -1985,12 +1955,12 @@ function formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecen section: { title: undefined, data: shouldGetOptionDetails - ? _.map(selectedParticipantsWithoutDetails, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + ? selectedParticipantsWithoutDetails.map((participant) => { + const isPolicyExpenseChat = participant.isPolicyExpenseChat ?? false; return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails); }) : selectedParticipantsWithoutDetails, - shouldShow: !_.isEmpty(selectedParticipantsWithoutDetails), + shouldShow: selectedParticipantsWithoutDetails.length > 0, indexOffset, }, newIndexOffset: indexOffset + selectedParticipantsWithoutDetails.length, diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 7fca9f54b744..52f64b2defb1 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -73,3 +73,4 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string // eslint-disable-next-line import/prefer-default-export export {getThumbnailAndImageURIs}; +export type {ThumbnailAndImageURI}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8b4170cca856..559994e2a172 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,8 +1,8 @@ +import fastMerge from 'expensify-common/lib/fastMerge'; import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import OnyxUtils from 'react-native-onyx/lib/utils'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -111,10 +111,6 @@ function isModifiedExpenseAction(reportAction: OnyxEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; } -function isSubmittedExpenseAction(reportAction: OnyxEntry): boolean { - return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED; -} - function isWhisperAction(reportAction: OnyxEntry): boolean { return (reportAction?.whisperedToAccountIDs ?? []).length > 0; } @@ -478,7 +474,7 @@ function replaceBaseURL(reportAction: ReportAction): ReportAction { /** */ function getLastVisibleAction(reportID: string, actionsToMerge: ReportActions = {}): OnyxEntry { - const reportActions = Object.values(OnyxUtils.fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge)); + const reportActions = Object.values(fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge, true)); const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { @@ -700,7 +696,7 @@ function isTaskAction(reportAction: OnyxEntry): boolean { * If there are no visible actions left (including system messages), we can hide the report from view entirely */ function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean { - const reportActions = Object.values(OnyxUtils.fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge)); + const reportActions = Object.values(fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge, true)); const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action)); // Exclude the task system message and the created message @@ -876,7 +872,6 @@ export { isDeletedParentAction, isMessageDeleted, isModifiedExpenseAction, - isSubmittedExpenseAction, isMoneyRequestAction, isNotifiableReportAction, isPendingRemove, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9c3b1710cc0..228db29aea6c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -14,27 +14,14 @@ import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { - Beta, - Login, - PersonalDetails, - PersonalDetailsList, - Policy, - PolicyReportField, - Report, - ReportAction, - ReportMetadata, - Session, - Transaction, - TransactionViolation, -} from '@src/types/onyx'; +import type {Beta, PersonalDetails, PersonalDetailsList, Policy, PolicyReportField, Report, ReportAction, ReportMetadata, Session, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import type {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference} from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -78,20 +65,6 @@ type ExpenseOriginalMessage = { oldBillable?: string; }; -type Participant = { - accountID: number; - alternateText: string; - firstName: string; - icons: Icon[]; - keyForList: string; - lastName: string; - login: string; - phoneNumber: string; - searchText: string; - selected: boolean; - text: string; -}; - type SpendBreakdown = { nonReimbursableSpend: number; reimbursableSpend: number; @@ -366,7 +339,7 @@ type CustomIcon = { }; type OptionData = { - text: string; + text?: string; alternateText?: string | null; allReportErrors?: Errors; brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; @@ -396,6 +369,12 @@ type OptionData = { isTaskReport?: boolean | null; parentReportAction?: OnyxEntry; displayNamesWithTooltips?: DisplayNameWithTooltips | null; + isDefaultRoom?: boolean; + isExpenseReport?: boolean; + isOptimisticPersonalDetail?: boolean; + selected?: boolean; + isOptimisticAccount?: boolean; + isSelected?: boolean; descriptiveText?: string; notificationPreference?: NotificationPreference | null; isDisabled?: boolean | null; @@ -460,12 +439,6 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); -let loginList: OnyxEntry; -Onyx.connect({ - key: ONYXKEYS.LOGIN_LIST, - callback: (value) => (loginList = value), -}); - let allTransactions: OnyxCollection = {}; Onyx.connect({ @@ -1606,7 +1579,7 @@ function getPersonalDetailsForAccountID(accountID: number): Partial transaction.reimbursable === false).length > 0; } -function getMoneyRequestReimbursableTotal(report: OnyxEntry, allReportsDict: OnyxCollection = null): number { - const allAvailableReports = allReportsDict ?? allReports; - let moneyRequestReport: OnyxEntry | undefined; - if (isMoneyRequestReport(report)) { - moneyRequestReport = report; - } - if (allAvailableReports && report?.iouReportID) { - moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; - } - if (moneyRequestReport) { - const total = moneyRequestReport?.total ?? 0; - - if (total !== 0) { - // There is a possibility that if the Expense report has a negative total. - // This is because there are instances where you can get a credit back on your card, - // or you enter a negative expense to “offset” future expenses - return isExpenseReport(moneyRequestReport) ? total * -1 : Math.abs(total); - } - } - return 0; -} - function getMoneyRequestSpendBreakdown(report: OnyxEntry, allReportsDict: OnyxCollection = null): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; @@ -1903,7 +1854,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry | undefined = undefined): string { - const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); + const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { @@ -2203,7 +2154,7 @@ function getReportPreviewMessage( } } - const totalAmount = getMoneyRequestReimbursableTotal(report); + const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const policyName = getPolicyName(report, false, policy); const payerName = isExpenseReport(report) ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport); @@ -2336,12 +2287,18 @@ function isChangeLogObject(originalMessage?: ChangeLog): ChangeLog | undefined { */ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Record, parentReportActionMessage: string) { if (!parentReportAction?.originalMessage) { - return ''; + return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } const originalMessage = isChangeLogObject(parentReportAction.originalMessage); const participantAccountIDs = originalMessage?.targetAccountIDs ?? []; - const participants = participantAccountIDs.map((id) => getDisplayNameForParticipant(id)); + const participants = participantAccountIDs.map((id) => { + const name = getDisplayNameForParticipant(id); + if (name && name?.length > 0) { + return name; + } + return Localize.translateLocal('common.hidden'); + }); const users = participants.length > 1 ? participants.join(` ${Localize.translateLocal('common.and')} `) : participants[0]; if (!users) { return parentReportActionMessage; @@ -2792,7 +2749,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num const report = getReport(iouReportID); const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(!isEmptyObject(report) ? report : null), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestSpendBreakdown(!isEmptyObject(report) ? report : null).totalDisplaySpend, currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -2864,7 +2821,7 @@ function buildOptimisticIOUReportAction( comment: string, participants: Participant[], transactionID: string, - paymentType: DeepValueOf, + paymentType: PaymentMethodType, iouReportID = '', isSettlingUp = false, isSendMoneyFlow = false, @@ -4233,9 +4190,8 @@ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry): boolean { ); } +/** + * Assume any report without a reportID is unusable. + */ +function isValidReport(report?: OnyxEntry): boolean { + return Boolean(report?.reportID); +} + +/** + * Check to see if we are a participant of this report. + */ +function isReportParticipant(accountID: number, report: OnyxEntry): boolean { + if (!accountID) { + return false; + } + + // If we have a DM AND the accountID we are checking is the current user THEN we won't find them as a participant and must assume they are a participant + if (isDM(report) && accountID === currentUserAccountID) { + return true; + } + + const possibleAccountIDs = report?.participantAccountIDs ?? []; + if (report?.ownerAccountID) { + possibleAccountIDs.push(report?.ownerAccountID); + } + if (report?.managerID) { + possibleAccountIDs.push(report?.managerID); + } + return possibleAccountIDs.includes(accountID); +} + function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } @@ -4616,7 +4602,7 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry 0; } +type DateTimeValidationErrorKeys = { + dateValidationErrorKey: string; + timeValidationErrorKey: string; +}; /** * Validates that the date and time are at least one minute in the future. * data - A date and time string in 'YYYY-MM-DD HH:mm:ss.sssZ' format * returns an object containing the error messages for the date and time */ -const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): {dateValidationErrorKey: string; timeValidationErrorKey: string} => { +const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): DateTimeValidationErrorKeys => { if (!data) { return { dateValidationErrorKey: '', @@ -413,6 +417,7 @@ const validateDateTimeIsAtLeastOneMinuteInFuture = (data: string): {dateValidati timeValidationErrorKey, }; }; + type ValuesType = Record; /** diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 768dc530cc51..930c31fde287 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -6,6 +6,16 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import type { + GetMissingOnyxMessagesParams, + HandleRestrictedEventParams, + OpenAppParams, + OpenOldDotLinkParams, + OpenProfileParams, + ReconnectAppParams, + UpdatePreferredLocaleParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Browser from '@libs/Browser'; import DateUtils from '@libs/DateUtils'; import Log from '@libs/Log'; @@ -103,15 +113,11 @@ function setLocale(locale: Locale) { }, ]; - type UpdatePreferredLocaleParams = { - value: Locale; - }; - const parameters: UpdatePreferredLocaleParams = { value: locale, }; - API.write('UpdatePreferredLocale', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE, parameters, {optimisticData}); } function setLocaleAndNavigate(locale: Locale) { @@ -203,13 +209,9 @@ function getOnyxDataForOpenOrReconnect(isOpenApp = false): OnyxData { */ function openApp() { getPolicyParamsForOpenOrReconnect().then((policyParams: PolicyParamsForOpenOrReconnect) => { - type OpenAppParams = PolicyParamsForOpenOrReconnect & { - enablePriorityModeFilter: boolean; - }; - const params: OpenAppParams = {enablePriorityModeFilter: true, ...policyParams}; - API.read('OpenApp', params, getOnyxDataForOpenOrReconnect(true)); + API.read(READ_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true)); }); } @@ -220,12 +222,6 @@ function openApp() { function reconnectApp(updateIDFrom: OnyxEntry = 0) { console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom}`); getPolicyParamsForOpenOrReconnect().then((policyParams) => { - type ReconnectParams = { - mostRecentReportActionLastModified?: string; - updateIDFrom?: number; - }; - type ReconnectAppParams = PolicyParamsForOpenOrReconnect & ReconnectParams; - const params: ReconnectAppParams = {...policyParams}; // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. @@ -243,7 +239,7 @@ function reconnectApp(updateIDFrom: OnyxEntry = 0) { params.updateIDFrom = updateIDFrom; } - API.write('ReconnectApp', params, getOnyxDataForOpenOrReconnect()); + API.write(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect()); }); } @@ -255,8 +251,6 @@ function reconnectApp(updateIDFrom: OnyxEntry = 0) { function finalReconnectAppAfterActivatingReliableUpdates(): Promise { console.debug(`[OnyxUpdates] Executing last reconnect app with promise`); return getPolicyParamsForOpenOrReconnect().then((policyParams) => { - type ReconnectAppParams = PolicyParamsForOpenOrReconnect & {mostRecentReportActionLastModified?: string}; - const params: ReconnectAppParams = {...policyParams}; // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. @@ -273,7 +267,7 @@ function finalReconnectAppAfterActivatingReliableUpdates(): Promise { console.debug(`[OnyxUpdates] Fetching missing updates updateIDFrom: ${updateIDFrom} and updateIDTo: ${updateIDTo}`); - type GetMissingOnyxMessagesParams = { - updateIDFrom: number; - updateIDTo: number | string; - }; - const parameters: GetMissingOnyxMessagesParams = { updateIDFrom, updateIDTo, @@ -299,7 +288,7 @@ function getMissingOnyxUpdates(updateIDFrom = 0, updateIDTo: number | string = 0 // DO NOT FOLLOW THIS PATTERN!!!!! // It was absolutely necessary in order to block OnyxUpdates while fetching the missing updates from the server or else the udpates aren't applied in the proper order. // eslint-disable-next-line rulesdir/no-api-side-effects-method - return API.makeRequestWithSideEffects('GetMissingOnyxMessages', parameters, getOnyxDataForOpenOrReconnect()); + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES, parameters, getOnyxDataForOpenOrReconnect()); } /** @@ -436,17 +425,13 @@ function openProfile(personalDetails: OnyxTypes.PersonalDetails) { newTimezoneData = DateUtils.formatToSupportedTimezone(newTimezoneData); - type OpenProfileParams = { - timezone: string; - }; - const parameters: OpenProfileParams = { timezone: JSON.stringify(newTimezoneData), }; // We expect currentUserAccountID to be a number because it doesn't make sense to open profile if currentUserAccountID is not set if (typeof currentUserAccountID === 'number') { - API.write('OpenProfile', parameters, { + API.write(WRITE_COMMANDS.OPEN_PROFILE, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -489,14 +474,10 @@ function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) { return; } - type OpenOldDotLinkParams = { - shouldRetry: boolean; - }; - const parameters: OpenOldDotLinkParams = {shouldRetry: false}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('OpenOldDotLink', parameters, {}).then((response) => { + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK, parameters, {}).then((response) => { if (!response) { Log.alert( 'Trying to redirect via deep link, but the response is empty. User likely not authenticated.', @@ -518,13 +499,9 @@ function beginDeepLinkRedirectAfterTransition(shouldAuthenticateWithCurrentAccou } function handleRestrictedEvent(eventName: string) { - type HandleRestrictedEventParams = { - eventName: string; - }; - const parameters: HandleRestrictedEventParams = {eventName}; - API.write('HandleRestrictedEvent', parameters); + API.write(WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT, parameters); } export { diff --git a/src/libs/actions/AppUpdate.ts b/src/libs/actions/AppUpdate/index.ts similarity index 71% rename from src/libs/actions/AppUpdate.ts rename to src/libs/actions/AppUpdate/index.ts index 29ee2a4547ab..69c80a089831 100644 --- a/src/libs/actions/AppUpdate.ts +++ b/src/libs/actions/AppUpdate/index.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import updateApp from './updateApp'; function triggerUpdateAvailable() { Onyx.set(ONYXKEYS.UPDATE_AVAILABLE, true); @@ -9,4 +10,4 @@ function setIsAppInBeta(isBeta: boolean) { Onyx.set(ONYXKEYS.IS_BETA, isBeta); } -export {triggerUpdateAvailable, setIsAppInBeta}; +export {triggerUpdateAvailable, setIsAppInBeta, updateApp}; diff --git a/src/libs/actions/AppUpdate/updateApp/index.android.ts b/src/libs/actions/AppUpdate/updateApp/index.android.ts new file mode 100644 index 000000000000..7b0022b3e970 --- /dev/null +++ b/src/libs/actions/AppUpdate/updateApp/index.android.ts @@ -0,0 +1,6 @@ +import {Linking} from 'react-native'; +import CONST from '@src/CONST'; + +export default function updateApp() { + Linking.openURL(CONST.APP_DOWNLOAD_LINKS.ANDROID); +} diff --git a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts new file mode 100644 index 000000000000..fb3a7d649baa --- /dev/null +++ b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts @@ -0,0 +1,6 @@ +import {Linking} from 'react-native'; +import CONST from '@src/CONST'; + +export default function updateApp() { + Linking.openURL(CONST.APP_DOWNLOAD_LINKS.DESKTOP); +} diff --git a/src/libs/actions/AppUpdate/updateApp/index.ios.ts b/src/libs/actions/AppUpdate/updateApp/index.ios.ts new file mode 100644 index 000000000000..59f25888de11 --- /dev/null +++ b/src/libs/actions/AppUpdate/updateApp/index.ios.ts @@ -0,0 +1,6 @@ +import {Linking} from 'react-native'; +import CONST from '@src/CONST'; + +export default function updateApp() { + Linking.openURL(CONST.APP_DOWNLOAD_LINKS.IOS); +} diff --git a/src/libs/actions/AppUpdate/updateApp/index.ts b/src/libs/actions/AppUpdate/updateApp/index.ts new file mode 100644 index 000000000000..8c2b191029a2 --- /dev/null +++ b/src/libs/actions/AppUpdate/updateApp/index.ts @@ -0,0 +1,6 @@ +/** + * On web or mWeb we can simply refresh the page and the user should have the new version of the app downloaded. + */ +export default function updateApp() { + window.location.reload(); +} diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 1ce6bc38191f..58509379b232 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1,5 +1,20 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type { + AddPersonalBankAccountParams, + BankAccountHandlePlaidErrorParams, + ConnectBankAccountManuallyParams, + ConnectBankAccountWithPlaidParams, + DeletePaymentBankAccountParams, + OpenReimbursementAccountPageParams, + UpdateCompanyInformationForBankAccountParams, + UpdatePersonalInformationForBankAccountParams, + ValidateBankAccountWithTransactionsParams, + VerifyIdentityForBankAccountParams, +} from '@libs/API/parameters'; +import type UpdateBeneficialOwnersForBankAccountParams from '@libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams'; +import type {BankAccountCompanyInformation} from '@libs/API/parameters/UpdateCompanyInformationForBankAccountParams'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; @@ -8,7 +23,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; import type {BankAccountStep, BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; -import type {ACHContractStepProps, BankAccountStepProps, CompanyStepProps, OnfidoData, ReimbursementAccountProps, RequestorStepProps} from '@src/types/onyx/ReimbursementAccountDraft'; +import type {OnfidoData} from '@src/types/onyx/ReimbursementAccountDraft'; import type {OnyxData} from '@src/types/onyx/Request'; import * as ReimbursementAccount from './ReimbursementAccount'; @@ -27,8 +42,6 @@ export { export {openPlaidBankAccountSelector, openPlaidBankLogin} from './Plaid'; export {openOnfidoFlow, answerQuestionsForWallet, verifyIdentity, acceptWalletTerms} from './Wallet'; -type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps; - type ReimbursementAccountStep = BankAccountStep | ''; type ReimbursementAccountSubStep = BankAccountSubStep | ''; @@ -123,17 +136,6 @@ function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData { * Submit Bank Account step with Plaid data so php can perform some checks. */ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount) { - const commandName = 'ConnectBankAccountWithPlaid'; - - type ConnectBankAccountWithPlaidParams = { - bankAccountID: number; - routingNumber: string; - accountNumber: string; - bank?: string; - plaidAccountID: string; - plaidAccessToken: string; - }; - const parameters: ConnectBankAccountWithPlaidParams = { bankAccountID, routingNumber: selectedPlaidBankAccount.routingNumber, @@ -143,7 +145,7 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken, }; - API.write(commandName, parameters, getVBBADataForOnyx()); + API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, parameters, getVBBADataForOnyx()); } /** @@ -152,19 +154,6 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc * TODO: offline pattern for this command will have to be added later once the pattern B design doc is complete */ function addPersonalBankAccount(account: PlaidBankAccount) { - const commandName = 'AddPersonalBankAccount'; - - type AddPersonalBankAccountParams = { - addressName: string; - routingNumber: string; - accountNumber: string; - isSavings: boolean; - setupType: string; - bank?: string; - plaidAccountID: string; - plaidAccessToken: string; - }; - const parameters: AddPersonalBankAccountParams = { addressName: account.addressName, routingNumber: account.routingNumber, @@ -211,12 +200,10 @@ function addPersonalBankAccount(account: PlaidBankAccount) { ], }; - API.write(commandName, parameters, onyxData); + API.write(WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT, parameters, onyxData); } function deletePaymentBankAccount(bankAccountID: number) { - type DeletePaymentBankAccountParams = {bankAccountID: number}; - const parameters: DeletePaymentBankAccountParams = {bankAccountID}; const onyxData: OnyxData = { @@ -239,7 +226,7 @@ function deletePaymentBankAccount(bankAccountID: number) { ], }; - API.write('DeletePaymentBankAccount', parameters, onyxData); + API.write(WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT, parameters, onyxData); } /** @@ -247,16 +234,11 @@ function deletePaymentBankAccount(bankAccountID: number) { * * This action is called by the requestor step in the Verified Bank Account flow */ -function updatePersonalInformationForBankAccount(params: RequestorStepProps) { - API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR)); +function updatePersonalInformationForBankAccount(params: UpdatePersonalInformationForBankAccountParams) { + API.write(WRITE_COMMANDS.UPDATE_PERSONAL_INFORMATION_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR)); } function validateBankAccount(bankAccountID: number, validateCode: string) { - type ValidateBankAccountWithTransactionsParams = { - bankAccountID: number; - validateCode: string; - }; - const parameters: ValidateBankAccountWithTransactionsParams = { bankAccountID, validateCode, @@ -293,7 +275,7 @@ function validateBankAccount(bankAccountID: number, validateCode: string) { ], }; - API.write('ValidateBankAccountWithTransactions', parameters, onyxData); + API.write(WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS, parameters, onyxData); } function clearReimbursementAccount() { @@ -331,37 +313,29 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS ], }; - type OpenReimbursementAccountPageParams = { - stepToOpen: ReimbursementAccountStep; - subStep: ReimbursementAccountSubStep; - localCurrentStep: ReimbursementAccountStep; - }; - const parameters: OpenReimbursementAccountPageParams = { stepToOpen, subStep, localCurrentStep, }; - return API.read('OpenReimbursementAccountPage', parameters, onyxData); + return API.read(READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE, parameters, onyxData); } /** * Updates the bank account in the database with the company step data */ function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyInformation, policyID: string) { - type UpdateCompanyInformationForBankAccountParams = BankAccountCompanyInformation & {policyID: string}; - const parameters: UpdateCompanyInformationForBankAccountParams = {...bankAccount, policyID}; - API.write('UpdateCompanyInformationForBankAccount', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY)); + API.write(WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY)); } /** * Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided */ -function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) { - API.write('UpdateBeneficialOwnersForBankAccount', params, getVBBADataForOnyx()); +function updateBeneficialOwnersForBankAccount(params: UpdateBeneficialOwnersForBankAccountParams) { + API.write(WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT, params, getVBBADataForOnyx()); } /** @@ -369,13 +343,6 @@ function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) { * */ function connectBankAccountManually(bankAccountID: number, accountNumber?: string, routingNumber?: string, plaidMask?: string) { - type ConnectBankAccountManuallyParams = { - bankAccountID: number; - accountNumber?: string; - routingNumber?: string; - plaidMask?: string; - }; - const parameters: ConnectBankAccountManuallyParams = { bankAccountID, accountNumber, @@ -383,29 +350,24 @@ function connectBankAccountManually(bankAccountID: number, accountNumber?: strin plaidMask, }; - API.write('ConnectBankAccountManually', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)); + API.write(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_MANUALLY, parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT)); } /** * Verify the user's identity via Onfido */ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoData) { - type VerifyIdentityForBankAccountParams = { - bankAccountID: number; - onfidoData: string; - }; - const parameters: VerifyIdentityForBankAccountParams = { bankAccountID, onfidoData: JSON.stringify(onfidoData), }; - API.write('VerifyIdentityForBankAccount', parameters, getVBBADataForOnyx()); + API.write(WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx()); } function openWorkspaceView() { API.read( - 'OpenWorkspaceView', + READ_COMMANDS.OPEN_WORKSPACE_VIEW, {}, { optimisticData: [ @@ -440,13 +402,6 @@ function openWorkspaceView() { } function handlePlaidError(bankAccountID: number, error: string, errorDescription: string, plaidRequestID: string) { - type BankAccountHandlePlaidErrorParams = { - bankAccountID: number; - error: string; - errorDescription: string; - plaidRequestID: string; - }; - const parameters: BankAccountHandlePlaidErrorParams = { bankAccountID, error, @@ -454,7 +409,7 @@ function handlePlaidError(bankAccountID: number, error: string, errorDescription plaidRequestID, }; - API.write('BankAccount_HandlePlaidError', parameters); + API.write(WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR, parameters); } /** diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index aa892d3817aa..38a421409ade 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -1,6 +1,8 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; +import type {ActivatePhysicalExpensifyCardParams, ReportVirtualExpensifyCardFraudParams, RequestReplacementExpensifyCardParams, RevealExpensifyCardDetailsParams} from '@libs/API/parameters'; +import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -39,15 +41,11 @@ function reportVirtualExpensifyCardFraud(cardID: number) { }, ]; - type ReportVirtualExpensifyCardFraudParams = { - cardID: number; - }; - const parameters: ReportVirtualExpensifyCardFraudParams = { cardID, }; - API.write('ReportVirtualExpensifyCardFraud', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD, parameters, {optimisticData, successData, failureData}); } /** @@ -87,17 +85,12 @@ function requestReplacementExpensifyCard(cardID: number, reason: ReplacementReas }, ]; - type RequestReplacementExpensifyCardParams = { - cardID: number; - reason: string; - }; - const parameters: RequestReplacementExpensifyCardParams = { cardID, reason, }; - API.write('RequestReplacementExpensifyCard', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD, parameters, {optimisticData, successData, failureData}); } /** @@ -141,17 +134,12 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe }, ]; - type ActivatePhysicalExpensifyCardParams = { - cardLastFourDigits: string; - cardID: number; - }; - const parameters: ActivatePhysicalExpensifyCardParams = { cardLastFourDigits, cardID, }; - API.write('ActivatePhysicalExpensifyCard', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD, parameters, {optimisticData, successData, failureData}); } /** @@ -173,12 +161,10 @@ function clearCardListErrors(cardID: number) { */ function revealVirtualCardDetails(cardID: number): Promise { return new Promise((resolve, reject) => { - type RevealExpensifyCardDetailsParams = {cardID: number}; - const parameters: RevealExpensifyCardDetailsParams = {cardID}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealExpensifyCardDetails', parameters) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.REVEAL_EXPENSIFY_CARD_DETAILS, parameters) .then((response) => { if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts index 0bb949687e6d..548b8398beec 100644 --- a/src/libs/actions/Chronos.ts +++ b/src/libs/actions/Chronos.ts @@ -1,6 +1,8 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {ChronosRemoveOOOEventParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ChronosOOOEvent} from '@src/types/onyx/OriginalMessage'; @@ -46,14 +48,12 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string, }, ]; - API.write( - 'Chronos_RemoveOOOEvent', - { - googleEventID: eventID, - reportActionID, - }, - {optimisticData, successData, failureData}, - ); + const parameters: ChronosRemoveOOOEventParams = { + googleEventID: eventID, + reportActionID, + }; + + API.write(WRITE_COMMANDS.CHRONOS_REMOVE_OOO_EVENT, parameters, {optimisticData, successData, failureData}); }; export { diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 6b73636e6d82..e32863cff0b1 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -1,34 +1,36 @@ import Onyx from 'react-native-onyx'; -import type {KeyValueMapping, NullishDeep} from 'react-native-onyx/lib/types'; +import type {KeyValueMapping, NullishDeep} from 'react-native-onyx'; +import type {OnyxFormKeyWithoutDraft} from '@components/Form/types'; import FormUtils from '@libs/FormUtils'; import type {OnyxFormKey} from '@src/ONYXKEYS'; -import type {Form} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -type ExcludeDraft = T extends `${string}Draft` ? never : T; -type OnyxFormKeyWithoutDraft = ExcludeDraft; - function setIsLoading(formID: OnyxFormKey, isLoading: boolean) { - Onyx.merge(formID, {isLoading} satisfies Form); + Onyx.merge(formID, {isLoading}); } function setErrors(formID: OnyxFormKey, errors: OnyxCommon.Errors) { - Onyx.merge(formID, {errors} satisfies Form); + Onyx.merge(formID, {errors}); } function setErrorFields(formID: OnyxFormKey, errorFields: OnyxCommon.ErrorFields) { - Onyx.merge(formID, {errorFields} satisfies Form); + Onyx.merge(formID, {errorFields}); +} + +function clearErrors(formID: OnyxFormKey) { + Onyx.merge(formID, {errors: null}); +} + +function clearErrorFields(formID: OnyxFormKey) { + Onyx.merge(formID, {errorFields: null}); } function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDeep) { Onyx.merge(FormUtils.getDraftKey(formID), draftValues); } -/** - * @param formID - */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), undefined); + Onyx.set(FormUtils.getDraftKey(formID), {}); } -export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; +export {setDraftValues, setErrorFields, setErrors, clearErrors, clearErrorFields, setIsLoading, clearDraftValues}; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index eb9541edcad2..3d6664099866 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1,15 +1,16 @@ import {format} from 'date-fns'; +import fastMerge from 'expensify-common/lib/fastMerge'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import lodashHas from 'lodash/has'; import Onyx from 'react-native-onyx'; -import OnyxUtils from 'react-native-onyx/lib/utils'; import _ from 'underscore'; import ReceiptGeneric from '@assets/images/receipt-generic.png'; import * as API from '@libs/API'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; +import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as IOUUtils from '@libs/IOUUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; @@ -311,6 +312,29 @@ function getReceiptError(receipt, filename, isScanRequest = true) { : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename}); } +/** + * Return the object to update hasOutstandingChildRequest + * @param {Object} [policy] + * @param {Boolean} needsToBeManuallySubmitted + * @returns {Object} + */ +function getOutstandingChildRequest(policy, needsToBeManuallySubmitted) { + if (!needsToBeManuallySubmitted) { + return { + hasOutstandingChildRequest: false, + }; + } + + if (PolicyUtils.isPolicyAdmin(policy)) { + return { + hasOutstandingChildRequest: true, + }; + } + + // We don't need to update hasOutstandingChildRequest in this case + return {}; +} + /** * Builds the Onyx data for a money request. * @@ -329,7 +353,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) * @param {Array} policyTags * @param {Array} policyCategories - * @param {Boolean} hasOutstandingChildRequest + * @param {Boolean} needsToBeManuallySubmitted * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -348,9 +372,10 @@ function buildOnyxDataForMoneyRequest( policy, policyTags, policyCategories, - hasOutstandingChildRequest = false, + needsToBeManuallySubmitted = true, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); + const outstandingChildRequest = getOutstandingChildRequest(needsToBeManuallySubmitted, policy); const optimisticData = [ { // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page @@ -361,7 +386,7 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, - hasOutstandingChildRequest, + ...outstandingChildRequest, ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }, @@ -506,6 +531,7 @@ function buildOnyxDataForMoneyRequest( iouReportID: chatReport.iouReportID, lastReadTime: chatReport.lastReadTime, pendingFields: null, + hasOutstandingChildRequest: chatReport.hasOutstandingChildRequest, ...(isNewChatReport ? { errorFields: { @@ -687,7 +713,7 @@ function getMoneyRequestInformation( let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; // Check if the Scheduled Submit is enabled in case of expense report - let needsToBeManuallySubmitted = false; + let needsToBeManuallySubmitted = true; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy); @@ -753,7 +779,7 @@ function getMoneyRequestInformation( // to remind me to do this. const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { - optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); + optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction); } // STEP 4: Build optimistic reportActions. We need: @@ -806,10 +832,6 @@ function getMoneyRequestInformation( } : undefined; - // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off - // so the employee has to submit to their manager manually. - const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; - // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, @@ -827,7 +849,7 @@ function getMoneyRequestInformation( policy, policyTags, policyCategories, - hasOutstandingChildRequest, + needsToBeManuallySubmitted, ); return { @@ -870,6 +892,7 @@ function createDistanceRequest(report, participant, comment, created, category, // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const optimisticReceipt = { source: ReceiptGeneric, @@ -881,7 +904,7 @@ function createDistanceRequest(report, participant, comment, created, category, comment, amount, currency, - created, + currentCreated, merchant, userAccountID, currentUserEmail, @@ -906,7 +929,7 @@ function createDistanceRequest(report, participant, comment, created, category, createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction.reportActionID, waypoints: JSON.stringify(validWaypoints), - created, + created: currentCreated, category, tag, billable, @@ -1290,6 +1313,7 @@ function requestMoney( // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( currentChatReport, @@ -1297,7 +1321,7 @@ function requestMoney( comment, amount, currency, - created, + currentCreated, merchant, payeeAccountID, payeeEmail, @@ -1320,7 +1344,7 @@ function requestMoney( amount, currency, comment, - created, + created: currentCreated, merchant, iouReportID: iouReport.reportID, chatReportID: chatReport.reportID, @@ -1823,6 +1847,9 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, receiptObject, filename, + undefined, + category, + tag, ); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat @@ -2624,7 +2651,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), }); updatedReportPreviewAction.message[0].text = messageText; - updatedReportPreviewAction.message[0].html = messageText; + updatedReportPreviewAction.message[0].html = shouldDeleteIOUReport ? '' : messageText; if (reportPreviewAction.childMoneyRequestCount > 0) { updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; @@ -3698,6 +3725,32 @@ function getIOUReportID(iou, route) { return lodashGet(route, 'params.reportID') || lodashGet(iou, 'participants.0.reportID', ''); } +/** + * @param {String} receiptFilename + * @param {String} receiptPath + * @param {Function} onSuccess + * @param {String} requestType + * @param {String} iouType + * @param {String} transactionID + * @param {String} reportID + */ +// eslint-disable-next-line rulesdir/no-negated-variables +function navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID) { + if (!receiptFilename || !receiptPath) { + return; + } + + const onFailure = () => { + setMoneyRequestReceipt(transactionID, '', '', true); + if (requestType === CONST.IOU.REQUEST_TYPE.MANUAL) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + return; + } + IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); + }; + FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure); +} + export { setMoneyRequestParticipants, createDistanceRequest, @@ -3757,4 +3810,5 @@ export { detachReceipt, getIOUReportID, editMoneyRequest, + navigateToStartStepIfScanFileCannotBeRead, }; diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 186c9beed970..ae95424f5776 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import asyncOpenURL from '@libs/asyncOpenURL'; import * as Environment from '@libs/Environment/Environment'; import Navigation from '@libs/Navigation/Navigation'; @@ -55,7 +56,7 @@ function openOldDotLink(url: string) { // If shortLivedAuthToken is not accessible, fallback to opening the link without the token. asyncOpenURL( // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('OpenOldDotLink', {}, {}) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_OLD_DOT_LINK, {}, {}) .then((response) => (response ? buildOldDotURL(url, response.shortLivedAuthToken) : buildOldDotURL(url))) .catch(() => buildOldDotURL(url)), (oldDotURL) => oldDotURL, diff --git a/src/libs/actions/MapboxToken.ts b/src/libs/actions/MapboxToken.ts index 54f99b58fbeb..3b98f79698ba 100644 --- a/src/libs/actions/MapboxToken.ts +++ b/src/libs/actions/MapboxToken.ts @@ -4,6 +4,7 @@ import {AppState} from 'react-native'; import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; +import {READ_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {MapboxAccessToken, Network} from '@src/types/onyx'; @@ -38,7 +39,7 @@ const setExpirationTimer = () => { return; } console.debug(`[MapboxToken] Fetching a new token after waiting ${REFRESH_INTERVAL / 1000 / 60} minutes`); - API.read('GetMapboxAccessToken', {}, {}); + API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, {}, {}); }, REFRESH_INTERVAL); }; @@ -51,7 +52,7 @@ const clearToken = () => { }; const fetchToken = () => { - API.read('GetMapboxAccessToken', {}, {}); + API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, {}, {}); isCurrentlyFetchingToken = true; }; diff --git a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts index 3e8c613187b4..15b9133f0aaf 100644 --- a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts +++ b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {OnyxKey} from 'react-native-onyx/lib/types'; +import type {OnyxKey} from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index e1e73d425281..71ba850e721f 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -1,9 +1,10 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -const closeModals: Array<(isNavigating: boolean) => void> = []; +const closeModals: Array<(isNavigating?: boolean) => void> = []; let onModalClose: null | (() => void); +let isNavigate: undefined | boolean; /** * Allows other parts of the app to call modal close function @@ -21,6 +22,20 @@ function setCloseModal(onClose: () => void) { }; } +/** + * Close topmost modal + */ +function closeTop() { + if (closeModals.length === 0) { + return; + } + if (onModalClose) { + closeModals[closeModals.length - 1](isNavigate); + return; + } + closeModals[closeModals.length - 1](); +} + /** * Close modal in other parts of the app */ @@ -30,17 +45,21 @@ function close(onModalCloseCallback: () => void, isNavigating = true) { return; } onModalClose = onModalCloseCallback; - [...closeModals].reverse().forEach((onClose) => { - onClose(isNavigating); - }); + isNavigate = isNavigating; + closeTop(); } function onModalDidClose() { if (!onModalClose) { return; } + if (closeModals.length) { + closeTop(); + return; + } onModalClose(); onModalClose = null; + isNavigate = undefined; } /** @@ -58,4 +77,4 @@ function willAlertModalBecomeVisible(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {willAlertModalBecomeVisible: isVisible}); } -export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible}; +export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, closeTop}; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index b9632d05d581..b4854562f7a8 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -1,12 +1,12 @@ import {createRef} from 'react'; import type {MutableRefObject} from 'react'; import type {GestureResponderEvent} from 'react-native'; -import type {OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; -import type {ValueOf} from 'type-fest'; import type {TransferMethod} from '@components/KYCWall/types'; import * as API from '@libs/API'; +import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams, TransferWalletBalanceParams} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -63,7 +63,7 @@ function openWalletPage() { ]; return API.read( - 'OpenPaymentsPage', + READ_COMMANDS.OPEN_PAYMENTS_PAGE, {}, { optimisticData, @@ -134,24 +134,17 @@ function getMakeDefaultPaymentOnyxData( * */ function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previousPaymentMethod: PaymentMethod, currentPaymentMethod: PaymentMethod) { - type MakeDefaultPaymentMethodParams = { - bankAccountID: number; - fundID: number; - }; - const parameters: MakeDefaultPaymentMethodParams = { bankAccountID, fundID, }; - API.write('MakeDefaultPaymentMethod', parameters, { + API.write(WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD, parameters, { optimisticData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, true), failureData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, false), }); } -type PaymentCardParams = {expirationDate: string; cardNumber: string; securityCode: string; nameOnCard: string; addressZipCode: string}; - /** * Calls the API to add a new card. * @@ -160,17 +153,6 @@ function addPaymentCard(params: PaymentCardParams) { const cardMonth = CardUtils.getMonthFromExpirationDateString(params.expirationDate); const cardYear = CardUtils.getYearFromExpirationDateString(params.expirationDate); - type AddPaymentCardParams = { - cardNumber: string; - cardYear: string; - cardMonth: string; - cardCVV: string; - addressName: string; - addressZip: string; - currency: ValueOf; - isP2PDebitCard: boolean; - }; - const parameters: AddPaymentCardParams = { cardNumber: params.cardNumber, cardYear, @@ -206,7 +188,7 @@ function addPaymentCard(params: PaymentCardParams) { }, ]; - API.write('AddPaymentCard', parameters, { + API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, { optimisticData, successData, failureData, @@ -232,9 +214,7 @@ function transferWalletBalance(paymentMethod: PaymentMethod) { const paymentMethodIDKey = paymentMethod.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ? CONST.PAYMENT_METHOD_ID_KEYS.BANK_ACCOUNT : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD; - type TransferWalletBalanceParameters = Partial, number | undefined>>; - - const parameters: TransferWalletBalanceParameters = { + const parameters: TransferWalletBalanceParams = { [paymentMethodIDKey]: paymentMethod.methodID, }; @@ -272,7 +252,7 @@ function transferWalletBalance(paymentMethod: PaymentMethod) { }, ]; - API.write('TransferWalletBalance', parameters, { + API.write(WRITE_COMMANDS.TRANSFER_WALLET_BALANCE, parameters, { optimisticData, successData, failureData, @@ -358,10 +338,6 @@ function clearWalletTermsError() { } function deletePaymentCard(fundID: number) { - type DeletePaymentCardParams = { - fundID: number; - }; - const parameters: DeletePaymentCardParams = { fundID, }; @@ -374,7 +350,7 @@ function deletePaymentCard(fundID: number) { }, ]; - API.write('DeletePaymentCard', parameters, { + API.write(WRITE_COMMANDS.DELETE_PAYMENT_CARD, parameters, { optimisticData, }); } diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 508cca34fb88..e7d9b48c46e9 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -2,6 +2,18 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type { + OpenPublicProfilePageParams, + UpdateAutomaticTimezoneParams, + UpdateDateOfBirthParams, + UpdateDisplayNameParams, + UpdateHomeAddressParams, + UpdateLegalNameParams, + UpdatePronounsParams, + UpdateSelectedTimezoneParams, + UpdateUserAvatarParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -107,13 +119,9 @@ function getCountryISO(countryName: string): string { function updatePronouns(pronouns: string) { if (currentUserAccountID) { - type UpdatePronounsParams = { - pronouns: string; - }; - const parameters: UpdatePronounsParams = {pronouns}; - API.write('UpdatePronouns', parameters, { + API.write(WRITE_COMMANDS.UPDATE_PRONOUNS, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -133,14 +141,9 @@ function updatePronouns(pronouns: string) { function updateDisplayName(firstName: string, lastName: string) { if (currentUserAccountID) { - type UpdateDisplayNameParams = { - firstName: string; - lastName: string; - }; - const parameters: UpdateDisplayNameParams = {firstName, lastName}; - API.write('UpdateDisplayName', parameters, { + API.write(WRITE_COMMANDS.UPDATE_DISPLAY_NAME, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -164,14 +167,9 @@ function updateDisplayName(firstName: string, lastName: string) { } function updateLegalName(legalFirstName: string, legalLastName: string) { - type UpdateLegalNameParams = { - legalFirstName: string; - legalLastName: string; - }; - const parameters: UpdateLegalNameParams = {legalFirstName, legalLastName}; - API.write('UpdateLegalName', parameters, { + API.write(WRITE_COMMANDS.UPDATE_LEGAL_NAME, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -191,13 +189,9 @@ function updateLegalName(legalFirstName: string, legalLastName: string) { * @param dob - date of birth */ function updateDateOfBirth({dob}: DateOfBirthForm) { - type UpdateDateOfBirthParams = { - dob?: string; - }; - const parameters: UpdateDateOfBirthParams = {dob}; - API.write('UpdateDateOfBirth', parameters, { + API.write(WRITE_COMMANDS.UPDATE_DATE_OF_BIRTH, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -213,16 +207,6 @@ function updateDateOfBirth({dob}: DateOfBirthForm) { } function updateAddress(street: string, street2: string, city: string, state: string, zip: string, country: string) { - type UpdateHomeAddressParams = { - homeAddressStreet: string; - addressStreet2: string; - homeAddressCity: string; - addressState: string; - addressZipCode: string; - addressCountry: string; - addressStateLong?: string; - }; - const parameters: UpdateHomeAddressParams = { homeAddressStreet: street, addressStreet2: street2, @@ -238,7 +222,7 @@ function updateAddress(street: string, street2: string, city: string, state: str parameters.addressStateLong = state; } - API.write('UpdateHomeAddress', parameters, { + API.write(WRITE_COMMANDS.UPDATE_HOME_ADDRESS, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -272,15 +256,12 @@ function updateAutomaticTimezone(timezone: Timezone) { return; } - type UpdateAutomaticTimezoneParams = { - timezone: string; - }; const formatedTimezone = DateUtils.formatToSupportedTimezone(timezone); const parameters: UpdateAutomaticTimezoneParams = { timezone: JSON.stringify(formatedTimezone), }; - API.write('UpdateAutomaticTimezone', parameters, { + API.write(WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -304,16 +285,12 @@ function updateSelectedTimezone(selectedTimezone: SelectedTimezone) { selected: selectedTimezone, }; - type UpdateSelectedTimezoneParams = { - timezone: string; - }; - const parameters: UpdateSelectedTimezoneParams = { timezone: JSON.stringify(timezone), }; if (currentUserAccountID) { - API.write('UpdateSelectedTimezone', parameters, { + API.write(WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE, parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -365,11 +342,7 @@ function openPersonalDetailsPage() { }, ]; - type OpenPersonalDetailsPageParams = Record; - - const parameters: OpenPersonalDetailsPageParams = {}; - - API.read('OpenPersonalDetailsPage', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.OPEN_PERSONAL_DETAILS_PAGE, {}, {optimisticData, successData, failureData}); } /** @@ -414,13 +387,9 @@ function openPublicProfilePage(accountID: number) { }, ]; - type OpenPublicProfilePageParams = { - accountID: number; - }; - const parameters: OpenPublicProfilePageParams = {accountID}; - API.read('OpenPublicProfilePage', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE, parameters, {optimisticData, successData, failureData}); } /** @@ -481,13 +450,9 @@ function updateAvatar(file: File | CustomRNImageManipulatorResult) { }, ]; - type UpdateUserAvatarParams = { - file: File | CustomRNImageManipulatorResult; - }; - const parameters: UpdateUserAvatarParams = {file}; - API.write('UpdateUserAvatar', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_USER_AVATAR, parameters, {optimisticData, successData, failureData}); } /** @@ -526,11 +491,7 @@ function deleteAvatar() { }, ]; - type DeleteUserAvatarParams = Record; - - const parameters: DeleteUserAvatarParams = {}; - - API.write('DeleteUserAvatar', parameters, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_USER_AVATAR, {}, {optimisticData, failureData}); } /** diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index ab828eefeece..78bc91618215 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {OpenPlaidBankAccountSelectorParams, OpenPlaidBankLoginParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import getPlaidLinkTokenParameters from '@libs/getPlaidLinkTokenParameters'; import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -10,11 +12,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { // redirect_uri needs to be in kebab case convention because that's how it's passed to the backend const {redirectURI} = getPlaidLinkTokenParameters(); - const params = { + + const params: OpenPlaidBankLoginParams = { redirectURI, allowDebit, bankAccountID, }; + const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -28,58 +32,56 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { }, { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, value: { plaidAccountID: '', }, }, ]; - API.read('OpenPlaidBankLogin', params, {optimisticData}); + API.read(READ_COMMANDS.OPEN_PLAID_BANK_LOGIN, params, {optimisticData}); } function openPlaidBankAccountSelector(publicToken: string, bankName: string, allowDebit: boolean, bankAccountID: number) { - API.read( - 'OpenPlaidBankAccountSelector', - { - publicToken, - allowDebit, - bank: bankName, - bankAccountID, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PLAID_DATA, - value: { - isLoading: true, - errors: null, - bankName, - }, + const parameters: OpenPlaidBankAccountSelectorParams = { + publicToken, + allowDebit, + bank: bankName, + bankAccountID, + }; + + API.read(READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR, parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: true, + errors: null, + bankName, }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PLAID_DATA, - value: { - isLoading: false, - errors: null, - }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: false, + errors: null, }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PLAID_DATA, - value: { - isLoading: false, - }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PLAID_DATA, + value: { + isLoading: false, }, - ], - }, - ); + }, + ], + }); } export {openPlaidBankAccountSelector, openPlaidBankLogin}; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index b47891e64350..fbe92aeb378d 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -4,10 +4,26 @@ import Str from 'expensify-common/lib/str'; import {escapeRegExp} from 'lodash'; import lodashClone from 'lodash/clone'; import lodashUnion from 'lodash/union'; -import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {NullishDeep, OnyxEntry} from 'react-native-onyx/lib/types'; import * as API from '@libs/API'; +import type { + AddMembersToWorkspaceParams, + CreateWorkspaceFromIOUPaymentParams, + CreateWorkspaceParams, + DeleteMembersFromWorkspaceParams, + DeleteWorkspaceAvatarParams, + DeleteWorkspaceParams, + OpenDraftWorkspaceRequestParams, + OpenWorkspaceInvitePageParams, + OpenWorkspaceMembersPageParams, + OpenWorkspaceParams, + OpenWorkspaceReimburseViewParams, + UpdateWorkspaceAvatarParams, + UpdateWorkspaceCustomUnitAndRateParams, + UpdateWorkspaceGeneralSettingsParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; @@ -276,13 +292,9 @@ function deleteWorkspace(policyID: string, reports: Report[], policyName: string }); }); - type DeleteWorkspaceParams = { - policyID: string; - }; - const params: DeleteWorkspaceParams = {policyID}; - API.write('DeleteWorkspace', params, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, failureData}); // Reset the lastAccessedWorkspacePolicyID if (policyID === lastAccessedWorkspacePolicyID) { @@ -492,17 +504,12 @@ function removeMembers(accountIDs: number[], policyID: string) { }); }); - type DeleteMembersFromWorkspaceParams = { - emailList: string; - policyID: string; - }; - const params: DeleteMembersFromWorkspaceParams = { emailList: accountIDs.map((accountID) => allPersonalDetails?.[accountID]?.login).join(','), policyID, }; - API.write('DeleteMembersFromWorkspace', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}); } /** @@ -669,13 +676,6 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: Record ...announceRoomMembers.onyxFailureData, ]; - type AddMembersToWorkspaceParams = { - employees: string; - welcomeNote: string; - policyID: string; - reportCreationData?: string; - }; - const params: AddMembersToWorkspaceParams = { employees: JSON.stringify(logins.map((login) => ({email: login}))), welcomeNote: new ExpensiMark().replace(welcomeNote), @@ -684,7 +684,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: Record if (!isEmptyObject(membersChats.reportCreationData)) { params.reportCreationData = JSON.stringify(membersChats.reportCreationData); } - API.write('AddMembersToWorkspace', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData}); } /** @@ -728,17 +728,12 @@ function updateWorkspaceAvatar(policyID: string, file: File) { }, ]; - type UpdateWorkspaceAvatarParams = { - policyID: string; - file: File; - }; - const params: UpdateWorkspaceAvatarParams = { policyID, file, }; - API.write('UpdateWorkspaceAvatar', params, {optimisticData, finallyData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_AVATAR, params, {optimisticData, finallyData, failureData}); } /** @@ -783,13 +778,9 @@ function deleteWorkspaceAvatar(policyID: string) { }, ]; - type DeleteWorkspaceAvatarParams = { - policyID: string; - }; - const params: DeleteWorkspaceAvatarParams = {policyID}; - API.write('DeleteWorkspaceAvatar', params, {optimisticData, finallyData, failureData}); + API.write(WRITE_COMMANDS.DELETE_WORKSPACE_AVATAR, params, {optimisticData, finallyData, failureData}); } /** @@ -886,19 +877,13 @@ function updateGeneralSettings(policyID: string, name: string, currency: string) }, ]; - type UpdateWorkspaceGeneralSettingsParams = { - policyID: string; - workspaceName: string; - currency: string; - }; - const params: UpdateWorkspaceGeneralSettingsParams = { policyID, workspaceName: name, currency, }; - API.write('UpdateWorkspaceGeneralSettings', params, { + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_GENERAL_SETTINGS, params, { optimisticData, finallyData, failureData, @@ -1018,21 +1003,14 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C const {pendingAction, errors, ...newRates} = newCustomUnitParam.rates ?? {}; newCustomUnitParam.rates = newRates; - type UpdateWorkspaceCustomUnitAndRate = { - policyID: string; - lastModified: number; - customUnit: string; - customUnitRate: string; - }; - - const params: UpdateWorkspaceCustomUnitAndRate = { + const params: UpdateWorkspaceCustomUnitAndRateParams = { policyID, lastModified, customUnit: JSON.stringify(newCustomUnitParam), customUnitRate: JSON.stringify(newCustomUnitParam.rates), }; - API.write('UpdateWorkspaceCustomUnitAndRate', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT_AND_RATE, params, {optimisticData, successData, failureData}); } /** @@ -1417,22 +1395,6 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName }, ]; - type CreateWorkspaceParams = { - policyID: string; - announceChatReportID: string; - adminsChatReportID: string; - expenseChatReportID: string; - ownerEmail: string; - makeMeAdmin: boolean; - policyName: string; - type: string; - announceCreatedReportActionID: string; - adminsCreatedReportActionID: string; - expenseCreatedReportActionID: string; - customUnitID: string; - customUnitRateID: string; - }; - const params: CreateWorkspaceParams = { policyID, announceChatReportID, @@ -1449,7 +1411,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName customUnitRateID, }; - API.write('CreateWorkspace', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return adminsChatReportID; } @@ -1480,13 +1442,9 @@ function openWorkspaceReimburseView(policyID: string) { }, ]; - type OpenWorkspaceReimburseViewParams = { - policyID: string; - }; - const params: OpenWorkspaceReimburseViewParams = {policyID}; - API.read('OpenWorkspaceReimburseView', params, {successData, failureData}); + API.read(READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW, params, {successData, failureData}); } /** @@ -1498,17 +1456,12 @@ function openWorkspace(policyID: string, clientMemberAccountIDs: number[]) { return; } - type OpenWorkspaceParams = { - policyID: string; - clientMemberAccountIDs: string; - }; - const params: OpenWorkspaceParams = { policyID, clientMemberAccountIDs: JSON.stringify(clientMemberAccountIDs), }; - API.read('OpenWorkspace', params); + API.read(READ_COMMANDS.OPEN_WORKSPACE, params); } function openWorkspaceMembersPage(policyID: string, clientMemberEmails: string[]) { @@ -1517,17 +1470,12 @@ function openWorkspaceMembersPage(policyID: string, clientMemberEmails: string[] return; } - type OpenWorkspaceMembersPageParams = { - policyID: string; - clientMemberEmails: string; - }; - const params: OpenWorkspaceMembersPageParams = { policyID, clientMemberEmails: JSON.stringify(clientMemberEmails), }; - API.read('OpenWorkspaceMembersPage', params); + API.read(READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE, params); } function openWorkspaceInvitePage(policyID: string, clientMemberEmails: string[]) { @@ -1536,27 +1484,18 @@ function openWorkspaceInvitePage(policyID: string, clientMemberEmails: string[]) return; } - type OpenWorkspaceInvitePageParams = { - policyID: string; - clientMemberEmails: string; - }; - const params: OpenWorkspaceInvitePageParams = { policyID, clientMemberEmails: JSON.stringify(clientMemberEmails), }; - API.read('OpenWorkspaceInvitePage', params); + API.read(READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE, params); } function openDraftWorkspaceRequest(policyID: string) { - type OpenDraftWorkspaceRequestParams = { - policyID: string; - }; - const params: OpenDraftWorkspaceRequestParams = {policyID}; - API.read('OpenDraftWorkspaceRequest', params); + API.read(READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST, params); } function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccountIDs: Record) { @@ -1667,12 +1606,12 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { const optimisticData: OnyxUpdate[] = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: newWorkspace, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: { [sessionAccountID]: { @@ -1686,7 +1625,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, value: { pendingFields: { @@ -1696,12 +1635,12 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, value: announceReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, value: { pendingFields: { @@ -1711,12 +1650,12 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, value: adminsReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChatReportID}`, value: { pendingFields: { @@ -1726,7 +1665,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChatReportID}`, value: workspaceChatReportActionData, }, @@ -2018,26 +1957,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { value: {[movedReportAction.reportActionID]: null}, }); - type CreateWorkspaceFromIOUPayment = { - policyID: string; - announceChatReportID: string; - adminsChatReportID: string; - expenseChatReportID: string; - ownerEmail: string; - makeMeAdmin: boolean; - policyName: string; - type: string; - announceCreatedReportActionID: string; - adminsCreatedReportActionID: string; - expenseCreatedReportActionID: string; - customUnitID: string; - customUnitRateID: string; - iouReportID: string; - memberData: string; - reportActionID: string; - }; - - const params: CreateWorkspaceFromIOUPayment = { + const params: CreateWorkspaceFromIOUPaymentParams = { policyID, announceChatReportID, adminsChatReportID, @@ -2056,7 +1976,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { reportActionID: movedReportAction.reportActionID, }; - API.write('CreateWorkspaceFromIOUPayment', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT, params, {optimisticData, successData, failureData}); return policyID; } diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts index 1d38d09e08a1..7ae174ac8606 100644 --- a/src/libs/actions/PriorityMode.ts +++ b/src/libs/actions/PriorityMode.ts @@ -3,6 +3,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as CollectionUtils from '@libs/CollectionUtils'; import Log from '@libs/Log'; +import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report} from '@src/types/onyx'; @@ -120,7 +121,21 @@ function tryFocusModeUpdate() { return; } - const reportCount = Object.keys(allReports ?? {}).length; + const validReports = []; + Object.keys(allReports ?? {}).forEach((key) => { + const report = allReports?.[key]; + if (!report) { + return; + } + + if (!ReportUtils.isValidReport(report) || !ReportUtils.isReportParticipant(currentUserAccountID ?? 0, report)) { + return; + } + + validReports.push(report); + }); + + const reportCount = validReports.length; if (reportCount < CONST.REPORT.MAX_COUNT_BEFORE_FOCUS_UPDATE) { Log.info('Not switching user to optimized focus mode as they do not have enough reports', false, {reportCount}); return; diff --git a/src/libs/actions/PushNotification.ts b/src/libs/actions/PushNotification.ts index 888892fdc188..bc4d4eb05c5a 100644 --- a/src/libs/actions/PushNotification.ts +++ b/src/libs/actions/PushNotification.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; import * as Device from './Device'; @@ -19,7 +20,7 @@ Onyx.connect({ */ function setPushNotificationOptInStatus(isOptingIn: boolean) { Device.getDeviceID().then((deviceID) => { - const commandName = isOptingIn ? 'OptInToPushNotifications' : 'OptOutOfPushNotifications'; + const commandName = isOptingIn ? WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS : WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS; const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js index f963640bc74e..217cacf921a6 100644 --- a/src/libs/actions/ReimbursementAccount/index.js +++ b/src/libs/actions/ReimbursementAccount/index.js @@ -31,7 +31,7 @@ function setWorkspaceIDForReimbursementAccount(workspaceID) { * @param {Object} bankAccountData */ function updateReimbursementAccountDraft(bankAccountData) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, bankAccountData); + Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); } diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js index 14c988033689..3110c059d2fc 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js @@ -60,7 +60,7 @@ function resetFreePlanBankAccount(bankAccountID, session) { }, { onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, value: {}, }, ], diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8beb054a7a9e..ae36780e0b18 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3,13 +3,44 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import isEmpty from 'lodash/isEmpty'; import {DeviceEventEmitter, InteractionManager, Linking} from 'react-native'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {NullishDeep} from 'react-native-onyx/lib/types'; import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; +import type { + AddCommentOrAttachementParams, + AddEmojiReactionParams, + AddWorkspaceRoomParams, + CompleteEngagementModalParams, + DeleteCommentParams, + ExpandURLPreviewParams, + FlagCommentParams, + GetNewerActionsParams, + GetOlderActionsParams, + GetReportPrivateNoteParams, + InviteToRoomParams, + LeaveRoomParams, + MarkAsUnreadParams, + OpenReportParams, + OpenRoomMembersPageParams, + ReadNewestActionParams, + ReconnectToReportParams, + RemoveEmojiReactionParams, + RemoveFromRoomParams, + ResolveActionableMentionWhisperParams, + SearchForReportsParams, + SetNameValuePairParams, + TogglePinnedChatParams, + UpdateCommentParams, + UpdatePolicyRoomNameParams, + UpdateReportNotificationPreferenceParams, + UpdateReportPrivateNoteParams, + UpdateReportWriteCapabilityParams, + UpdateWelcomeMessageParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -32,7 +63,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportUserIsTyping} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; @@ -125,6 +156,13 @@ Onyx.connect({ }, }); +let reportMetadata: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + waitForCollectionCallback: true, + callback: (value) => (reportMetadata = value), +}); + const allReports: OnyxCollection = {}; let conciergeChatReportID: string | undefined; const typingWatchTimers: Record = {}; @@ -298,7 +336,7 @@ function addActions(reportID: string, text = '', file?: File) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; - let commandName = 'AddComment'; + let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT; if (text) { const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text); @@ -309,7 +347,7 @@ function addActions(reportID: string, text = '', file?: File) { if (file) { // When we are adding an attachment we will call AddAttachment. // It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only. - commandName = 'AddAttachment'; + commandName = WRITE_COMMANDS.ADD_ATTACHMENT; const attachment = ReportUtils.buildOptimisticAddCommentReportAction('', file); attachmentAction = attachment.reportAction; } @@ -344,19 +382,7 @@ function addActions(reportID: string, text = '', file?: File) { optimisticReportActions[attachmentAction.reportActionID] = attachmentAction; } - type AddCommentOrAttachementParameters = { - reportID: string; - reportActionID?: string; - commentReportActionID?: string | null; - reportComment?: string; - file?: File; - timezone?: string; - shouldAllowActionableMentionWhispers?: boolean; - clientCreatedTime?: string; - isOldDotConciergeChat?: boolean; - }; - - const parameters: AddCommentOrAttachementParameters = { + const parameters: AddCommentOrAttachementParams = { reportID, reportActionID: file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID, commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null, @@ -509,8 +535,6 @@ function openReport( reportName: allReports?.[reportID]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME, }; - const commandName = 'OpenReport'; - const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -562,25 +586,13 @@ function openReport( }, ]; - type OpenReportParameters = { - reportID: string; - emailList?: string; - reportActionID?: string; - accountIDList?: string; - parentReportActionID?: string; - shouldRetry?: boolean; - createdReportActionID?: string; - clientLastReadTime?: string; - idempotencyKey?: string; - }; - - const parameters: OpenReportParameters = { + const parameters: OpenReportParams = { reportID, reportActionID, emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', parentReportActionID, - idempotencyKey: `${commandName}_${reportID}`, + idempotencyKey: `${SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT}_${reportID}`, }; if (isFromDeepLink) { @@ -685,12 +697,12 @@ function openReport( if (isFromDeepLink) { // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(commandName, parameters, {optimisticData, successData, failureData}).finally(() => { + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}).finally(() => { Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); }); } else { // eslint-disable-next-line rulesdir/no-multiple-api-calls - API.write(commandName, parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}); } } @@ -759,7 +771,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P parentReport?.policyID ?? CONST.POLICY.OWNER_EMAIL_FAKE, CONST.POLICY.OWNER_ACCOUNT_ID_FAKE, false, - '', + parentReport?.policyName ?? '', undefined, undefined, ReportUtils.getChildReportNotificationPreference(parentReportAction), @@ -814,15 +826,11 @@ function reconnect(reportID: string) { }, ]; - type ReconnectToReportParameters = { - reportID: string; - }; - - const parameters: ReconnectToReportParameters = { + const parameters: ReconnectToReportParams = { reportID, }; - API.write('ReconnectToReport', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.RECONNECT_TO_REPORT, parameters, {optimisticData, successData, failureData}); } /** @@ -860,17 +868,12 @@ function getOlderActions(reportID: string, reportActionID: string) { }, ]; - type GetOlderActionsParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: GetOlderActionsParameters = { + const parameters: GetOlderActionsParams = { reportID, reportActionID, }; - API.read('GetOlderActions', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData}); } /** @@ -908,38 +911,28 @@ function getNewerActions(reportID: string, reportActionID: string) { }, ]; - type GetNewerActionsParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: GetNewerActionsParameters = { + const parameters: GetNewerActionsParams = { reportID, reportActionID, }; - API.read('GetNewerActions', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData}); } /** * Gets metadata info about links in the provided report action */ function expandURLPreview(reportID: string, reportActionID: string) { - type ExpandURLPreviewParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: ExpandURLPreviewParameters = { + const parameters: ExpandURLPreviewParams = { reportID, reportActionID, }; - API.read('ExpandURLPreview', parameters); + API.read(READ_COMMANDS.EXPAND_URL_PREVIEW, parameters); } /** Marks the new report actions as read */ -function readNewestAction(reportID: string, shouldEmitEvent = true) { +function readNewestAction(reportID: string) { const lastReadTime = DateUtils.getDBTime(); const optimisticData: OnyxUpdate[] = [ @@ -952,22 +945,12 @@ function readNewestAction(reportID: string, shouldEmitEvent = true) { }, ]; - type ReadNewestActionParameters = { - reportID: string; - lastReadTime: string; - }; - - const parameters: ReadNewestActionParameters = { + const parameters: ReadNewestActionParams = { reportID, lastReadTime, }; - API.write('ReadNewestAction', parameters, {optimisticData}); - - if (!shouldEmitEvent) { - return; - } - + API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}); DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } @@ -1003,17 +986,12 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) { }, ]; - type MarkAsUnreadParameters = { - reportID: string; - lastReadTime: string; - }; - - const parameters: MarkAsUnreadParameters = { + const parameters: MarkAsUnreadParams = { reportID, lastReadTime, }; - API.write('MarkAsUnread', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.MARK_AS_UNREAD, parameters, {optimisticData}); DeviceEventEmitter.emit(`unreadAction_${reportID}`, lastReadTime); } @@ -1030,17 +1008,12 @@ function togglePinnedState(reportID: string, isPinnedChat: boolean) { }, ]; - type TogglePinnedChatParameters = { - reportID: string; - pinnedValue: boolean; - }; - - const parameters: TogglePinnedChatParameters = { + const parameters: TogglePinnedChatParams = { reportID, pinnedValue, }; - API.write('TogglePinnedChat', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.TOGGLE_PINNED_CHAT, parameters, {optimisticData}); } /** @@ -1223,17 +1196,12 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { } } - type DeleteCommentParameters = { - reportID: string; - reportActionID: string; - }; - - const parameters: DeleteCommentParameters = { + const parameters: DeleteCommentParams = { reportID: originalReportID, reportActionID, }; - API.write('DeleteComment', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.DELETE_COMMENT, parameters, {optimisticData, successData, failureData}); } /** @@ -1376,19 +1344,13 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry { // If we don't have a chat with Concierge then create it - navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], false); + navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal); }); + } else if (shouldDismissModal) { + Navigation.dismissModal(conciergeChatReportID); } else { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID)); } @@ -1670,17 +1619,7 @@ function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) { }, ]; - type AddWorkspaceRoomParameters = { - reportID: string; - createdReportActionID: string; - policyID?: string; - reportName?: string; - visibility?: ValueOf; - writeCapability?: WriteCapability; - welcomeMessage?: string; - }; - - const parameters: AddWorkspaceRoomParameters = { + const parameters: AddWorkspaceRoomParams = { policyID: policyReport.policyID, reportName: policyReport.reportName, visibility: policyReport.visibility, @@ -1690,7 +1629,7 @@ function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) { welcomeMessage: policyReport.welcomeMessage, }; - API.write('AddWorkspaceRoom', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ADD_WORKSPACE_ROOM, parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(policyReport.reportID); } @@ -1780,14 +1719,9 @@ function updatePolicyRoomNameAndNavigate(policyRoomReport: Report, policyRoomNam }, ]; - type UpdatePolicyRoomNameParameters = { - reportID: string; - policyRoomName: string; - }; + const parameters: UpdatePolicyRoomNameParams = {reportID, policyRoomName}; - const parameters: UpdatePolicyRoomNameParameters = {reportID, policyRoomName}; - - API.write('UpdatePolicyRoomName', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_POLICY_ROOM_NAME, parameters, {optimisticData, successData, failureData}); Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(reportID)); } @@ -1947,16 +1881,7 @@ function addEmojiReaction(reportID: string, reportActionID: string, emoji: Emoji }, ]; - type AddEmojiReactionParameters = { - reportID: string; - skinTone: string | number; - emojiCode: string; - reportActionID: string; - createdAt: string; - useEmojiReactions: boolean; - }; - - const parameters: AddEmojiReactionParameters = { + const parameters: AddEmojiReactionParams = { reportID, skinTone, emojiCode: emoji.name, @@ -1966,7 +1891,7 @@ function addEmojiReaction(reportID: string, reportActionID: string, emoji: Emoji useEmojiReactions: true, }; - API.write('AddEmojiReaction', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ADD_EMOJI_REACTION, parameters, {optimisticData, successData, failureData}); } /** @@ -1988,14 +1913,7 @@ function removeEmojiReaction(reportID: string, reportActionID: string, emoji: Em }, ]; - type RemoveEmojiReactionParameters = { - reportID: string; - reportActionID: string; - emojiCode: string; - useEmojiReactions: boolean; - }; - - const parameters: RemoveEmojiReactionParameters = { + const parameters: RemoveEmojiReactionParams = { reportID, reportActionID, emojiCode: emoji.name, @@ -2003,7 +1921,7 @@ function removeEmojiReaction(reportID: string, reportActionID: string, emoji: Em useEmojiReactions: true, }; - API.write('RemoveEmojiReaction', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.REMOVE_EMOJI_REACTION, parameters, {optimisticData}); } /** @@ -2167,17 +2085,36 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal }); } - type LeaveRoomParameters = { - reportID: string; - }; - - const parameters: LeaveRoomParameters = { + const parameters: LeaveRoomParams = { reportID, }; - API.write('LeaveRoom', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.LEAVE_ROOM, parameters, {optimisticData, successData, failureData}); + + const sortedReportsByLastRead = ReportUtils.sortReportsByLastRead(Object.values(allReports ?? {}) as Report[], reportMetadata); + + // We want to filter out the current report, hidden reports and empty chats + const filteredReportsByLastRead = sortedReportsByLastRead.filter( + (sortedReport) => + sortedReport?.reportID !== reportID && + sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && + ReportUtils.shouldReportBeInOptionList({ + report: sortedReport, + currentReportId: '', + isInGSDMode: false, + betas: [], + policies: {}, + excludeEmptyChats: true, + doesReportHaveViolations: false, + }), + ); + const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; - if (isWorkspaceMemberLeavingWorkspaceRoom) { + if (lastAccessedReportID) { + // We should call Navigation.goBack to pop the current route first before navigating to Concierge. + Navigation.goBack(ROUTES.HOME); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID)); + } else { const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); const chat = ReportUtils.getChatByParticipants(participantAccountIDs); if (chat?.reportID) { @@ -2234,17 +2171,12 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record, se }, ]; - type FlagCommentParameters = { - severity: string; - reportActionID: string; - isDevRequest: boolean; - }; - - const parameters: FlagCommentParameters = { + const parameters: FlagCommentParams = { severity, reportActionID, // This check is to prevent flooding Concierge with test flags @@ -2401,7 +2322,7 @@ function flagComment(reportID: string, reportAction: OnyxEntry, se isDevRequest: Environment.isDevelopment(), }; - API.write('FlagComment', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.FLAG_COMMENT, parameters, {optimisticData, successData, failureData}); } /** Updates a given user's private notes on a report */ @@ -2451,14 +2372,9 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) = }, ]; - type UpdateReportPrivateNoteParameters = { - reportID: string; - privateNotes: string; - }; - - const parameters: UpdateReportPrivateNoteParameters = {reportID, privateNotes: note}; + const parameters: UpdateReportPrivateNoteParams = {reportID, privateNotes: note}; - API.write('UpdateReportPrivateNote', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.UPDATE_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); }; /** Fetches all the private notes for a given report */ @@ -2501,13 +2417,9 @@ function getReportPrivateNote(reportID: string) { }, ]; - type GetReportPrivateNoteParameters = { - reportID: string; - }; - - const parameters: GetReportPrivateNoteParameters = {reportID}; + const parameters: GetReportPrivateNoteParams = {reportID}; - API.read('GetReportPrivateNote', parameters, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } /** @@ -2517,7 +2429,6 @@ function getReportPrivateNote(reportID: string) { * - Creates an optimistic report comment from concierge */ function completeEngagementModal(text: string, choice: ValueOf) { - const commandName = 'CompleteEngagementModal'; const conciergeAccountID = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE])[0]; const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text, undefined, conciergeAccountID); const reportCommentAction: OptimisticAddCommentReportAction = reportComment.reportAction; @@ -2550,16 +2461,7 @@ function completeEngagementModal(text: string, choice: ValueOf; - }; - const parameters: ResolveActionableMentionWhisperParams = { reportActionID: reportAction.reportActionID, resolution, }; - API.write('ResolveActionableMentionWhisper', parameters, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.RESOLVE_ACTIONABLE_MENTION_WHISPER, parameters, {optimisticData, failureData}); } export { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index bde2954e191a..4fbeba0abaa6 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -7,6 +7,22 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; +import type { + AuthenticatePusherParams, + BeginAppleSignInParams, + BeginGoogleSignInParams, + BeginSignInParams, + LogOutParams, + RequestAccountValidationLinkParams, + RequestNewValidateCodeParams, + RequestUnlinkValidationLinkParams, + SignInUserWithLinkParams, + SignInWithShortLivedAuthTokenParams, + UnlinkLoginParams, + ValidateTwoFactorAuthParams, +} from '@libs/API/parameters'; +import type SignInUserParams from '@libs/API/parameters/SignInUserParams'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; import HttpUtils from '@libs/HttpUtils'; @@ -70,14 +86,6 @@ Onyx.connect({ function signOut() { Log.info('Flushing logs before signing out', true, {}, true); - type LogOutParams = { - authToken: string | null; - partnerUserID: string; - partnerName: string; - partnerPassword: string; - shouldRetry: boolean; - }; - const params: LogOutParams = { // Send current authToken because we will immediately clear it once triggering this command authToken: NetworkStore.getAuthToken(), @@ -87,7 +95,7 @@ function signOut() { shouldRetry: false, }; - API.write('LogOut', params); + API.write(WRITE_COMMANDS.LOG_OUT, params); clearCache().then(() => { Log.info('Cleared all cache data', true, {}, true); }); @@ -177,13 +185,9 @@ function resendValidationLink(login = credentials.login) { }, ]; - type ResendValidationLinkParams = { - email?: string; - }; - - const params: ResendValidationLinkParams = {email: login}; + const params: RequestAccountValidationLinkParams = {email: login}; - API.write('RequestAccountValidationLink', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_ACCOUNT_VALIDATION_LINK, params, {optimisticData, successData, failureData}); } /** @@ -210,13 +214,9 @@ function resendValidateCode(login = credentials.login) { }, ]; - type RequestNewValidateCodeParams = { - email?: string; - }; - const params: RequestNewValidateCodeParams = {email: login}; - API.write('RequestNewValidateCode', params, {optimisticData, finallyData}); + API.write(WRITE_COMMANDS.REQUEST_NEW_VALIDATE_CODE, params, {optimisticData, finallyData}); } type OnyxData = { @@ -279,13 +279,9 @@ function signInAttemptState(): OnyxData { function beginSignIn(email: string) { const {optimisticData, successData, failureData} = signInAttemptState(); - type BeginSignInParams = { - email: string; - }; - const params: BeginSignInParams = {email}; - API.read('BeginSignIn', params, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.BEGIN_SIGNIN, params, {optimisticData, successData, failureData}); } /** @@ -295,14 +291,9 @@ function beginSignIn(email: string) { function beginAppleSignIn(idToken: string | undefined | null) { const {optimisticData, successData, failureData} = signInAttemptState(); - type BeginAppleSignInParams = { - idToken: typeof idToken; - preferredLocale: ValueOf | null; - }; - const params: BeginAppleSignInParams = {idToken, preferredLocale}; - API.write('SignInWithApple', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_WITH_APPLE, params, {optimisticData, successData, failureData}); } /** @@ -312,14 +303,9 @@ function beginAppleSignIn(idToken: string | undefined | null) { function beginGoogleSignIn(token: string | null) { const {optimisticData, successData, failureData} = signInAttemptState(); - type BeginGoogleSignInParams = { - token: string | null; - preferredLocale: ValueOf | null; - }; - const params: BeginGoogleSignInParams = {token, preferredLocale}; - API.write('SignInWithGoogle', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_WITH_GOOGLE, params, {optimisticData, successData, failureData}); } /** @@ -373,15 +359,9 @@ function signInWithShortLivedAuthToken(email: string, authToken: string) { // scene 2: the user is transitioning to desktop app from a different account on web app. const oldPartnerUserID = credentials.login === email && credentials.autoGeneratedLogin ? credentials.autoGeneratedLogin : ''; - type SignInWithShortLivedAuthTokenParams = { - authToken: string; - oldPartnerUserID: string; - skipReauthentication: boolean; - }; - const params: SignInWithShortLivedAuthTokenParams = {authToken, oldPartnerUserID, skipReauthentication: true}; - API.read('SignInWithShortLivedAuthToken', params, {optimisticData, successData, failureData}); + API.read(READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN, params, {optimisticData, successData, failureData}); } /** @@ -434,14 +414,6 @@ function signIn(validateCode: string, twoFactorAuthCode?: string) { ]; Device.getDeviceInfoWithID().then((deviceInfo) => { - type SignInUserParams = { - twoFactorAuthCode?: string; - email?: string; - preferredLocale: ValueOf | null; - validateCode?: string; - deviceInfo: string; - }; - const params: SignInUserParams = { twoFactorAuthCode, email: credentials.login, @@ -454,7 +426,7 @@ function signIn(validateCode: string, twoFactorAuthCode?: string) { params.validateCode = validateCode || credentials.validateCode; } - API.write('SigninUser', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_USER, params, {optimisticData, successData, failureData}); }); } @@ -520,14 +492,6 @@ function signInWithValidateCode(accountID: number, code: string, twoFactorAuthCo }, ]; Device.getDeviceInfoWithID().then((deviceInfo) => { - type SignInUserWithLinkParams = { - accountID: number; - validateCode?: string; - twoFactorAuthCode?: string; - preferredLocale: ValueOf | null; - deviceInfo: string; - }; - const params: SignInUserWithLinkParams = { accountID, validateCode, @@ -536,7 +500,7 @@ function signInWithValidateCode(accountID: number, code: string, twoFactorAuthCo deviceInfo, }; - API.write('SigninUserWithLink', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK, params, {optimisticData, successData, failureData}); }); } @@ -652,7 +616,7 @@ function setAccountError(error: string) { const reauthenticatePusher = throttle( () => { Log.info('[Pusher] Re-authenticating and then reconnecting'); - Authentication.reauthenticate('AuthenticatePusher') + Authentication.reauthenticate(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER) .then(Pusher.reconnect) .catch(() => { console.debug('[PusherConnectionManager]', 'Unable to re-authenticate Pusher because we are offline.'); @@ -665,15 +629,6 @@ const reauthenticatePusher = throttle( function authenticatePusher(socketID: string, channelName: string, callback: ChannelAuthorizationCallback) { Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName}); - type AuthenticatePusherParams = { - // eslint-disable-next-line @typescript-eslint/naming-convention - socket_id: string; - // eslint-disable-next-line @typescript-eslint/naming-convention - channel_name: string; - shouldRetry: boolean; - forceNetworkRequest: boolean; - }; - const params: AuthenticatePusherParams = { // eslint-disable-next-line @typescript-eslint/naming-convention socket_id: socketID, @@ -685,7 +640,7 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha // We use makeRequestWithSideEffects here because we need to authorize to Pusher (an external service) each time a user connects to any channel. // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('AuthenticatePusher', params) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params) .then((response) => { if (response?.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher because authToken is expired'); @@ -749,13 +704,9 @@ function requestUnlinkValidationLink() { }, ]; - type RequestUnlinkValidationLinkParams = { - email?: string; - }; - const params: RequestUnlinkValidationLinkParams = {email: credentials.login}; - API.write('RequestUnlinkValidationLink', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK, params, {optimisticData, successData, failureData}); } function unlinkLogin(accountID: number, validateCode: string) { @@ -796,17 +747,12 @@ function unlinkLogin(accountID: number, validateCode: string) { }, ]; - type UnlinkLoginParams = { - accountID: number; - validateCode: string; - }; - const params: UnlinkLoginParams = { accountID, validateCode, }; - API.write('UnlinkLogin', params, { + API.write(WRITE_COMMANDS.UNLINK_LOGIN, params, { optimisticData, successData, failureData, @@ -847,7 +793,7 @@ function toggleTwoFactorAuth(enable: boolean) { }, ]; - API.write(enable ? 'EnableTwoFactorAuth' : 'DisableTwoFactorAuth', {}, {optimisticData, successData, failureData}); + API.write(enable ? WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH : WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, {}, {optimisticData, successData, failureData}); } function validateTwoFactorAuth(twoFactorAuthCode: string) { @@ -881,13 +827,9 @@ function validateTwoFactorAuth(twoFactorAuthCode: string) { }, ]; - type ValidateTwoFactorAuthParams = { - twoFactorAuthCode: string; - }; - const params: ValidateTwoFactorAuthParams = {twoFactorAuthCode}; - API.write('TwoFactorAuth_Validate', params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE, params, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c03fa15fe1ae..a7aab98f02c6 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -3,6 +3,8 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; import * as API from '@libs/API'; +import type {CancelTaskParams, CompleteTaskParams, CreateTaskParams, EditTaskAssigneeParams, EditTaskParams, ReopenTaskParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -226,21 +228,7 @@ function createTaskAndNavigate( clearOutTaskInfo(); - type CreateTaskParameters = { - parentReportActionID?: string; - parentReportID?: string; - taskReportID?: string; - createdTaskReportActionID?: string; - title?: string; - description?: string; - assignee?: string; - assigneeAccountID?: number; - assigneeChatReportID?: string; - assigneeChatReportActionID?: string; - assigneeChatCreatedReportActionID?: string; - }; - - const parameters: CreateTaskParameters = { + const parameters: CreateTaskParams = { parentReportActionID: optimisticAddCommentReport.reportAction.reportActionID, parentReportID, taskReportID: optimisticTaskReport.reportID, @@ -254,7 +242,7 @@ function createTaskAndNavigate( assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction?.reportActionID, }; - API.write('CreateTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CREATE_TASK, parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(parentReportID); } @@ -316,17 +304,12 @@ function completeTask(taskReport: OnyxEntry) { }, ]; - type CompleteTaskParameters = { - taskReportID?: string; - completedTaskReportActionID?: string; - }; - - const parameters: CompleteTaskParameters = { + const parameters: CompleteTaskParams = { taskReportID, completedTaskReportActionID: completedTaskReportAction.reportActionID, }; - API.write('CompleteTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.COMPLETE_TASK, parameters, {optimisticData, successData, failureData}); } /** @@ -388,17 +371,12 @@ function reopenTask(taskReport: OnyxEntry) { }, ]; - type ReopenTaskParameters = { - taskReportID?: string; - reopenedTaskReportActionID?: string; - }; - - const parameters: ReopenTaskParameters = { + const parameters: ReopenTaskParams = { taskReportID, reopenedTaskReportActionID: reopenedTaskReportAction.reportActionID, }; - API.write('ReopenTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REOPEN_TASK, parameters, {optimisticData, successData, failureData}); } function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task) { @@ -461,21 +439,14 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task }, ]; - type EditTaskParameters = { - taskReportID?: string; - title?: string; - description?: string; - editedTaskReportActionID?: string; - }; - - const parameters: EditTaskParameters = { + const parameters: EditTaskParams = { taskReportID: report.reportID, title: reportName, description: reportDescription, editedTaskReportActionID: editTaskReportAction.reportActionID, }; - API.write('EditTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.EDIT_TASK, parameters, {optimisticData, successData, failureData}); } function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: OnyxEntry = null) { @@ -555,16 +526,7 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi failureData.push(...assigneeChatReportOnyxData.failureData); } - type EditTaskAssigneeParameters = { - taskReportID?: string; - assignee?: string; - editedTaskReportActionID?: string; - assigneeChatReportID?: string; - assigneeChatReportActionID?: string; - assigneeChatCreatedReportActionID?: string; - }; - - const parameters: EditTaskAssigneeParameters = { + const parameters: EditTaskAssigneeParams = { taskReportID: report.reportID, assignee: assigneeEmail, editedTaskReportActionID: editTaskReportAction.reportActionID, @@ -573,7 +535,7 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction?.reportActionID, }; - API.write('EditTaskAssignee', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.EDIT_TASK_ASSIGNEE, parameters, {optimisticData, successData, failureData}); } /** @@ -718,11 +680,7 @@ function getShareDestination(reportID: string, reports: OnyxCollection 1; - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. - OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); let subtitle = ''; if (ReportUtils.isChatReport(report) && ReportUtils.isDM(report) && ReportUtils.hasSingleParticipant(report)) { @@ -872,17 +830,12 @@ function deleteTask(taskReportID: string, taskTitle: string, originalStateNum: n }, ]; - type CancelTaskParameters = { - cancelledTaskReportActionID?: string; - taskReportID?: string; - }; - - const parameters: CancelTaskParameters = { + const parameters: CancelTaskParams = { cancelledTaskReportActionID: optimisticReportActionID, taskReportID, }; - API.write('CancelTask', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.CANCEL_TASK, parameters, {optimisticData, successData, failureData}); if (shouldDeleteTaskReport) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReport?.reportID ?? '')); diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 14b1a6455349..055d1f2b53a2 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -1,6 +1,8 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; +import type {AddSchoolPrincipalParams, ReferTeachersUniteVolunteerParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -51,13 +53,6 @@ function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, l }, ]; - type ReferTeachersUniteVolunteerParams = { - reportID: string; - firstName: string; - lastName: string; - partnerUserID: string; - }; - const parameters: ReferTeachersUniteVolunteerParams = { reportID: publicRoomReportID, firstName, @@ -65,7 +60,7 @@ function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, l partnerUserID, }; - API.write('ReferTeachersUniteVolunteer', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER, parameters, {optimisticData}); Navigation.dismissModal(publicRoomReportID); } @@ -177,14 +172,6 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: }, ]; - type AddSchoolPrincipalParams = { - firstName: string; - lastName: string; - partnerUserID: string; - policyID: string; - reportCreationData: string; - }; - const parameters: AddSchoolPrincipalParams = { firstName, lastName, @@ -193,7 +180,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: reportCreationData: JSON.stringify(reportCreationData), }; - API.write('AddSchoolPrincipal', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL, parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(expenseChatReportID); } diff --git a/src/libs/actions/Timing.ts b/src/libs/actions/Timing.ts index 9e40f088f1c2..28ffdd92ffba 100644 --- a/src/libs/actions/Timing.ts +++ b/src/libs/actions/Timing.ts @@ -1,4 +1,6 @@ import * as API from '@libs/API'; +import type {SendPerformanceTimingParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import * as Environment from '@libs/Environment/Environment'; import Firebase from '@libs/Firebase'; import getPlatform from '@libs/getPlatform'; @@ -62,15 +64,13 @@ function end(eventName: string, secondaryName = '', maxExecutionTime = 0) { Log.warn(`${eventName} exceeded max execution time of ${maxExecutionTime}.`, {eventTime, eventName}); } - API.read( - 'SendPerformanceTiming', - { - name: grafanaEventName, - value: eventTime, - platform: `${getPlatform()}`, - }, - {}, - ); + const parameters: SendPerformanceTimingParams = { + name: grafanaEventName, + value: eventTime, + platform: `${getPlatform()}`, + }; + + API.read(READ_COMMANDS.SEND_PERFORMANCE_TIMING, parameters, {}); }); } diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..7d273d8045f0 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -3,6 +3,8 @@ import lodashClone from 'lodash/clone'; import lodashHas from 'lodash/has'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; +import type {GetRouteForDraftParams, GetRouteParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -73,6 +75,8 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp // Clear the existing route so that we don't show an old route routes: { route0: { + // Clear the existing distance to recalculate next time + distance: null, geometry: { coordinates: null, }, @@ -146,6 +150,7 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: // Clear the existing route so that we don't show an old route routes: { route0: { + // Clear the existing distance to recalculate next time distance: null, geometry: { coordinates: null, @@ -209,14 +214,12 @@ function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): Ony * Used so we can generate a map view of the provided waypoints */ function getRoute(transactionID: string, waypoints: WaypointCollection) { - API.read( - 'GetRoute', - { - transactionID, - waypoints: JSON.stringify(waypoints), - }, - getOnyxDataForRouteRequest(transactionID), - ); + const parameters: GetRouteParams = { + transactionID, + waypoints: JSON.stringify(waypoints), + }; + + API.read(READ_COMMANDS.GET_ROUTE, parameters, getOnyxDataForRouteRequest(transactionID)); } /** @@ -224,14 +227,12 @@ function getRoute(transactionID: string, waypoints: WaypointCollection) { * Used so we can generate a map view of the provided waypoints */ function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) { - API.read( - 'GetRouteForDraft', - { - transactionID, - waypoints: JSON.stringify(waypoints), - }, - getOnyxDataForRouteRequest(transactionID, true), - ); + const parameters: GetRouteForDraftParams = { + transactionID, + waypoints: JSON.stringify(waypoints), + }; + + API.read(READ_COMMANDS.GET_ROUTE_FOR_DRAFT, parameters, getOnyxDataForRouteRequest(transactionID, true)); } /** @@ -255,6 +256,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i // Clear the existing route so that we don't show an old route routes: { route0: { + // Clear the existing distance to recalculate next time distance: null, geometry: { coordinates: null, diff --git a/src/libs/actions/UpdateRequired.ts b/src/libs/actions/UpdateRequired.ts new file mode 100644 index 000000000000..078bcc41fd71 --- /dev/null +++ b/src/libs/actions/UpdateRequired.ts @@ -0,0 +1,11 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function alertUser() { + Onyx.set(ONYXKEYS.UPDATE_REQUIRED, true); +} + +export { + // eslint-disable-next-line import/prefer-default-export + alertUser, +}; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index df5709ac68e2..d6ed882be54a 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,10 +1,27 @@ import {isBefore} from 'date-fns'; -import type {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import type { + AddNewContactMethodParams, + CloseAccountParams, + DeleteContactMethodParams, + GetStatementPDFParams, + RequestContactMethodValidateCodeParams, + SetContactMethodAsDefaultParams, + UpdateChatPriorityModeParams, + UpdateFrequentlyUsedEmojisParams, + UpdateNewsletterSubscriptionParams, + UpdatePreferredEmojiSkinToneParams, + UpdateStatusParams, + UpdateThemeParams, + ValidateLoginParams, + ValidateSecondaryLoginParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import * as Pusher from '@libs/Pusher/pusher'; @@ -73,11 +90,9 @@ function closeAccount(reason: string) { }, ]; - type CloseAccountParams = {message: string}; - const parameters: CloseAccountParams = {message: reason}; - API.write('CloseAccount', parameters, { + API.write(WRITE_COMMANDS.CLOSE_ACCOUNT, parameters, { optimisticData, failureData, }); @@ -147,11 +162,9 @@ function requestContactMethodValidateCode(contactMethod: string) { }, ]; - type RequestContactMethodValidateCodeParams = {email: string}; - const parameters: RequestContactMethodValidateCodeParams = {email: contactMethod}; - API.write('RequestContactMethodValidateCode', parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.REQUEST_CONTACT_METHOD_VALIDATE_CODE, parameters, {optimisticData, successData, failureData}); } /** @@ -173,11 +186,9 @@ function updateNewsletterSubscription(isSubscribed: boolean) { }, ]; - type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; - const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; - API.write('UpdateNewsletterSubscription', parameters, { + API.write(WRITE_COMMANDS.UPDATE_NEWSLETTER_SUBSCRIPTION, parameters, { optimisticData, failureData, }); @@ -234,11 +245,9 @@ function deleteContactMethod(contactMethod: string, loginList: Record { PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data); }); @@ -543,13 +544,9 @@ function updatePreferredSkinTone(skinTone: number) { }, ]; - type UpdatePreferredEmojiSkinToneParams = { - value: number; - }; - const parameters: UpdatePreferredEmojiSkinToneParams = {value: skinTone}; - API.write('UpdatePreferredEmojiSkinTone', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE, parameters, {optimisticData}); } /** @@ -563,11 +560,10 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) value: frequentlyUsedEmojis, }, ]; - type UpdateFrequentlyUsedEmojisParams = {value: string}; const parameters: UpdateFrequentlyUsedEmojisParams = {value: JSON.stringify(frequentlyUsedEmojis)}; - API.write('UpdateFrequentlyUsedEmojis', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS, parameters, {optimisticData}); } /** @@ -593,17 +589,12 @@ function updateChatPriorityMode(mode: ValueOf, autom }); } - type UpdateChatPriorityModeParams = { - value: ValueOf; - automatic: boolean; - }; - const parameters: UpdateChatPriorityModeParams = { value: mode, automatic, }; - API.write('UpdateChatPriorityMode', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE, parameters, {optimisticData}); if (!autoSwitchedToFocusMode) { Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); @@ -672,11 +663,9 @@ function generateStatementPDF(period: string) { }, ]; - type GetStatementPDFParams = {period: string}; - const parameters: GetStatementPDFParams = {period}; - API.read('GetStatementPDF', parameters, { + API.read(READ_COMMANDS.GET_STATEMENT_PDF, parameters, { optimisticData, successData, failureData, @@ -765,15 +754,11 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, ]; - type SetContactMethodAsDefaultParams = { - partnerUserID: string; - }; - const parameters: SetContactMethodAsDefaultParams = { partnerUserID: newDefaultContactMethod, }; - API.write('SetContactMethodAsDefault', parameters, { + API.write(WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT, parameters, { optimisticData, successData, failureData, @@ -790,15 +775,11 @@ function updateTheme(theme: ValueOf) { }, ]; - type UpdateThemeParams = { - value: string; - }; - const parameters: UpdateThemeParams = { value: theme, }; - API.write('UpdateTheme', parameters, {optimisticData}); + API.write(WRITE_COMMANDS.UPDATE_THEME, parameters, {optimisticData}); Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); } @@ -819,15 +800,9 @@ function updateCustomStatus(status: Status) { }, ]; - type UpdateStatusParams = { - text?: string; - emojiCode: string; - clearAfter?: string; - }; - const parameters: UpdateStatusParams = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; - API.write('UpdateStatus', parameters, { + API.write(WRITE_COMMANDS.UPDATE_STATUS, parameters, { optimisticData, }); } @@ -847,9 +822,7 @@ function clearCustomStatus() { }, }, ]; - API.write('ClearStatus', undefined, { - optimisticData, - }); + API.write(WRITE_COMMANDS.CLEAR_STATUS, {}, {optimisticData}); } /** diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index e7a272343209..b03b5e8f6d3d 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -2,38 +2,25 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; +import type { + AcceptWalletTermsParams, + AnswerQuestionsForWalletParams, + RequestPhysicalExpensifyCardParams, + UpdatePersonalDetailsForWalletParams, + VerifyIdentityParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletAdditionalQuestionDetails} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -type WalletTerms = { - hasAcceptedTerms: boolean; - reportID: string; -}; - type WalletQuestionAnswer = { question: string; answer: string; }; -type IdentityVerification = { - onfidoData: string; -}; - -type PersonalDetails = { - phoneNumber: string; - legalFirstName: string; - legalLastName: string; - addressStreet: string; - addressCity: string; - addressState: string; - addressZip: string; - dob: string; - ssn: string; -}; - /** * Fetch and save locally the Onfido SDK token and applicantID * - The sdkToken is used to initialize the Onfido SDK client @@ -62,14 +49,7 @@ function openOnfidoFlow() { }, ]; - API.read( - 'OpenOnfidoFlow', - {}, - { - optimisticData, - finallyData, - }, - ); + API.read(READ_COMMANDS.OPEN_ONFIDO_FLOW, {}, {optimisticData, finallyData}); } function setAdditionalDetailsQuestions(questions: WalletAdditionalQuestionDetails[], idNumber: string) { @@ -95,7 +75,7 @@ function setKYCWallSource(source?: ValueOf, chatRe /** * Validates a user's provided details against a series of checks */ -function updatePersonalDetails(personalDetails: PersonalDetails) { +function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -118,7 +98,7 @@ function updatePersonalDetails(personalDetails: PersonalDetails) { }, ]; - API.write('UpdatePersonalDetailsForWallet', personalDetails, { + API.write(WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET, personalDetails, { optimisticData, finallyData, }); @@ -130,7 +110,7 @@ function updatePersonalDetails(personalDetails: PersonalDetails) { * The API will always return the updated userWallet in the response as a convenience so we can avoid an additional * API request to fetch the userWallet after we call VerifyIdentity */ -function verifyIdentity(parameters: IdentityVerification) { +function verifyIdentity(parameters: VerifyIdentityParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -171,7 +151,7 @@ function verifyIdentity(parameters: IdentityVerification) { }, }, ]; - API.write('VerifyIdentity', parameters, { + API.write(WRITE_COMMANDS.VERIFY_IDENTITY, parameters, { optimisticData, successData, failureData, @@ -183,7 +163,7 @@ function verifyIdentity(parameters: IdentityVerification) { * * @param parameters.chatReportID When accepting the terms of wallet to pay an IOU, indicates the parent chat ID of the IOU */ -function acceptWalletTerms(parameters: WalletTerms) { +function acceptWalletTerms(parameters: AcceptWalletTermsParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -223,23 +203,23 @@ function acceptWalletTerms(parameters: WalletTerms) { }, ]; - const requestParams: WalletTerms = {hasAcceptedTerms: parameters.hasAcceptedTerms, reportID: parameters.reportID}; + const requestParams: AcceptWalletTermsParams = {hasAcceptedTerms: parameters.hasAcceptedTerms, reportID: parameters.reportID}; - API.write('AcceptWalletTerms', requestParams, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.ACCEPT_WALLET_TERMS, requestParams, {optimisticData, successData, failureData}); } /** * Fetches data when the user opens the InitialSettingsPage */ function openInitialSettingsPage() { - API.read('OpenInitialSettingsPage', {}); + API.read(READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE, {}); } /** * Fetches data when the user opens the EnablePaymentsPage */ function openEnablePaymentsPage() { - API.read('OpenEnablePaymentsPage', {}); + API.read(READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE, {}); } function updateCurrentStep(currentStep: ValueOf) { @@ -269,17 +249,12 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str }, ]; - type AnswerQuestionsForWallet = { - idologyAnswers: string; - idNumber: string; - }; - - const requestParams: AnswerQuestionsForWallet = { + const requestParams: AnswerQuestionsForWalletParams = { idologyAnswers, idNumber, }; - API.write('AnswerQuestionsForWallet', requestParams, { + API.write(WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET, requestParams, { optimisticData, finallyData, }); @@ -293,18 +268,6 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private address: {city, country, state, street, zip}, } = privatePersonalDetails; - type RequestPhysicalExpensifyCardParams = { - authToken: string; - legalFirstName: string; - legalLastName: string; - phoneNumber: string; - addressCity: string; - addressCountry: string; - addressState: string; - addressStreet: string; - addressZip: string; - }; - const requestParams: RequestPhysicalExpensifyCardParams = { authToken, legalFirstName, @@ -334,7 +297,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private }, ]; - API.write('RequestPhysicalExpensifyCard', requestParams, {optimisticData}); + API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData}); } export { diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 3b6617aa3ed0..0f1e383522eb 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ -import type {View} from 'react-native'; +/* eslint-disable no-restricted-imports */ +import type {Text as RNText, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -13,7 +13,7 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent) { return resolve({horizontal: 0, vertical: 0}); diff --git a/src/libs/focusAndUpdateMultilineInputRange.ts b/src/libs/focusAndUpdateMultilineInputRange.ts deleted file mode 100644 index 2e4a3d23631e..000000000000 --- a/src/libs/focusAndUpdateMultilineInputRange.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type {TextInput} from 'react-native'; - -/** - * Focus a multiline text input and place the cursor at the end of the value (if there is a value in the input). - * - * When a multiline input contains a text value that goes beyond the scroll height, the cursor will be placed - * at the end of the text value, and automatically scroll the input field to this position after the field gains - * focus. This provides a better user experience in cases where the text in the field has to be edited. The auto- - * scroll behaviour works on all platforms except iOS native. - * See https://github.com/Expensify/App/issues/20836 for more details. - */ -export default function focusAndUpdateMultilineInputRange(input: TextInput | HTMLTextAreaElement) { - if (!input) { - return; - } - - input.focus(); - if ('setSelectionRange' in input && input.value) { - const length = input.value.length; - input.setSelectionRange(length, length); - // eslint-disable-next-line no-param-reassign - input.scrollTop = input.scrollHeight; - } -} diff --git a/src/libs/migrations/PersonalDetailsByAccountID.js b/src/libs/migrations/PersonalDetailsByAccountID.js index c08ec6fb2c43..24aece8f5a97 100644 --- a/src/libs/migrations/PersonalDetailsByAccountID.js +++ b/src/libs/migrations/PersonalDetailsByAccountID.js @@ -251,12 +251,6 @@ export default function () { delete newReport.lastActorEmail; } - if (lodashHas(newReport, ['participants'])) { - reportWasModified = true; - Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing participants from report ${newReport.reportID}`); - delete newReport.participants; - } - if (lodashHas(newReport, ['ownerEmail'])) { reportWasModified = true; Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing ownerEmail from report ${newReport.reportID}`); diff --git a/src/libs/migrations/RenameReceiptFilename.ts b/src/libs/migrations/RenameReceiptFilename.ts index dff2be5c286d..b867024fc74e 100644 --- a/src/libs/migrations/RenameReceiptFilename.ts +++ b/src/libs/migrations/RenameReceiptFilename.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import type {NullishDeep, OnyxCollection} from 'react-native-onyx/lib/types'; +import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; import type Transaction from '@src/types/onyx/Transaction'; diff --git a/src/libs/onyxSubscribe.ts b/src/libs/onyxSubscribe.ts index 4572ca35a4f2..af775842fc16 100644 --- a/src/libs/onyxSubscribe.ts +++ b/src/libs/onyxSubscribe.ts @@ -1,6 +1,6 @@ import type {ConnectOptions} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {OnyxKey} from '@src/ONYXKEYS'; +import type {OnyxCollectionKey, OnyxKey} from '@src/ONYXKEYS'; /** * Connect to onyx data. Same params as Onyx.connect(), but returns a function to unsubscribe. @@ -8,7 +8,7 @@ import type {OnyxKey} from '@src/ONYXKEYS'; * @param mapping Same as for Onyx.connect() * @return Unsubscribe callback */ -function onyxSubscribe(mapping: ConnectOptions) { +function onyxSubscribe(mapping: ConnectOptions) { const connectionId = Onyx.connect(mapping); return () => Onyx.disconnect(connectionId); } diff --git a/src/libs/tryResolveUrlFromApiRoot.ts b/src/libs/tryResolveUrlFromApiRoot.ts index adf717d500be..8eb5bdba0129 100644 --- a/src/libs/tryResolveUrlFromApiRoot.ts +++ b/src/libs/tryResolveUrlFromApiRoot.ts @@ -1,3 +1,4 @@ +import type {ImageSourcePropType} from 'react-native'; import Config from '@src/CONFIG'; import type {Request} from '@src/types/onyx'; import * as ApiUtils from './ApiUtils'; @@ -18,12 +19,12 @@ const ORIGIN_PATTERN = new RegExp(`^(${ORIGINS_TO_REPLACE.join('|')})`); * - Unmatched URLs (non expensify) are returned with no modifications */ function tryResolveUrlFromApiRoot(url: string): string; -function tryResolveUrlFromApiRoot(url: number): number; -function tryResolveUrlFromApiRoot(url: string | number): string | number; -function tryResolveUrlFromApiRoot(url: string | number): string | number { +function tryResolveUrlFromApiRoot(url: ImageSourcePropType): number; +function tryResolveUrlFromApiRoot(url: string | ImageSourcePropType): string | ImageSourcePropType; +function tryResolveUrlFromApiRoot(url: string | ImageSourcePropType): string | ImageSourcePropType { // in native, when we import an image asset, it will have a number representation which can be used in `source` of Image // in this case we can skip the url resolving - if (typeof url === 'number') { + if (typeof url !== 'string') { return url; } const apiRoot = ApiUtils.getApiRoot({shouldUseSecure: false} as Request); diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 5ee86b2bf8e6..6faa84ef8b43 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -3,12 +3,15 @@ import {View} from 'react-native'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; type EditReportFieldDatePageProps = { /** Value of the policy report field */ @@ -27,12 +30,13 @@ type EditReportFieldDatePageProps = { function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const inputRef = useRef(null); + const inputRef = useRef(null); const validate = useCallback( - (value: Record) => { - const errors: Record = {}; - if (value[fieldID].trim() === '') { + (values: OnyxFormValuesFields) => { + const errors: Errors = {}; + const value = values[fieldID]; + if (typeof value === 'string' && value.trim() === '') { errors[fieldID] = 'common.error.fieldRequired'; } return errors; @@ -48,7 +52,6 @@ function EditReportFieldDatePage({fieldName, onSubmit, fieldValue, fieldID}: Edi testID={EditReportFieldDatePage.displayName} > - {/* @ts-expect-error TODO: TS migration */} - InputComponent={DatePicker} inputID={fieldID} name={fieldID} diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index b468861e9a27..733bfd6e5fee 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -2,13 +2,16 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} 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 useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; type EditReportFieldTextPageProps = { /** Value of the policy report field */ @@ -27,12 +30,13 @@ type EditReportFieldTextPageProps = { function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const inputRef = useRef(null); + const inputRef = useRef(null); const validate = useCallback( - (value: Record) => { - const errors: Record = {}; - if (value[fieldID].trim() === '') { + (values: OnyxFormValuesFields) => { + const errors: Errors = {}; + const value = values[fieldID]; + if (typeof value === 'string' && value.trim() === '') { errors[fieldID] = 'common.error.fieldRequired'; } return errors; @@ -48,7 +52,6 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, fieldID}: Edi testID={EditReportFieldTextPage.displayName} > - {/* @ts-expect-error TODO: TS migration */} + + +

+ + + + + + + {translate('updateRequiredView.pleaseInstall')} + + + {translate('updateRequiredView.toGetLatestChanges')} + + + +