diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f4f6a90ae6db..1446f1e4d851 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -94,7 +94,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] I followed the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md) - [ ] I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like `Avatar`, I verified the components using `Avatar` are working as expected) - [ ] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests) -- [ ] I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such +- [ ] I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such - [ ] I verified that if a function's arguments changed that all usages have also been updated correctly - [ ] If any new file was added I verified that: - [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory @@ -109,6 +109,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] I verified that all the inputs inside a form are aligned with each other. - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes. - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. +- [ ] I added [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. ### Screenshots/Videos diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1ceb12a30af5..2cacdf557560 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -114,51 +114,6 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - submitAndroid: - name: Submit Android app for production review - needs: prep - if: ${{ github.ref == 'refs/heads/production' }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 - with: - bundler-cache: true - - - name: Get Android native version - id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" - - - name: Decrypt json w/ Google Play credentials - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg - working-directory: android/app - - - name: Submit Android build for review - run: bundle exec fastlane android upload_google_play_production - env: - VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - - - name: Warn deployers if Android production deploy failed - if: ${{ failure() }} - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - channel: '#deployer', - attachments: [{ - color: "#DB4545", - pretext: ``, - text: `💥 Android production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the . 💥`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - android_hybrid: name: Build and deploy Android HybridApp needs: prep @@ -431,12 +386,6 @@ jobs: APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} - - name: Submit build for App Store review - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios submit_for_review - env: - VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} - - name: Upload iOS build to Browser Stack if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/Users/runner/work/App/App/New Expensify.ipa" @@ -730,7 +679,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] + needs: [buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] steps: - name: Checkout uses: actions/checkout@v4 @@ -745,21 +694,15 @@ jobs: outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] + needs: [buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ ${{ github.ref }} == 'refs/heads/production' ]; then - if [ "${{ needs.submitAndroid.result }}" == "success" ]; then - isAtLeastOnePlatformDeployed="true" - fi - else - if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then - isAtLeastOnePlatformDeployed="true" - fi + if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then + isAtLeastOnePlatformDeployed="true" fi if [ "${{ needs.iOS.result }}" == "success" ] || \ @@ -784,14 +727,8 @@ jobs: isAllPlatformsDeployed="true" fi - if [ ${{ github.ref }} == 'refs/heads/production' ]; then - if [ "${{ needs.submitAndroid.result }}" != "success" ]; then - isAllPlatformsDeployed="false" - fi - else - if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then - isAllPlatformsDeployed="false" - fi + if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then + isAllPlatformsDeployed="false" fi echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT" @@ -939,7 +876,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] steps: - name: 'Announces the deploy in the #announce Slack room' uses: 8398a7/action-slack@v3 @@ -993,11 +930,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }} + android: ${{ github.ref == 'refs/heads/production' && needs.uploadAndroid.result }} android_hybrid: ${{ needs.android_hybrid.result }} ios: ${{ needs.iOS.result }} ios_hybrid: ${{ needs.iOS_hybrid.result }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 5d10e3d6f6f8..0c391025ebc1 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 1009006606 - versionName "9.0.66-6" + versionCode 1009006707 + versionName "9.0.67-7" // 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/contributingGuides/PROPOSAL_TEMPLATE.md b/contributingGuides/PROPOSAL_TEMPLATE.md index 8c9fa7968fe2..d5ab0bf4a864 100644 --- a/contributingGuides/PROPOSAL_TEMPLATE.md +++ b/contributingGuides/PROPOSAL_TEMPLATE.md @@ -7,6 +7,9 @@ ### What changes do you think we should make in order to solve the problem? +### What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future? + + ### What alternative solutions did you explore? (Optional) **Reminder:** Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job. diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 5fc14328f3b4..545c79a95af1 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -30,7 +30,7 @@ - [ ] I verified that this PR follows the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md) - [ ] I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like `Avatar`, I verified the components using `Avatar` have been tested & I retested again) - [ ] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests) -- [ ] I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such +- [ ] I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such - [ ] If a new component is created I verified that: - [ ] A similar component doesn't exist in the codebase - [ ] All props are defined accurately and each prop has a `/** comment above it */` @@ -54,6 +54,7 @@ - [ ] I verified that all the inputs inside a form are aligned with each other. - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes. - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. +- [ ] For any bug fix or new feature in this PR, I verified that sufficient [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) are included to prevent regressions in this flow. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. - [ ] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR. diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds.md index f46c1a1442c2..e5d80b80017d 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds.md +++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds.md @@ -82,7 +82,7 @@ Any transactions that were posted prior to this date will not be imported into E Click the Assign button Once assigned, you will see each cardholder associated with their card as well as the start date listed. -If you're using a connected accounting system such as NetSuite, Xero, Intacct, Quickbooks Desktop, or QuickBooks Online, you can also connect the card to export to a specific credit card GL account. +If you're using a connected accounting system such as NetSuite, Xero, Intacct, QuickBooks Desktop, or QuickBooks Online, you can also connect the card to export to a specific credit card GL account. Go to Settings > Domains > [Domain name] > Company Cards Click Edit Exports on the right-hand side of the card table and select the GL account you want to export expenses to. diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md index 553171d73dde..7492d705c2ef 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md +++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md @@ -49,7 +49,7 @@ If Scheduled Submit is disabled on the group workspace level (or set to a manual # How to connect company cards to an accounting integration -If you're using a connected accounting system such as NetSuite, Xero, Intacct, Quickbooks Desktop, or QuickBooks Online, you can also connect the card to export to a specific credit card GL account. First, connect the card itself, and once completed, follow the steps below: +If you're using a connected accounting system such as NetSuite, Xero, Intacct, QuickBooks Desktop, or QuickBooks Online, you can also connect the card to export to a specific credit card GL account. First, connect the card itself, and once completed, follow the steps below: Go to Settings > Domains > Domain name > Company Cards Click Edit Exports on the right-hand side of the card table and select the GL account you want to export expenses to You're all done. After the account is set, exported expenses will be mapped to the specific account selected when exported by a Domain Admin. diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections.md index c9720177a8fc..f790309fbefa 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections.md +++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections.md @@ -56,7 +56,7 @@ To completely remove the card connection, unassign every card from the list and # Deep Dive ## Configure card settings Once you’ve imported your company cards, the next step is configuring the cards’ settings. -If you're using a connected accounting system such as NetSuite, Xero, Sage Intacct, Quickbooks Desktop, or QuickBooks Online. In that case, you can connect the card to export to a specific credit card GL account. +If you're using a connected accounting system such as NetSuite, Xero, Sage Intacct, QuickBooks Desktop, or QuickBooks Online. In that case, you can connect the card to export to a specific credit card GL account. 1. Go to **Settings > Domains > _Domain Name_ > Company Cards** 2. Click **Edit Exports** on the right-hand side of the card table and select the GL account you want to export expenses to 3. You're all done. After the account is set, exported expenses will be mapped to the specific account selected when exported by a Domain Admin. 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 dd913af1c497..b0ef7c5c3d1c 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 @@ -1,6 +1,6 @@ --- -title: Configure Quickbooks Desktop -description: Configure Quickbooks Desktop +title: Configure QuickBooks Desktop +description: Configure QuickBooks Desktop --- Our new QuickBooks Desktop integration allows you to automate the import and export process with Expensify. @@ -67,15 +67,15 @@ To manually sync your connection: For manual syncing, we recommend completing this process at least once a week and/or after making changes in QuickBooks Desktop that could impact how reports export from Expensify. Changes may include adjustments to your chart of accounts, vendors, employees, customers/jobs, or items. Remember: Both the Web Connector and QuickBooks Desktop need to be running for syncing or exporting to work. {% include end-info.html %} -## **Can I sync Expensify and QuickBooks Desktop (QBD) and use the platforms at the same time?** +## **Can I sync Expensify and QuickBooks Desktop and use the platforms at the same time?** When syncing Expensify to QuickBooks Desktop, we recommend waiting until the sync finishes to access either Expensify and/or QuickBooks Desktop, as performance may vary during this process. You cannot open an instance of QuickBooks Desktop while a program is syncing - this may cause QuickBooks Desktop to behave unexpectedly. -## **What are the different types of accounts that can be imported from Quickbooks Desktop?** +## **What are the different types of accounts that can be imported from QuickBooks Desktop?** Here is the list of accounts from QuickBooks Desktop and how they are pulled into Expensify: -| QBD account type | How it imports to Expensify | +| QuickBooks Desktop account type | How it imports to Expensify | | ------------- | ------------- | | Accounts payable | Vendor bill or journal entry export options | | Accounts receivable | Do not import | diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md index 3fd1df0c0a1c..d9b4d846110e 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -1,6 +1,6 @@ --- -title: Configure Quickbooks Online -description: Configure Quickbooks Online +title: Configure QuickBooks Online +description: Configure QuickBooks Online --- # Best Practices Using QuickBooks Online @@ -88,7 +88,7 @@ The following steps help you determine the advanced settings for your connection - _Automatically Create Entities_: If you export reimbursable expenses as Vendor Bills or Journal Entries, Expensify will automatically create a vendor in QuickBooks (If one does not already exist). Expensify will also automatically create a customer when exporting Invoices. - _Sync Reimbursed Reports_: Enabling will mark the Vendor Bill as paid in QuickBooks Online if you reimburse a report via ACH direct deposit in Expensify. If you reimburse outside of Expensify, then marking the Vendor Bill as paid in QuickBooks Online will automatically mark the report as reimbursed in Expensify. - _QuickBooks Account_: Select the bank account your reimbursements are coming out of, and we'll create the payment in QuickBooks. - - _Collection Account_: When exporting invoices from Expensify to Quickbooks Online, the invoice will appear against the Collection Account once marked as Paid. + - _Collection Account_: When exporting invoices from Expensify to QuickBooks Online, the invoice will appear against the Collection Account once marked as Paid. {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md index a397e34accb0..66cf4df2788f 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md @@ -1,6 +1,6 @@ --- -title: Quickbooks Online Troubleshooting -description: Quickbooks Online Troubleshooting +title: QuickBooks Online Troubleshooting +description: QuickBooks Online Troubleshooting --- # ExpensiError QBO022: When exporting billable expenses, please make sure the account in QuickBooks Online has been marked as billable. diff --git a/docs/articles/expensify-classic/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/Apply-Tax.md index c89176bcc0e8..9360962cb2ba 100644 --- a/docs/articles/expensify-classic/expenses/Apply-Tax.md +++ b/docs/articles/expensify-classic/expenses/Apply-Tax.md @@ -28,6 +28,21 @@ To handle these, you can create a single tax that combines both taxes into a sin From the Reports page, you can select Reports and then click **Export To > Tax Report** to generate a CSV containing all the expense information, including the split-out taxes. +## Why is the tax amount different than I expect? + +In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. + +To determine the inclusive tax from a total price that already includes tax, you can use the following formula: + +### **Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** + +For example, if an item costs $100 and the tax rate is 20%: +Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** +This means the tax amount $16.67 is included in the total. + +If you are simply trying to calculate the price before tax, you can use the formula: + +### **Price before tax = (Total price) ÷ (1 + Tax rate)** # Deep Dive diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md index 73e3340d41a2..19e30196e023 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -1,5 +1,5 @@ --- -title: Configure Quickbooks Online +title: Configure QuickBooks Online description: Configure your QuickBooks Online connection with Expensify --- 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 ff1b9bfab9fb..497c618442b1 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 @@ -1,5 +1,5 @@ --- -title: Quickbooks Online Troubleshooting +title: QuickBooks Online Troubleshooting description: A list of common QuickBooks Online errors and how to resolve them --- diff --git a/help/map.md b/help/map.md index eb218e67dcc0..73940652ff22 100644 --- a/help/map.md +++ b/help/map.md @@ -254,8 +254,8 @@ Lost in the app? Let this map guide you! * Delete * Accounting * Connections list - * Quickbooks Online Connect - * Quickbooks Desktop Connect + * QuickBooks Online Connect + * QuickBooks Desktop Connect * Xero * NetSuite * Sage Intacct diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index cd38fcaaaf6c..cd2598608a0f 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -638,6 +638,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", @@ -658,6 +659,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", @@ -842,6 +844,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", @@ -862,6 +865,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 2cd9c81c19ca..57840732c3a6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.66 + 9.0.67 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.66.6 + 9.0.67.7 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 57ba616450b6..652e726351f2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.66 + 9.0.67 CFBundleSignature ???? CFBundleVersion - 9.0.66.6 + 9.0.67.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 27a481ab98ef..2fa95b645f9d 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.66 + 9.0.67 CFBundleVersion - 9.0.66.6 + 9.0.67.7 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5ea5b19896e4..1f1c87db2176 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1722,7 +1722,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.14.1): + - react-native-keyboard-controller (1.14.4): - DoubleConversion - glog - hermes-engine @@ -2391,7 +2391,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.183): + - RNLiveMarkdown (0.1.187): - DoubleConversion - glog - hermes-engine @@ -2411,9 +2411,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.183) + - RNLiveMarkdown/newarch (= 0.1.187) - Yoga - - RNLiveMarkdown/newarch (0.1.183): + - RNLiveMarkdown/newarch (0.1.187): - DoubleConversion - glog - hermes-engine @@ -2503,7 +2503,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.16.1): + - RNReanimated (3.16.3): - DoubleConversion - glog - hermes-engine @@ -2523,10 +2523,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.16.1) - - RNReanimated/worklets (= 3.16.1) + - RNReanimated/reanimated (= 3.16.3) + - RNReanimated/worklets (= 3.16.3) - Yoga - - RNReanimated/reanimated (3.16.1): + - RNReanimated/reanimated (3.16.3): - DoubleConversion - glog - hermes-engine @@ -2546,9 +2546,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.16.1) + - RNReanimated/reanimated/apple (= 3.16.3) - Yoga - - RNReanimated/reanimated/apple (3.16.1): + - RNReanimated/reanimated/apple (3.16.3): - DoubleConversion - glog - hermes-engine @@ -2569,7 +2569,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.16.1): + - RNReanimated/worklets (3.16.3): - DoubleConversion - glog - hermes-engine @@ -3236,7 +3236,7 @@ SPEC CHECKSUMS: react-native-geolocation: b9bd12beaf0ebca61a01514517ca8455bd26fa06 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: aae312752fcdfaa2240be9a015fc41ce54087546 - react-native-keyboard-controller: 902c07f41a415b632583b384427a71770a8b02a3 + react-native-keyboard-controller: 97bb7b48fa427c7455afdc8870c2978efd9bfa3a react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 react-native-pager-view: c64a744211a46202619a77509f802765d1659dba @@ -3286,12 +3286,12 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: fa9c6451960d09209bb5698745a0a66330ec53cc + RNLiveMarkdown: 8338447b39fcd86596c74b9e0e9509e365a2dd3b RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 - RNReanimated: 2d728bad3a69119be89c3431ee0ccda026ecffdc + RNReanimated: 03ba2447d5a7789e2843df2ee05108d93b6441d6 RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 diff --git a/package-lock.json b/package-lock.json index a58e33023eef..5b9ab45e8122 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.66-6", + "version": "9.0.67-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.66-6", + "version": "9.0.67-7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.183", + "@expensify/react-native-live-markdown": "0.1.187", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -95,7 +95,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.81", + "react-native-onyx": "2.0.82", "react-native-pager-view": "6.5.0", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -104,7 +104,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-nitro-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.16.1", + "react-native-reanimated": "3.16.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -3632,14 +3632,14 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.183", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.183.tgz", - "integrity": "sha512-egxknos7ghe4M5Z2rK7DvphcaxQBdxyppu5N2tdCVc/3oPO2ZtBNjDjtksqywC12wPtIYgHSgxrzvLEfbh5skw==", + "version": "0.1.187", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.187.tgz", + "integrity": "sha512-bw+dfhRN31u2xfG8LCI3e28g5EG/BfkyX1EqjPBRQlDZo4fZsdA61UFW6P8Y4rHlqspjYXJ0vk4ctECRWYl4Yg==", "license": "MIT", "workspaces": [ - "parser", - "example", - "WebExample" + "./parser", + "./example", + "./WebExample" ], "engines": { "node": ">= 18.0.0" @@ -35765,9 +35765,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.81", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.81.tgz", - "integrity": "sha512-EwBqruX4lLnlk3KyZp4bst/voekLJFus7UhtvKmDuqR2Iz/FremwE04JW6YxGyc7C6KpbQrCFdWg/oF9ptRAtg==", + "version": "2.0.82", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.82.tgz", + "integrity": "sha512-12+NgkC4fOeGu2J6s985NKUuLHP4aijBhpE6Us5IfVL+9dwxr/KqUVgV00OzXtYAABcWcpMC5PrvESqe8T5Iyw==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -35894,9 +35894,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.1.tgz", - "integrity": "sha512-Wnbo7toHZ6kPLAD8JWKoKCTfNoqYOMW5vUEP76Rr4RBmJCrdXj6oauYP0aZnZq8NCbiP5bwwu7+RECcWtoetnQ==", + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz", + "integrity": "sha512-OWlA6e1oHhytTpc7WiSZ7Tmb8OYwLKYZz29Sz6d6WAg60Hm5GuAiKIWUG7Ako7FLcYhFkA0pEQ2xPMEYUo9vlw==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", diff --git a/package.json b/package.json index c82882a2c9cf..0e1dbd25b195 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.66-6", + "version": "9.0.67-7", "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.", @@ -68,7 +68,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.183", + "@expensify/react-native-live-markdown": "0.1.187", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -152,7 +152,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.81", + "react-native-onyx": "2.0.82", "react-native-pager-view": "6.5.0", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -161,7 +161,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-nitro-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.16.1", + "react-native-reanimated": "3.16.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", diff --git a/patches/react-native-reanimated+3.16.1+003+include-missing-header.patch b/patches/react-native-reanimated+3.16.1+003+include-missing-header.patch deleted file mode 100644 index 80244991a890..000000000000 --- a/patches/react-native-reanimated+3.16.1+003+include-missing-header.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp -index 475ec7a..832fb06 100644 ---- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp -+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp -@@ -32,6 +32,8 @@ - - #ifdef RCT_NEW_ARCH_ENABLED - #include -+#include -+#include - #endif // RCT_NEW_ARCH_ENABLED - - // Standard `__cplusplus` macro reference: \ No newline at end of file diff --git a/patches/react-native-reanimated+3.16.1+001+hybrid-app.patch b/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch similarity index 100% rename from patches/react-native-reanimated+3.16.1+001+hybrid-app.patch rename to patches/react-native-reanimated+3.16.3+001+hybrid-app.patch diff --git a/patches/react-native-reanimated+3.16.1+002+dontWhitelistTextProp.patch b/patches/react-native-reanimated+3.16.3+002+dontWhitelistTextProp.patch similarity index 100% rename from patches/react-native-reanimated+3.16.1+002+dontWhitelistTextProp.patch rename to patches/react-native-reanimated+3.16.3+002+dontWhitelistTextProp.patch diff --git a/src/CONST.ts b/src/CONST.ts index ee70e3b29668..eccd28aea810 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -101,9 +101,9 @@ const selfGuidedTourTask: OnboardingTask = { const onboardingEmployerOrSubmitMessage: OnboardingMessage = { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back-v2.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back-v3.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, - duration: 55, + duration: 26, width: 1280, height: 960, }, @@ -932,6 +932,7 @@ const CONST = { CONFIGURE_REIMBURSEMENT_SETTINGS_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings', COPILOT_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot', DELAYED_SUBMISSION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports', + PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing', // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', NAVATTIC: { @@ -2575,8 +2576,8 @@ const CONST = { }, NAME_USER_FRIENDLY: { netsuite: 'NetSuite', - quickbooksOnline: 'Quickbooks Online', - quickbooksDesktop: 'Quickbooks Desktop', + quickbooksOnline: 'QuickBooks Online', + quickbooksDesktop: 'QuickBooks Desktop', xero: 'Xero', intacct: 'Sage Intacct', financialForce: 'FinancialForce', @@ -3815,8 +3816,8 @@ const CONST = { }, GA: {}, GB: { - regex: /^[A-Z]{1,2}[0-9R][0-9A-Z]?\s*[0-9][A-Z-CIKMOV]{2}$/, - samples: 'LA102UX, BL2F8FX, BD1S9LU, WR4G 6LH', + regex: /^[A-Z]{1,2}[0-9R][0-9A-Z]?\s*([0-9][ABD-HJLNP-UW-Z]{2})?$/, + samples: 'LA102UX, BL2F8FX, BD1S9LU, WR4G 6LH, W1U', }, GD: {}, GE: { @@ -4974,9 +4975,8 @@ const CONST = { '2. Go to *Workspaces*.\n' + '3. Select your workspace.\n' + '4. Click *Categories*.\n' + - '5. Add or import your own categories.\n' + - "6. Disable any default categories you don't need.\n" + - '7. Require a category for every expense in *Settings*.\n' + + "5. Disable any categories you don't need.\n" + + '6. Add your own categories in the top right.\n' + '\n' + `[Take me to workspace category settings](${workspaceCategoriesLink}).`, }, @@ -5949,6 +5949,7 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + MIN_TAX_RATE_DECIMAL_PLACES: 2, DOWNLOADS_PATH: '/Downloads', DOWNLOADS_TIMEOUT: 5000, @@ -5978,6 +5979,8 @@ const CONST = { }, BULK_ACTION_TYPES: { EXPORT: 'export', + APPROVE: 'approve', + PAY: 'pay', HOLD: 'hold', UNHOLD: 'unhold', DELETE: 'delete', @@ -6150,6 +6153,14 @@ const CONST = { description: 'workspace.upgrade.reportFields.description' as const, icon: 'Pencil', }, + categories: { + id: 'categories' as const, + alias: 'categories', + name: 'Categories', + title: 'workspace.upgrade.categories.title' as const, + description: 'workspace.upgrade.categories.description' as const, + icon: 'FolderOpen', + }, [this.POLICY.CONNECTIONS.NAME.NETSUITE]: { id: this.POLICY.CONNECTIONS.NAME.NETSUITE, alias: 'netsuite', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d8f8b0f91105..6eafb3a02650 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -156,7 +156,11 @@ const ROUTES = { SETTINGS_ABOUT: 'settings/about', SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', - SETTINGS_WALLET_VERIFY_ACCOUNT: {route: 'settings/wallet/verify', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/wallet/verify', backTo)}, + SETTINGS_WALLET_VERIFY_ACCOUNT: { + route: 'settings/wallet/verify', + getRoute: (backTo?: string, forwardTo?: string) => + getUrlWithBackToParam(forwardTo ? `settings/wallet/verify?forwardTo=${encodeURIComponent(forwardTo)}` : 'settings/wallet/verify', backTo), + }, SETTINGS_WALLET_DOMAINCARD: { route: 'settings/wallet/card/:cardID?', getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const, @@ -471,6 +475,11 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_UPGRADE: { + route: ':action/:iouType/upgrade/:transactionID/:reportID', + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action as string}/${iouType as string}/upgrade/${transactionID}/${reportID}`, backTo), + }, SETTINGS_TAGS_ROOT: { route: 'settings/:policyID/tags', getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo), @@ -702,6 +711,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile/address', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), }, + WORKSPACE_PROFILE_PLAN: { + route: 'settings/workspaces/:policyID/profile/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -965,9 +978,9 @@ 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) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), }, WORKSPACE_DOWNGRADE: { route: 'settings/workspaces/:policyID/downgrade/', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5fd64b0fc0d0..0e9c54352c32 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -199,6 +199,7 @@ const SCREENS = { HOLD: 'Money_Request_Hold_Reason', STEP_CONFIRMATION: 'Money_Request_Step_Confirmation', START: 'Money_Request_Start', + STEP_UPGRADE: 'Money_Request_Step_Upgrade', STEP_AMOUNT: 'Money_Request_Step_Amount', STEP_CATEGORY: 'Money_Request_Step_Category', STEP_CURRENCY: 'Money_Request_Step_Currency', @@ -497,6 +498,7 @@ const SCREENS = { TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', + PLAN: 'Workspace_Profile_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index 0baab49d3010..68d6591c0df6 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -70,14 +70,13 @@ function AttachmentCarouselPager( const pageScrollHandler = usePageScrollHandler((e) => { 'worklet'; - // eslint-disable-next-line react-compiler/react-compiler - activePage.value = e.position; - isPagerScrolling.value = e.offset !== 0; + activePage.set(e.position); + isPagerScrolling.set(e.offset !== 0); }, []); useEffect(() => { setActivePageIndex(initialPage); - activePage.value = initialPage; + activePage.set(initialPage); }, [activePage, initialPage]); /** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */ @@ -106,7 +105,7 @@ function AttachmentCarouselPager( ); const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: isScrollEnabled.value, + scrollEnabled: isScrollEnabled.get(), })); /** diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 3a7540f65055..f169416f1812 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -253,18 +253,18 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi Gesture.Pan() .enabled(canUseTouchScreen) .onUpdate(({translationX}) => { - if (!isScrollEnabled.value) { + if (!isScrollEnabled.get()) { return; } if (translationX !== 0) { - isPagerScrolling.value = true; + isPagerScrolling.set(true); } scrollTo(scrollRef, page * cellWidth - translationX, 0, false); }) .onEnd(({translationX, velocityX}) => { - if (!isScrollEnabled.value) { + if (!isScrollEnabled.get()) { return; } @@ -281,7 +281,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi newIndex = Math.min(attachments.length - 1, Math.max(0, page + delta)); } - isPagerScrolling.value = false; + isPagerScrolling.set(false); scrollTo(scrollRef, newIndex * cellWidth, 0, true); }) // eslint-disable-next-line react-compiler/react-compiler diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts index 1c54d7841347..3311f6476194 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts +++ b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts @@ -35,12 +35,11 @@ function useCarouselContextEvents(setShouldShowArrows: (show?: SetStateAction { - if (!isScrollEnabled.value) { + if (!isScrollEnabled.get()) { return; } onRequestToggleArrows(); - }, [isScrollEnabled.value, onRequestToggleArrows]); + }, [isScrollEnabled, onRequestToggleArrows]); return {handleTap, handleScaleChange, scale, isScrollEnabled}; } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx index 7c1d350fc307..4da481809b46 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx @@ -58,7 +58,7 @@ function BaseAttachmentViewPdf({ onPressProp(event); } - if (attachmentCarouselPagerContext !== null && isScrollEnabled?.value) { + if (attachmentCarouselPagerContext !== null && isScrollEnabled?.get()) { attachmentCarouselPagerContext.onTap(); } }, diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx index c6e7984b793f..c756345664cc 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx @@ -32,32 +32,32 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled && scale.value === 1) { - const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.value); - const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.value); - const allowEnablingScroll = !isPanGestureActive.value || isScrollEnabled.value; + if (offsetX.get() !== 0 && offsetY.get() !== 0 && isScrollEnabled && scale.get() === 1) { + const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.get()); + const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.get()); + const allowEnablingScroll = !isPanGestureActive.get() || isScrollEnabled.get(); // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (translateX > translateY && translateX > SCROLL_THRESHOLD && allowEnablingScroll) { // eslint-disable-next-line react-compiler/react-compiler - isScrollEnabled.value = true; + isScrollEnabled.set(true); } else if (translateY > SCROLL_THRESHOLD) { - isScrollEnabled.value = false; + isScrollEnabled.set(false); } } - isPanGestureActive.value = true; - offsetX.value = evt.allTouches.at(0)?.absoluteX ?? 0; - offsetY.value = evt.allTouches.at(0)?.absoluteY ?? 0; + isPanGestureActive.set(true); + offsetX.set(evt.allTouches.at(0)?.absoluteX ?? 0); + offsetY.set(evt.allTouches.at(0)?.absoluteY ?? 0); }) .onTouchesUp(() => { - isPanGestureActive.value = false; + isPanGestureActive.set(false); if (!isScrollEnabled) { return; } - isScrollEnabled.value = scale.value === 1; + isScrollEnabled.set(scale.get() === 1); }); const Content = useMemo( @@ -69,7 +69,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1, // even though we're not pinching/zooming // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored. - scale.value = Math.round(newScale * 1e2) / 1e2; + scale.set(Math.round(newScale * 1e2) / 1e2); }} /> ), diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 2d22a2560bb0..abc221ed646a 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -50,24 +50,27 @@ function BaseAutoCompleteSuggestions({ const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; const animatedStyles = useAnimatedStyle(() => ({ - opacity: fadeInOpacity.value, - ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value), + opacity: fadeInOpacity.get(), + ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.get()), })); useEffect(() => { if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) { - // eslint-disable-next-line react-compiler/react-compiler - fadeInOpacity.value = withTiming(1, { - duration: 70, - easing: Easing.inOut(Easing.ease), - }); - rowHeight.value = measuredHeightOfSuggestionRows; + fadeInOpacity.set( + withTiming(1, { + duration: 70, + easing: Easing.inOut(Easing.ease), + }), + ); + rowHeight.set(measuredHeightOfSuggestionRows); } else { - fadeInOpacity.value = 1; - rowHeight.value = withTiming(measuredHeightOfSuggestionRows, { - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1), - }); + fadeInOpacity.set(1); + rowHeight.set( + withTiming(measuredHeightOfSuggestionRows, { + duration: 100, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }), + ); } prevRowHeightRef.current = measuredHeightOfSuggestionRows; @@ -103,7 +106,7 @@ function BaseAutoCompleteSuggestions({ renderItem={renderItem} keyExtractor={keyExtractor} removeClippedSubviews={false} - showsVerticalScrollIndicator={innerHeight > rowHeight.value} + showsVerticalScrollIndicator={innerHeight > rowHeight.get()} extraData={[highlightedSuggestionIndex, renderSuggestionMenuItem]} /> diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 3d1b91dce4b5..9703bb739785 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -1,9 +1,12 @@ import React, {useEffect} from 'react'; +// The coordinates are based on the App's height, not the device height. +// So we need to get the height from useWindowDimensions to calculate the position correctly. More details: https://github.com/Expensify/App/issues/53180 +// eslint-disable-next-line no-restricted-imports +import {useWindowDimensions} from 'react-native'; import useKeyboardState from '@hooks/useKeyboardState'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import AutoCompleteSuggestionsPortal from './AutoCompleteSuggestionsPortal'; @@ -54,7 +57,7 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu const isSuggestionMenuAboveRef = React.useRef(false); const leftValue = React.useRef(0); const prevLeftValue = React.useRef(0); - const {windowHeight, windowWidth} = useWindowDimensions(); + const {height: windowHeight, width: windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [suggestionHeight, setSuggestionHeight] = React.useState(0); const [containerState, setContainerState] = React.useState(initialContainerState); @@ -125,6 +128,7 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT - keyboardHeight; } + setSuggestionHeight(measuredHeight); setContainerState({ left: leftValue.current, diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 7911255ba49c..3ff9ccc4e3f8 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -97,16 +97,16 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Changes the modal state values to initial const resetState = useCallback(() => { - originalImageWidth.value = CONST.AVATAR_CROP_MODAL.INITIAL_SIZE; - originalImageHeight.value = CONST.AVATAR_CROP_MODAL.INITIAL_SIZE; - translateY.value = 0; - translateX.value = 0; - scale.value = CONST.AVATAR_CROP_MODAL.MIN_SCALE; - rotation.value = 0; - translateSlider.value = 0; - prevMaxOffsetX.value = 0; - prevMaxOffsetY.value = 0; - isLoading.value = false; + originalImageWidth.set(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + originalImageHeight.set(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + translateY.set(0); + translateX.set(0); + scale.set(CONST.AVATAR_CROP_MODAL.MIN_SCALE); + rotation.set(0); + translateSlider.set(0); + prevMaxOffsetX.set(0); + prevMaxOffsetY.set(0); + isLoading.set(false); setImageContainerSize(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); setSliderContainerSize(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); setIsImageContainerInitialized(false); @@ -123,12 +123,11 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose ImageSize.getSize(imageUri).then(({width, height, rotation: orginalRotation}) => { // On Android devices ImageSize library returns also rotation parameter. if (orginalRotation === 90 || orginalRotation === 270) { - // eslint-disable-next-line react-compiler/react-compiler - originalImageHeight.value = width; - originalImageWidth.value = height; + originalImageHeight.set(width); + originalImageWidth.set(height); } else { - originalImageHeight.value = height; - originalImageWidth.value = width; + originalImageHeight.set(height); + originalImageWidth.set(width); } setIsImageInitialized(true); @@ -136,8 +135,8 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Because the reanimated library has some internal optimizations, // sometimes when the modal is hidden styles of the image and slider might not be updated. // To trigger the update we need to slightly change the following values: - translateSlider.value += 0.01; - rotation.value += 360; + translateSlider.set((value) => value + 0.01); + rotation.set((value) => value + 360); }); }, [imageUri, originalImageHeight, originalImageWidth, rotation, translateSlider]); @@ -156,19 +155,19 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose const getDisplayedImageSize = useCallback(() => { 'worklet'; - let height = imageContainerSize * scale.value; - let width = imageContainerSize * scale.value; + let height = imageContainerSize * scale.get(); + let width = imageContainerSize * scale.get(); // Since the smaller side will be always equal to the imageContainerSize multiplied by scale, // another side can be calculated with aspect ratio. - if (originalImageWidth.value > originalImageHeight.value) { - width *= originalImageWidth.value / originalImageHeight.value; + if (originalImageWidth.get() > originalImageHeight.get()) { + width *= originalImageWidth.get() / originalImageHeight.get(); } else { - height *= originalImageHeight.value / originalImageWidth.value; + height *= originalImageHeight.get() / originalImageWidth.get(); } return {height, width}; - }, [imageContainerSize, scale, originalImageWidth, originalImageHeight]); + }, [imageContainerSize, originalImageHeight, originalImageWidth, scale]); /** * Validates the offset to prevent overflow, and updates the image offset. @@ -180,13 +179,12 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose const {height, width} = getDisplayedImageSize(); const maxOffsetX = (width - imageContainerSize) / 2; const maxOffsetY = (height - imageContainerSize) / 2; - translateX.value = clamp(offsetX, [maxOffsetX * -1, maxOffsetX]); - translateY.value = clamp(offsetY, [maxOffsetY * -1, maxOffsetY]); - // eslint-disable-next-line react-compiler/react-compiler - prevMaxOffsetX.value = maxOffsetX; - prevMaxOffsetY.value = maxOffsetY; + translateX.set(clamp(offsetX, [maxOffsetX * -1, maxOffsetX])); + translateY.set(clamp(offsetY, [maxOffsetY * -1, maxOffsetY])); + prevMaxOffsetX.set(maxOffsetX); + prevMaxOffsetY.set(maxOffsetY); }, - [getDisplayedImageSize, imageContainerSize, translateX, translateY, prevMaxOffsetX, prevMaxOffsetY, clamp], + [getDisplayedImageSize, imageContainerSize, translateX, clamp, translateY, prevMaxOffsetX, prevMaxOffsetY], ); const newScaleValue = useCallback((newSliderValue: number, containerSize: number) => { @@ -201,8 +199,8 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * and updates image's offset. */ const panGesture = Gesture.Pan().onChange((event) => { - const newX = translateX.value + event.changeX; - const newY = translateY.value + event.changeY; + const newX = translateX.get() + event.changeX; + const newY = translateY.get() + event.changeY; updateImageOffset(newX, newY); }); @@ -211,7 +209,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // when the browser window is resized. useEffect(() => { // If no panning has happened and the value is 0, do an early return. - if (!prevMaxOffsetX.value && !prevMaxOffsetY.value) { + if (!prevMaxOffsetX.get() && !prevMaxOffsetY.get()) { return; } const {height, width} = getDisplayedImageSize(); @@ -221,14 +219,14 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose // Since interpolation is expensive, we only want to do it if // image has been panned across X or Y axis by the user. if (prevMaxOffsetX) { - translateX.value = interpolate(translateX.value, [prevMaxOffsetX.value * -1, prevMaxOffsetX.value], [maxOffsetX * -1, maxOffsetX]); + translateX.set(interpolate(translateX.get(), [prevMaxOffsetX.get() * -1, prevMaxOffsetX.get()], [maxOffsetX * -1, maxOffsetX])); } if (prevMaxOffsetY) { - translateY.value = interpolate(translateY.value, [prevMaxOffsetY.value * -1, prevMaxOffsetY.value], [maxOffsetY * -1, maxOffsetY]); + translateY.set(interpolate(translateY.get(), [prevMaxOffsetY.get() * -1, prevMaxOffsetY.get()], [maxOffsetY * -1, maxOffsetY])); } - prevMaxOffsetX.value = maxOffsetX; - prevMaxOffsetY.value = maxOffsetY; + prevMaxOffsetX.set(maxOffsetX); + prevMaxOffsetY.set(maxOffsetY); }, [getDisplayedImageSize, imageContainerSize, prevMaxOffsetX, prevMaxOffsetY, translateX, translateY]); /** @@ -239,65 +237,69 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose onBegin: () => { 'worklet'; - isPressableEnabled.value = false; + isPressableEnabled.set(false); }, onChange: (event: GestureUpdateEvent) => { 'worklet'; - const newSliderValue = clamp(translateSlider.value + event.changeX, [0, sliderContainerSize]); + const newSliderValue = clamp(translateSlider.get() + event.changeX, [0, sliderContainerSize]); const newScale = newScaleValue(newSliderValue, sliderContainerSize); - const differential = newScale / scale.value; + const differential = newScale / scale.get(); - scale.value = newScale; - translateSlider.value = newSliderValue; + scale.set(newScale); + translateSlider.set(newSliderValue); - const newX = translateX.value * differential; - const newY = translateY.value * differential; + const newX = translateX.get() * differential; + const newY = translateY.get() * differential; updateImageOffset(newX, newY); }, onFinalize: () => { 'worklet'; - isPressableEnabled.value = true; + isPressableEnabled.set(true); }, }; // This effect is needed to prevent the incorrect position of // the slider's knob when the window's layout changes useEffect(() => { - translateSlider.value = interpolate(scale.value, [CONST.AVATAR_CROP_MODAL.MIN_SCALE, CONST.AVATAR_CROP_MODAL.MAX_SCALE], [0, sliderContainerSize]); - }, [scale.value, sliderContainerSize, translateSlider]); + translateSlider.set(interpolate(scale.get(), [CONST.AVATAR_CROP_MODAL.MIN_SCALE, CONST.AVATAR_CROP_MODAL.MAX_SCALE], [0, sliderContainerSize])); + }, [scale, sliderContainerSize, translateSlider]); // Rotates the image by changing the rotation value by 90 degrees // and updating the position so the image remains in the same place after rotation const rotateImage = useCallback(() => { - rotation.value -= 90; + runOnUI(() => { + rotation.set((value) => value - 90); - // Rotating 2d coordinates by applying the formula (x, y) → (-y, x). - [translateX.value, translateY.value] = [translateY.value, translateX.value * -1]; + const oldTranslateX = translateX.get(); + translateX.set(translateY.get()); + translateY.set(oldTranslateX * -1); - // Since we rotated the image by 90 degrees, now width becomes height and vice versa. - [originalImageHeight.value, originalImageWidth.value] = [originalImageWidth.value, originalImageHeight.value]; - }, [originalImageHeight.value, originalImageWidth.value, rotation, translateX.value, translateY.value]); + const oldOriginalImageHeight = originalImageHeight.get(); + originalImageHeight.set(originalImageWidth.get()); + originalImageWidth.set(oldOriginalImageHeight); + })(); + }, [originalImageHeight, originalImageWidth, rotation, translateX, translateY]); // Crops an image that was provided in the imageUri prop, using the current position/scale // then calls onSave and onClose callbacks const cropAndSaveImage = useCallback(() => { - if (isLoading.value) { + if (isLoading.get()) { return; } - isLoading.value = true; - const smallerSize = Math.min(originalImageHeight.value, originalImageWidth.value); - const size = smallerSize / scale.value; - const imageCenterX = originalImageWidth.value / 2; - const imageCenterY = originalImageHeight.value / 2; + isLoading.set(true); + const smallerSize = Math.min(originalImageHeight.get(), originalImageWidth.get()); + const size = smallerSize / scale.get(); + const imageCenterX = originalImageWidth.get() / 2; + const imageCenterY = originalImageHeight.get() / 2; const apothem = size / 2; // apothem for squares is equals to half of it size // Since the translate value is only a distance from the image center, we are able to calculate // the originX and the originY - start coordinates of cropping view. - const originX = imageCenterX - apothem - (translateX.value / imageContainerSize / scale.value) * smallerSize; - const originY = imageCenterY - apothem - (translateY.value / imageContainerSize / scale.value) * smallerSize; + const originX = imageCenterX - apothem - (translateX.get() / imageContainerSize / scale.get()) * smallerSize; + const originY = imageCenterY - apothem - (translateY.get() / imageContainerSize / scale.get()) * smallerSize; const crop = { height: size, @@ -312,29 +314,15 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose const name = isSvg ? 'fileName.png' : imageName; const type = isSvg ? 'image/png' : imageType; - cropOrRotateImage(imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name, type}) + cropOrRotateImage(imageUri, [{rotate: rotation.get() % 360}, {crop}], {compress: 1, name, type}) .then((newImage) => { onClose?.(); onSave?.(newImage); }) .catch(() => { - isLoading.value = false; + isLoading.set(false); }); - }, [ - imageUri, - imageName, - imageType, - onClose, - onSave, - originalImageHeight.value, - originalImageWidth.value, - scale.value, - translateX.value, - imageContainerSize, - translateY.value, - rotation.value, - isLoading, - ]); + }, [isLoading, originalImageHeight, originalImageWidth, scale, translateX, imageContainerSize, translateY, imageType, imageName, imageUri, rotation, onClose, onSave]); const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated @@ -342,17 +330,16 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose 'worklet'; - if (!locationX || !isPressableEnabled.value) { + if (!locationX || !isPressableEnabled.get()) { return; } const newSliderValue = clamp(locationX, [0, sliderContainerSize]); const newScale = newScaleValue(newSliderValue, sliderContainerSize); - // eslint-disable-next-line react-compiler/react-compiler - translateSlider.value = newSliderValue; - const differential = newScale / scale.value; - scale.value = newScale; - const newX = translateX.value * differential; - const newY = translateY.value * differential; + translateSlider.set(newSliderValue); + const differential = newScale / scale.get(); + scale.set(newScale); + const newX = translateX.get() * differential; + const newY = translateY.get() * differential; updateImageOffset(newX, newY); }; diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index 5bfb0d5f6557..1f11986a99f9 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -59,12 +59,12 @@ function ImageCropView({imageUri = '', containerSize = 0, panGesture = Gesture.P const imageStyle = useAnimatedStyle(() => { 'worklet'; - const height = originalImageHeight.value; - const width = originalImageWidth.value; + const height = originalImageHeight.get(); + const width = originalImageWidth.get(); const aspectRatio = height > width ? height / width : width / height; - const rotate = interpolate(rotation.value, [0, 360], [0, 360]); + const rotate = interpolate(rotation.get(), [0, 360], [0, 360]); return { - transform: [{translateX: translateX.value}, {translateY: translateY.value}, {scale: scale.value * aspectRatio}, {rotate: `${rotate}deg`}], + transform: [{translateX: translateX.get()}, {translateY: translateY.get()}, {scale: scale.get() * aspectRatio}, {rotate: `${rotate}deg`}], }; }, [originalImageHeight, originalImageWidth, rotation, translateX, translateY, scale]); diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index bac581da25e6..2f8a8fb6ef53 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -34,7 +34,7 @@ function Slider({sliderValue, gestureCallbacks}: SliderProps) { 'worklet'; return { - transform: [{translateX: sliderValue.value}], + transform: [{translateX: sliderValue.get()}], }; }); diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 0ae3afbf98bb..8ca56da524b2 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -8,6 +8,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; +import Button from './Button'; import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -46,6 +47,12 @@ type BannerProps = { /** Styles to be assigned to the Banner text */ textStyles?: StyleProp; + + /** Whether to display button in the banner */ + shouldShowButton?: boolean; + + /** Callback called when pressing the button */ + onButtonPress?: () => void; }; function Banner({ @@ -54,11 +61,13 @@ function Banner({ icon = Expensicons.Exclamation, onClose, onPress, + onButtonPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false, + shouldShowButton = false, }: BannerProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -68,7 +77,7 @@ function Banner({ return ( {(isHovered) => { - const isClickable = onClose ?? onPress; + const isClickable = onClose && onPress; const shouldHighlight = isClickable && isHovered; return ( ))} + {shouldShowButton && ( +