diff --git a/android/app/build.gradle b/android/app/build.gradle
index 901ef0ccbbbf..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 1009006701
- versionName "9.0.67-1"
+ 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/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/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/Info.plist b/ios/NewExpensify/Info.plist
index 2b1de6fa329f..57840732c3a6 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.67.1
+ 9.0.67.7
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 998f520e3128..652e726351f2 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.67.1
+ 9.0.67.7
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 55a083959f45..2fa95b645f9d 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.67
CFBundleVersion
- 9.0.67.1
+ 9.0.67.7
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 21633b432c12..1f1c87db2176 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -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
@@ -3291,7 +3291,7 @@ SPEC CHECKSUMS:
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 468d451e4b6f..5b9ab45e8122 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.67-1",
+ "version": "9.0.67-7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.67-1",
+ "version": "9.0.67-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -104,7 +104,7 @@
"react-native-plaid-link-sdk": "11.11.0",
"react-native-qrcode-svg": "6.3.11",
"react-native-quick-sqlite": "git+https://github.com/margelo/react-native-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",
@@ -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 6671a935a68c..0e1dbd25b195 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.67-1",
+ "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.",
@@ -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 4aaa7736b6f9..3426bd64b0ff 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,
},
@@ -935,6 +935,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: {
@@ -2578,8 +2579,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',
@@ -6161,6 +6162,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/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx
index ec52f07d211c..ac1fc77dff96 100644
--- a/src/components/CustomStatusBarAndBackground/index.tsx
+++ b/src/components/CustomStatusBarAndBackground/index.tsx
@@ -44,14 +44,14 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
const statusBarAnimation = useSharedValue(0);
useAnimatedReaction(
- () => statusBarAnimation.value,
+ () => statusBarAnimation.get(),
(current, previous) => {
// Do not run if either of the animated value is null
// or previous animated value is greater than or equal to the current one
if (previous === null || current === null || current <= previous) {
return;
}
- const backgroundColor = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.value, statusBarBackgroundColor.value]);
+ const backgroundColor = interpolateColor(statusBarAnimation.get(), [0, 1], [prevStatusBarBackgroundColor.get(), statusBarBackgroundColor.get()]);
runOnJS(updateStatusBarAppearance)({backgroundColor});
},
);
@@ -92,8 +92,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor;
}
- prevStatusBarBackgroundColor.value = statusBarBackgroundColor.value;
- statusBarBackgroundColor.value = currentScreenBackgroundColor;
+ prevStatusBarBackgroundColor.set(statusBarBackgroundColor.get());
+ statusBarBackgroundColor.set(currentScreenBackgroundColor);
const callUpdateStatusBarAppearance = () => {
updateStatusBarAppearance({statusBarStyle: newStatusBarStyle});
@@ -101,8 +101,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
};
const callUpdateStatusBarBackgroundColor = () => {
- statusBarAnimation.value = 0;
- statusBarAnimation.value = withDelay(300, withTiming(1));
+ statusBarAnimation.set(0);
+ statusBarAnimation.set(withDelay(300, withTiming(1)));
};
// Don't update the status bar style if it's the same as the current one, to prevent flashing.
@@ -121,7 +121,7 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
callUpdateStatusBarAppearance();
}
- if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.value !== theme.appBG) {
+ if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.get() !== theme.appBG) {
callUpdateStatusBarBackgroundColor();
}
},
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 81a31174a2ce..6d51d1b3c5c3 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -99,7 +99,7 @@ function EmptyStateComponent({
{title}
{typeof subtitle === 'string' ? {subtitle} : subtitle}
- {buttons?.map(({buttonText, buttonAction, success}, index) => (
+ {buttons?.map(({buttonText, buttonAction, success, icon, isDisabled}, index) => (
))}
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts
index 354141ae672c..a3778459b2e6 100644
--- a/src/components/EmptyStateComponent/types.ts
+++ b/src/components/EmptyStateComponent/types.ts
@@ -9,7 +9,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton;
type MediaTypes = ValueOf;
-type Button = {buttonText?: string; buttonAction?: () => void; success?: boolean};
+type Button = {buttonText?: string; buttonAction?: () => void; success?: boolean; icon?: IconAsset; isDisabled?: boolean};
type SharedProps = {
SkeletonComponent: ValidSkeletons;
diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index ecf72f89134b..3c831301db8b 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -60,18 +60,19 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
const buttonRef = ref;
useEffect(() => {
- // eslint-disable-next-line react-compiler/react-compiler
- sharedValue.value = withTiming(isActive ? 1 : 0, {
- duration: 340,
- easing: Easing.inOut(Easing.ease),
- });
+ sharedValue.set(
+ withTiming(isActive ? 1 : 0, {
+ duration: 340,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ );
}, [isActive, sharedValue]);
const animatedStyle = useAnimatedStyle(() => {
- const backgroundColor = interpolateColor(sharedValue.value, [0, 1], [success, buttonDefaultBG]);
+ const backgroundColor = interpolateColor(sharedValue.get(), [0, 1], [success, buttonDefaultBG]);
return {
- transform: [{rotate: `${sharedValue.value * 135}deg`}],
+ transform: [{rotate: `${sharedValue.get() * 135}deg`}],
backgroundColor,
borderRadius,
};
@@ -79,7 +80,7 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
const animatedProps = useAnimatedProps(
() => {
- const fill = interpolateColor(sharedValue.value, [0, 1], [textLight, textDark]);
+ const fill = interpolateColor(sharedValue.get(), [0, 1], [textLight, textDark]);
return {
fill,
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index 8baaf0c40576..2731d6bd1f98 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -4,6 +4,7 @@ import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'rea
import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import {useOnyx} from 'react-native-onyx';
+import useDebounceNonReactive from '@hooks/useDebounceNonReactive';
import useLocalize from '@hooks/useLocalize';
import * as ValidationUtils from '@libs/ValidationUtils';
import Visibility from '@libs/Visibility';
@@ -185,30 +186,34 @@ function FormProvider(
[touchedInputs],
);
- const submit = useCallback(() => {
- // Return early if the form is already submitting to avoid duplicate submission
- if (formState?.isLoading) {
- return;
- }
+ const submit = useDebounceNonReactive(
+ useCallback(() => {
+ // Return early if the form is already submitting to avoid duplicate submission
+ if (formState?.isLoading) {
+ return;
+ }
- // Prepare values before submitting
- const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues;
+ // Prepare values before submitting
+ const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues;
- // Touches all form inputs, so we can validate the entire form
- Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true));
+ // Touches all form inputs, so we can validate the entire form
+ Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true));
- // Validate form and return early if any errors are found
- if (!isEmptyObject(onValidate(trimmedStringValues))) {
- return;
- }
+ // Validate form and return early if any errors are found
+ if (!isEmptyObject(onValidate(trimmedStringValues))) {
+ return;
+ }
- // Do not submit form if network is offline and the form is not enabled when offline
- if (network?.isOffline && !enabledWhenOffline) {
- return;
- }
+ // Do not submit form if network is offline and the form is not enabled when offline
+ if (network?.isOffline && !enabledWhenOffline) {
+ return;
+ }
- KeyboardUtils.dismiss().then(() => onSubmit(trimmedStringValues));
- }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate, shouldTrimValues]);
+ KeyboardUtils.dismiss().then(() => onSubmit(trimmedStringValues));
+ }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate, shouldTrimValues]),
+ 1000,
+ {leading: true, trailing: false},
+ );
// Keep track of the focus state of the current screen.
// This is used to prevent validating the form on blur before it has been interacted with.
diff --git a/src/components/KeyboardAvoidingView/index.android.tsx b/src/components/KeyboardAvoidingView/index.android.tsx
index e8eb79d18bbd..4d758511d7ad 100644
--- a/src/components/KeyboardAvoidingView/index.android.tsx
+++ b/src/components/KeyboardAvoidingView/index.android.tsx
@@ -9,8 +9,8 @@ const useKeyboardAnimation = () => {
const {reanimated} = useKeyboardContext();
// calculate it only once on mount, to avoid `SharedValue` reads during a render
- const [initialHeight] = useState(() => -reanimated.height.value);
- const [initialProgress] = useState(() => reanimated.progress.value);
+ const [initialHeight] = useState(() => -reanimated.height.get());
+ const [initialProgress] = useState(() => reanimated.progress.get());
const heightWhenOpened = useSharedValue(initialHeight);
const height = useSharedValue(initialHeight);
@@ -22,22 +22,20 @@ const useKeyboardAnimation = () => {
onStart: (e) => {
'worklet';
- progress.value = e.progress;
- height.value = e.height;
+ progress.set(e.progress);
+ height.set(e.height);
if (e.height > 0) {
- // eslint-disable-next-line react-compiler/react-compiler
- isClosed.value = false;
- heightWhenOpened.value = e.height;
+ isClosed.set(false);
+ heightWhenOpened.set(e.height);
}
},
onEnd: (e) => {
'worklet';
- isClosed.value = e.height === 0;
-
- height.value = e.height;
- progress.value = e.progress;
+ isClosed.set(e.height === 0);
+ height.set(e.height);
+ progress.set(e.progress);
},
},
[],
@@ -63,7 +61,7 @@ const defaultLayout: LayoutRectangle = {
const KeyboardAvoidingView = forwardRef>(
({behavior, children, contentContainerStyle, enabled = true, keyboardVerticalOffset = 0, style, onLayout: onLayoutProps, ...props}, ref) => {
const initialFrame = useSharedValue(null);
- const frame = useDerivedValue(() => initialFrame.value ?? defaultLayout);
+ const frame = useDerivedValue(() => initialFrame.get() ?? defaultLayout);
const keyboard = useKeyboardAnimation();
const {height: screenHeight} = useSafeAreaFrame();
@@ -71,21 +69,21 @@ const KeyboardAvoidingView = forwardRef {
'worklet';
- const keyboardY = screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset;
+ const keyboardY = screenHeight - keyboard.heightWhenOpened.get() - keyboardVerticalOffset;
- return Math.max(frame.value.y + frame.value.height - keyboardY, 0);
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [screenHeight, keyboardVerticalOffset]);
+ return Math.max(frame.get().y + frame.get().height - keyboardY, 0);
+ }, [screenHeight, keyboard.heightWhenOpened, keyboardVerticalOffset, frame]);
- const onLayoutWorklet = useCallback((layout: LayoutRectangle) => {
- 'worklet';
+ const onLayoutWorklet = useCallback(
+ (layout: LayoutRectangle) => {
+ 'worklet';
- if (keyboard.isClosed.value || initialFrame.value === null) {
- // eslint-disable-next-line react-compiler/react-compiler
- initialFrame.value = layout;
- }
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, []);
+ if (keyboard.isClosed.get() || initialFrame.get() === null) {
+ initialFrame.set(layout);
+ }
+ },
+ [initialFrame, keyboard.isClosed],
+ );
const onLayout = useCallback>(
(e) => {
runOnUI(onLayoutWorklet)(e.nativeEvent.layout);
@@ -95,14 +93,14 @@ const KeyboardAvoidingView = forwardRef {
- const bottom = interpolate(keyboard.progress.value, [0, 1], [0, relativeKeyboardHeight()]);
+ const bottom = interpolate(keyboard.progress.get(), [0, 1], [0, relativeKeyboardHeight()]);
const bottomHeight = enabled ? bottom : 0;
switch (behavior) {
case 'height':
- if (!keyboard.isClosed.value) {
+ if (!keyboard.isClosed.get()) {
return {
- height: frame.value.height - bottomHeight,
+ height: frame.get().height - bottomHeight,
flex: 0,
};
}
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index 0fe2a1542ca3..ccf12aa4ce24 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -38,7 +38,10 @@ function OptionRowLHNData({
const optionItemRef = useRef();
const shouldDisplayViolations = ReportUtils.shouldDisplayViolationsRBRInLHN(fullReport, transactionViolations);
- const shouldDisplayReportViolations = ReportUtils.isReportOwner(fullReport) && ReportUtils.hasReportViolations(reportID);
+ const isSettled = ReportUtils.isSettled(fullReport);
+ const shouldDisplayReportViolations = !isSettled && ReportUtils.isReportOwner(fullReport) && ReportUtils.hasReportViolations(reportID);
+ // We only want to show RBR for expense reports with transaction violations not for transaction threads reports.
+ const doesExpenseReportHasViolations = ReportUtils.isExpenseReport(fullReport) && !isSettled && ReportUtils.hasViolations(reportID, transactionViolations, true);
const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
@@ -49,7 +52,7 @@ function OptionRowLHNData({
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
- hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations,
+ hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations || doesExpenseReportHasViolations,
lastMessageTextFromReport,
transactionViolations,
invoiceReceiverPolicy,
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index c809e1b050bd..19703f7a3c92 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -224,6 +224,9 @@ type MenuItemBaseProps = {
/** Whether the secondary right avatar should show as a subscript */
shouldShowSubscriptRightAvatar?: boolean;
+ /** Whether the secondary avatar should show as a subscript */
+ shouldShowSubscriptAvatar?: boolean;
+
/** Affects avatar size */
viewMode?: ValueOf;
@@ -348,6 +351,15 @@ type MenuItemBaseProps = {
};
type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
+
+const getSubscriptpAvatarBackgroundColor = (isHovered: boolean, isPressed: boolean, hoveredBackgroundColor: string, pressedBackgroundColor: string) => {
+ if (isPressed) {
+ return pressedBackgroundColor;
+ }
+ if (isHovered) {
+ return hoveredBackgroundColor;
+ }
+};
function MenuItem(
{
interactive = true,
@@ -410,6 +422,7 @@ function MenuItem(
floatRightAvatars = [],
floatRightAvatarSize,
shouldShowSubscriptRightAvatar = false,
+ shouldShowSubscriptAvatar: shouldShowSubscriptAvatarProp = false,
avatarSize = CONST.AVATAR_SIZE.DEFAULT,
isSmallAvatarSubscriptMenu = false,
brickRoadIndicator,
@@ -461,7 +474,7 @@ function MenuItem(
const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false;
const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT;
- const firstIcon = floatRightAvatars.at(0);
+ const firstRightIcon = floatRightAvatars.at(0);
const combinedTitleTextStyle = StyleUtils.combineStyles(
[
styles.flexShrink1,
@@ -477,6 +490,9 @@ function MenuItem(
],
titleStyle ?? {},
);
+ const shouldShowAvatar = !!icon && Array.isArray(icon);
+ const firstIcon = Array.isArray(icon) && !!icon.length ? icon.at(0) : undefined;
+ const shouldShowSubscriptAvatar = shouldShowSubscriptAvatarProp && !!firstIcon;
const descriptionTextStyles = StyleUtils.combineStyles([
styles.textLabelSupporting,
icon && !Array.isArray(icon) ? styles.ml3 : {},
@@ -626,7 +642,7 @@ function MenuItem(
)}
- {!!icon && Array.isArray(icon) && (
+ {shouldShowAvatar && !shouldShowSubscriptAvatar && (
)}
+ {shouldShowAvatar && shouldShowSubscriptAvatar && (
+
+ )}
{!icon && shouldPutLeftPaddingWhenNoIcon && (
{subtitle}
)}
- {floatRightAvatars?.length > 0 && !!firstIcon && (
+ {floatRightAvatars?.length > 0 && !!firstRightIcon && (
{shouldShowSubscriptRightAvatar ? (
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 83636ef38828..d01b69ed5649 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -120,11 +120,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction);
const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID);
const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason});
- const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const getCanIOUBePaid = useCallback(
- (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, transactionViolations, onlyShowPayElsewhere),
- [moneyRequestReport, chatReport, policy, transaction, transactionViolations],
+ (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere),
+ [moneyRequestReport, chatReport, policy, transaction],
);
const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
@@ -136,10 +135,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere;
- const shouldShowApproveButton = useMemo(
- () => IOU.canApproveIOU(moneyRequestReport, policy, transactionViolations) && !hasOnlyPendingTransactions,
- [moneyRequestReport, policy, hasOnlyPendingTransactions, transactionViolations],
- );
+ const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]);
const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);
diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx
index cfbd5215f5cc..7e5c5d2365d8 100644
--- a/src/components/MultiGestureCanvas/index.tsx
+++ b/src/components/MultiGestureCanvas/index.tsx
@@ -100,7 +100,7 @@ function MultiGestureCanvas({
// Adding together zoom scale and the initial scale to fit the content into the canvas
// Using the minimum content scale, so that the image is not bigger than the canvas
// and not smaller than needed to fit
- const totalScale = useDerivedValue(() => zoomScale.value * minContentScale, [minContentScale]);
+ const totalScale = useDerivedValue(() => zoomScale.get() * minContentScale, [minContentScale]);
const panTranslateX = useSharedValue(0);
const panTranslateY = useSharedValue(0);
@@ -116,13 +116,13 @@ function MultiGestureCanvas({
const offsetY = useSharedValue(0);
useAnimatedReaction(
- () => isSwipingDownToClose.value,
+ () => isSwipingDownToClose.get(),
(current) => {
if (!isUsedInCarousel) {
return;
}
// eslint-disable-next-line react-compiler/react-compiler, no-param-reassign
- isPagerScrollEnabled.value = !current;
+ isPagerScrollEnabled.set(!current);
},
);
@@ -145,26 +145,25 @@ function MultiGestureCanvas({
stopAnimation();
- // eslint-disable-next-line react-compiler/react-compiler
- offsetX.value = 0;
- offsetY.value = 0;
- pinchScale.value = 1;
+ offsetX.set(0);
+ offsetY.set(0);
+ pinchScale.set(1);
if (animated) {
- panTranslateX.value = withSpring(0, SPRING_CONFIG);
- panTranslateY.value = withSpring(0, SPRING_CONFIG);
- pinchTranslateX.value = withSpring(0, SPRING_CONFIG);
- pinchTranslateY.value = withSpring(0, SPRING_CONFIG);
- zoomScale.value = withSpring(1, SPRING_CONFIG, callback);
+ panTranslateX.set(withSpring(0, SPRING_CONFIG));
+ panTranslateY.set(withSpring(0, SPRING_CONFIG));
+ pinchTranslateX.set(withSpring(0, SPRING_CONFIG));
+ pinchTranslateY.set(withSpring(0, SPRING_CONFIG));
+ zoomScale.set(withSpring(1, SPRING_CONFIG, callback));
return;
}
- panTranslateX.value = 0;
- panTranslateY.value = 0;
- pinchTranslateX.value = 0;
- pinchTranslateY.value = 0;
- zoomScale.value = 1;
+ panTranslateX.set(0);
+ panTranslateY.set(0);
+ pinchTranslateX.set(0);
+ pinchTranslateY.set(0);
+ zoomScale.set(1);
if (callback === undefined) {
return;
@@ -172,7 +171,7 @@ function MultiGestureCanvas({
callback();
},
- [stopAnimation, offsetX, offsetY, pinchScale, panTranslateX, panTranslateY, pinchTranslateX, pinchTranslateY, zoomScale],
+ [offsetX, offsetY, panTranslateX, panTranslateY, pinchScale, pinchTranslateX, pinchTranslateY, stopAnimation, zoomScale],
);
const {singleTapGesture: baseSingleTapGesture, doubleTapGesture} = useTapGestures({
@@ -245,8 +244,8 @@ function MultiGestureCanvas({
// Animate the x and y position of the content within the canvas based on all of the gestures
const animatedStyles = useAnimatedStyle(() => {
- const x = pinchTranslateX.value + panTranslateX.value + offsetX.value;
- const y = pinchTranslateY.value + panTranslateY.value + offsetY.value;
+ const x = pinchTranslateX.get() + panTranslateX.get() + offsetX.get();
+ const y = pinchTranslateY.get() + panTranslateY.get() + offsetY.get();
return {
transform: [
@@ -256,7 +255,7 @@ function MultiGestureCanvas({
{
translateY: y,
},
- {scale: totalScale.value},
+ {scale: totalScale.get()},
],
// Hide the image if the size is not ready yet
opacity: contentSizeProp?.width ? 1 : 0,
diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts
index b94ed77f150b..f2f33aa87e7e 100644
--- a/src/components/MultiGestureCanvas/usePanGesture.ts
+++ b/src/components/MultiGestureCanvas/usePanGesture.ts
@@ -48,8 +48,8 @@ const usePanGesture = ({
onSwipeDown,
}: UsePanGestureProps): PanGesture => {
// The content size after fitting it to the canvas and zooming
- const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]);
- const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]);
+ const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.get(), [contentSize.width]);
+ const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.get(), [contentSize.height]);
// Used to track previous touch position for the "swipe down to close" gesture
const previousTouch = useSharedValue<{x: number; y: number} | null>(null);
@@ -62,7 +62,7 @@ const usePanGesture = ({
const isMobileBrowser = Browser.isMobile();
// Disable "swipe down to close" gesture when content is bigger than the canvas
- const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]);
+ const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.get(), [canvasSize.height]);
// Calculates bounds of the scaled content
// Can we pan left/right/up/down
@@ -73,25 +73,25 @@ const usePanGesture = ({
let horizontalBoundary = 0;
let verticalBoundary = 0;
- if (canvasSize.width < zoomedContentWidth.value) {
- horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.value) / 2;
+ if (canvasSize.width < zoomedContentWidth.get()) {
+ horizontalBoundary = Math.abs(canvasSize.width - zoomedContentWidth.get()) / 2;
}
- if (canvasSize.height < zoomedContentHeight.value) {
- verticalBoundary = Math.abs(zoomedContentHeight.value - canvasSize.height) / 2;
+ if (canvasSize.height < zoomedContentHeight.get()) {
+ verticalBoundary = Math.abs(zoomedContentHeight.get() - canvasSize.height) / 2;
}
const horizontalBoundaries = {min: -horizontalBoundary, max: horizontalBoundary};
const verticalBoundaries = {min: -verticalBoundary, max: verticalBoundary};
const clampedOffset = {
- x: MultiGestureCanvasUtils.clamp(offsetX.value, horizontalBoundaries.min, horizontalBoundaries.max),
- y: MultiGestureCanvasUtils.clamp(offsetY.value, verticalBoundaries.min, verticalBoundaries.max),
+ x: MultiGestureCanvasUtils.clamp(offsetX.get(), horizontalBoundaries.min, horizontalBoundaries.max),
+ y: MultiGestureCanvasUtils.clamp(offsetY.get(), verticalBoundaries.min, verticalBoundaries.max),
};
// If the horizontal/vertical offset is the same after clamping to the min/max boundaries, the content is within the boundaries
- const isInHorizontalBoundary = clampedOffset.x === offsetX.value;
- const isInVerticalBoundary = clampedOffset.y === offsetY.value;
+ const isInHorizontalBoundary = clampedOffset.x === offsetX.get();
+ const isInVerticalBoundary = clampedOffset.y === offsetY.get();
return {
horizontalBoundaries,
@@ -109,7 +109,7 @@ const usePanGesture = ({
'worklet';
// If the content is centered within the canvas, we don't need to run any animations
- if (offsetX.value === 0 && offsetY.value === 0 && panTranslateX.value === 0 && panTranslateY.value === 0) {
+ if (offsetX.get() === 0 && offsetY.get() === 0 && panTranslateX.get() === 0 && panTranslateY.get() === 0) {
return;
}
@@ -119,88 +119,96 @@ const usePanGesture = ({
// If not, we need to snap back to the boundaries
if (isInHorizontalBoundary) {
// If the (absolute) velocity is 0, we don't need to run an animation
- if (Math.abs(panVelocityX.value) !== 0) {
+ if (Math.abs(panVelocityX.get()) !== 0) {
// Phase out the pan animation
// eslint-disable-next-line react-compiler/react-compiler
- offsetX.value = withDecay({
- velocity: panVelocityX.value,
- clamp: [horizontalBoundaries.min, horizontalBoundaries.max],
- deceleration: PAN_DECAY_DECELARATION,
- rubberBandEffect: false,
- });
+ offsetX.set(
+ withDecay({
+ velocity: panVelocityX.get(),
+ clamp: [horizontalBoundaries.min, horizontalBoundaries.max],
+ deceleration: PAN_DECAY_DECELARATION,
+ rubberBandEffect: false,
+ }),
+ );
}
} else {
// Animated back to the boundary
- offsetX.value = withSpring(clampedOffset.x, SPRING_CONFIG);
+ offsetX.set(withSpring(clampedOffset.x, SPRING_CONFIG));
}
if (isInVerticalBoundary) {
// If the (absolute) velocity is 0, we don't need to run an animation
- if (Math.abs(panVelocityY.value) !== 0) {
+ if (Math.abs(panVelocityY.get()) !== 0) {
// Phase out the pan animation
- offsetY.value = withDecay({
- velocity: panVelocityY.value,
- clamp: [verticalBoundaries.min, verticalBoundaries.max],
- deceleration: PAN_DECAY_DECELARATION,
- });
+ offsetY.set(
+ withDecay({
+ velocity: panVelocityY.get(),
+ clamp: [verticalBoundaries.min, verticalBoundaries.max],
+ deceleration: PAN_DECAY_DECELARATION,
+ }),
+ );
}
} else {
- const finalTranslateY = offsetY.value + panVelocityY.value * 0.2;
-
- if (finalTranslateY > SNAP_POINT && zoomScale.value <= 1) {
- offsetY.value = withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => {
- isSwipingDownToClose.value = false;
-
- if (onSwipeDown) {
- runOnJS(onSwipeDown)();
- }
- });
+ const finalTranslateY = offsetY.get() + panVelocityY.get() * 0.2;
+
+ if (finalTranslateY > SNAP_POINT && zoomScale.get() <= 1) {
+ offsetY.set(
+ withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => {
+ isSwipingDownToClose.set(false);
+
+ if (onSwipeDown) {
+ runOnJS(onSwipeDown)();
+ }
+ }),
+ );
} else {
// Animated back to the boundary
- offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG, () => {
- isSwipingDownToClose.value = false;
- });
+ offsetY.set(
+ withSpring(clampedOffset.y, SPRING_CONFIG, () => {
+ isSwipingDownToClose.set(false);
+ }),
+ );
}
}
// Reset velocity variables after we finished the pan gesture
- panVelocityX.value = 0;
- panVelocityY.value = 0;
- }, [offsetX, offsetY, panTranslateX, panTranslateY, panVelocityX, panVelocityY, zoomScale, isSwipingDownToClose, getBounds, onSwipeDown]);
+ panVelocityX.set(0);
+ panVelocityY.set(0);
+ }, [getBounds, isSwipingDownToClose, offsetX, offsetY, onSwipeDown, panTranslateX, panTranslateY, panVelocityX, panVelocityY, zoomScale]);
const panGesture = Gesture.Pan()
.manualActivation(true)
.averageTouches(true)
.onTouchesUp(() => {
- previousTouch.value = null;
+ previousTouch.set(null);
})
.onTouchesMove((evt, state) => {
// We only allow panning when the content is zoomed in
- if (zoomScale.value > 1 && !shouldDisableTransformationGestures.value) {
+ if (zoomScale.get() > 1 && !shouldDisableTransformationGestures.get()) {
state.activate();
}
// TODO: this needs tuning to work properly
- if (!shouldDisableTransformationGestures.value && zoomScale.value === 1 && previousTouch.value !== null) {
- const velocityX = Math.abs((evt.allTouches.at(0)?.x ?? 0) - previousTouch.value.x);
- const velocityY = (evt.allTouches.at(0)?.y ?? 0) - previousTouch.value.y;
+ const previousTouchValue = previousTouch.get();
+ if (!shouldDisableTransformationGestures.get() && zoomScale.get() === 1 && previousTouchValue !== null) {
+ const velocityX = Math.abs((evt.allTouches.at(0)?.x ?? 0) - previousTouchValue.x);
+ const velocityY = (evt.allTouches.at(0)?.y ?? 0) - previousTouchValue.y;
if (Math.abs(velocityY) > velocityX && velocityY > 20) {
state.activate();
- // eslint-disable-next-line react-compiler/react-compiler
- isSwipingDownToClose.value = true;
- previousTouch.value = null;
+ isSwipingDownToClose.set(true);
+ previousTouch.set(null);
return;
}
}
- if (previousTouch.value === null) {
- previousTouch.value = {
+ if (previousTouch.get() === null) {
+ previousTouch.set({
x: evt.allTouches.at(0)?.x ?? 0,
y: evt.allTouches.at(0)?.y ?? 0,
- };
+ });
}
})
.onStart(() => {
@@ -213,31 +221,31 @@ const usePanGesture = ({
return;
}
- panVelocityX.value = evt.velocityX;
- panVelocityY.value = evt.velocityY;
+ panVelocityX.set(evt.velocityX);
+ panVelocityY.set(evt.velocityY);
- if (!isSwipingDownToClose.value) {
- if (!isMobileBrowser || (isMobileBrowser && zoomScale.value !== 1)) {
- panTranslateX.value += evt.changeX;
+ if (!isSwipingDownToClose.get()) {
+ if (!isMobileBrowser || (isMobileBrowser && zoomScale.get() !== 1)) {
+ panTranslateX.set((value) => value + evt.changeX);
}
}
- if (enableSwipeDownToClose.value || isSwipingDownToClose.value) {
- panTranslateY.value += evt.changeY;
+ if (enableSwipeDownToClose.get() || isSwipingDownToClose.get()) {
+ panTranslateY.set((value) => value + evt.changeY);
}
})
.onEnd(() => {
// Add pan translation to total offset and reset gesture variables
- offsetX.value += panTranslateX.value;
- offsetY.value += panTranslateY.value;
+ offsetX.set((value) => value + panTranslateX.get());
+ offsetY.set((value) => value + panTranslateY.get());
// Reset pan gesture variables
- panTranslateX.value = 0;
- panTranslateY.value = 0;
- previousTouch.value = null;
+ panTranslateX.set(0);
+ panTranslateY.set(0);
+ previousTouch.set(null);
// If we are swiping (in the pager), we don't want to return to boundaries
- if (shouldDisableTransformationGestures.value) {
+ if (shouldDisableTransformationGestures.get()) {
return;
}
diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts
index 01be2d00194a..7f5cecc4e949 100644
--- a/src/components/MultiGestureCanvas/usePinchGesture.ts
+++ b/src/components/MultiGestureCanvas/usePinchGesture.ts
@@ -61,16 +61,16 @@ const usePinchGesture = ({
return;
}
- runOnJS(onScaleChanged)(zoomScale.value);
+ runOnJS(onScaleChanged)(zoomScale.get());
};
// Update the total (pinch) translation based on the regular pinch + bounce
useAnimatedReaction(
- () => [pinchTranslateX.value, pinchTranslateY.value, pinchBounceTranslateX.value, pinchBounceTranslateY.value],
+ () => [pinchTranslateX.get(), pinchTranslateY.get(), pinchBounceTranslateX.get(), pinchBounceTranslateY.get()],
([translateX, translateY, bounceX, bounceY]) => {
// eslint-disable-next-line react-compiler/react-compiler
- totalPinchTranslateX.value = translateX + bounceX;
- totalPinchTranslateY.value = translateY + bounceY;
+ totalPinchTranslateX.set(translateX + bounceX);
+ totalPinchTranslateY.set(translateY + bounceY);
},
);
@@ -83,8 +83,8 @@ const usePinchGesture = ({
'worklet';
return {
- x: focalX - (canvasSize.width / 2 + offsetX.value),
- y: focalY - (canvasSize.height / 2 + offsetY.value),
+ x: focalX - (canvasSize.width / 2 + offsetX.get()),
+ y: focalY - (canvasSize.height / 2 + offsetY.get()),
};
},
[canvasSize.width, canvasSize.height, offsetX, offsetY],
@@ -105,7 +105,7 @@ const usePinchGesture = ({
// The first argument is not used, but must be defined
.onTouchesDown((_evt, state) => {
// We don't want to activate pinch gesture when we are swiping in the pager
- if (!shouldDisableTransformationGestures.value) {
+ if (!shouldDisableTransformationGestures.get()) {
return;
}
@@ -116,8 +116,8 @@ const usePinchGesture = ({
// Set the origin focal point of the pinch gesture at the start of the gesture
const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY);
- pinchOrigin.x.value = adjustedFocal.x;
- pinchOrigin.y.value = adjustedFocal.y;
+ pinchOrigin.x.set(adjustedFocal.x);
+ pinchOrigin.y.set(adjustedFocal.y);
})
.onChange((evt) => {
// Disable the pinch gesture if one finger is released,
@@ -127,58 +127,57 @@ const usePinchGesture = ({
return;
}
- const newZoomScale = pinchScale.value * evt.scale;
-
+ const newZoomScale = pinchScale.get() * evt.scale;
// Limit the zoom scale to zoom range including bounce range
- if (zoomScale.value >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScale.value <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) {
- zoomScale.value = newZoomScale;
- currentPinchScale.value = evt.scale;
+ if (zoomScale.get() >= zoomRange.min * ZOOM_RANGE_BOUNCE_FACTORS.min && zoomScale.get() <= zoomRange.max * ZOOM_RANGE_BOUNCE_FACTORS.max) {
+ zoomScale.set(newZoomScale);
+ currentPinchScale.set(evt.scale);
triggerScaleChangedEvent();
}
// Calculate new pinch translation
const adjustedFocal = getAdjustedFocal(evt.focalX, evt.focalY);
- const newPinchTranslateX = adjustedFocal.x + currentPinchScale.value * pinchOrigin.x.value * -1;
- const newPinchTranslateY = adjustedFocal.y + currentPinchScale.value * pinchOrigin.y.value * -1;
+ const newPinchTranslateX = adjustedFocal.x + currentPinchScale.get() * pinchOrigin.x.get() * -1;
+ const newPinchTranslateY = adjustedFocal.y + currentPinchScale.get() * pinchOrigin.y.get() * -1;
// If the zoom scale is within the zoom range, we perform the regular pinch translation
// Otherwise it means that we are "overzoomed" or "underzoomed", so we need to bounce back
- if (zoomScale.value >= zoomRange.min && zoomScale.value <= zoomRange.max) {
- pinchTranslateX.value = newPinchTranslateX;
- pinchTranslateY.value = newPinchTranslateY;
+ if (zoomScale.get() >= zoomRange.min && zoomScale.get() <= zoomRange.max) {
+ pinchTranslateX.set(newPinchTranslateX);
+ pinchTranslateY.set(newPinchTranslateY);
} else {
// Store x and y translation that is produced while bouncing
// so we can revert the bounce once pinch gesture is released
- pinchBounceTranslateX.value = newPinchTranslateX - pinchTranslateX.value;
- pinchBounceTranslateY.value = newPinchTranslateY - pinchTranslateY.value;
+ pinchBounceTranslateX.set(newPinchTranslateX - pinchTranslateX.get());
+ pinchBounceTranslateY.set(newPinchTranslateY - pinchTranslateY.get());
}
})
.onEnd(() => {
// Add pinch translation to total offset and reset gesture variables
- offsetX.value += pinchTranslateX.value;
- offsetY.value += pinchTranslateY.value;
- pinchTranslateX.value = 0;
- pinchTranslateY.value = 0;
- currentPinchScale.value = 1;
+ offsetX.set((value) => value + pinchTranslateX.get());
+ offsetY.set((value) => value + pinchTranslateY.get());
+ pinchTranslateX.set(0);
+ pinchTranslateY.set(0);
+ currentPinchScale.set(1);
// If the content was "overzoomed" or "underzoomed", we need to bounce back with an animation
- if (pinchBounceTranslateX.value !== 0 || pinchBounceTranslateY.value !== 0) {
- pinchBounceTranslateX.value = withSpring(0, SPRING_CONFIG);
- pinchBounceTranslateY.value = withSpring(0, SPRING_CONFIG);
+ if (pinchBounceTranslateX.get() !== 0 || pinchBounceTranslateY.get() !== 0) {
+ pinchBounceTranslateX.set(withSpring(0, SPRING_CONFIG));
+ pinchBounceTranslateY.set(withSpring(0, SPRING_CONFIG));
}
- if (zoomScale.value < zoomRange.min) {
+ if (zoomScale.get() < zoomRange.min) {
// If the zoom scale is less than the minimum zoom scale, we need to set the zoom scale to the minimum
- pinchScale.value = zoomRange.min;
- zoomScale.value = withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent);
- } else if (zoomScale.value > zoomRange.max) {
+ pinchScale.set(zoomRange.min);
+ zoomScale.set(withSpring(zoomRange.min, SPRING_CONFIG, triggerScaleChangedEvent));
+ } else if (zoomScale.get() > zoomRange.max) {
// If the zoom scale is higher than the maximum zoom scale, we need to set the zoom scale to the maximum
- pinchScale.value = zoomRange.max;
- zoomScale.value = withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent);
+ pinchScale.set(zoomRange.max);
+ zoomScale.set(withSpring(zoomRange.max, SPRING_CONFIG, triggerScaleChangedEvent));
} else {
// Otherwise, we just update the pinch scale offset
- pinchScale.value = zoomScale.value;
+ pinchScale.set(zoomScale.get());
triggerScaleChangedEvent();
}
});
diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts
index 4faacc8ac972..a918310d2862 100644
--- a/src/components/MultiGestureCanvas/useTapGestures.ts
+++ b/src/components/MultiGestureCanvas/useTapGestures.ts
@@ -111,19 +111,18 @@ const useTapGestures = ({
offsetAfterZooming.y = 0;
}
- // eslint-disable-next-line react-compiler/react-compiler
- offsetX.value = withSpring(offsetAfterZooming.x, SPRING_CONFIG);
- offsetY.value = withSpring(offsetAfterZooming.y, SPRING_CONFIG);
- zoomScale.value = withSpring(doubleTapScale, SPRING_CONFIG, callback);
- pinchScale.value = doubleTapScale;
+ offsetX.set(withSpring(offsetAfterZooming.x, SPRING_CONFIG));
+ offsetY.set(withSpring(offsetAfterZooming.y, SPRING_CONFIG));
+ zoomScale.set(withSpring(doubleTapScale, SPRING_CONFIG, callback));
+ pinchScale.set(doubleTapScale);
},
- [stopAnimation, scaledContentWidth, scaledContentHeight, canvasSize, doubleTapScale, offsetX, offsetY, zoomScale, pinchScale],
+ [stopAnimation, canvasSize.width, canvasSize.height, scaledContentWidth, scaledContentHeight, doubleTapScale, offsetX, offsetY, zoomScale, pinchScale],
);
const doubleTapGesture = Gesture.Tap()
// The first argument is not used, but must be defined
.onTouchesDown((_evt, state) => {
- if (!shouldDisableTransformationGestures.value) {
+ if (!shouldDisableTransformationGestures.get()) {
return;
}
@@ -137,13 +136,13 @@ const useTapGestures = ({
'worklet';
if (onScaleChanged != null) {
- runOnJS(onScaleChanged)(zoomScale.value);
+ runOnJS(onScaleChanged)(zoomScale.get());
}
};
// If the content is already zoomed, we want to reset the zoom,
// otherwise we want to zoom in
- if (zoomScale.value > 1) {
+ if (zoomScale.get() > 1) {
reset(true, triggerScaleChangedEvent);
} else {
zoomToCoordinates(evt.x, evt.y, triggerScaleChangedEvent);
diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx
index f4884fd3c0f8..6c7aa26d05ba 100644
--- a/src/components/OpacityView.tsx
+++ b/src/components/OpacityView.tsx
@@ -44,16 +44,11 @@ function OpacityView({
}: OpacityViewProps) {
const opacity = useSharedValue(1);
const opacityStyle = useAnimatedStyle(() => ({
- opacity: opacity.value,
+ opacity: opacity.get(),
}));
React.useEffect(() => {
- if (shouldDim) {
- // eslint-disable-next-line react-compiler/react-compiler
- opacity.value = withTiming(dimmingValue, {duration: dimAnimationDuration});
- } else {
- opacity.value = withTiming(1, {duration: dimAnimationDuration});
- }
+ opacity.set(withTiming(shouldDim ? dimmingValue : 1, {duration: dimAnimationDuration}));
}, [shouldDim, dimmingValue, opacity, dimAnimationDuration]);
return (
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index cd9ed19a31ee..62c1ed22b42c 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -106,6 +106,8 @@ function MoneyRequestPreviewContent({
currency: requestCurrency,
comment: requestComment,
merchant,
+ tag,
+ category,
} = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]);
const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
@@ -145,6 +147,7 @@ function MoneyRequestPreviewContent({
// When there are no settled transactions in duplicates, show the "Keep this one" button
const shouldShowKeepButton = !!(allDuplicates.length && duplicates.length && allDuplicates.length === duplicates.length);
+ const shouldShowCategoryOrTag = !!tag || !!category;
const shouldShowRBR = hasNoticeTypeViolations || hasWarningTypeViolations || hasViolations || hasFieldErrors || (!isFullySettled && !isFullyApproved && isOnHold);
const showCashOrCard = isCardTransaction ? translate('iou.card') : translate('iou.cash');
// We don't use isOnHold because it's true for duplicated transaction too and we only want to show hold message if the transaction is truly on hold
@@ -297,7 +300,11 @@ function MoneyRequestPreviewContent({
// Clear the draft before selecting a different expense to prevent merging fields from the previous expense
// (e.g., category, tag, tax) that may be not enabled/available in the new expense's policy.
Transaction.abandonReviewDuplicateTransactions();
- const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(reviewingTransactionID, transaction?.reportID ?? '');
+ const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(
+ reviewingTransactionID,
+ transaction?.reportID ?? '',
+ transaction?.transactionID ?? reviewingTransactionID,
+ );
Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID ?? '', reportID: transaction?.reportID});
if ('merchant' in comparisonResult.change) {
@@ -427,6 +434,43 @@ function MoneyRequestPreviewContent({
)}
+ {shouldShowCategoryOrTag && }
+ {shouldShowCategoryOrTag && (
+
+ {!!category && (
+
+
+
+ {category}
+
+
+ )}
+ {!!tag && (
+
+
+
+ {tag}
+
+
+ )}
+
+ )}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 0dbff0fe18e1..e3ddb91d0528 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -136,7 +136,7 @@ function ReportPreview({
const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
const previewMessageOpacity = useSharedValue(1);
const previewMessageStyle = useAnimatedStyle(() => ({
- opacity: previewMessageOpacity.value,
+ opacity: previewMessageOpacity.get(),
}));
const checkMarkScale = useSharedValue(iouSettled ? 1 : 0);
@@ -330,21 +330,21 @@ function ReportPreview({
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const getCanIOUBePaid = useCallback(
- (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, transactionViolations, onlyShowPayElsewhere),
- [iouReport, chatReport, policy, allTransactions, transactionViolations],
+ (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere),
+ [iouReport, chatReport, policy, allTransactions],
);
const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
- const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy, transactionViolations), [iouReport, policy, transactionViolations]);
+ const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]);
const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport);
const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID) || ReportUtils.hasMissingInvoiceBankAccount(iouReportID);
- const shouldShowRBR = hasErrors;
+ const shouldShowRBR = hasErrors && !iouSettled;
/*
Show subtitle if at least one of the expenses is not being smart scanned, and either:
@@ -428,11 +428,11 @@ function ReportPreview({
return;
}
- // eslint-disable-next-line react-compiler/react-compiler
- previewMessageOpacity.value = withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => {
- // eslint-disable-next-line react-compiler/react-compiler
- previewMessageOpacity.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2});
- });
+ previewMessageOpacity.set(
+ withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => {
+ previewMessageOpacity.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2}));
+ }),
+ );
// We only want to animate the text when the text changes
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [previewMessage, previewMessageOpacity]);
@@ -442,12 +442,7 @@ function ReportPreview({
return;
}
- if (isPaidAnimationRunning) {
- // eslint-disable-next-line react-compiler/react-compiler
- checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION}));
- } else {
- checkMarkScale.value = 1;
- }
+ checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1);
}, [isPaidAnimationRunning, iouSettled, checkMarkScale]);
return (
@@ -492,7 +487,6 @@ function ReportPreview({
fill={theme.danger}
/>
)}
-
{!shouldShowRBR && shouldPromptUserToAddBankAccount && (
(null);
return (
{
+ pressableRef?.current?.blur();
+
Timing.start(CONST.TIMING.OPEN_SEARCH);
Performance.markStart(CONST.TIMING.OPEN_SEARCH);
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index 8b98858405c2..34cfe1c2f2da 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -129,14 +129,14 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
const recentReports: OptionData[] = useMemo(() => {
if (debouncedInputValue === '') {
- return searchOptions.recentReports.slice(0, 10);
+ return searchOptions.recentReports.slice(0, 20);
}
const reportOptions: OptionData[] = [...filteredOptions.recentReports, ...filteredOptions.personalDetails];
if (filteredOptions.userToInvite) {
reportOptions.push(filteredOptions.userToInvite);
}
- return reportOptions.slice(0, 10);
+ return reportOptions.slice(0, 20);
}, [debouncedInputValue, filteredOptions, searchOptions]);
const reportForContextualSearch = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined;
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 0e12e993cc79..9358c4ad822c 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -67,6 +67,7 @@ function BaseSelectionList(
showScrollIndicator = true,
showLoadingPlaceholder = false,
showConfirmButton = false,
+ shouldUseDefaultTheme = false,
shouldPreventDefaultFocusOnSelectRow = false,
containerStyle,
sectionListStyle,
@@ -832,7 +833,7 @@ function BaseSelectionList(
{showConfirmButton && (