diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 4f0c3b75c0ea..e2076c029f70 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -2,7 +2,7 @@ self-hosted-runner: labels: - ubuntu-latest-xl - - macos-13-large - - macos-13-xlarge + - macos-15-large + - macos-15-xlarge - ubuntu-latest-reassure-tests - macos-12 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 86bfa195cd75..d90df954b216 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -137,7 +137,7 @@ jobs: - name: Setup Node id: setup-node uses: ./.github/actions/composite/setupNode - with: + with: IS_HYBRID_BUILD: 'true' - name: Run grunt build @@ -166,8 +166,9 @@ jobs: env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op document get --output ./upload-key.keystore upload-key.keystore - op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json + op read op://Mobile-Deploy-CI/firebase.json/firebase.json --force --out-file ./firebase.json + op read op://Mobile-Deploy-CI/upload-key.keystore/upload-key.keystore --force --out-file ./upload-key.keystore + op read op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json --force --out-file ./android-fastlane-json-key.json # Copy the keystore to the Android directory for Fullstory cp ./upload-key.keystore Mobile-Expensify/Android @@ -218,7 +219,7 @@ jobs: else echo "Skipping the completion of the previous version rollout" fi - + # Submit the new version for review and slow rollout when it's approved bundle exec fastlane android upload_google_play_production_hybrid_rollout env: @@ -230,6 +231,31 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + - name: Generate APK from AAB + run: | + json=$(curl -s https://api.github.com/repos/google/bundletool/releases/latest) + downloadUrl=$(echo "$json" | jq -r ".assets | .[].browser_download_url") + curl "$downloadUrl" -4 -sL -o 'bundletool.jar' + java -jar bundletool.jar build-apks --bundle=${{ env.aabPath }} --output=Expensify.apks \ + --mode=universal \ + --ks=upload-key.keystore \ + --ks-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} \ + --ks-key-alias=${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} \ + --key-pass=pass:${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + + unzip -p Expensify.apks universal.apk > Expensify.apk + + - name: Upload Android APK build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: android-hybrid-apk-artifact + path: Expensify.apk + + - name: Upload Android build to Firebase distribution + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane android upload_firebase_distribution + - name: Upload Android build artifact if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: actions/upload-artifact@v4 @@ -313,8 +339,8 @@ jobs: name: Build and deploy iOS needs: prep env: - DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer - runs-on: macos-13-xlarge + DEVELOPER_DIR: /Applications/Xcode_16.2.0.app/Contents/Developer + runs-on: macos-15-xlarge steps: - name: Checkout uses: actions/checkout@v4 @@ -327,7 +353,7 @@ jobs: uses: ./.github/actions/composite/setupNode - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 + uses: ruby/setup-ruby@v1.204.0 with: bundler-cache: true @@ -428,9 +454,9 @@ jobs: iOS_hybrid: name: Build and deploy iOS HybridApp needs: prep - runs-on: macos-13-xlarge + runs-on: macos-15-xlarge env: - DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.2.0.app/Contents/Developer steps: - name: Checkout uses: actions/checkout@v4 @@ -451,11 +477,11 @@ jobs: - name: Setup Node id: setup-node uses: ./.github/actions/composite/setupNode - with: + with: IS_HYBRID_BUILD: 'true' - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 + uses: ruby/setup-ruby@v1.204.0 with: bundler-cache: true @@ -488,9 +514,10 @@ jobs: env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op document get --output ./OldApp_AppStore.mobileprovision OldApp_AppStore - op document get --output ./OldApp_AppStore_Share_Extension.mobileprovision OldApp_AppStore_Share_Extension - op document get --output ./OldApp_AppStore_Notification_Service.mobileprovision OldApp_AppStore_Notification_Service + op read op://Mobile-Deploy-CI/firebase.json/firebase.json --force --out-file ./firebase.json + op read op://Mobile-Deploy-CI/OldApp_AppStore/OldApp_AppStore.mobileprovision --force --out-file ./OldApp_AppStore.mobileprovision + op read op://Mobile-Deploy-CI/OldApp_AppStore_Share_Extension/OldApp_AppStore_Share_Extension.mobileprovision --force --out-file ./OldApp_AppStore_Share_Extension.mobileprovision + op read op://Mobile-Deploy-CI/OldApp_AppStore_Notification_Service/OldApp_AppStore_Notification_Service.mobileprovision --force --out-file ./OldApp_AppStore_Notification_Service.mobileprovision - name: Decrypt AppStore profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg @@ -537,8 +564,8 @@ jobs: run: | # Complete the previous version rollout bundle exec fastlane ios complete_hybrid_rollout - - # Submit the new version for review and phased rollout when it's approved + + # Submit the new version for review and phased rollout when it's approved bundle exec fastlane ios submit_hybrid_for_rollout env: VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} @@ -549,6 +576,10 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + - name: Upload iOS build to Firebase distribution + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios upload_firebase_distribution + - name: Upload iOS build artifact if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: actions/upload-artifact@v4 @@ -756,6 +787,7 @@ jobs: "./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap.js.map" "./android-aab-artifact/app-production-release.aab#android.aab" "./android-hybrid-build-artifact/Expensify-release.aab#android-hybrid.aab" + "./android-hybrid-apk-artifact/Expensify.apk#android-hybrid.apk" "./android-hybrid-sourcemap-artifact/index.android.bundle.map#android-hybrid-sourcemap.js.map" "./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap.js.map" "./desktop-staging-build-artifact/NewExpensify.dmg#desktop-staging.dmg" diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 1ce493715b78..b05a308c49de 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -141,8 +141,8 @@ jobs: needs: [validateActor, getBranchRef] if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} env: - DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer - runs-on: macos-13-xlarge + DEVELOPER_DIR: /Applications/Xcode_16.2.0.app/Contents/Developer + runs-on: macos-15-xlarge steps: - name: Checkout uses: actions/checkout@v4 @@ -163,10 +163,10 @@ jobs: uses: ./.github/actions/composite/setupNode - name: Setup XCode - run: sudo xcode-select -switch /Applications/Xcode_15.2.0.app + run: sudo xcode-select -switch /Applications/Xcode_16.2.0.app - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 + uses: ruby/setup-ruby@v1.204.0 with: bundler-cache: true diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index d9f22ecb9178..58398223e190 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -197,8 +197,8 @@ jobs: needs: [validateActor, getBranchRef] if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} env: - DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer - runs-on: macos-13-xlarge + DEVELOPER_DIR: /Applications/Xcode_16.2.0.app/Contents/Developer + runs-on: macos-15-xlarge steps: - name: Checkout uses: actions/checkout@v4 @@ -235,7 +235,7 @@ jobs: echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 + uses: ruby/setup-ruby@v1.204.0 with: bundler-cache: true diff --git a/Gemfile.lock b/Gemfile.lock index 3c0202ff325c..53a966923ee9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -165,6 +165,9 @@ GEM apktools (~> 0.7) aws-sdk-s3 (~> 1) mime-types (~> 3.3) + fastlane-plugin-firebase_app_distribution (0.10.0) + google-apis-firebaseappdistribution_v1 (~> 0.3.0) + google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) ffi (1.17.0) @@ -184,6 +187,10 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml + google-apis-firebaseappdistribution_v1 (0.3.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.2.0) + google-apis-core (>= 0.11.0, < 2.a) google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) @@ -229,7 +236,7 @@ GEM molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.4.0) + nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) @@ -274,12 +281,12 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.27.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.4.0) + nanaimo (~> 0.3.0) rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) @@ -298,6 +305,7 @@ DEPENDENCIES cocoapods (= 1.15.2) fastlane (~> 2, >= 2.222.0) fastlane-plugin-aws_s3 + fastlane-plugin-firebase_app_distribution xcodeproj (< 1.26.0) xcpretty (~> 0) diff --git a/Mobile-Expensify b/Mobile-Expensify index b6ef9f667f25..1ff20b6812d2 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit b6ef9f667f25bfad2f932a020fbfc8ec5dd57e5a +Subproject commit 1ff20b6812d20869b877b5cc48d01440054b3923 diff --git a/android/app/build.gradle b/android/app/build.gradle index e144ee7d5082..8dbf42ce117d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009008200 - versionName "9.0.82-0" + versionCode 1009008401 + versionName "9.0.84-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/bolt-slash.svg b/assets/images/bolt-slash.svg index 268e1b98c4c6..2637fdf5f5d8 100644 --- a/assets/images/bolt-slash.svg +++ b/assets/images/bolt-slash.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index ecb3d3a45b48..9307b3e712d8 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -131,7 +131,7 @@ details[open] > summary { h1, summary { - font-family: 'Expensify Mono', 'SFMono-Regular', monospace, Arial, sans-serif; + font-family: 'Expensify New Kansas', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; font-weight: 500; font-size: larger; } diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md index 964664c1d519..5d1eda3c0819 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md @@ -42,7 +42,7 @@ Expensify makes it easy to manage and pay vendor bills with a straightforward wo ## Bill Pay Workflow 1. **SmartScan & Create**: When a vendor sends a bill, Expensify automatically SmartScans the document and creates a bill. -2. **Submission to Primary Contact**: The bill is submitted to the primary contact, who can review it on the Reports page under their default group policy. +2. **Submission to Primary Contact**: The bill is submitted to the primary contact, who can review it on the Reports page under their default group workspace. 3. **Communication**: If the approver needs clarification, they can communicate directly with the sender via the invoice linked to the bill. 4. **Approval Workflow**: Once reviewed, the bill follows your workspace’s approval process. The final approver handles the payment. 5. **Accounting Integration**: During approval, the bill is coded with the correct GL codes from your connected accounting software. Once approved, it can be exported back to your accounting system. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md index 90d3970cbc94..ed390e928fa4 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md @@ -22,9 +22,9 @@ The following steps will determine how data will be exported from Expensify to Q - **Check**: A single itemized check for each Expensify report. - **Journal Entry**: A single itemized journal entry for each Expensify report. - **Non-reimbursable expenses**: Non-reimbursable options include: + - **Credit Card**: Each expense appears as a separate credit card transaction with a post date that matches your credit card statement. If you centrally manage company cards through your domain, you can export expenses from each card to a specific QuickBooks account by clicking Edit Exports next to each user’s card. To display the merchant name in the payee field in QuickBooks Desktop, ensure that a matching Vendor exists in QuickBooks. Expensify searches for an exact match during export. If no match is found, the payee is mapped to a Credit Card Misc. Vendor created by Expensify. + - **Check**: Expenses are exported as individual itemized checks for each Expensify report. The check is written to the “vendor,” which is the person who created or submitted the report in Expensify. - **Vendor Bill**: Each Expensify report results in a single itemized vendor bill. The bill is associated with the “vendor,” which is the individual responsible for creating or submitting the report in Expensify. - - **Credit Card expenses**: Each expense appears as a separate credit card transaction with a post date that matches your credit card statement. If you centrally manage company cards through your domain, you can export expenses from each card to a specific QuickBooks account by clicking Edit Exports next to each user’s card. To display the merchant name in the payee field in QuickBooks Desktop, ensure that a matching Vendor exists in QuickBooks. Expensify searches for an exact match during export. If no match is found, the payee is mapped to a Credit Card Misc. Vendor created by Expensify. - - **Debit Card expenses**: Expenses are exported as individual itemized checks for each Expensify report. The check is written to the “vendor,” which is the person who created or submitted the report in Expensify. # Step 2: Configure coding/import settings diff --git a/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md b/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md index d7fa33221834..7a704f024ce7 100644 --- a/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md @@ -4,25 +4,41 @@ description: Close an Expensify Card ---
-A cardholder or a Domain Admin can cancel an Expensify Card. You may want to cancel a card: -- To cancel an old Expensify Card after upgrading to the new Expensify Visa® Commercial Card +A cardholder can cancel an Expensify Card themselves, or a Domain Admin can deactivate it. You may want to cancel or deactivate a card: - After a fraudulent or suspicious charge +- When an Expensify Card is lost or damaged - After an employee leaves the company +# Cardholders + +To cancel an Expensify Card assigned to you, + +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Click **Request a New Card** next to the card. +4. Choose a reason. +5. Confirm your address details for shipping a new card. +6. Consult this [guide](https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction) for how to dispute fraudulent transactions (where relevant). + # Domain Admins -To cancel an employee's Expensify Card as a Domain Admin, +To deactivate an employee's Expensify Card as a Domain Admin, 1. Hover over Settings, then click **Domains**. 2. Click the name of the domain. -3. Next to the card, click **Terminate**. +3. Next to the card, click **Edit Limit**. +4. Ensure the Custom Smart Limit toggle is enabled to be able to set a specific card limit. Otherwise, the card limit will be determined by the limit set for the group that the employee is in. +5. In the Limit Amount field, set the limit to $0. The card will be disabled for use until the limit is increased. +6. Click **Save**. -# Cardholders +Note: If you have concerns about fraudulent access to a Domain Admin's user account, please message Concierge or email concierge@expensify.com immediately. If necessary, our support team can manually suspend Expensify cards outside of the Expensify Domain as a temporary measure if your account is compromised. -To cancel an Expensify Card assigned to you, +# Terminating an old Expensify Card after upgrading to the new Expensify Visa® Commercial Card -1. Hover over Settings, then click **Account**. -2. Click the **Credit Card Import** tab. -3. Click **Cancel** next to the card. +To terminate old Expensify Cards that have since been upgraded, + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Next to the card, click **Terminate**.
diff --git a/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md b/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md index a507ff263c92..abbd51f3efb7 100644 --- a/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md +++ b/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md @@ -39,10 +39,10 @@ When you log in to Expensify in the future, you’ll be emailed a magic code tha If you have lost your mobile device and can’t find your recovery codes, your Domain Admin can complete the steps below to reset your 2FA **only if (1) you use a company email address or email address on a domain that you own and (2) the Domain Admin also has 2FA enabled**: -Go to Settings > Domains > Domain Members and click **Edit Settings** for your email address. -They then click **Reset** to reset two-factor authentication (2FA) on your account. +If your domain has 2FA enabled, a domain admin can follow Settings > Domains > Domain Members and click **Edit Settings** for your email address. +They can then click **Reset** to reset two-factor authentication (2FA) on your account. This will allow you to gain access to your account on the web or mobile app and configure 2FA again. -This will allow you to gain access to your account on the web or mobile app and reconfigure 2FA again. +If your domain does not have 2FA enabled, a domain admin can follow Settings > Domains > Domain Members and enable Two Factor Authentication. Then they can follow the previously mentioned steps to reset 2FA for your account. {% include info.html %} If you use a public email address such as gmail, hotmail, or yahoo, we unfortunately can’t help you disable your 2FA setting. If you are unable to find your recovery codes, you may need to create a new Expensify account with a different email address. diff --git a/docs/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports.md b/docs/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports.md index eac2723e5c9c..5636d1e49f4c 100644 --- a/docs/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports.md +++ b/docs/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports.md @@ -82,7 +82,7 @@ Enter any of the following formulas into the Formula field for each column. Be s | {report:title} | Would output "Expense Expenses to 2019-11-05" assuming that is the report's title.| | Report ID | Number is a unique number per report and can be used to identify specific reports.| | {report:id} | Would output R00I7J3xs5fn assuming that is the report's ID.| -| Old Report ID | A unique number per report and can be used to identify specific reports as well. Every report has both an ID and an old ID - they're simply different ways of showing the same information in either [base10](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fwww.twinkl.co.uk%2Fteaching-wiki%2Fbase-10) or base62. | +| Old Report ID | A unique number per report and can be used to identify specific reports as well. Every report has both an ID and an old ID - they're simply different ways of showing the same information in either base10 or base62. | | {report:oldID} | Would output R3513250790654885 assuming that is the report's old ID.| | Reimbursement ID | The unique number for a report that's been reimbursed via ACH in Expensify. The reimbursement ID is searchable on the Reports page and is found on your bank statement in the line-item detail for the reimbursed amount.| | {report:reimbursementid} | Would output 123456789109876 assuming that is the ID on the line-item detail for the reimbursed amount in your business bank account.| @@ -100,7 +100,7 @@ Enter any of the following formulas into the Formula field for each column. Be s | Created date | The expense report was originally created by the user.| | {report:created} | Would output 2010-09-15 12:00:00 assuming the expense report was created on September 15th, 2010 at noon.| | {report:created:yyyy-MM-dd} | Would output 2010-09-15 assuming the expense report was created on September 15, 2010.| -| | Note - All Date Formulas accept an optional format string. The default if one is not provided is yyyy-MM-dd hh:mm:ss. For a full breakdown, check out the Date Formatting [here](https://community.expensify.com/discussion/5799/deep-dive-date-formating-for-formulas/p1?new=1).| +| | Note - All Date Formulas accept an optional format string. The default if one is not provided is yyyy-MM-dd hh:mm:ss. For a full breakdown, check out the Date Formatting [here](https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports#date-formats).| | StartDate | Is the date of the earliest expense on the report.| | {report:startdate} | Would output 2010-09-15 assuming that is the date of the earliest expense on the report.| | EndDate| Is the date of the last expense on the report.| diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md index 497c618442b1..3b32f33266e7 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md @@ -3,7 +3,9 @@ title: QuickBooks Online Troubleshooting description: A list of common QuickBooks Online errors and how to resolve them --- -## Report won’t automatically export to QuickBooks Online +Occasionally, you might run into errors when exporting reports or syncing QuickBooks Online with Expensify. Below, you'll find detailed instructions to help you troubleshoot and resolve the most common connection and export issues quickly. + +# Issue: Report won’t automatically export to QuickBooks Online If an error occurs during an automatic export to QuickBooks Online: @@ -13,7 +15,7 @@ If an error occurs during an automatic export to QuickBooks Online: An error on a report will prevent it from automatically exporting. -### How to resolve +## How to resolve Open the expense and make the required changes. Then an admin must manually export the report to QuickBooks Online by clicking Details > Export. @@ -21,32 +23,210 @@ Open the expense and make the required changes. Then an admin must manually expo ![Select QuickBooks Online in the Export tab](https://help.expensify.com/assets/images/QBO_help_03.png){:width="100%"} -## Unable to manually export a report +# Issue: Unable to manually export a report To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking “Export” will lead to an empty page, as the data is not yet available for export: ![If the Report is in the Open status, the Not Ready to Export message shows](https://help.expensify.com/assets/images/QBO_help_04.png){:width="100%"} -### How to resolve +## How to resolve Open the report and make the required changes: 1. If the report is in the Open status, ensure that it is submitted. 2. If the Report is in the Processing status, an admin or approver will need to approve it. -Once this is done, an admin must manually export the report to QuickBooks Online. +Once this is done, Workspace Admins must manually export the report to QuickBooks Online. + +# Error: When exporting billable expenses, please make sure the account in QuickBooks Online has been marked as billable + +**Why does this happen?** + +This error occurs when the account applied as a category to the expense in Expensify is not marked as a billable type account. + +## How to resolve +1. Log in to QuickBooks Online. +2. Click the Gear in the upper right-hand corner. +3. Under Company Settings, click Expenses. +4. Enable the option “Make expenses and items billable.” +5. Click on the pencil icon on the right to check if you have "In multiple accounts" selected: +6. If "In multiple accounts" is selected, go to Chart of Accounts and click Edit for the account in question. +7. Check the billable option and select an income account within your Chart of Accounts. +8. Sync your QuickBooks Online connection in **Settings > Workspaces > Workspace Name > Accounting**. +9. Open the report and click on Details, then the Export button to re-export the data to QuickBooks Online. + +# Error: Feature Not Included in Subscription + +**Why does this happen?** + +This error occurs when your version of QuickBooks Online doesn’t support the feature you are using in Expensify. + +## How to resolve + +Though you will see all of these features available in Expensify, you will receive an error trying to export to QuickBooks if you have a feature enabled that isn't available with your QuickBooks Online subscription. + +**Here is a list of the features supported by each version:** +![QuickBooks Online - Subscription types]({{site.url}}/assets/images/QBO1.png){:width="100%"} + +_Please note: Self-employed is not supported._ + +# Error: Expenses are not categorized with a QuickBooks Online account + +**Why does this happen?** + +QuickBooks Online requires all expenses exported from Expensify to use a category matching an account in your Chart of Accounts. If a category from another source is used, QuickBooks Online will reject the expense. This error occurs when an expense on the report has a category applied that is not valid in QuickBooks Online. + +## How to resolve + +1. Sync your QuickBooks Online connection in Expensify from **Settings > Workspaces > Workspace Name > Accounting**, and click the **Sync Now** button. +2. Review your expenses. If any appear with a red _Category no longer valid_ violation, recategorize the expense until all expenses are violation-free. +3. Click the **Details** tab, then the **Export** button to export the data to QuickBooks Online. + - If you receive the same error, continue to the next step. +4. Note the categories used on the expenses and check the **Settings > Workspaces > Workspace Name > Categories** page to confirm the exact categories used on the report are enabled and connected to QuickBooks Online (you'll see a green QB icon next to all connected categories). +5. Confirm the categories used on the expenses in the report match exactly the accounts in your QuickBooks Online chart of accounts. +6. If you make any changes in QuickBooks Online or in Expensify, always sync the connection and then try to export again. + +# Error: Error Creating Vendor + +**Why does this happen?** + +This error occurs when you have an Employee Record set up with the employee's name. This prevents the Expensify integration from automatically creating the Vendor Record with the same name since QuickBooks Online won't allow you to have an employee and vendor with the same name. + +## How to resolve + +There are two different ways you can resolve this error. + +**Option 1**: +1. Log into QuickBooks Online. +2. Access the Employee Records for your submitters. +3. Edit the name to differentiate them from the name they have on their account in Expensify. +4. Sync your QuickBooks Online connection in **Settings > Workspaces > Workspace Name > Accounting**. +5. Open the report and click on the Details tab, then the Export button to export the data to QuickBooks Online. + +**Option 2**: +1. Log into QuickBooks Online. +2. Manually create all of your Vendor Records, making sure that the email matches the email address associated with the user in Expensify. + +With this option, we recommend disabling _Automatically Create Entities_ under **Settings > Workspaces > Workspace Name > Accounting > Configure > Advanced**. That way, you will receive the corresponding error messages if a vendor record doesn't exist. + +# Error: When You Use Accounts Payable, You Must Choose a Vendor in the Name Field + +**Why does this happen?** + +This error occurs when you are exporting reimbursable expenses as Journal Entries against an A/P account and also use Employee Records in QuickBooks Online. + +## How to resolve + +There are three different ways you can resolve this error: +- **Option 1**: Under **Settings > Workspaces > Workspace Name > Accounting > Configure > Export tab**, select a different type of export for reimbursable expenses. +- **Option 2**: Enable _Automatically Create Entities_ under **Settings > Workspaces > Workspace Name > Accounting > Configure > Advanced** to create vendor records automatically. +- **Option 3**: Manually create vendor records in QuickBooks Online for each employee. + +# Error: Items marked as billable must have sales information checked + +**Why does this happen?** + +This error occurs when an Item category on an expense does not have sales information in QuickBooks Online. + +## How to resolve + +1. Log into QuickBooks Online. +2. Navigate to your items list. +3. Click **Edit** to the right of the item used on the report with the error. Here you will see an option to check either "Sales" or "Purchasing". +4. Check the option for **Sales**. +5. Select an income account. +6. Save your changes. +7. Sync your QuickBooks Online connection in **Settings > Workspaces > Workspace Name > Accounting**. +8. Open the report, click on Details, and then click the Export button to re-export the data to QuickBooks Online. + +# Error: Couldn't Connect to QuickBooks Online + +**Why does this happen?** + +This error occurs when the QuickBooks Online credentials used to make the connection have changed. + +_Note: This error message can also show up as, "QuickBooks Reconnect error: OAuth Token rejected.”_ + +## How to resolve + +1. Navigate to **Settings > Workspaces > Workspace Name > Accounting**. +2. Click the **Sync Now** button. +3. In the pop-up window, click **Reconnect** and enter your current QuickBooks Online credentials. + +If you are connecting with new credentials, you will need to reconfigure your settings and re-select the categories and tags you want enabled. We recommend taking a screenshot of your configuration settings beforehand so that you can reset the connection with those settings. + +# Error: Duplicate Document Number, This bill number has already been used. + +**Why does this happen?** + +This error happens when QuickBooks Online is set to flag duplicate document numbers. + +## How to resolve + +1. Log into QuickBooks Online. +2. Navigate to Settings > Advanced. +3. Under the Other Preferences section, make sure "Warn if duplicate bill number is used" is set to "Off." +4. Sync your QuickBooks Online connection in **Settings > Workspaces > Workspace Name > Accounting**. +5. Open the report and click on Details, then the Export button to re-export the data to QuickBooks Online + +# Error: The transaction needs to be in the same currency as the A/R and A/P accounts + +**Why does this happen?** + +This error occurs because the currency on the Vendor record in QuickBooks Online doesn't match the currency on the A/P account. + +## How to resolve + +1. Log into QuickBooks Online. +2. Open the vendor record. +3. Update the record to use with the correct A/P account, currency, and email matching their Expensify email. + +_Note: You can find the correct Vendor record by exporting your QuickBooks Online vendor list to a spreadsheet (click the export icon on the right-hand side of the page), and searching for the email address of the person who submitted the report._ + +If you have multiple vendors with different currencies with the same email, Expensify is likely trying to export to the wrong one. + +**In that case, run through the following steps**: +1. Try removing the email address from the vendor in QuickBooks Online that you aren't trying to export to. +2. Sync your QuickBooks Online connection in **Settings > Workspaces > Workspace Name > Accounting**. +3. Open the report and click on Details, then the Export button to re-export the data to QuickBooks Online. + +**If this still fails, you'll need to confirm that the A/P account selected in Expensify is set to the correct currency for the export**: +1. Navigate to **Settings > Workspaces > Workspace Name > Accounting**. +2. Under the Exports tab check that both A/P accounts are the correct currency. + {% include faq-begin.md %} -**How do I disconnect the QuickBooks Online connection?** +# Why are company card expenses exported to the wrong account in QuickBooks Online? +Multiple factors could be causing your company card transactions to export to the wrong place in your accounting system, but the best place to start is always the same. + +1. Confirm that the company cards have been mapped to the correct accounts in Settings > Domains > Company Cards > click the **Edit Export** button for the card to view the account. +2. Make sure the expenses in question have been imported from the company card. + - Only expenses with the Card+Lock icon next to them will export according to the mapping settings that you configure in the domain settings. + +It’s important to note that expenses imported from a card linked at the individual account level, expenses created from a SmartScanned receipt, and manually created cash expenses will export to the default bank account selected in your accounting connection's configuration settings. + +**Is the report exporter a domain admin?** + +The user exporting the report must be a domain admin. You can check the history and comment section at the bottom of the report to see who exported the report: +- If your reports are being exported automatically by Concierge, the user listed as the Preferred Exporter under **Settings > Workspaces > Workspace Name > Accounting > Export** must also be a domain admin. +- If the report exporter is not a domain admin, all company card expenses will be exported to the account set in **Settings > Workspaces > Workspace Name > Accounting > Export Company Card Expenses As**. + +# How do I disconnect the QuickBooks Online connection? + +You can disconnect QuickBooks Online from Expensify by running through the following steps: 1. Click your profile image or icon in the bottom left menu. 2. Scroll down and click **Workspaces** in the left menu. 3. Select the workspace you want to disconnect from QuickBooks Online. 4. Click **Accounting** in the left menu. -5. Click the three dot menu icon to the right of QuickBooks Online and select **Disconnect**. +5. Click the three-dot menu icon to the right of QuickBooks Online and select **Disconnect**. 6. Click **Disconnect** to confirm. -You will no longer see the imported options from QuickBooks Online. +Once you disconnect from QuickBooks, that will clear all of the previously imported options from Expensify. + +# Can I export negative expenses to QuickBooks Online? + +Yes, in general, you can export negative expenses successfully to QuickBooks Online regardless of which export method you choose. {% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md index 77587cc124f0..f933edfd6d3a 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md @@ -6,19 +6,19 @@ description: Approve, hold, and unapprove submitted expenses Expenses can be created through manual entry, tracking distance, or scanning a receipt. They can be submitted to an individual or a workspace. -This help article has more details about creating and submitting an expense to an individual or a workspace. +This [help article](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Create-an-expense) has more details about creating and submitting an expense to an individual or a workspace. # Receiving an expense from an Individual When an expense is submitted to an individual, it doesn’t need approval. It only needs to be paid. -This help article has the steps to pay the expense. +This [help article](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Pay-an-expense) has the steps to pay the expense. # Receiving a workspace expense -When an expense is submitted to a workspace with an “approval workflow”, it must be approved before it can be paid. +When an expense is submitted to a workspace with an **approval workflow**, it must be approved before it can be paid. -As a workspace admin, you can set an [approval workflow](https://help.expensify.com/articles/new-expensify/workspaces/Add-approvals) in the workspace settings. For each expense report, you’ll have the option to: +A workspace admin can set an [approval workflow](https://help.expensify.com/articles/new-expensify/workspaces/Add-approvals) in the workspace settings. For each expense report, there will be an option to: - **Approve:** Click Approve if you’re satisfied with the expense details. - **Hold the expense:** If you need to delay a payment or provide more information before approval, you can hold an expense. @@ -29,11 +29,11 @@ As a workspace admin, you can set an [approval workflow](https://help.expensify. {% include selector.html values="desktop, mobile" %} {% include option.html value="desktop or WebApp" %} -1. When an expense is submitted, you will receive an email and in-app notification with the details of the expense. +1. When an expense is submitted, the approver will receive an email and in-app notification with the details of the expense. 2. Click the expense in the email to be directed to New Expensify, where you can review it. 3. Click on the expense to view the receipt, amount, description, and additional details the submitter provides. 4. Click **Approve**. -5. When you are ready to pay the expense, follow the steps in this help article. +5. When you are ready to pay the expense, follow the steps in this [help article](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Pay-an-expense). {% include end-option.html %} {% include option.html value="mobile" %} @@ -46,7 +46,7 @@ As a workspace admin, you can set an [approval workflow](https://help.expensify. {% include end-selector.html %} {% include info.html %} -If the transaction is pending (a common occurrence with recent company cards or SmartScan expenses), you’ll need to wait until the transaction posts before approving it. +If the transaction is pending, common for recent Expensify Card transactions or SmartScanning expenses, you’ll need to wait until the transaction posts before approving it. {% include end-info.html %} @@ -68,7 +68,7 @@ When you’re ready to remove the hold, 1. Locate the expense on the Search page. 2. Click **View**. 3. Click the drop-down arrow at the top of the expense. -4. Select **UnHold**. +4. Select **Unhold**. 5. Complete the steps above to “Approve expenses.” Once the expense has been approved, you can pay it. {% include end-option.html %} @@ -85,12 +85,18 @@ When you’re ready to remove the hold, 1. Tap **Search** and select the expense. 2. Tap the drop-down arrow at the top of the expense. -3. Select **UnHold**. +3. Select **Unhold**. 4. Complete the steps above to “Approve expenses.” Once the expense has been approved, you can pay it. {% include end-option.html %} {% include end-selector.html %} +![Use Search to find an expense]({{site.url}}/assets/images/search-hold-01.png){:width="100%"} +![Click on top of expense]({{site.url}}/assets/images/search-hold-02.png){:width="100%"} +![Click Hold]({{site.url}}/assets/images/search-hold-03.png){:width="100%"} +![Click Unhold]({{site.url}}/assets/images/search-hold-04.png){:width="100%"} +![Click Approve]({{site.url}}/assets/images/search-hold-05.png){:width="100%"} + {% include info.html %} Held expenses will not be available for payment until they have been approved. {% include end-info.html %} @@ -125,7 +131,7 @@ If the approved expense has already been exported to an accounting package, you {% include faq-begin.md %} -**Why is an employee expense showing as ‘pending?’** +**Why is an employee expense showing as pending?** An Expensify Card expense will show as pending if the merchant hasn’t posted it. This is usually the case with hotel holds, or card rental holds. A hold will normally last no more than 7-10 business days unless it’s a hotel hold, which can last 31 days. diff --git a/docs/articles/new-expensify/travel/manage-travel-member-roles.md b/docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md similarity index 88% rename from docs/articles/new-expensify/travel/manage-travel-member-roles.md rename to docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md index 954e24550f05..33e260cb4d90 100644 --- a/docs/articles/new-expensify/travel/manage-travel-member-roles.md +++ b/docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md @@ -2,27 +2,23 @@ title: Manage Travel Member Roles description: Modify member roles within Expensify Travel --- -
+
Admins can assign roles to different travel members to determine who they can book travel for (whether for themselves and/or for others) and whether they can adjust administrative settings. - -
- -
To assign a role to a travel member, 1. Click the + icon in the bottom left menu and select **Book travel**. 2. Click **Book or manage travel**. -3. Click the **Program** tab at the top and select Users. +3. Click the **Program** tab at the top and select **Users**. 4. Click the name of the member whose role you wish to update. 5. Click the **Roles** tab and select a role. - **Traveler**: Can only book travel for themselves. - **Travel Arranger**: Can book travel for themselves and for other workspace members. Arrangers can be set to arrange travel for everyone in the workspace or for specific individuals only. - **Company Admin**: Can book travel for themselves as well as any other workspace members. They can also access administrative features to: - Define travel policies - - Add users - - Remove users + - Add Users + - Remove Users - Add and configure corporate cards as payment methods - View analytics and metrics - Use the Safety feature diff --git a/docs/assets/images/ExpensifyHelp-AttendeeTracking-1.png b/docs/assets/images/ExpensifyHelp-AttendeeTracking-1.png index e3c08b9133b8..270d6538146b 100644 Binary files a/docs/assets/images/ExpensifyHelp-AttendeeTracking-1.png and b/docs/assets/images/ExpensifyHelp-AttendeeTracking-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-1.png b/docs/assets/images/ExpensifyHelp-CreateExpense-1.png index 7b6459440d5e..5f78346b2dfe 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpense-1.png and b/docs/assets/images/ExpensifyHelp-CreateExpense-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-2.png b/docs/assets/images/ExpensifyHelp-CreateExpense-2.png index 65aaf8017a32..53b5cfc32c25 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpense-2.png and b/docs/assets/images/ExpensifyHelp-CreateExpense-2.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-3.png b/docs/assets/images/ExpensifyHelp-CreateExpense-3.png index 0173de29d68d..84aeebdfc6fc 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpense-3.png and b/docs/assets/images/ExpensifyHelp-CreateExpense-3.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-4.png b/docs/assets/images/ExpensifyHelp-CreateExpense-4.png index 901d08f1771d..10349a60ff7d 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpense-4.png and b/docs/assets/images/ExpensifyHelp-CreateExpense-4.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-1.png b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-1.png index 18318f782466..f6f7af6b7a64 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-1.png and b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-2.png b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-2.png index 641c32a6a6b6..f4fb5e078402 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-2.png and b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-2.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png index 48c6f12fb75c..6d9bae5b84cd 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png and b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png index 5f8af1e46ac4..fc79f44b0341 100644 Binary files a/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png and b/docs/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png differ diff --git a/docs/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png b/docs/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png index 484c7b0d9e33..94a01e129899 100644 Binary files a/docs/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png and b/docs/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png differ diff --git a/docs/assets/images/ExpensifyHelp-Subscription-Billing.png b/docs/assets/images/ExpensifyHelp-Subscription-Billing.png index 8a8c430e8020..d936b70be8b3 100644 Binary files a/docs/assets/images/ExpensifyHelp-Subscription-Billing.png and b/docs/assets/images/ExpensifyHelp-Subscription-Billing.png differ diff --git a/docs/assets/images/ExpensifyHelp-Subscription-Default.png b/docs/assets/images/ExpensifyHelp-Subscription-Default.png index ae289a8f29f8..a54228f344a0 100644 Binary files a/docs/assets/images/ExpensifyHelp-Subscription-Default.png and b/docs/assets/images/ExpensifyHelp-Subscription-Default.png differ diff --git a/docs/assets/images/ExpensifyHelp-Subscription-Details.png b/docs/assets/images/ExpensifyHelp-Subscription-Details.png index c96b39c4a3ec..352e0bf2bb30 100644 Binary files a/docs/assets/images/ExpensifyHelp-Subscription-Details.png and b/docs/assets/images/ExpensifyHelp-Subscription-Details.png differ diff --git a/docs/assets/images/ExpensifyHelp-Subscription-YourPlan.png b/docs/assets/images/ExpensifyHelp-Subscription-YourPlan.png index 3d958edefd3c..1e9fc4e090c0 100644 Binary files a/docs/assets/images/ExpensifyHelp-Subscription-YourPlan.png and b/docs/assets/images/ExpensifyHelp-Subscription-YourPlan.png differ diff --git a/docs/assets/images/ExpensifyHelp-Subscription.png b/docs/assets/images/ExpensifyHelp-Subscription.png index 403dd276743f..0321c1f04bff 100644 Binary files a/docs/assets/images/ExpensifyHelp-Subscription.png and b/docs/assets/images/ExpensifyHelp-Subscription.png differ diff --git a/docs/assets/images/ExpensifyHelp-Workflows-1.png b/docs/assets/images/ExpensifyHelp-Workflows-1.png index b0841232f77c..2eb2a8a79b04 100644 Binary files a/docs/assets/images/ExpensifyHelp-Workflows-1.png and b/docs/assets/images/ExpensifyHelp-Workflows-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-Workflows-2.png b/docs/assets/images/ExpensifyHelp-Workflows-2.png index f7e845fbe81c..64561611c59c 100644 Binary files a/docs/assets/images/ExpensifyHelp-Workflows-2.png and b/docs/assets/images/ExpensifyHelp-Workflows-2.png differ diff --git a/docs/assets/images/ExpensifyHelp-Workflows-3.png b/docs/assets/images/ExpensifyHelp-Workflows-3.png index dc3358ab484e..e714a221c421 100644 Binary files a/docs/assets/images/ExpensifyHelp-Workflows-3.png and b/docs/assets/images/ExpensifyHelp-Workflows-3.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png index 53c637736c95..5603aa75dfa8 100644 Binary files a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png and b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png index 92e607756de2..de99d1912570 100644 Binary files a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png and b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png index 402afb86cc40..e64764141d96 100644 Binary files a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png and b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png index 7aeb0fdfb7c5..3e7bffba43d8 100644 Binary files a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png and b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png differ diff --git a/docs/assets/images/FAB_track_expense.png b/docs/assets/images/FAB_track_expense.png index 6ee0cf5abba4..0f2a87529515 100644 Binary files a/docs/assets/images/FAB_track_expense.png and b/docs/assets/images/FAB_track_expense.png differ diff --git a/docs/assets/images/NetSuite_Configure_06.png b/docs/assets/images/NetSuite_Configure_06.png index cddfe2fabcd6..ee36eab2553b 100644 Binary files a/docs/assets/images/NetSuite_Configure_06.png and b/docs/assets/images/NetSuite_Configure_06.png differ diff --git a/docs/assets/images/NetSuite_Configure_08.png b/docs/assets/images/NetSuite_Configure_08.png index 77690a2c3aa1..af22b0d2738c 100644 Binary files a/docs/assets/images/NetSuite_Configure_08.png and b/docs/assets/images/NetSuite_Configure_08.png differ diff --git a/docs/assets/images/NetSuite_Configure_09.png b/docs/assets/images/NetSuite_Configure_09.png index 8da56f22838d..eff9030a646e 100644 Binary files a/docs/assets/images/NetSuite_Configure_09.png and b/docs/assets/images/NetSuite_Configure_09.png differ diff --git a/docs/assets/images/NetSuite_Connect_Customization_01.png b/docs/assets/images/NetSuite_Connect_Customization_01.png index 8a0c53b45d7f..5996593a21c2 100644 Binary files a/docs/assets/images/NetSuite_Connect_Customization_01.png and b/docs/assets/images/NetSuite_Connect_Customization_01.png differ diff --git a/docs/assets/images/NetSuite_Expense_Categories_04.png b/docs/assets/images/NetSuite_Expense_Categories_04.png index d13e9f95cfea..1fc2cb662ee5 100644 Binary files a/docs/assets/images/NetSuite_Expense_Categories_04.png and b/docs/assets/images/NetSuite_Expense_Categories_04.png differ diff --git a/docs/assets/images/OldDot - Create & Pay Bills 1.png b/docs/assets/images/OldDot - Create & Pay Bills 1.png index a880e012408a..cce667607f26 100644 Binary files a/docs/assets/images/OldDot - Create & Pay Bills 1.png and b/docs/assets/images/OldDot - Create & Pay Bills 1.png differ diff --git a/docs/assets/images/OldDot - Create & Pay Bills 2.png b/docs/assets/images/OldDot - Create & Pay Bills 2.png index ce022a95c6a1..2ab214d49f4e 100644 Binary files a/docs/assets/images/OldDot - Create & Pay Bills 2.png and b/docs/assets/images/OldDot - Create & Pay Bills 2.png differ diff --git a/docs/assets/images/OldDot - Create & Pay Bills 3.png b/docs/assets/images/OldDot - Create & Pay Bills 3.png index 071bcc997934..d83e3236e3ab 100644 Binary files a/docs/assets/images/OldDot - Create & Pay Bills 3.png and b/docs/assets/images/OldDot - Create & Pay Bills 3.png differ diff --git a/docs/assets/images/Reports_PayExpense_01.png b/docs/assets/images/Reports_PayExpense_01.png new file mode 100644 index 000000000000..665924146116 Binary files /dev/null and b/docs/assets/images/Reports_PayExpense_01.png differ diff --git a/docs/assets/images/Reports_PayExpense_02.png b/docs/assets/images/Reports_PayExpense_02.png new file mode 100644 index 000000000000..e17081de1fd9 Binary files /dev/null and b/docs/assets/images/Reports_PayExpense_02.png differ diff --git a/docs/assets/images/SageConfigureIntegrationConfigureButton.png b/docs/assets/images/SageConfigureIntegrationConfigureButton.png index e3ec52bacbb0..7a99190cfd30 100644 Binary files a/docs/assets/images/SageConfigureIntegrationConfigureButton.png and b/docs/assets/images/SageConfigureIntegrationConfigureButton.png differ diff --git a/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png b/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png index f126bb10dc51..5345951d7a65 100644 Binary files a/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png and b/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png differ diff --git a/docs/assets/images/SageConnectCreatingWorkspace.png b/docs/assets/images/SageConnectCreatingWorkspace.png index 6084d0a8c7fb..1595b5e2daab 100644 Binary files a/docs/assets/images/SageConnectCreatingWorkspace.png and b/docs/assets/images/SageConnectCreatingWorkspace.png differ diff --git a/docs/assets/images/SageConnectEnableSage.png b/docs/assets/images/SageConnectEnableSage.png index 25b43a510c15..9ce8cf4e2b3f 100644 Binary files a/docs/assets/images/SageConnectEnableSage.png and b/docs/assets/images/SageConnectEnableSage.png differ diff --git a/docs/assets/images/SageConnectEnterCredentials.png b/docs/assets/images/SageConnectEnterCredentials.png index 63772972290d..bd197c1f1968 100644 Binary files a/docs/assets/images/SageConnectEnterCredentials.png and b/docs/assets/images/SageConnectEnterCredentials.png differ diff --git a/docs/assets/images/SageConnectSubscriptionSettings.png b/docs/assets/images/SageConnectSubscriptionSettings.png index 2e74d27c71e6..eaccf66f936f 100644 Binary files a/docs/assets/images/SageConnectSubscriptionSettings.png and b/docs/assets/images/SageConnectSubscriptionSettings.png differ diff --git a/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png b/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png index 8750c1ed596b..e4d174780291 100644 Binary files a/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png and b/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png differ diff --git a/docs/assets/images/SageConnectWebServicesAuthorizations.png b/docs/assets/images/SageConnectWebServicesAuthorizations.png index d0b9a786d1cc..b6b74bb93fbe 100644 Binary files a/docs/assets/images/SageConnectWebServicesAuthorizations.png and b/docs/assets/images/SageConnectWebServicesAuthorizations.png differ diff --git a/docs/assets/images/Tax Exempt - Classic.png b/docs/assets/images/Tax Exempt - Classic.png index 0987f5e4ca7d..92c0dc94960c 100644 Binary files a/docs/assets/images/Tax Exempt - Classic.png and b/docs/assets/images/Tax Exempt - Classic.png differ diff --git a/docs/assets/images/Tax Exempt - New Expensify.png b/docs/assets/images/Tax Exempt - New Expensify.png index 9ff6673da6b3..6544c08a31b5 100644 Binary files a/docs/assets/images/Tax Exempt - New Expensify.png and b/docs/assets/images/Tax Exempt - New Expensify.png differ diff --git a/docs/assets/images/Workspace_category_toggle.png b/docs/assets/images/Workspace_category_toggle.png index c6af6fe183c0..2c858d4a72af 100644 Binary files a/docs/assets/images/Workspace_category_toggle.png and b/docs/assets/images/Workspace_category_toggle.png differ diff --git a/docs/assets/images/addbankaccount_01.png b/docs/assets/images/addbankaccount_01.png index d1646127373f..66968cec580e 100644 Binary files a/docs/assets/images/addbankaccount_01.png and b/docs/assets/images/addbankaccount_01.png differ diff --git a/docs/assets/images/addbankaccount_02.png b/docs/assets/images/addbankaccount_02.png index 6fe22ca5b0db..fe06a876bfcb 100644 Binary files a/docs/assets/images/addbankaccount_02.png and b/docs/assets/images/addbankaccount_02.png differ diff --git a/docs/assets/images/addbankaccount_03.png b/docs/assets/images/addbankaccount_03.png index 3f571621eaa0..3afd2bae3540 100644 Binary files a/docs/assets/images/addbankaccount_03.png and b/docs/assets/images/addbankaccount_03.png differ diff --git a/docs/assets/images/invoice-bulk-01.png b/docs/assets/images/invoice-bulk-01.png index 1dbf7fa5088d..885d099bf174 100644 Binary files a/docs/assets/images/invoice-bulk-01.png and b/docs/assets/images/invoice-bulk-01.png differ diff --git a/docs/assets/images/invoice-bulk-02.png b/docs/assets/images/invoice-bulk-02.png index 82e388b0125f..238b68f7b4dd 100644 Binary files a/docs/assets/images/invoice-bulk-02.png and b/docs/assets/images/invoice-bulk-02.png differ diff --git a/docs/assets/images/invoice-bulk-03.png b/docs/assets/images/invoice-bulk-03.png index f51abec046b7..2e10025b1c82 100644 Binary files a/docs/assets/images/invoice-bulk-03.png and b/docs/assets/images/invoice-bulk-03.png differ diff --git a/docs/assets/images/invoice-bulk-04.png b/docs/assets/images/invoice-bulk-04.png index 35e12a095ba6..e5df20e5fd68 100644 Binary files a/docs/assets/images/invoice-bulk-04.png and b/docs/assets/images/invoice-bulk-04.png differ diff --git a/docs/assets/images/invoice-bulk-05.png b/docs/assets/images/invoice-bulk-05.png index c7044c259de2..d43da7b9dd74 100644 Binary files a/docs/assets/images/invoice-bulk-05.png and b/docs/assets/images/invoice-bulk-05.png differ diff --git a/docs/assets/images/invoice_01.png b/docs/assets/images/invoice_01.png index 8d6091471d21..3333cfa17006 100644 Binary files a/docs/assets/images/invoice_01.png and b/docs/assets/images/invoice_01.png differ diff --git a/docs/assets/images/invoice_02.png b/docs/assets/images/invoice_02.png index 97facb60bbb2..ae1b1ec63773 100644 Binary files a/docs/assets/images/invoice_02.png and b/docs/assets/images/invoice_02.png differ diff --git a/docs/assets/images/invoices_01.png b/docs/assets/images/invoices_01.png index fc6d5587bb03..67b37b2e8f33 100644 Binary files a/docs/assets/images/invoices_01.png and b/docs/assets/images/invoices_01.png differ diff --git a/docs/assets/images/invoices_02.png b/docs/assets/images/invoices_02.png index 29038987c18a..dc2631f91b69 100644 Binary files a/docs/assets/images/invoices_02.png and b/docs/assets/images/invoices_02.png differ diff --git a/docs/assets/images/invoices_03.png b/docs/assets/images/invoices_03.png index fd78aa731784..1170a667000d 100644 Binary files a/docs/assets/images/invoices_03.png and b/docs/assets/images/invoices_03.png differ diff --git a/docs/assets/images/invoices_04.png b/docs/assets/images/invoices_04.png index d2e301a9d1a5..abafcdafeb1b 100644 Binary files a/docs/assets/images/invoices_04.png and b/docs/assets/images/invoices_04.png differ diff --git a/docs/assets/images/invoices_05.png b/docs/assets/images/invoices_05.png index 8eae5efaa9df..cc39a3ce20d1 100644 Binary files a/docs/assets/images/invoices_05.png and b/docs/assets/images/invoices_05.png differ diff --git a/docs/assets/images/invoices_06.png b/docs/assets/images/invoices_06.png index 2858227891eb..9c489bb8d9cd 100644 Binary files a/docs/assets/images/invoices_06.png and b/docs/assets/images/invoices_06.png differ diff --git a/docs/assets/images/profile-picture.png b/docs/assets/images/profile-picture.png new file mode 100644 index 000000000000..96201c51c973 Binary files /dev/null and b/docs/assets/images/profile-picture.png differ diff --git a/docs/assets/images/quickbooks-desktop-access-rights.png b/docs/assets/images/quickbooks-desktop-access-rights.png index bcdd35b8c827..39eb90c16826 100644 Binary files a/docs/assets/images/quickbooks-desktop-access-rights.png and b/docs/assets/images/quickbooks-desktop-access-rights.png differ diff --git a/docs/assets/images/quickbooks-desktop-advanced-settings.png b/docs/assets/images/quickbooks-desktop-advanced-settings.png index 181380ed7674..081aa958d0e5 100644 Binary files a/docs/assets/images/quickbooks-desktop-advanced-settings.png and b/docs/assets/images/quickbooks-desktop-advanced-settings.png differ diff --git a/docs/assets/images/quickbooks-desktop-coding-settings.png b/docs/assets/images/quickbooks-desktop-coding-settings.png index 7b9fc8086c9f..41fce0180810 100644 Binary files a/docs/assets/images/quickbooks-desktop-coding-settings.png and b/docs/assets/images/quickbooks-desktop-coding-settings.png differ diff --git a/docs/assets/images/quickbooks-desktop-company-preferences.png b/docs/assets/images/quickbooks-desktop-company-preferences.png index 31f2be54bfb8..43eff226b28d 100644 Binary files a/docs/assets/images/quickbooks-desktop-company-preferences.png and b/docs/assets/images/quickbooks-desktop-company-preferences.png differ diff --git a/docs/assets/images/quickbooks-desktop-export-settings.png b/docs/assets/images/quickbooks-desktop-export-settings.png index 3ff190bc2d60..a1b1d6d16936 100644 Binary files a/docs/assets/images/quickbooks-desktop-export-settings.png and b/docs/assets/images/quickbooks-desktop-export-settings.png differ diff --git a/docs/assets/images/quickbooks-desktop-exported-report-comments.png b/docs/assets/images/quickbooks-desktop-exported-report-comments.png index 2b0d2939e4b0..cde2acf397fc 100644 Binary files a/docs/assets/images/quickbooks-desktop-exported-report-comments.png and b/docs/assets/images/quickbooks-desktop-exported-report-comments.png differ diff --git a/docs/assets/images/quickbooks-desktop-web-connector.png b/docs/assets/images/quickbooks-desktop-web-connector.png index b2086420edd8..b7275dda4482 100644 Binary files a/docs/assets/images/quickbooks-desktop-web-connector.png and b/docs/assets/images/quickbooks-desktop-web-connector.png differ diff --git a/docs/assets/images/search-download.png b/docs/assets/images/search-download.png index eb8591dea110..8d9210a82fba 100644 Binary files a/docs/assets/images/search-download.png and b/docs/assets/images/search-download.png differ diff --git a/docs/assets/images/search-hold-03.png b/docs/assets/images/search-hold-03.png index 81fbddcf5d75..0c2014c94a3c 100644 Binary files a/docs/assets/images/search-hold-03.png and b/docs/assets/images/search-hold-03.png differ diff --git a/docs/assets/images/search-hold-04.png b/docs/assets/images/search-hold-04.png index e5c1b71c0e37..8fe1df28d92b 100644 Binary files a/docs/assets/images/search-hold-04.png and b/docs/assets/images/search-hold-04.png differ diff --git a/docs/assets/images/workspace_gl_payroll_codes.png b/docs/assets/images/workspace_gl_payroll_codes.png index 6b7770dc01b0..2b48f64df5b7 100644 Binary files a/docs/assets/images/workspace_gl_payroll_codes.png and b/docs/assets/images/workspace_gl_payroll_codes.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 04eba2e6152c..be546f165bf1 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -609,4 +609,5 @@ https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments https://help.expensify.com/articles/expensify-classic/settings/Set-Notifications,https://help.expensify.com/articles/expensify-classic/settings/Email-Notifications https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-download-expenses https://help.expensify.com/articles/expensify-classic/expenses/Apply-Tax,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes -https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes \ No newline at end of file +https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes +https://help.expensify.com/articles/new-expensify/travel/manage-travel-member-roles,https://help.expensify.com/articles/new-expensify/travel/Manage-Travel-Member-Roles diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 798e328f73fa..806ffe574031 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -71,7 +71,7 @@ platform :android do desc "Generate a production HybridApp AAB" lane :build_hybrid do - ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp.android" gradle( project_dir: 'Mobile-Expensify/Android', task: "bundleRelease", @@ -88,7 +88,7 @@ platform :android do desc "Generate AdHoc HybridApp apk" lane :build_adhoc_hybrid do - ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp.android" gradle( project_dir: 'Mobile-Expensify/Android', task: 'assembleAdhoc', @@ -116,7 +116,7 @@ platform :android do desc "Generate a new local HybridApp APK" lane :build_local_hybrid do - ENV["ENVFILE"]=".env.production" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp.android" gradle( project_dir: 'Mobile-Expensify/Android', task: 'assemble', @@ -197,6 +197,17 @@ platform :android do ) end + desc "Upload app to Firebase distribution" + lane :upload_firebase_distribution do + firebase_app_distribution( + app: "1:1008697809946:android:2e48f9ffe8d0b6a2", + service_credentials_file: "./firebase.json", + groups: "applause", + android_artifact_path: ENV[KEY_GRADLE_AAB_PATH], + android_artifact_type: "AAB" + ) + end + desc "Upload HybridApp to Google Play for internal testing" lane :upload_google_play_internal_hybrid do # Google is very unreliable, so we retry a few times @@ -372,7 +383,7 @@ platform :ios do desc "Build an iOS HybridApp production build" lane :build_hybrid do - ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp.ios" setupIOSSigningCertificate() @@ -408,7 +419,7 @@ platform :ios do desc "Build an iOS HybridApp Adhoc build" lane :build_adhoc_hybrid do - ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp.ios" setupIOSSigningCertificate() @@ -454,7 +465,7 @@ platform :ios do desc "Build an unsigned iOS HybridApp production build" lane :build_unsigned_hybrid do - ENV["ENVFILE"]="./Mobile-Expensify/.env.production.hybridapp" + ENV["ENVFILE"]="./Mobile-Expensify/.env.production.hybridapp.ios" build_app( workspace: "./Mobile-Expensify/iOS/Expensify.xcworkspace", scheme: "Expensify" @@ -507,6 +518,16 @@ platform :ios do sh("echo '{\"ipa_path\": \"#{lane_context[SharedValues::S3_IPA_OUTPUT_PATH]}\",\"html_path\": \"#{lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}\"}' > ../ios_paths.json") end + desc "Upload app to Firebase distribution" + lane :upload_firebase_distribution do + firebase_app_distribution( + app: "1:1008697809946:ios:3ffad71f664f2886", + service_credentials_file: "./firebase.json", + groups: "applause", + ipa_path: ENV[KEY_IPA_PATH], + ) + end + desc "Upload app to TestFlight" lane :upload_testflight do upload_to_testflight( diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index c6ab0dfb46a4..3844e2a6958c 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -3,3 +3,4 @@ # Ensure this file is checked in to source control! gem 'fastlane-plugin-aws_s3' +gem 'fastlane-plugin-firebase_app_distribution' diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 46101127a9c0..4ff19211ccd1 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.82 + 9.0.84 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.82.0 + 9.0.84.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b3c1f5286ef7..bc4eedf042cf 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.82 + 9.0.84 CFBundleSignature ???? CFBundleVersion - 9.0.82.0 + 9.0.84.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 09ab2083a815..faddb3d2cdcc 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.82 + 9.0.84 CFBundleVersion - 9.0.82.0 + 9.0.84.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1ec4c0cd21a0..f516193d5246 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1775,7 +1775,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.15.0): + - react-native-keyboard-controller (1.15.2): - DoubleConversion - glog - hermes-engine @@ -3307,7 +3307,7 @@ SPEC CHECKSUMS: react-native-geolocation: b9bd12beaf0ebca61a01514517ca8455bd26fa06 react-native-image-picker: ba5067f7d833b9081102c0a33dd0188eb21d92dc react-native-key-command: aae312752fcdfaa2240be9a015fc41ce54087546 - react-native-keyboard-controller: 3428e4761623fd6a242d9bf3573112f8ebe92238 + react-native-keyboard-controller: dbd7fb6a233505f937c9242d6d8bb5ebe659ec32 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 react-native-pager-view: abc5ef92699233eb726442c7f452cac82f73d0cb diff --git a/package-lock.json b/package-lock.json index 655256a64fd6..0b5a9a92fdb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.82-0", + "version": "9.0.84-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.82-0", + "version": "9.0.84-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -51,7 +51,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.109", + "expensify-common": "2.0.114", "expo": "52.0.14", "expo-asset": "^11.0.1", "expo-av": "^15.0.1", @@ -93,7 +93,7 @@ "react-native-image-picker": "^7.1.2", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.15.0", + "react-native-keyboard-controller": "1.15.2", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", @@ -104,7 +104,7 @@ "react-native-permissions": "^3.10.0", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.11.0", - "react-native-qrcode-svg": "6.3.11", + "react-native-qrcode-svg": "6.3.14", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-nitro-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", "react-native-reanimated": "3.16.4", "react-native-release-profiler": "^0.2.1", @@ -19229,6 +19229,8 @@ }, "node_modules/dijkstrajs": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, "node_modules/dir-compare": { @@ -19777,10 +19779,6 @@ "node": ">= 4" } }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "1.0.2", "license": "MIT", @@ -21550,9 +21548,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.109", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.109.tgz", - "integrity": "sha512-5XTrJxiDSjQhojnJfXH1G+fSgRM92oAJ5HiLo28HppmJQuA350GOONVo88rRalcI029rlYGIMh8WfhMlOuE/gA==", + "version": "2.0.114", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.114.tgz", + "integrity": "sha512-sfGUhZp11x/hDlyJ6GzK5Sr4A5zVV5jVrpOey99IpVXRlpKZMPthsa+ll5S0CJQJro4gE4FBb5qf5kLzADV6Mg==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", @@ -31073,6 +31071,8 @@ }, "node_modules/pngjs": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -31441,11 +31441,12 @@ } }, "node_modules/qrcode": { - "version": "1.5.3", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, @@ -32249,9 +32250,9 @@ "license": "MIT" }, "node_modules/react-native-keyboard-controller": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.15.0.tgz", - "integrity": "sha512-Laqszs0Uciu9MFkHurLwaHs9kftzUueew75HVOndbdcGR3MbKs2MqKdQEg1AgXSHcGoGg5nKafMOLVIoYjK6kA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.15.2.tgz", + "integrity": "sha512-ZN151OyMJ2GQkhebARY/5G9rXgSlNCKy+WjS6p4o7S+5ulb4nGzl6UkpEuT7/C6bHDeAjDupdrET9tyyTee3nA==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.1.6" @@ -32405,18 +32406,19 @@ } }, "node_modules/react-native-qrcode-svg": { - "version": "6.3.11", - "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.11.tgz", - "integrity": "sha512-bhjh4KT8NTQjJyu/tGaplR53OIqtvUJcWZ713H++GLKRpldNDyywwLVW+HdlGZ3N7jk3TxCchQMDMzndLlV4sA==", + "version": "6.3.14", + "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.14.tgz", + "integrity": "sha512-YtMI/C3Vfhs/3Y/g3DEiEEG7ZOgLNsACiTjKMNYPr66Z1pgTG5Ci7KDoZFYuXwD5OSN19Lxi52QarZdybd0dWw==", + "license": "MIT", "dependencies": { "prop-types": "^15.8.0", - "qrcode": "^1.5.1", + "qrcode": "^1.5.4", "text-encoding": "^0.7.0" }, "peerDependencies": { "react": "*", "react-native": ">=0.63.4", - "react-native-svg": ">=13.2.0" + "react-native-svg": ">=14.0.0" } }, "node_modules/react-native-quick-sqlite": { diff --git a/package.json b/package.json index f3e0112acf77..bf0e29afd252 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.82-0", + "version": "9.0.84-1", "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.", @@ -116,7 +116,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.109", + "expensify-common": "2.0.114", "expo": "52.0.14", "expo-asset": "^11.0.1", "expo-av": "^15.0.1", @@ -158,7 +158,7 @@ "react-native-image-picker": "^7.1.2", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.15.0", + "react-native-keyboard-controller": "1.15.2", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", @@ -169,7 +169,7 @@ "react-native-permissions": "^3.10.0", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.11.0", - "react-native-qrcode-svg": "6.3.11", + "react-native-qrcode-svg": "6.3.14", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-nitro-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", "react-native-reanimated": "3.16.4", "react-native-release-profiler": "^0.2.1", diff --git a/patches/react-native+0.76.3+027+disable-status-bar-hiding-fixes-keyboard-flicker-in-modals.patch b/patches/react-native+0.76.3+027+disable-status-bar-hiding-fixes-keyboard-flicker-in-modals.patch deleted file mode 100644 index dd55ed2c88e8..000000000000 --- a/patches/react-native+0.76.3+027+disable-status-bar-hiding-fixes-keyboard-flicker-in-modals.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/StatusBar/StatusBar.js b/node_modules/react-native/Libraries/Components/StatusBar/StatusBar.js -index 53a01ef..d4de477 100644 ---- a/node_modules/react-native/Libraries/Components/StatusBar/StatusBar.js -+++ b/node_modules/react-native/Libraries/Components/StatusBar/StatusBar.js -@@ -456,9 +456,9 @@ class StatusBar extends React.Component { - mergedProps.backgroundColor.animated, - ); - } -- if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) { -- NativeStatusBarManagerAndroid.setHidden(mergedProps.hidden.value); -- } -+ // if (!oldProps || oldProps.hidden.value !== mergedProps.hidden.value) { -+ // NativeStatusBarManagerAndroid.setHidden(mergedProps.hidden.value); -+ // } - // Activities are not translucent by default, so always set if true. - if ( - (oldProps && oldProps.translucent !== mergedProps.translucent) || diff --git a/patches/react-native+0.76.3+028+measureText-full-width-if-wraps.patch b/patches/react-native+0.76.3+027+measureText-full-width-if-wraps.patch similarity index 100% rename from patches/react-native+0.76.3+028+measureText-full-width-if-wraps.patch rename to patches/react-native+0.76.3+027+measureText-full-width-if-wraps.patch diff --git a/patches/react-native+0.76.3+029+fix-scroll-the-cursor-into-view-when-focus.patch b/patches/react-native+0.76.3+028+fix-scroll-the-cursor-into-view-when-focus.patch similarity index 100% rename from patches/react-native+0.76.3+029+fix-scroll-the-cursor-into-view-when-focus.patch rename to patches/react-native+0.76.3+028+fix-scroll-the-cursor-into-view-when-focus.patch diff --git a/patches/react-native+0.76.3+030+fix-crash-when-deleting-expense.patch b/patches/react-native+0.76.3+029+fix-crash-when-deleting-expense.patch similarity index 100% rename from patches/react-native+0.76.3+030+fix-crash-when-deleting-expense.patch rename to patches/react-native+0.76.3+029+fix-crash-when-deleting-expense.patch diff --git a/patches/react-native-image-picker+7.1.2+002+callback-in-completion-block.patch b/patches/react-native-image-picker+7.1.2+002+callback-in-completion-block.patch new file mode 100644 index 000000000000..6011a582dcf5 --- /dev/null +++ b/patches/react-native-image-picker+7.1.2+002+callback-in-completion-block.patch @@ -0,0 +1,150 @@ +diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.mm b/node_modules/react-native-image-picker/ios/ImagePickerManager.mm +index 93e99be..0ef2a8a 100644 +--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.mm ++++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.mm +@@ -506,81 +506,85 @@ @implementation ImagePickerManager (PHPickerViewControllerDelegate) + + - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) + { +- [picker dismissViewControllerAnimated:YES completion:nil]; +- +- if (photoSelected == YES) { +- return; +- } +- photoSelected = YES; +- +- if (results.count == 0) { +- dispatch_async(dispatch_get_main_queue(), ^{ +- self.callback(@[@{@"didCancel": @YES}]); +- }); +- return; +- } ++ dispatch_block_t dismissCompletionBlock = ^{ + +- dispatch_group_t completionGroup = dispatch_group_create(); +- NSMutableArray *assets = [[NSMutableArray alloc] initWithCapacity:results.count]; +- for (int i = 0; i < results.count; i++) { +- [assets addObject:(NSDictionary *)[NSNull null]]; +- } ++ if (photoSelected == YES) { ++ return; ++ } ++ photoSelected = YES; + +- [results enumerateObjectsUsingBlock:^(PHPickerResult *result, NSUInteger index, BOOL *stop) { +- PHAsset *asset = nil; +- NSItemProvider *provider = result.itemProvider; ++ if (results.count == 0) { ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ self.callback(@[@{@"didCancel": @YES}]); ++ }); ++ return; ++ } + +- // If include extra, we fetch the PHAsset, this required library permissions +- if([self.options[@"includeExtra"] boolValue] && result.assetIdentifier != nil) { +- PHFetchResult* fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[result.assetIdentifier] options:nil]; +- asset = fetchResult.firstObject; ++ dispatch_group_t completionGroup = dispatch_group_create(); ++ NSMutableArray *assets = [[NSMutableArray alloc] initWithCapacity:results.count]; ++ for (int i = 0; i < results.count; i++) { ++ [assets addObject:(NSDictionary *)[NSNull null]]; + } + +- dispatch_group_enter(completionGroup); ++ [results enumerateObjectsUsingBlock:^(PHPickerResult *result, NSUInteger index, BOOL *stop) { ++ PHAsset *asset = nil; ++ NSItemProvider *provider = result.itemProvider; + +- if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { +- NSString *identifier = provider.registeredTypeIdentifiers.firstObject; +- // Matches both com.apple.live-photo-bundle and com.apple.private.live-photo-bundle +- if ([identifier containsString:@"live-photo-bundle"]) { +- // Handle live photos +- identifier = @"public.jpeg"; ++ // If include extra, we fetch the PHAsset, this required library permissions ++ if([self.options[@"includeExtra"] boolValue] && result.assetIdentifier != nil) { ++ PHFetchResult* fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[result.assetIdentifier] options:nil]; ++ asset = fetchResult.firstObject; + } + +- [provider loadFileRepresentationForTypeIdentifier:identifier completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) { +- NSData *data = [[NSData alloc] initWithContentsOfURL:url]; +- UIImage *image = [[UIImage alloc] initWithData:data]; ++ dispatch_group_enter(completionGroup); + +- assets[index] = [self mapImageToAsset:image data:data phAsset:asset]; +- dispatch_group_leave(completionGroup); +- }]; +- } else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) { +- [provider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) { +- NSDictionary *mappedAsset = [self mapVideoToAsset:url phAsset:asset error:nil]; +- if (nil != mappedAsset) { +- assets[index] = mappedAsset; ++ if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { ++ NSString *identifier = provider.registeredTypeIdentifiers.firstObject; ++ // Matches both com.apple.live-photo-bundle and com.apple.private.live-photo-bundle ++ if ([identifier containsString:@"live-photo-bundle"]) { ++ // Handle live photos ++ identifier = @"public.jpeg"; + } ++ ++ [provider loadFileRepresentationForTypeIdentifier:identifier completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) { ++ NSData *data = [[NSData alloc] initWithContentsOfURL:url]; ++ UIImage *image = [[UIImage alloc] initWithData:data]; ++ ++ assets[index] = [self mapImageToAsset:image data:data phAsset:asset]; ++ dispatch_group_leave(completionGroup); ++ }]; ++ } else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]) { ++ [provider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeMovie completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) { ++ NSDictionary *mappedAsset = [self mapVideoToAsset:url phAsset:asset error:nil]; ++ if (nil != mappedAsset) { ++ assets[index] = mappedAsset; ++ } ++ dispatch_group_leave(completionGroup); ++ }]; ++ } else { ++ // The provider didn't have an item matching photo or video (fails on M1 Mac Simulator) + dispatch_group_leave(completionGroup); +- }]; +- } else { +- // The provider didn't have an item matching photo or video (fails on M1 Mac Simulator) +- dispatch_group_leave(completionGroup); +- } +- }]; ++ } ++ }]; + +- dispatch_group_notify(completionGroup, dispatch_get_main_queue(), ^{ +- // mapVideoToAsset can fail and return nil, leaving asset NSNull. +- for (NSDictionary *asset in assets) { +- if ([asset isEqual:[NSNull null]]) { +- self.callback(@[@{@"errorCode": errOthers}]); +- return; ++ dispatch_group_notify(completionGroup, dispatch_get_main_queue(), ^{ ++ // mapVideoToAsset can fail and return nil, leaving asset NSNull. ++ for (NSDictionary *asset in assets) { ++ if ([asset isEqual:[NSNull null]]) { ++ self.callback(@[@{@"errorCode": errOthers}]); ++ return; ++ } + } +- } + +- NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; +- [response setObject:assets forKey:@"assets"]; ++ NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; ++ [response setObject:assets forKey:@"assets"]; ++ ++ self.callback(@[response]); ++ }); ++ }; ++ ++ [picker dismissViewControllerAnimated:YES completion:dismissCompletionBlock]; + +- self.callback(@[response]); +- }); + } + + @end diff --git a/patches/react-native-render-html+6.3.1+002+fix-console-warning.patch b/patches/react-native-render-html+6.3.1+002+fix-console-warning.patch new file mode 100644 index 000000000000..b364e484ba59 --- /dev/null +++ b/patches/react-native-render-html+6.3.1+002+fix-console-warning.patch @@ -0,0 +1,719 @@ +diff --git a/node_modules/react-native-render-html/lib/commonjs/TChildrenRenderer.js b/node_modules/react-native-render-html/lib/commonjs/TChildrenRenderer.js +index 9d16738..bbc66a0 100644 +--- a/node_modules/react-native-render-html/lib/commonjs/TChildrenRenderer.js ++++ b/node_modules/react-native-render-html/lib/commonjs/TChildrenRenderer.js +@@ -3,7 +3,7 @@ + Object.defineProperty(exports, "__esModule", { + value: true + }); +-exports.default = exports.tchildrenRendererDefaultProps = void 0; ++exports.default = void 0; + + var _renderChildren = _interopRequireDefault(require("./renderChildren")); + +@@ -15,15 +15,6 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de + */ + const TChildrenRenderer = _renderChildren.default.bind(null); + +-const tchildrenRendererDefaultProps = { +- propsForChildren: {} +-}; +-/** +- * @ignore +- */ +- +-exports.tchildrenRendererDefaultProps = tchildrenRendererDefaultProps; +-TChildrenRenderer.defaultProps = tchildrenRendererDefaultProps; + var _default = TChildrenRenderer; + exports.default = _default; + //# sourceMappingURL=TChildrenRenderer.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-render-html/lib/commonjs/TNodeChildrenRenderer.js b/node_modules/react-native-render-html/lib/commonjs/TNodeChildrenRenderer.js +index 50b43ca..5ecf4a4 100644 +--- a/node_modules/react-native-render-html/lib/commonjs/TNodeChildrenRenderer.js ++++ b/node_modules/react-native-render-html/lib/commonjs/TNodeChildrenRenderer.js +@@ -8,8 +8,6 @@ exports.default = void 0; + + var _SharedPropsProvider = require("./context/SharedPropsProvider"); + +-var _TChildrenRenderer = require("./TChildrenRenderer"); +- + var _renderChildren = _interopRequireDefault(require("./renderChildren")); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +@@ -78,12 +76,7 @@ function TNodeChildrenRenderer(props) { + + return (0, _renderChildren.default)(useTNodeChildrenProps(props)); + } +-/** +- * @ignore +- */ +- + +-TNodeChildrenRenderer.defaultProps = _TChildrenRenderer.tchildrenRendererDefaultProps; + var _default = TNodeChildrenRenderer; + exports.default = _default; + //# sourceMappingURL=TNodeChildrenRenderer.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-render-html/lib/commonjs/TNodeRenderer.js b/node_modules/react-native-render-html/lib/commonjs/TNodeRenderer.js +index eafc942..e083941 100644 +--- a/node_modules/react-native-render-html/lib/commonjs/TNodeRenderer.js ++++ b/node_modules/react-native-render-html/lib/commonjs/TNodeRenderer.js +@@ -57,7 +57,11 @@ const TNodeRenderer = /*#__PURE__*/(0, _react.memo)(function MemoizedTNodeRender + const sharedProps = (0, _SharedPropsProvider.useSharedProps)(); + const renderRegistry = (0, _RenderRegistryProvider.useRendererRegistry)(); + const TNodeChildrenRenderer = (0, _TChildrenRendererContext.useTNodeChildrenRenderer)(); +- const tnodeProps = { ...props, ++ const tnodeProps = { ++ propsFromParent: { ++ collapsedMarginTop: null ++ }, ++ ...props, + TNodeChildrenRenderer, + sharedProps + }; +@@ -109,13 +113,6 @@ const TNodeRenderer = /*#__PURE__*/(0, _react.memo)(function MemoizedTNodeRender + const renderFn = tnode.type === 'block' || tnode.type === 'document' ? _renderBlockContent.default : _renderTextualContent.default; + return Renderer === null ? renderFn(assembledProps) : /*#__PURE__*/_react.default.createElement(Renderer, assembledProps); + }); +-const defaultProps = { +- propsFromParent: { +- collapsedMarginTop: null +- } +-}; // @ts-expect-error default props must be defined +- +-TNodeRenderer.defaultProps = defaultProps; + var _default = TNodeRenderer; + exports.default = _default; + //# sourceMappingURL=TNodeRenderer.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-render-html/lib/commonjs/TRenderEngineProvider.js b/node_modules/react-native-render-html/lib/commonjs/TRenderEngineProvider.js +index 3a700b6..4011a1b 100644 +--- a/node_modules/react-native-render-html/lib/commonjs/TRenderEngineProvider.js ++++ b/node_modules/react-native-render-html/lib/commonjs/TRenderEngineProvider.js +@@ -5,7 +5,6 @@ Object.defineProperty(exports, "__esModule", { + }); + exports.useAmbientTRenderEngine = useAmbientTRenderEngine; + exports.default = TRenderEngineProvider; +-exports.defaultTRenderEngineProviderProps = exports.defaultFallbackFonts = exports.tRenderEngineProviderPropTypes = void 0; + + var _react = _interopRequireDefault(require("react")); + +@@ -23,38 +22,6 @@ const defaultTRenderEngine = {}; + + const TRenderEngineContext = /*#__PURE__*/_react.default.createContext(defaultTRenderEngine); + +-const tRenderEngineProviderPropTypes = { +- customHTMLElementModels: _propTypes.default.object.isRequired, +- enableCSSInlineProcessing: _propTypes.default.bool, +- enableUserAgentStyles: _propTypes.default.bool, +- idsStyles: _propTypes.default.object, +- ignoredDomTags: _propTypes.default.array, +- ignoreDomNode: _propTypes.default.func, +- domVisitors: _propTypes.default.object, +- ignoredStyles: _propTypes.default.array.isRequired, +- allowedStyles: _propTypes.default.array, +- htmlParserOptions: _propTypes.default.object, +- tagsStyles: _propTypes.default.object, +- classesStyles: _propTypes.default.object, +- emSize: _propTypes.default.number.isRequired, +- baseStyle: _propTypes.default.object, +- systemFonts: _propTypes.default.arrayOf(_propTypes.default.string), +- fallbackFonts: _propTypes.default.shape({ +- serif: _propTypes.default.string, +- 'sans-serif': _propTypes.default.string, +- monospace: _propTypes.default.string +- }), +- setMarkersForTNode: _propTypes.default.func, +- dangerouslyDisableHoisting: _propTypes.default.bool, +- dangerouslyDisableWhitespaceCollapsing: _propTypes.default.bool, +- selectDomRoot: _propTypes.default.func +-}; +-/** +- * Default fallback font for special keys such as 'sans-serif', 'monospace', +- * 'serif', based on current platform. +- */ +- +-exports.tRenderEngineProviderPropTypes = tRenderEngineProviderPropTypes; + const defaultFallbackFonts = { + 'sans-serif': _reactNative.Platform.select({ + ios: 'system', +@@ -70,33 +37,6 @@ const defaultFallbackFonts = { + }) + }; + exports.defaultFallbackFonts = defaultFallbackFonts; +-const defaultTRenderEngineProviderProps = { +- htmlParserOptions: { +- decodeEntities: true +- }, +- emSize: 14, +- ignoredDomTags: [], +- ignoredStyles: [], +- baseStyle: { +- fontSize: 14 +- }, +- tagsStyles: {}, +- classesStyles: {}, +- enableUserAgentStyles: true, +- enableCSSInlineProcessing: true, +- customHTMLElementModels: {}, +- fallbackFonts: defaultFallbackFonts, +- systemFonts: _defaultSystemFonts.default +-}; +-/** +- * Use the ambient transient render engine. +- * +- * @returns The ambient transient render engine. +- * +- * @public +- */ +- +-exports.defaultTRenderEngineProviderProps = defaultTRenderEngineProviderProps; + + function useAmbientTRenderEngine() { + const engine = _react.default.useContext(TRenderEngineContext); +@@ -119,22 +59,26 @@ function useAmbientTRenderEngine() { + + function TRenderEngineProvider({ + children, ++ htmlParserOptions = { ++ decodeEntities: true ++ }, ++ emSize = 14, ++ ignoredDomTags = [], ++ ignoredStyles = [], ++ baseStyle = { fontSize: 14 }, ++ tagsStyles = {}, ++ classesStyles = {}, ++ enableUserAgentStyles = true, ++ enableCSSInlineProcessing = true, ++ customHTMLElementModels = {}, ++ fallbackFonts = defaultFallbackFonts, ++ systemFonts = defaultSystemFonts, + ...config + }) { +- const engine = (0, _useTRenderEngine.default)(config); ++ const engine = (0, _useTRenderEngine.default)({htmlParserOptions, emSize, ignoredDomTags, ignoredStyles, baseStyle, tagsStyles, classesStyles, enableUserAgentStyles, enableCSSInlineProcessing, customHTMLElementModels, fallbackFonts, systemFonts, ...config}); + return /*#__PURE__*/_react.default.createElement(TRenderEngineContext.Provider, { + value: engine + }, children); + } +-/** +- * @ignore +- */ +- +- +-TRenderEngineProvider.defaultProps = defaultTRenderEngineProviderProps; +-/** +- * @ignore +- */ + +-TRenderEngineProvider.propTypes = tRenderEngineProviderPropTypes; + //# sourceMappingURL=TRenderEngineProvider.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-render-html/lib/commonjs/elements/IMGElement.js b/node_modules/react-native-render-html/lib/commonjs/elements/IMGElement.js +index 1be151a..3a076d4 100644 +--- a/node_modules/react-native-render-html/lib/commonjs/elements/IMGElement.js ++++ b/node_modules/react-native-render-html/lib/commonjs/elements/IMGElement.js +@@ -7,8 +7,6 @@ exports.default = void 0; + + var _react = _interopRequireDefault(require("react")); + +-var _propTypes = _interopRequireDefault(require("prop-types")); +- + var _useIMGElementState = _interopRequireDefault(require("./useIMGElementState")); + + var _IMGElementContentSuccess = _interopRequireDefault(require("./IMGElementContentSuccess")); +@@ -19,15 +17,10 @@ var _IMGElementContentLoading = _interopRequireDefault(require("./IMGElementCont + + var _IMGElementContentError = _interopRequireDefault(require("./IMGElementContentError")); + +-var _defaultInitialImageDimensions = _interopRequireDefault(require("./defaultInitialImageDimensions")); +- + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } + +-function identity(arg) { +- return arg; +-} + /** + * A component to render images based on an internal loading state. + * +@@ -37,8 +30,6 @@ function identity(arg) { + * {@link IMGElementContentSuccess}, {@link IMGElementContentLoading} + * and {@link IMGElementContentError} for customization. + */ +- +- + function IMGElement(props) { + const state = (0, _useIMGElementState.default)(props); + let content; +@@ -59,43 +50,6 @@ function IMGElement(props) { + }), content); + } + +-const imgDimensionsType = _propTypes.default.shape({ +- width: _propTypes.default.number, +- height: _propTypes.default.number +-}); +- +-const propTypes = { +- source: _propTypes.default.object.isRequired, +- alt: _propTypes.default.string, +- altColor: _propTypes.default.string, +- height: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]), +- width: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]), +- style: _propTypes.default.oneOfType([_propTypes.default.object, _propTypes.default.array]), +- computeMaxWidth: _propTypes.default.func.isRequired, +- contentWidth: _propTypes.default.number, +- enableExperimentalPercentWidth: _propTypes.default.bool, +- initialDimensions: imgDimensionsType, +- onPress: _propTypes.default.func, +- testID: _propTypes.default.string, +- objectFit: _propTypes.default.string, +- cachedNaturalDimensions: imgDimensionsType, +- containerProps: _propTypes.default.object +-}; +-/** +- * @ignore +- */ +- +-IMGElement.propTypes = propTypes; +-/** +- * @ignore +- */ +- +-IMGElement.defaultProps = { +- enableExperimentalPercentWidth: false, +- computeMaxWidth: identity, +- imagesInitialDimensions: _defaultInitialImageDimensions.default, +- style: {} +-}; + var _default = IMGElement; + exports.default = _default; + //# sourceMappingURL=IMGElement.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-render-html/src/RenderHTMLConfigProvider.tsx b/node_modules/react-native-render-html/src/RenderHTMLConfigProvider.tsx +index 0df5375..925062a 100644 +--- a/node_modules/react-native-render-html/src/RenderHTMLConfigProvider.tsx ++++ b/node_modules/react-native-render-html/src/RenderHTMLConfigProvider.tsx +@@ -1,5 +1,4 @@ + import React, { PropsWithChildren, ReactElement, useMemo } from 'react'; +-import PropTypes from 'prop-types'; + import RenderersPropsProvider from './context/RenderersPropsProvider'; + import SharedPropsProvider from './context/SharedPropsProvider'; + import TChildrenRenderersContext from './context/TChildrenRendererContext'; +@@ -20,29 +19,6 @@ const childrenRendererContext = { + TNodeChildrenRenderer + }; + +-export type RenderHTMLConfigPropTypes = Record; +- +-export const renderHTMLConfigPropTypes: RenderHTMLConfigPropTypes = { +- bypassAnonymousTPhrasingNodes: PropTypes.bool, +- defaultTextProps: PropTypes.object, +- defaultViewProps: PropTypes.object, +- enableExperimentalBRCollapsing: PropTypes.bool, +- enableExperimentalGhostLinesPrevention: PropTypes.bool, +- enableExperimentalMarginCollapsing: PropTypes.bool, +- remoteErrorView: PropTypes.func, +- remoteLoadingView: PropTypes.func, +- debug: PropTypes.bool, +- computeEmbeddedMaxWidth: PropTypes.func, +- renderersProps: PropTypes.object, +- WebView: PropTypes.any, +- GenericPressable: PropTypes.any, +- defaultWebViewProps: PropTypes.object, +- pressableHightlightColor: PropTypes.string, +- customListStyleSpecs: PropTypes.object, +- renderers: PropTypes.object, +- provideEmbeddedHeaders: PropTypes.func +-}; +- + /** + * A component to provide configuration for {@link RenderHTMLSource} + * descendants, to be used in conjunction with {@link TRenderEngineProvider}. +@@ -85,8 +61,3 @@ export default function RenderHTMLConfigProvider( + + ); + } +- +-/** +- * @ignore +- */ +-RenderHTMLConfigProvider.propTypes = renderHTMLConfigPropTypes; +diff --git a/node_modules/react-native-render-html/src/RenderHTMLSource.tsx b/node_modules/react-native-render-html/src/RenderHTMLSource.tsx +index c91da52..fd0e052 100644 +--- a/node_modules/react-native-render-html/src/RenderHTMLSource.tsx ++++ b/node_modules/react-native-render-html/src/RenderHTMLSource.tsx +@@ -1,7 +1,6 @@ + import equals from 'ramda/src/equals'; + import React, { memo, ReactElement, useMemo } from 'react'; + import { Dimensions } from 'react-native'; +-import PropTypes from 'prop-types'; + import ttreeEventsContext from './context/ttreeEventsContext'; + import isUriSource from './helpers/isUriSource'; + import { SourceLoaderProps, TTreeEvents } from './internal-types'; +@@ -25,29 +24,6 @@ export type RenderHTMLSourcePropTypes = Record< + any + >; + +-export const renderSourcePropTypes: RenderHTMLSourcePropTypes = { +- source: PropTypes.oneOfType([ +- PropTypes.shape({ +- html: PropTypes.string.isRequired, +- baseUrl: PropTypes.string +- }), +- PropTypes.shape({ +- dom: PropTypes.object.isRequired, +- baseUrl: PropTypes.string +- }), +- PropTypes.shape({ +- uri: PropTypes.string.isRequired, +- method: PropTypes.string, +- body: PropTypes.any, +- headers: PropTypes.object +- }) +- ]), +- onTTreeChange: PropTypes.func, +- onHTMLLoaded: PropTypes.func, +- onDocumentMetadataLoaded: PropTypes.func, +- contentWidth: PropTypes.number +-}; +- + function isEmptySource(source: undefined | HTMLSource) { + return ( + !source || +@@ -136,9 +112,4 @@ const RenderHTMLSource = memo( + } + ); + +-/** +- * @ignore +- */ +-(RenderHTMLSource as any).propTypes = renderSourcePropTypes; +- + export default RenderHTMLSource; +diff --git a/node_modules/react-native-render-html/src/TChildrenRenderer.tsx b/node_modules/react-native-render-html/src/TChildrenRenderer.tsx +index 618a592..e12888e 100644 +--- a/node_modules/react-native-render-html/src/TChildrenRenderer.tsx ++++ b/node_modules/react-native-render-html/src/TChildrenRenderer.tsx +@@ -9,16 +9,4 @@ import renderChildren from './renderChildren'; + const TChildrenRenderer: FunctionComponent = + renderChildren.bind(null); + +-export const tchildrenRendererDefaultProps: Pick< +- TChildrenRendererProps, +- 'propsForChildren' +-> = { +- propsForChildren: {} +-}; +- +-/** +- * @ignore +- */ +-TChildrenRenderer.defaultProps = tchildrenRendererDefaultProps; +- + export default TChildrenRenderer; +diff --git a/node_modules/react-native-render-html/src/TNodeChildrenRenderer.tsx b/node_modules/react-native-render-html/src/TNodeChildrenRenderer.tsx +index bf5aef6..b820de0 100644 +--- a/node_modules/react-native-render-html/src/TNodeChildrenRenderer.tsx ++++ b/node_modules/react-native-render-html/src/TNodeChildrenRenderer.tsx +@@ -1,7 +1,6 @@ + import { ReactElement } from 'react'; + import { TNode } from '@native-html/transient-render-engine'; + import { useSharedProps } from './context/SharedPropsProvider'; +-import { tchildrenRendererDefaultProps } from './TChildrenRenderer'; + import { + TChildrenRendererProps, + TNodeChildrenRendererProps +@@ -73,9 +72,4 @@ function TNodeChildrenRenderer( + return renderChildren(useTNodeChildrenProps(props)); + } + +-/** +- * @ignore +- */ +-TNodeChildrenRenderer.defaultProps = tchildrenRendererDefaultProps; +- + export default TNodeChildrenRenderer; +diff --git a/node_modules/react-native-render-html/src/TNodeRenderer.tsx b/node_modules/react-native-render-html/src/TNodeRenderer.tsx +index d32140f..0804ba7 100644 +--- a/node_modules/react-native-render-html/src/TNodeRenderer.tsx ++++ b/node_modules/react-native-render-html/src/TNodeRenderer.tsx +@@ -49,6 +49,7 @@ const TNodeRenderer = memo(function MemoizedTNodeRenderer( + const renderRegistry = useRendererRegistry(); + const TNodeChildrenRenderer = useTNodeChildrenRenderer(); + const tnodeProps = { ++ propsFromParent: { collapsedMarginTop: null }, + ...props, + TNodeChildrenRenderer, + sharedProps +@@ -120,16 +121,6 @@ const TNodeRenderer = memo(function MemoizedTNodeRenderer( + : React.createElement(Renderer as any, assembledProps); + }); + +-const defaultProps: Required, 'propsFromParent'>> = +- { +- propsFromParent: { +- collapsedMarginTop: null +- } +- }; +- +-// @ts-expect-error default props must be defined +-TNodeRenderer.defaultProps = defaultProps; +- + export { + TDefaultBlockRenderer, + TDefaultPhrasingRenderer, +diff --git a/node_modules/react-native-render-html/src/TRenderEngineProvider.tsx b/node_modules/react-native-render-html/src/TRenderEngineProvider.tsx +index 95b60df..db6fe0b 100644 +--- a/node_modules/react-native-render-html/src/TRenderEngineProvider.tsx ++++ b/node_modules/react-native-render-html/src/TRenderEngineProvider.tsx +@@ -1,7 +1,6 @@ + import TRenderEngine from '@native-html/transient-render-engine'; + import React, { PropsWithChildren, ReactElement } from 'react'; + import { Platform } from 'react-native'; +-import PropTypes from 'prop-types'; + import useTRenderEngine from './hooks/useTRenderEngine'; + import { TRenderEngineConfig } from './shared-types'; + import defaultSystemFonts from './defaultSystemFonts'; +@@ -11,36 +10,6 @@ const defaultTRenderEngine = {} as any; + const TRenderEngineContext = + React.createContext(defaultTRenderEngine); + +-export const tRenderEngineProviderPropTypes: Record< +- keyof TRenderEngineConfig, +- any +-> = { +- customHTMLElementModels: PropTypes.object.isRequired, +- enableCSSInlineProcessing: PropTypes.bool, +- enableUserAgentStyles: PropTypes.bool, +- idsStyles: PropTypes.object, +- ignoredDomTags: PropTypes.array, +- ignoreDomNode: PropTypes.func, +- domVisitors: PropTypes.object, +- ignoredStyles: PropTypes.array.isRequired, +- allowedStyles: PropTypes.array, +- htmlParserOptions: PropTypes.object, +- tagsStyles: PropTypes.object, +- classesStyles: PropTypes.object, +- emSize: PropTypes.number.isRequired, +- baseStyle: PropTypes.object, +- systemFonts: PropTypes.arrayOf(PropTypes.string), +- fallbackFonts: PropTypes.shape({ +- serif: PropTypes.string, +- 'sans-serif': PropTypes.string, +- monospace: PropTypes.string +- }), +- setMarkersForTNode: PropTypes.func, +- dangerouslyDisableHoisting: PropTypes.bool, +- dangerouslyDisableWhitespaceCollapsing: PropTypes.bool, +- selectDomRoot: PropTypes.func +-}; +- + /** + * Default fallback font for special keys such as 'sans-serif', 'monospace', + * 'serif', based on current platform. +@@ -51,23 +20,6 @@ export const defaultFallbackFonts = { + serif: Platform.select({ ios: 'Times New Roman', default: 'serif' }) + }; + +-export const defaultTRenderEngineProviderProps: TRenderEngineConfig = { +- htmlParserOptions: { +- decodeEntities: true +- }, +- emSize: 14, +- ignoredDomTags: [], +- ignoredStyles: [], +- baseStyle: { fontSize: 14 }, +- tagsStyles: {}, +- classesStyles: {}, +- enableUserAgentStyles: true, +- enableCSSInlineProcessing: true, +- customHTMLElementModels: {}, +- fallbackFonts: defaultFallbackFonts, +- systemFonts: defaultSystemFonts +-}; +- + /** + * Use the ambient transient render engine. + * +@@ -97,22 +49,26 @@ export function useAmbientTRenderEngine() { + */ + export default function TRenderEngineProvider({ + children, ++ htmlParserOptions = { ++ decodeEntities: true ++ }, ++ emSize = 14, ++ ignoredDomTags = [], ++ ignoredStyles = [], ++ baseStyle = { fontSize: 14 }, ++ tagsStyles = {}, ++ classesStyles = {}, ++ enableUserAgentStyles = true, ++ enableCSSInlineProcessing = true, ++ customHTMLElementModels = {}, ++ fallbackFonts = defaultFallbackFonts, ++ systemFonts = defaultSystemFonts, + ...config + }: PropsWithChildren): ReactElement { +- const engine = useTRenderEngine(config); ++ const engine = useTRenderEngine({htmlParserOptions, emSize, ignoredDomTags, ignoredStyles, baseStyle, tagsStyles, classesStyles, enableUserAgentStyles, enableCSSInlineProcessing, customHTMLElementModels, fallbackFonts, systemFonts, ...config}); + return ( + + {children} + + ); + } +- +-/** +- * @ignore +- */ +-TRenderEngineProvider.defaultProps = defaultTRenderEngineProviderProps; +- +-/** +- * @ignore +- */ +-TRenderEngineProvider.propTypes = tRenderEngineProviderPropTypes; +diff --git a/node_modules/react-native-render-html/src/elements/IMGElement.tsx b/node_modules/react-native-render-html/src/elements/IMGElement.tsx +index 573e7c1..a6fc90b 100644 +--- a/node_modules/react-native-render-html/src/elements/IMGElement.tsx ++++ b/node_modules/react-native-render-html/src/elements/IMGElement.tsx +@@ -1,19 +1,13 @@ + import React, { ReactElement, ReactNode } from 'react'; +-import PropTypes from 'prop-types'; + import useIMGElementState from './useIMGElementState'; + import IMGElementContentSuccess from './IMGElementContentSuccess'; + import IMGElementContainer from './IMGElementContainer'; + import IMGElementContentLoading from './IMGElementContentLoading'; + import IMGElementContentError from './IMGElementContentError'; + import type { IMGElementProps } from './img-types'; +-import defaultImageInitialDimensions from './defaultInitialImageDimensions'; + + export type { IMGElementProps } from './img-types'; + +-function identity(arg: any) { +- return arg; +-} +- + /** + * A component to render images based on an internal loading state. + * +@@ -44,42 +38,4 @@ function IMGElement(props: IMGElementProps): ReactElement { + ); + } + +-const imgDimensionsType = PropTypes.shape({ +- width: PropTypes.number, +- height: PropTypes.number +-}); +- +-const propTypes: Record = { +- source: PropTypes.object.isRequired, +- alt: PropTypes.string, +- altColor: PropTypes.string, +- height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +- width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +- style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), +- computeMaxWidth: PropTypes.func.isRequired, +- contentWidth: PropTypes.number, +- enableExperimentalPercentWidth: PropTypes.bool, +- initialDimensions: imgDimensionsType, +- onPress: PropTypes.func, +- testID: PropTypes.string, +- objectFit: PropTypes.string, +- cachedNaturalDimensions: imgDimensionsType, +- containerProps: PropTypes.object +-}; +- +-/** +- * @ignore +- */ +-IMGElement.propTypes = propTypes; +- +-/** +- * @ignore +- */ +-IMGElement.defaultProps = { +- enableExperimentalPercentWidth: false, +- computeMaxWidth: identity, +- imagesInitialDimensions: defaultImageInitialDimensions, +- style: {} +-}; +- + export default IMGElement; +diff --git a/node_modules/react-native-render-html/src/elements/useIMGElementState.ts b/node_modules/react-native-render-html/src/elements/useIMGElementState.ts +index 6590d21..b603f26 100644 +--- a/node_modules/react-native-render-html/src/elements/useIMGElementState.ts ++++ b/node_modules/react-native-render-html/src/elements/useIMGElementState.ts +@@ -63,6 +63,10 @@ function useImageNaturalDimensions

(props: { + }; + } + ++function identity(arg: any) { ++ return arg; ++} ++ + function useFetchedNaturalDimensions(props: { + cachedNaturalDimensions?: ImageDimensions; + source: ImageURISource; +@@ -116,7 +120,7 @@ export default function useIMGElementState( + altColor, + source, + contentWidth, +- computeMaxWidth, ++ computeMaxWidth = identity, + objectFit, + initialDimensions = defaultImageInitialDimensions, + cachedNaturalDimensions +diff --git a/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts b/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts +index 5d6271b..710c73f 100644 +--- a/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts ++++ b/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts +@@ -71,8 +71,7 @@ function deriveSpecifiedDimensionsFromProps({ + export default function useImageSpecifiedDimensions( + props: UseIMGElementStateProps + ) { +- const { contentWidth, enableExperimentalPercentWidth, style, width, height } = +- props; ++ const { contentWidth, enableExperimentalPercentWidth = false, style = {}, width, height } = props + const flatStyle = useMemo(() => StyleSheet.flatten(style) || {}, [style]); + const specifiedDimensions = useMemo( + () => +diff --git a/node_modules/react-native-render-html/src/index.ts b/node_modules/react-native-render-html/src/index.ts +index 8569583..b59ec49 100644 +--- a/node_modules/react-native-render-html/src/index.ts ++++ b/node_modules/react-native-render-html/src/index.ts +@@ -128,7 +128,6 @@ export { + export { default as TNodeRenderer } from './TNodeRenderer'; + export { + default as TRenderEngineProvider, +- defaultFallbackFonts, + useAmbientTRenderEngine + } from './TRenderEngineProvider'; + export { default as RenderHTMLConfigProvider } from './RenderHTMLConfigProvider'; +diff --git a/node_modules/react-native-render-html/src/renderChildren.tsx b/node_modules/react-native-render-html/src/renderChildren.tsx +index a669402..be9ffd6 100644 +--- a/node_modules/react-native-render-html/src/renderChildren.tsx ++++ b/node_modules/react-native-render-html/src/renderChildren.tsx +@@ -4,8 +4,6 @@ import TNodeRenderer from './TNodeRenderer'; + import { TChildrenRendererProps } from './shared-types'; + import collapseTopMarginForChild from './helpers/collapseTopMarginForChild'; + +-const empty = {}; +- + const mapCollapsibleChildren = ( + propsForChildren: TChildrenRendererProps['propsForChildren'], + renderChild: TChildrenRendererProps['renderChild'], +@@ -39,7 +37,7 @@ const mapCollapsibleChildren = ( + + export default function renderChildren({ + tchildren, +- propsForChildren = empty, ++ propsForChildren = {}, + disableMarginCollapsing, + renderChild + }: TChildrenRendererProps): ReactElement { diff --git a/src/App.tsx b/src/App.tsx index cc824b78fa4c..de209d6f6631 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,11 +29,11 @@ import {FullScreenContextProvider} from './components/VideoPlayerContexts/FullSc import {PlaybackContextProvider} from './components/VideoPlayerContexts/PlaybackContext'; import {VideoPopoverMenuContextProvider} from './components/VideoPlayerContexts/VideoPopoverMenuContext'; import {VolumeContextProvider} from './components/VideoPlayerContexts/VolumeContext'; -import {CurrentReportIDContextProvider} from './components/withCurrentReportID'; import {EnvironmentProvider} from './components/withEnvironment'; import {KeyboardStateProvider} from './components/withKeyboardState'; import CONFIG from './CONFIG'; import Expensify from './Expensify'; +import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; diff --git a/src/CONST.ts b/src/CONST.ts index 61528931b7c6..f2b9bf9a63fa 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -651,8 +651,6 @@ const CONST = { P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', SPOTNANA_TRAVEL: 'spotnanaTravel', REPORT_FIELDS_FEATURE: 'reportFieldsFeature', - COMPANY_CARD_FEEDS: 'companyCardFeeds', - DIRECT_FEEDS: 'directFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', @@ -926,6 +924,7 @@ const CONST = { ADMIN_TOUR_STAGING: 'https://expensify.navattic.com/3i300k18', EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', + COMPLETED: 'completed', }, OLD_DOT_PUBLIC_URLS: { TERMS_URL: `${EXPENSIFY_URL}/terms`, @@ -1445,6 +1444,8 @@ const CONST = { UNKNOWN: 'unknown', }, }, + // The number of milliseconds for an idle session to expire + SESSION_EXPIRATION_TIME_MS: 2 * 3600 * 1000, // 2 hours WEEK_STARTS_ON: 1, // Monday DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, @@ -1467,8 +1468,8 @@ const CONST = { // at least 8 characters, 1 capital letter, 1 lowercase number, 1 number PASSWORD_COMPLEXITY_REGEX_STRING: '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$', - // 6 numeric digits - VALIDATE_CODE_REGEX_STRING: /^\d{6}$/, + // We allow either 6 digits for validated users or 9-character base26 for unvalidated users + VALIDATE_CODE_REGEX_STRING: /^\d{6}$|^[A-Z]{9}$/, // 8 alphanumeric characters RECOVERY_CODE_REGEX_STRING: /^[a-zA-Z0-9]{8}$/, @@ -1560,6 +1561,7 @@ const CONST = { ATTACHMENT_PREVIEW_ATTRIBUTE: 'src', ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE: 'data-name', ATTACHMENT_LOCAL_URL_PREFIX: ['blob:', 'file:'], + ATTACHMENT_OR_RECEIPT_LOCAL_URL: /^https:\/\/(www\.)?([a-z0-9_-]+\.)*expensify.com(:[0-9]+)?\/(chat-attachments|receipts)/, ATTACHMENT_THUMBNAIL_URL_ATTRIBUTE: 'data-expensify-thumbnail-url', ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE: 'data-expensify-width', ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE: 'data-expensify-height', @@ -1673,6 +1675,16 @@ const CONST = { STUDENT_AMBASSADOR: 'studentambassadors@expensify.com', SVFG: 'svfg@expensify.com', EXPENSIFY_EMAIL_DOMAIN: '@expensify.com', + EXPENSIFY_TEAM_EMAIL_DOMAIN: '@team.expensify.com', + }, + + FULL_STORY: { + MASK: 'fs-mask', + UNMASK: 'fs-unmask', + CUSTOMER: 'customer', + CONCIERGE: 'concierge', + OTHER: 'other', + WEB_PROP_ATTR: 'data-testid', }, CONCIERGE_DISPLAY_NAME: 'Concierge', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 020eb5262200..6b26ecd73700 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -115,6 +115,9 @@ const ONYXKEYS = { STASHED_SESSION: 'stashedSession', BETAS: 'betas', + /** Whether the user is a member of a policy other than their personal */ + HAS_NON_PERSONAL_POLICY: 'hasNonPersonalPolicy', + /** NVP keys */ /** This NVP contains list of at most 5 recent attendees */ @@ -944,6 +947,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_EXPORT_METHOD]: OnyxTypes.LastExportMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; + [ONYXKEYS.HAS_NON_PERSONAL_POLICY]: boolean; [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; [ONYXKEYS.NVP_SEEN_NEW_USER_MODAL]: boolean; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 371cef9ef788..7f8b75f353e1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3,6 +3,7 @@ import type {SearchQueryString} from './components/Search/types'; import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; +import Log from './libs/Log'; import type {ExitReason} from './types/form/ExitSurveyReasonForm'; import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; @@ -327,7 +328,7 @@ const ROUTES = { ATTACHMENTS: { route: 'attachment', getRoute: ( - reportID: string, + reportID: string | undefined, type: ValueOf, url: string, accountID?: number, @@ -895,7 +896,7 @@ const ROUTES = { }, WORKSPACE_PROFILE_DESCRIPTION: { route: 'settings/workspaces/:policyID/profile/description', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/description` as const, + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/profile/description` as const, }, WORKSPACE_PROFILE_SHARE: { route: 'settings/workspaces/:policyID/profile/share', @@ -1017,13 +1018,13 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const, }, WORKSPACE_UPGRADE: { - route: 'settings/workspaces/:policyID/upgrade/:featureName?', - getRoute: (policyID: string, featureName?: string, backTo?: string) => - getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), + route: 'settings/workspaces/:policyID?/upgrade/:featureName?', + getRoute: (policyID?: string, featureName?: string, backTo?: string) => + policyID ? getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo) : (`settings/workspaces/upgrade` as const), }, WORKSPACE_DOWNGRADE: { - route: 'settings/workspaces/:policyID/downgrade/', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/downgrade/` as const, + route: 'settings/workspaces/:policyID?/downgrade/', + getRoute: (policyID?: string) => (policyID ? (`settings/workspaces/${policyID}/downgrade/` as const) : `settings/workspaces/downgrade`), }, WORKSPACE_CATEGORIES_SETTINGS: { route: 'settings/workspaces/:policyID/categories/settings', @@ -1221,7 +1222,12 @@ const ROUTES = { }, WORKSPACE_COMPANY_CARDS: { route: 'settings/workspaces/:policyID/company-cards', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const, + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_COMPANY_CARDS route'); + } + return `settings/workspaces/${policyID}/company-cards` as const; + }, }, WORKSPACE_COMPANY_CARDS_ADD_NEW: { route: 'settings/workspaces/:policyID/company-cards/add-card-feed', diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index c0010af468af..62660980f56f 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -58,7 +58,7 @@ function extractAttachments( } if (name === 'img' && attribs.src) { - const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; + const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] ?? (new RegExp(CONST.ATTACHMENT_OR_RECEIPT_LOCAL_URL, 'i').test(attribs.src) ? attribs.src : null); const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); const previewSource = tryResolveUrlFromApiRoot(attribs.src); const sourceLinkKey = `${source}|${currentLink}`; diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 4cbf85cb0014..3f1d78eae06b 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -52,6 +52,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { height={variables.lhnLogoHeight * fontScale} /> } + style={styles.justifyContentCenter} shouldShowEnvironmentBadge /> diff --git a/src/components/ExplanationModal.tsx b/src/components/ExplanationModal.tsx index 9c44bc4d0fd3..d846dd4d28ba 100644 --- a/src/components/ExplanationModal.tsx +++ b/src/components/ExplanationModal.tsx @@ -1,19 +1,11 @@ import React from 'react'; -import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import FeatureTrainingModal from './FeatureTrainingModal'; function ExplanationModal() { const {translate} = useLocalize(); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); - const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; - - if (hasBeenAddedToNudgeMigration) { - return null; - } return ( { + if (tnode.attributes.islarge === undefined) { + return {whiteSpace: 'pre'}; + } + return {whiteSpace: 'pre', ...styles.onlyEmojisText}; + }, contentModel: HTMLContentModel.block, }), 'email-comment': HTMLElementModel.fromCustomModel({ tagName: 'email-comment', - mixedUAStyles: {whiteSpace: 'normal'}, + getMixedUAStyles: (tnode) => { + if (tnode.attributes.islarge === undefined) { + return {whiteSpace: 'normal'}; + } + return {whiteSpace: 'normal', ...styles.onlyEmojisText}; + }, contentModel: HTMLContentModel.block, }), strong: HTMLElementModel.fromCustomModel({ @@ -102,6 +112,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim styles.textSupporting, styles.textLineThrough, styles.mutedNormalTextLabel, + styles.onlyEmojisText, styles.onlyEmojisTextLineHeight, ], ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx index 88e5c1f42555..e8d7e0e85fdf 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx @@ -12,9 +12,8 @@ function EditedRenderer({tnode, TDefaultRenderer, style, ...defaultRendererProps const styles = useThemeStyles(); const {translate} = useLocalize(); const isPendingDelete = !!(tnode.attributes.deleted !== undefined); - const isLarge = !!(tnode.attributes.islarge !== undefined); return ( - + { if (isDisabled) { return; } - showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)); }} shouldUseHapticsOnLongPress accessibilityRole={CONST.ROLE.BUTTON} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 960d5647127b..25fcbccd8c9e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -20,6 +20,9 @@ type HeaderProps = { /** Additional text styles */ textStyles?: StyleProp; + /** Additional header styles */ + style?: StyleProp; + /** Additional header container styles */ containerStyles?: StyleProp; @@ -27,7 +30,7 @@ type HeaderProps = { subTitleLink?: string; }; -function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, subTitleLink = ''}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], style, containerStyles = [], shouldShowEnvironmentBadge = false, subTitleLink = ''}: HeaderProps) { const styles = useThemeStyles(); const renderedSubtitle = useMemo( () => ( @@ -65,7 +68,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ return ( - + {typeof title === 'string' ? !!title && ( (null); const isObjectPositionTop = objectPosition === CONST.IMAGE_OBJECT_POSITION.TOP; + const session = useSession(); const {shouldSetAspectRatioInStyle} = useContext(ImageBehaviorContext); @@ -37,6 +40,49 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, onLoa }, [onLoad, updateAspectRatio], ); + + // accepted sessions are sessions of a certain criteria that we think can necessitate a reload of the images + // because images sources barely changes unless specific events occur like network issues (offline/online) per example. + // Here we target new session received less than 60s after the previous session (that could be from fresh reauthentication, the previous session was not necessarily expired) + // or new session after the previous session was expired (based on timestamp gap between the 2 creationDate and the freshness of the new session). + const isAcceptedSession = useCallback((sessionCreationDateDiff: number, sessionCreationDate: number) => { + return sessionCreationDateDiff < 60000 || (sessionCreationDateDiff >= CONST.SESSION_EXPIRATION_TIME_MS && new Date().getTime() - sessionCreationDate < 60000); + }, []); + + /** + * trying to figure out if the current session is expired or fresh from a necessary reauthentication + */ + const previousSessionAge = useRef(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const validSessionAge: number | undefined = useMemo(() => { + // Authentication is required only for certain types of images (attachments and receipts), + // so we only calculate the session age for those + if (!isAuthTokenRequired) { + return undefined; + } + if (session?.creationDate) { + if (previousSessionAge.current) { + // Most likely a reauthentication happened, but unless the calculated source is different from the previous, the image won't reload + if (isAcceptedSession(session.creationDate - previousSessionAge.current, session.creationDate)) { + return session.creationDate; + } + return previousSessionAge.current; + } + if (isExpiredSession(session.creationDate)) { + // reset the countdown to now so future sessions creationDate can be compared to that time + return new Date().getTime(); + } + return session.creationDate; + } + return undefined; + }, [session, isAuthTokenRequired, isAcceptedSession]); + useEffect(() => { + if (!isAuthTokenRequired) { + return; + } + previousSessionAge.current = validSessionAge; + }); + /** * Check if the image source is a URL - if so the `encryptedAuthToken` is appended * to the source. @@ -48,24 +94,44 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, onLoa } const authToken = session?.encryptedAuthToken ?? null; if (isAuthTokenRequired && authToken) { - return { - ...propsSource, - headers: { - [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, - }, - }; + if (!!session?.creationDate && !isExpiredSession(session.creationDate)) { + return { + ...propsSource, + headers: { + [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, + }, + }; + } + if (session) { + activateReauthenticator(session); + } + return undefined; } } return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. + // but we still need the image to reload sometimes (example : when the current session is expired) + // by forcing a recalculation of the source (which value could indeed change) through the modification of the variable validSessionAge // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [propsSource, isAuthTokenRequired]); + }, [propsSource, isAuthTokenRequired, validSessionAge]); + useEffect(() => { + if (!isAuthTokenRequired || source !== undefined) { + return; + } + forwardedProps?.waitForSession?.(); + }, [source, isAuthTokenRequired, forwardedProps]); /** * If the image fails to load and the object position is top, we should hide the image by setting the opacity to 0. */ const shouldOpacityBeZero = isObjectPositionTop && !aspectRatio; + if (source === undefined && !!forwardedProps?.waitForSession) { + return undefined; + } + if (source === undefined) { + return ; + } return ( ({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Image), - imagePropsAreEqual, -); +const ImageWithOnyx = React.memo(Image, imagePropsAreEqual); ImageWithOnyx.displayName = 'Image'; diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts index 27964d8a6764..d6f2a0e51ff9 100644 --- a/src/components/Image/types.ts +++ b/src/components/Image/types.ts @@ -1,19 +1,12 @@ import type {ImageSource} from 'expo-image'; import type {ImageRequireSource, ImageResizeMode, ImageStyle, ImageURISource, StyleProp} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; -import type {Session} from '@src/types/onyx'; type ExpoImageSource = ImageSource | number | ImageSource[]; type ImageObjectPosition = ValueOf; -type ImageOnyxProps = { - /** Session info for the currently logged in user. */ - session: OnyxEntry; -}; - type ImageOnLoadEvent = { nativeEvent: { width: number; @@ -53,8 +46,15 @@ type ImageOwnProps = BaseImageProps & { /** The object position of image */ objectPosition?: ImageObjectPosition; + + /** + * Called when the image should wait for a valid session to reload + * At the moment this function is called, the image is not in cache anymore + * cf https://github.com/Expensify/App/issues/51888 + */ + waitForSession?: () => void; }; -type ImageProps = ImageOnyxProps & ImageOwnProps; +type ImageProps = ImageOwnProps; -export type {BaseImageProps, ImageOwnProps, ImageOnyxProps, ImageProps, ExpoImageSource, ImageOnLoadEvent, ImageObjectPosition}; +export type {BaseImageProps, ImageOwnProps, ImageProps, ExpoImageSource, ImageOnLoadEvent, ImageObjectPosition}; diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 0bce2fd38432..319e81ac8c64 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -212,6 +212,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV /> ); } + return ( { + setIsLoading(true); + setZoomScale(0); + setIsZoomed(false); + }} onError={onError} /> diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index ebea1a90efca..f4b26597fd69 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -111,6 +111,13 @@ function ImageWithSizeCalculation({url, altText, style, onMeasure, onLoadFailure }} onError={onError} onLoad={imageLoadedSuccessfully} + waitForSession={() => { + // Called when the image should wait for a valid session to reload + // At the moment this function is called, the image is not in cache anymore + isLoadedRef.current = false; + setIsImageCached(false); + setIsLoading(true); + }} objectPosition={objectPosition} /> {isLoading && !isImageCached && !isOffline && } diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 9e1b007321cc..ec0c6e5efcaa 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -234,6 +234,14 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan updateContentSize(e); setLightboxImageLoaded(true); }} + waitForSession={() => { + // only active lightbox should call this function + if (!isActive || isFallbackVisible || !isLightboxVisible) { + return; + } + setContentSize(cachedImageDimensions.get(uri)); + setLightboxImageLoaded(false); + }} /> diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8ae468262cca..7535a148261f 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -611,7 +611,7 @@ function MenuItem( onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} wrapperStyle={outerWrapperStyle} - activeOpacity={variables.pressDimValue} + activeOpacity={!interactive ? 1 : variables.pressDimValue} opacityAnimationDuration={0} testID={pressableTestID} style={({pressed}) => diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx index 23ddf2b0c4dd..796920372c9d 100644 --- a/src/components/OnyxProvider.tsx +++ b/src/components/OnyxProvider.tsx @@ -6,7 +6,7 @@ import createOnyxContext from './createOnyxContext'; // Set up any providers for individual keys. This should only be used in cases where many components will subscribe to // the same key (e.g. FlatList renderItem components) const [withNetwork, NetworkProvider, NetworkContext] = createOnyxContext(ONYXKEYS.NETWORK); -const [, PersonalDetailsProvider, , usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST); +const [, PersonalDetailsProvider, PersonalDetailsContext, usePersonalDetails] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS_LIST); const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE); const [, BlockedFromConciergeProvider, , useBlockedFromConcierge] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); const [, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS); @@ -55,6 +55,7 @@ export { PreferredThemeContext, useBetas, useFrequentlyUsedEmojis, + PersonalDetailsContext, PreferredEmojiSkinToneContext, useBlockedFromConcierge, useSession, diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f5e27896c8e4..03b6c820da00 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -520,6 +520,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo scrollEventThrottle={1} shouldKeepFocusedItemAtTopOfViewableArea={type === CONST.SEARCH.DATA_TYPES.CHAT} isScreenFocused={isSearchScreenFocused} + initialNumToRender={shouldUseNarrowLayout ? 5 : undefined} /> ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index e946435a6890..f00454099a03 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -119,6 +119,7 @@ function BaseSelectionList( shouldScrollToFocusedIndex = true, onContentSizeChange, listItemTitleStyles, + initialNumToRender = 12, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -320,7 +321,7 @@ function BaseSelectionList( initialFocusedIndex: flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey), maxIndex: Math.min(flattenedSections.allOptions.length - 1, CONST.MAX_SELECTION_LIST_PAGE_LENGTH * currentPage - 1), disabledIndexes: disabledArrowKeyIndexes, - isActive: isFocused, + isActive: true, onFocusedIndexChange: (index: number) => { const focusedItem = flattenedSections.allOptions.at(index); if (focusedItem) { @@ -333,14 +334,15 @@ function BaseSelectionList( isFocused, }); + const selectedItemIndex = useMemo(() => flattenedSections.allOptions.findIndex((option) => option.isSelected), [flattenedSections.allOptions]); + useEffect(() => { - const selectedItemIndex = flattenedSections.allOptions.findIndex((option) => option.isSelected); if (selectedItemIndex === -1 || selectedItemIndex === focusedIndex) { return; } setFocusedIndex(selectedItemIndex); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [flattenedSections]); + }, [selectedItemIndex]); const clearInputAfterSelect = useCallback(() => { onChangeText?.(''); @@ -753,14 +755,11 @@ function BaseSelectionList( isTextInputFocusedRef.current = isTextInputFocused; }, []); - useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex, getFocusedOption}), [ - scrollAndHighlightItem, - clearInputAfterSelect, - updateAndScrollToFocusedIndex, - updateExternalTextInputFocus, - scrollToIndex, - getFocusedOption, - ]); + useImperativeHandle( + ref, + () => ({scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex, getFocusedOption, focusTextInput}), + [scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex, getFocusedOption, focusTextInput], + ); /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { @@ -830,7 +829,7 @@ function BaseSelectionList( indicatorStyle="white" keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} - initialNumToRender={12} + initialNumToRender={initialNumToRender} maxToRenderPerBatch={maxToRenderPerBatch} windowSize={windowSize} updateCellsBatchingPeriod={updateCellsBatchingPeriod} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 19c47414b089..213fd71d0632 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -633,6 +633,9 @@ type BaseSelectionListProps = Partial & { /** Called when scrollable content view of the ScrollView changes */ onContentSizeChange?: (w: number, h: number) => void; + + /** Initial number of items to render */ + initialNumToRender?: number; } & TRightHandSideComponent; type SelectionListHandle = { @@ -642,6 +645,7 @@ type SelectionListHandle = { updateAndScrollToFocusedIndex: (newFocusedIndex: number) => void; updateExternalTextInputFocus: (isTextInputFocused: boolean) => void; getFocusedOption: () => ListItem | undefined; + focusTextInput: () => void; }; type ItemLayout = { diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 6fefa987fac3..3bd98e90eec3 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -44,7 +44,7 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; function showContextMenuForReport( event: GestureResponderEvent | MouseEvent, anchor: ContextMenuAnchor, - reportID: string, + reportID: string | undefined, action: OnyxEntry, checkIfContextMenuActive: () => void, isArchivedRoom = false, @@ -60,7 +60,7 @@ function showContextMenuForReport( anchor, reportID, action?.reportActionID, - ReportUtils.getOriginalReportID(reportID, action), + reportID ? ReportUtils.getOriginalReportID(reportID, action) : undefined, undefined, checkIfContextMenuActive, checkIfContextMenuActive, diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 89f3fbc528ef..83aba3d4b61e 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -111,6 +111,15 @@ function TestToolMenu() { /> + {/* Sends an expired session to the FE and invalidates the session by the same time in the BE. Action is delayed for 15s */} + +