diff --git a/android/app/build.gradle b/android/app/build.gradle index 1b651bd29def..da340184e7c8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046206 - versionName "1.4.62-6" + versionCode 1001046210 + versionName "1.4.62-10" // 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/bank-accounts-and-payments/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-USD.md new file mode 100644 index 000000000000..0e195d5e3f1c --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Deposit-Accounts-USD.md @@ -0,0 +1,77 @@ +--- +title: Deposit Accounts - USD +description: How to add a deposit account to receive payments for yourself or your business (US) +--- +# Overview + +There are two types of deposit-only accounts: + +1. If you're an employee seeking reimbursement for expenses you’ve incurred, you’ll add a **Personal deposit-only bank account**. +2. If you're a vendor seeking payment for goods or services, you’ll add a **Business deposit-only account**. + +# How to connect a personal deposit-only bank account + +**Connect a personal deposit-only bank account if you are:** + +- An employee based in the US who gets reimbursed by their employer +- An employee based in Australia who gets reimbursed by their company via batch payments +- An international (non-US) employee whose US-based employers send international reimbursements + +**To establish the connection to a personal bank account, follow these steps:** + +1. Navigate to your **Settings > Account > Payments** and click the **Add Deposit-Only Bank Account** button. +2. Click **Log into your bank** button and click **Continue** on the Plaid connection pop-up window. +3. Search for your bank account in the list of banks and follow the prompts to sign-in to your bank account. +4. Enter your bank login credentials when prompted. + - If your bank doesn't appear, click the 'x' in the upper right corner of the Plaid pop-up window and click **Connect Manually**. + - Enter your account information, then click **Save & Continue**. + +You should be all set! You’ll receive reimbursement for your expense reports directly to this bank account. + +# How to connect a business deposit-only bank account + +**Connect a business deposit-only bank account if you are:** + +- A US-based vendor who wants to be paid directly for bills sent to customers/clients +- A US-based vendor who wants to pay invoices directly via Expensify + +**To establish the connection to a business bank account, follow these steps:** + +1. Navigate to your **Settings > Account > Payments and click the Add Deposit-Only Bank Account** button. +2. Click **Log into your bank** button and click **Continue** on the Plaid connection pop-up window. +3. Search for your bank account in the list of banks and follow the prompts to sign-in to your bank account. +4. Enter your bank login credentials when prompted. + - If your bank doesn't appear, click the 'x' in the upper right corner of the Plaid pop-up window and click **Connect Manually**. + - Enter your account information, then click **Save & Continue**. +5. If you see the option to “Switch to Business” after entering the account owner information, click that link. +6. Enter your Company Name and FEIN or TIN information. +7. Enter your company’s website formatted as https://www.domain.com. + +You should be all set! The bank account will display as a deposit-only business account, and you’ll be paid directly for any invoices you submit for payment. + +# How to delete a deposit-only bank account + +**To delete a deposit-only bank account, do the following:** + +1. Navigate to **Settings > Account > Payments > Bank Accounts** +2. Click the **Delete** next to the bank account you want to remove + +{% include faq-begin.md %} + +## **What happens if my bank requires an additional security check before adding it to a third-party?** + +If your bank account has 2FA enabled or another security step, you should be prompted to complete this when adding the account. If not, and you encounter an error, you can always select the option to “Connect Manually”. Either way, please double check that you are entering the correct bank account details to ensure successful payments. + +## **What if I also want to pay employees with my business bank account?** + +If you’ve added a business deposit-only account and also wish to also pay employees, vendors, or utilize the Expensify Card with this bank account, select “Verify” on the listed bank account. This will take you through the additional verification steps to use this account to issue payments. + +## **I connected my deposit-only bank account – Why haven’t I received my reimbursement?** + +There are a few reasons a reimbursement may be unsuccessful. The first step is to review the estimated deposit date on the report. If it’s after that date and you still haven’t seen the funds, it could have been unsuccessful because: + - The incorrect account was added. If you believe you may have entered the wrong account, please reach out to Concierge and provide the Report ID for the missing reimbursement. + - Your account wasn’t set up for Direct Deposit/ACH. You may want to contact your bank to confirm. + +If you aren’t sure, please reach out to Concierge and we can assist! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md deleted file mode 100644 index 4ae2c669561f..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: Business Bank Accounts - USD -description: How to add/remove Business Bank Accounts (US) ---- -# Overview -Adding a verified business bank account unlocks a myriad of features and automation in Expensify. -Once you connect your business bank account, you can: -- Pay employee expense reports via direct deposit (US) -- Settle company bills via direct transfer -- Accept invoice payments through direct transfer -- Access the Expensify Card - -# How to add a verified business bank account -To connect a business bank account to Expensify, follow the below steps: -1. Go to **Settings > Account > Payments** -2. Click **Add Verified Bank Account** -3. Click **Log into your bank** -4. Click **Continue** -5. When you hit the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access -6. Login to the business bank account -- If the bank is not listed, click the X to go back to the connection type -- Here you’ll see the option to **Connect Manually** -- Enter your account and routing numbers -7. Enter your bank login credentials. -- If your bank requires additional security measures, you will be directed to obtain and enter a security code -- If you have more than one account available to choose from, you will be directed to choose the desired account -Next, to verify the bank account, you’ll enter some details about the business as well as some personal information. - -## Enter company information -This is where you’ll add the legal business name as well as several other company details. - -### Company address -The company address must: -- Be located in the US -- Be a physical location -If you input a maildrop address (PO box, UPS Store, etc.), the address will likely be flagged for review and adding the bank account to Expensify will be delayed. - -### Tax Identification Number -This is the identification number that was assigned to the business by the IRS. -### Company website -A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com. -### Industry Classification Code -You can locate a list of Industry Classification Codes here. -## Enter personal information -Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: -- The address must be a physical address -- The address must be located in the US -- The SSN must be US-issued -This does not need to be a signor on the bank account. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review and adding the bank account to Expensify will be delayed. - -## Upload ID -After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: -1. Upload the front and back of your ID -2. Use your device to take a selfie and record a short video of yourself -It’s required that your ID is: -- Issued in the US -- Unexpired - -## Additional Information -Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: -- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. -- If you or another **individual** owns 25% or more of the business, please check the appropriate box -- If someone else owns 25% or more of the business, you will be prompted to provide their personal information -If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. - -# How to validate the bank account -The account you set up can be found under **Settings > Account > Payment > Bank Accounts** section in either **Verifying** or **Pending** status. -If it is **Verifying**, then this means we sent you a message and need more information from you. Please check your Concierge chat which should include a message with specific details about what we require to move forward. -If it is **Pending**, then in 1-2 business days Expensify will administer 3 test transactions to your bank account. Please check your Concierge chat for further instructions. If you do not see these test transactions -After these transactions (2 withdrawals and 1 deposit) have been processed in your account, visit your Expensify Inbox, where you'll see a prompt to input the transaction amounts. -Once you've finished these steps, your business bank account is ready to use in Expensify! - -# How to share a verified bank account -Only admins with access to the verified bank account can reimburse employees or pay vendor bills. To grant another admin access to the bank account in Expensify, go to **Settings > Account > Payments > Bank Accounts** and click **"Share"**. Enter their email address, and they will receive instructions from us. Please note, they must be a policy admin on a policy you also have access to in order to share the bank account with them. -When a bank account is shared, it must be revalidated with three new microtransactions to ensure the shared admin has access. This process takes 1-2 business days. Once received, the shared admin can enter the transactions via their Expensify account's Inbox tab. - -Note: A report is shared with all individuals with access to the same business bank account in Expensify for audit purposes. - - -# How to remove access to a verified bank account -This step is important when accountants and staff leave your business. -To remove an admin's access to a shared bank account, go to **Settings > Account > Payments > Shared Business Bank Accounts**. -You'll find a list of individuals who have access to the bank account. Next to each user, you'll see the option to Unshare the bank account. - -# How to delete a verified bank account -If you need to delete a bank account from Expensify, run through the following steps: -1. Head to **Settings > Account > Payments** -2. Click the red **Delete** button under the corresponding bank account - -Be cautious, as if it hasn't been shared with someone else, the next user will need to set it up from the beginning. - -If the bank account is set as the settlement account for your Expensify Cards, you’ll need to designate another bank account as your settlement account under **Settings > Domains > Company Cards > Settings** before this account can be deleted. - -# Deep Dive - -## Verified bank account requirements - -To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US) or utilize the Expensify Card: -- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. -- If you are adding the bank account to Expensify, you must add it from **your** Expensify account settings. -- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US issued photo ID. For utilizing features related to US ACH, your idea must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address -- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. - -## Locked bank account -When you reimburse a report, you authorize Expensify to withdraw the funds from your account. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved. - -Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit. -If you need to enable direct debits from your verified bank account, your bank will require the following details: -- The ACH CompanyIDs (1270239450 and 4270239450) -- The ACH Originator Name (Expensify) -To request to unlock the bank account, click **Fix** on your bank account under **Settings > Account > Payments > Bank Accounts**. -This sends a request to our support team to review exactly why the bank account was locked. -Please note, unlocking a bank account can take 4-5 business days to process. - -## Error adding ID to Onfido -Expensify is required by both our sponsor bank and federal law to verify the identity of the individual that is initiating the movement of money. We use Onfido to confirm that the person adding a payment method is genuine and not impersonating someone else. - -If you get a generic error message that indicates, "Something's gone wrong", please go through the following steps: - -1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser. -2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow" -3. Clear your web cache for Safari (on iPhone) or Chrome (on Android). -4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website. -5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video. -6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari. -7. If possible, try these steps on another device -8. If you have another phone available, try to follow these steps on that device -If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. - -{% include faq-begin.md %} -## What is a Beneficial Owner? - -A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. - - -## What do I do if the Beneficial Owner section only asks for personal details, but our business is owned by another company? - - -Please only indicate you have a Beneficial Owner, if it is an individual that owns 25% or more of the business. - -## Why can’t I input my address or upload my ID? - - -Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account, and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account, and then share access with you once verified. - - -## Why am I being asked for documentation when adding my bank account? -When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. -If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. - -If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. - - -## I don’t see all three microtransactions I need to validate my bank account. What should I do? - -It's a good idea to wait till the end of that second business day. If you still don’t see them, please reach out to your bank and ask them to whitelist our ACH ID's **1270239450** and **4270239450**. Expensify’s ACH Originator Name is "Expensify". - -Make sure to reach out to your Account Manager or to Concierge once you have done so and our team will be able to re-trigger those 3 transactions! - - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md b/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md deleted file mode 100644 index 6114e98883e0..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/deposit-accounts/Deposit-Accounts-AUD.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Deposit Accounts (AUD) -description: Expensify allows you to add a personal bank account to receive reimbursements for your expenses. We never take money out of this account — it is only a place for us to deposit funds from your employer. This article covers deposit accounts for Australian banks. ---- - -## How-to add your Australian personal deposit account information -1. Confirm with your Policy Admin that they’ve set up Global Reimbursment -2. Set your default policy (by selecting the correct policy after clicking on your profile picture) before adding your deposit account. -3. Go to **Settings > Account > Payments** and click **Add Deposit-Only Bank Account** -![Click the Add Deposit-Only Bank Account button](https://help.expensify.com/assets/images/add-australian-deposit-only-account.png){:width="100%"} - -4. Enter your BSB, account number and name. If your screen looks different than the image below, that means your company hasn't enabled reimbursements through Expensify. Please contact your administrator and ask them to enable reimbursements. - -![Fill in the required fields](https://help.expensify.com/assets/images/add-australian-deposit-only-account-modal.png){:width="100%"} - -# How-to delete a bank account -Bank accounts are easy to delete! Simply click the red **Delete** button in the bank account under **Settings > Account > Payments**. - -![Click the Delete button](https://help.expensify.com/assets/images/delete-australian-bank-account.png){:width="100%"} - -You can complete this process on a computer or on the mobile app. - diff --git a/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md index 29fbc8b46323..6a6f99fa398f 100644 --- a/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md +++ b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md @@ -20,6 +20,7 @@ To change the roles and permissions for members of your workspace, | Approve workspace reports | Only reports submitted to them | Yes | Yes | | Edit workspace settings | No | No | Yes | +{:start="7"} 7. If your workspace uses Advanced Approvals, select an “Approves to.” This determines who the member’s reports must be approved by, if applicable. If “no one” is selected, then any one with the Auditor or Workspace Admin role can approve the member’s reports. 8. Click **Save**. diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 9d09b4dc04f2..7f50db5da85a 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -626,9 +626,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipAutomationResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipCoreResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipExtendedActionsResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipMessageCenterResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipPreferenceCenterResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", @@ -641,9 +639,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipAutomationResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipCoreResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipExtendedActionsResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipMessageCenterResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipPreferenceCenterResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", @@ -793,9 +789,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipAutomationResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipCoreResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipExtendedActionsResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipMessageCenterResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/Airship/AirshipPreferenceCenterResources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher_Core_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleDataTransport/GoogleDataTransport_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", @@ -808,9 +802,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipAutomationResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipCoreResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipExtendedActionsResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipMessageCenterResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AirshipPreferenceCenterResources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMSessionFetcher_Core_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleDataTransport_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f0bb2ace4f4b..4dea8203b477 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.62.6 + 1.4.62.10 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index b8c2140b8546..0d1e81ade440 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.62.6 + 1.4.62.10 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 0d7c6d12f53c..a2dfb017df48 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.62 CFBundleVersion - 1.4.62.6 + 1.4.62.10 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f9244c515a2c..1ebfc6bb1b62 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,25 +1,24 @@ PODS: - - Airship (16.12.1): - - Airship/Automation (= 16.12.1) - - Airship/Basement (= 16.12.1) - - Airship/Core (= 16.12.1) - - Airship/ExtendedActions (= 16.12.1) - - Airship/MessageCenter (= 16.12.1) - - Airship/Automation (16.12.1): + - Airship (17.7.3): + - Airship/Automation (= 17.7.3) + - Airship/Basement (= 17.7.3) + - Airship/Core (= 17.7.3) + - Airship/FeatureFlags (= 17.7.3) + - Airship/MessageCenter (= 17.7.3) + - Airship/PreferenceCenter (= 17.7.3) + - Airship/Automation (17.7.3): - Airship/Core - - Airship/Basement (16.12.1) - - Airship/Core (16.12.1): + - Airship/Basement (17.7.3) + - Airship/Core (17.7.3): - Airship/Basement - - Airship/ExtendedActions (16.12.1): + - Airship/FeatureFlags (17.7.3): - Airship/Core - - Airship/MessageCenter (16.12.1): + - Airship/MessageCenter (17.7.3): - Airship/Core - - Airship/PreferenceCenter (16.12.1): + - Airship/PreferenceCenter (17.7.3): - Airship/Core - - AirshipFrameworkProxy (2.1.1): - - Airship (= 16.12.1) - - Airship/MessageCenter (= 16.12.1) - - Airship/PreferenceCenter (= 16.12.1) + - AirshipFrameworkProxy (5.1.1): + - Airship (= 17.7.3) - AirshipServiceExtension (17.8.0) - AppAuth (1.6.2): - AppAuth/Core (= 1.6.2) @@ -1162,8 +1161,8 @@ PODS: - React-Mapbuffer (0.73.4): - glog - React-debug - - react-native-airship (15.3.1): - - AirshipFrameworkProxy (= 2.1.1) + - react-native-airship (17.2.1): + - AirshipFrameworkProxy (= 5.1.1) - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1237,7 +1236,7 @@ PODS: - React-Codegen - React-Core - ReactCommon/turbomodule/core - - react-native-geolocation (3.0.6): + - react-native-geolocation (3.2.1): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2438,8 +2437,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - Airship: 2f4510b497a8200780752a5e0304a9072bfffb6d - AirshipFrameworkProxy: ea1b6c665c798637b93c465b5e505be3011f1d9d + Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 + AirshipFrameworkProxy: 7255f4ed9836dc2920f2f1ea5657ced4cee8a35c AirshipServiceExtension: 0a5fb14c3fd1879355ab05a81d10f64512a4f79c AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 boost: d3f49c53809116a5d38da093a8aa78bf551aed09 @@ -2508,12 +2507,12 @@ SPEC CHECKSUMS: React-jsitracing: e8a2dafb9878dbcad02b6b2b88e66267fb427b74 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad - react-native-airship: 2ed75ff2278f11ff1c1ab08ed68f5bf02727b971 + react-native-airship: 6ab7a7974d53f92b0c46548fc198f797fdbf371f react-native-blob-util: a3ee23cfdde79c769c138d505670055de233b07a react-native-cameraroll: 95ce0d1a7d2d1fe55bf627ab806b64de6c3e69e9 react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 - react-native-geolocation: dcc37809bc117ffdb5946fecc127d62319ccd4a9 + react-native-geolocation: c1c21a8cda4abae6724a322458f64ac6889b8c2b react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: 74d18ad516037536c2f671ef0914bcce7739b2f5 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d diff --git a/package-lock.json b/package-lock.json index 289880189422..203e062de680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.62-6", + "version": "1.4.62-10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.62-6", + "version": "1.4.62-10", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -28,7 +28,7 @@ "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", - "@react-native-community/geolocation": "^3.0.6", + "@react-native-community/geolocation": "3.2.1", "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", @@ -47,7 +47,7 @@ "@storybook/cli": "^8.0.6", "@storybook/react": "^8.0.6", "@storybook/theming": "^8.0.6", - "@ua/react-native-airship": "^15.3.1", + "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", @@ -101,7 +101,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.23", + "react-native-onyx": "2.0.27", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -8790,8 +8790,12 @@ "license": "MIT" }, "node_modules/@react-native-community/geolocation": { - "version": "3.0.6", - "license": "MIT", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.2.1.tgz", + "integrity": "sha512-/+HNzuRl4UCMma7KK+KYL8k2nxAGuW+DGxqmqfpiqKBlCkCUbuFHaZZdqVD6jpsn9r/ghe583ECLmd9SV9I4Bw==", + "engines": { + "node": ">=18.0.0" + }, "peerDependencies": { "react": "*", "react-native": "*" @@ -13336,8 +13340,9 @@ } }, "node_modules/@ua/react-native-airship": { - "version": "15.3.1", - "license": "Apache-2.0", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@ua/react-native-airship/-/react-native-airship-17.2.1.tgz", + "integrity": "sha512-+C5fuPU4MMEpN7I5NbrR8F8awPyaHC732ONxMAZhrjVbfNVuZlpCwptz1xmiRkfiH/nzxhF5uvf+CiOKVYamPQ==", "engines": { "node": ">= 16.0.0" }, @@ -31336,9 +31341,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.23.tgz", - "integrity": "sha512-A0SuipCwAswl8+hBlr9tNPTBdxixKBJkcdP8YpgUC072/4Kcfvv0pfpfQOHoeCR/bP2wvDagpdN3VtebRfoVMg==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.27.tgz", + "integrity": "sha512-mNtXmJ2r7UwEym2J7Tu09M42QoxIhwEdiGYDw9v26wp/kQCJChKTP0yUrp8QdPKkcwywRFPVlNxt3Rx8Mp0hFg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 9e2846f469d9..43a3ed8cae6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.62-6", + "version": "1.4.62-10", "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.", @@ -79,7 +79,7 @@ "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", - "@react-native-community/geolocation": "^3.0.6", + "@react-native-community/geolocation": "3.2.1", "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", @@ -98,7 +98,7 @@ "@storybook/cli": "^8.0.6", "@storybook/react": "^8.0.6", "@storybook/theming": "^8.0.6", - "@ua/react-native-airship": "^15.3.1", + "@ua/react-native-airship": "17.2.1", "@vue/preload-webpack-plugin": "^2.0.0", "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", @@ -152,7 +152,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.23", + "react-native-onyx": "2.0.27", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", diff --git a/patches/@react-native-community+geolocation+3.1.0.patch b/patches/@react-native-community+geolocation+3.1.0.patch deleted file mode 100644 index 5afa0b8e0897..000000000000 --- a/patches/@react-native-community+geolocation+3.1.0.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/node_modules/@react-native-community/geolocation/react-native-geolocation.podspec b/node_modules/@react-native-community/geolocation/react-native-geolocation.podspec -index a319e73..c1ea11c 100644 ---- a/node_modules/@react-native-community/geolocation/react-native-geolocation.podspec -+++ b/node_modules/@react-native-community/geolocation/react-native-geolocation.podspec -@@ -1,8 +1,6 @@ - require 'json' - - package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) --folly_version = '2021.07.22.00' --folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' - - Pod::Spec.new do |s| - s.name = "react-native-geolocation" -@@ -17,20 +15,11 @@ Pod::Spec.new do |s| - s.source = { :git => "https://github.com/react-native-community/react-native-geolocation.git", :tag => "v#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}" - -- s.dependency 'React-Core' - s.frameworks = 'CoreLocation' - -- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then -- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" -- s.pod_target_xcconfig = { -- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", -- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" -- } -- -- s.dependency "React-Codegen" -- s.dependency "RCT-Folly", folly_version -- s.dependency "RCTRequired" -- s.dependency "RCTTypeSafety" -- s.dependency "ReactCommon/turbomodule/core" -+ if defined?(install_modules_dependencies()) != nil -+ install_modules_dependencies(s) -+ else -+ s.dependency "React-Core" - end - end diff --git a/patches/@ua+react-native-airship+15.3.1.patch b/patches/@ua+react-native-airship+15.3.1.patch deleted file mode 100644 index 1ab11c1f7444..000000000000 --- a/patches/@ua+react-native-airship+15.3.1.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/node_modules/@ua/react-native-airship/react-native-airship.podspec b/node_modules/@ua/react-native-airship/react-native-airship.podspec -index 5e0ce2d..4456f61 100644 ---- a/node_modules/@ua/react-native-airship/react-native-airship.podspec -+++ b/node_modules/@ua/react-native-airship/react-native-airship.podspec -@@ -20,18 +20,7 @@ Pod::Spec.new do |s| - - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then -- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" -- s.pod_target_xcconfig = { -- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", -- "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", -- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" -- } -- s.dependency "React-Codegen" -- s.dependency "React-RCTFabric" -- s.dependency "RCT-Folly" -- s.dependency "RCTRequired" -- s.dependency "RCTTypeSafety" -- s.dependency "ReactCommon/turbomodule/core" -+ install_modules_dependencies(s) - end - - diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cda74da86a54..3959f76a626f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -157,7 +157,7 @@ const ONYXKEYS = { FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', /** The NVP with the last distance rate used per policy */ - NVP_LAST_SELECTED_DISTANCE_RATES: 'lastSelectedDistanceRates', + NVP_LAST_SELECTED_DISTANCE_RATES: 'nvp_expensify_lastSelectedDistanceRates', /** The NVP with the last action taken (for the Quick Action Button) */ NVP_QUICK_ACTION_GLOBAL_CREATE: 'nvp_quickActionGlobalCreate', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 49f75de606bc..921eed1fc380 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -352,8 +352,8 @@ const ROUTES = { }, MONEY_REQUEST_STEP_CURRENCY: { route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?', - getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => - getUrlWithBackToParam(`${action}/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}`, backTo), + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}?currency=${currency}`, backTo), }, MONEY_REQUEST_STEP_DATE: { route: ':action/:iouType/date/:transactionID/:reportID', diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 35638a0b604e..ef4d7e3e4064 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -237,7 +237,9 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s const validateAndCompleteAttachmentSelection = useCallback( (fileData: FileResponse) => { - if (fileData.width === -1 || fileData.height === -1) { + // Check if the file dimensions indicate corruption + // The width/height for corrupt file is -1 on android native and 0 on ios native + if (!fileData.width || !fileData.height || (fileData.width <= 0 && fileData.height <= 0)) { showImageCorruptionAlert(); return Promise.resolve(); } @@ -283,16 +285,18 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s }; /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ if (fileDataName && Str.isImage(fileDataName)) { - ImageSize.getSize(fileDataUri).then(({width, height}) => { - fileDataObject.width = width; - fileDataObject.height = height; - validateAndCompleteAttachmentSelection(fileDataObject); - }); + ImageSize.getSize(fileDataUri) + .then(({width, height}) => { + fileDataObject.width = width; + fileDataObject.height = height; + validateAndCompleteAttachmentSelection(fileDataObject); + }) + .catch(() => showImageCorruptionAlert()); } else { return validateAndCompleteAttachmentSelection(fileDataObject); } }, - [validateAndCompleteAttachmentSelection], + [validateAndCompleteAttachmentSelection, showImageCorruptionAlert], ); /** diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx index 5b3442d0e4ef..03a27c88fa68 100644 --- a/src/components/SelectionList/InviteMemberListItem.tsx +++ b/src/components/SelectionList/InviteMemberListItem.tsx @@ -13,9 +13,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {InviteMemberListItemProps} from './types'; +import type {InviteMemberListItemProps, ListItem} from './types'; -function InviteMemberListItem({ +function InviteMemberListItem({ item, isFocused, showTooltip, @@ -26,7 +26,7 @@ function InviteMemberListItem({ onDismissError, shouldPreventDefaultFocusOnSelectRow, rightHandSideComponent, -}: InviteMemberListItemProps) { +}: InviteMemberListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx index 808fa740bfb3..e26926e75e13 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.tsx @@ -4,9 +4,9 @@ import TextWithTooltip from '@components/TextWithTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {RadioListItemProps} from './types'; +import type {ListItem, RadioListItemProps} from './types'; -function RadioListItem({ +function RadioListItem({ item, isFocused, showTooltip, @@ -17,7 +17,7 @@ function RadioListItem({ rightHandSideComponent, isMultilineSupported = false, onFocus, -}: RadioListItemProps) { +}: RadioListItemProps) { const styles = useThemeStyles(); const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text; const indentsLength = (item.text?.length ?? 0) - (fullTitle?.length ?? 0); diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx index cc87d84baf03..c2680c92780a 100644 --- a/src/components/SelectionList/TableListItem.tsx +++ b/src/components/SelectionList/TableListItem.tsx @@ -10,9 +10,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {TableListItemProps} from './types'; +import type {ListItem, TableListItemProps} from './types'; -function TableListItem({ +function TableListItem({ item, isFocused, showTooltip, @@ -24,7 +24,7 @@ function TableListItem({ shouldPreventDefaultFocusOnSelectRow, rightHandSideComponent, onFocus, -}: TableListItemProps) { +}: TableListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index 7ab777e7e0f1..940828ebcac3 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -14,9 +14,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {UserListItemProps} from './types'; +import type {ListItem, UserListItemProps} from './types'; -function UserListItem({ +function UserListItem({ item, isFocused, showTooltip, @@ -27,7 +27,7 @@ function UserListItem({ onDismissError, shouldPreventDefaultFocusOnSelectRow, rightHandSideComponent, -}: UserListItemProps) { +}: UserListItemProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index e15dea542be6..d89f4d5b92f3 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -12,7 +12,7 @@ import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; -type CommonListItemProps = { +type CommonListItemProps = { /** Whether this item is focused (for arrow key controls) */ isFocused?: boolean; @@ -122,9 +122,9 @@ type ListItem = { brickRoadIndicator?: BrickRoad | '' | null; }; -type ListItemProps = CommonListItemProps & { +type ListItemProps = CommonListItemProps & { /** The section list item */ - item: ListItem; + item: TItem; /** Additional styles to apply to text */ style?: StyleProp; @@ -146,10 +146,10 @@ type BaseListItemProps = CommonListItemProps & { errors?: Errors | ReceiptErrors | null; pendingAction?: PendingAction | null; FooterComponent?: ReactElement; - children?: ReactElement | ((hovered: boolean) => ReactElement); + children?: ReactElement> | ((hovered: boolean) => ReactElement>); }; -type UserListItemProps = ListItemProps & { +type UserListItemProps = ListItemProps & { /** Errors that this user may contain */ errors?: Errors | ReceiptErrors | null; @@ -160,11 +160,11 @@ type UserListItemProps = ListItemProps & { FooterComponent?: ReactElement; }; -type InviteMemberListItemProps = UserListItemProps; +type InviteMemberListItemProps = UserListItemProps; -type RadioListItemProps = ListItemProps; +type RadioListItemProps = ListItemProps; -type TableListItemProps = ListItemProps; +type TableListItemProps = ListItemProps; type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem; @@ -294,7 +294,7 @@ type BaseSelectionListProps = Partial & { shouldDelayFocus?: boolean; /** Component to display on the right side of each child */ - rightHandSideComponent?: ((item: ListItem) => ReactElement | null) | ReactElement | null; + rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null; /** Whether to show the loading indicator for new options */ isLoadingNewOptions?: boolean; diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 9380ce43c46a..31cd0aee37da 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -10,6 +10,7 @@ import Hoverable from '@components/Hoverable'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -45,8 +46,18 @@ function BaseVideoPlayer({ isVideoHovered = false, }: VideoPlayerProps) { const styles = useThemeStyles(); - const {pauseVideo, playVideo, currentlyPlayingURL, sharedElement, originalParent, shareVideoPlayerElements, currentVideoPlayerRef, updateCurrentlyPlayingURL, videoResumeTryNumber} = - usePlaybackContext(); + const { + pauseVideo, + playVideo, + currentlyPlayingURL, + sharedElement, + originalParent, + shareVideoPlayerElements, + currentVideoPlayerRef, + updateCurrentlyPlayingURL, + videoResumeTryNumber, + setCurrentlyPlayingURL, + } = usePlaybackContext(); const {isFullScreenRef} = useFullScreenContext(); const {isOffline} = useNetwork(); const [duration, setDuration] = useState(videoDuration * 1000); @@ -67,6 +78,7 @@ function BaseVideoPlayer({ const isCurrentlyURLSet = currentlyPlayingURL === url; const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const videoStateRef = useRef(null); + const {updateVolume} = useVolumeContext(); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; @@ -146,6 +158,16 @@ function BaseVideoPlayer({ if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) { isFullScreenRef.current = false; + + // Sync volume updates in full screen mode after leaving it + currentVideoPlayerRef.current?.getStatusAsync?.().then((status) => { + if (!('isMuted' in status)) { + return; + } + + updateVolume(status.isMuted ? 0 : status.volume || 1); + }); + // we need to use video state ref to check if video is playing, to catch proper state after exiting fullscreen // and also fix a bug with fullscreen mode dismissing when handleFullscreenUpdate function changes if (videoStateRef.current && (!('isPlaying' in videoStateRef.current) || videoStateRef.current.isPlaying)) { @@ -155,7 +177,7 @@ function BaseVideoPlayer({ } } }, - [isFullScreenRef, onFullscreenUpdate, pauseVideo, playVideo, videoResumeTryNumber], + [isFullScreenRef, onFullscreenUpdate, pauseVideo, playVideo, videoResumeTryNumber, updateVolume, currentVideoPlayerRef], ); const bindFunctions = useCallback(() => { @@ -181,6 +203,19 @@ function BaseVideoPlayer({ currentVideoPlayerRef.current = videoPlayerRef.current; }, [url, currentVideoPlayerRef, isUploading]); + const isCurrentlyURLSetRef = useRef(); + isCurrentlyURLSetRef.current = isCurrentlyURLSet; + + useEffect( + () => () => { + if (!isCurrentlyURLSetRef.current) { + return; + } + + setCurrentlyPlayingURL(null); + }, + [setCurrentlyPlayingURL], + ); // update shared video elements useEffect(() => { if (shouldUseSharedVideoElement || url !== currentlyPlayingURL || isFullScreenRef.current) { diff --git a/src/components/VideoPlayerContexts/PlaybackContext.tsx b/src/components/VideoPlayerContexts/PlaybackContext.tsx index 5306dccb565c..0fe0fe378ba6 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext.tsx +++ b/src/components/VideoPlayerContexts/PlaybackContext.tsx @@ -95,12 +95,13 @@ function PlaybackContextProvider({children}: ChildrenProps) { sharedElement, currentVideoPlayerRef, shareVideoPlayerElements, + setCurrentlyPlayingURL, playVideo, pauseVideo, checkVideoPlaying, videoResumeTryNumber, }), - [updateCurrentlyPlayingURL, currentlyPlayingURL, originalParent, sharedElement, shareVideoPlayerElements, playVideo, pauseVideo, checkVideoPlaying], + [updateCurrentlyPlayingURL, currentlyPlayingURL, originalParent, sharedElement, shareVideoPlayerElements, playVideo, pauseVideo, checkVideoPlaying, setCurrentlyPlayingURL], ); return {children}; } diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts index 3dd29795199d..e6a20ec090fe 100644 --- a/src/components/VideoPlayerContexts/types.ts +++ b/src/components/VideoPlayerContexts/types.ts @@ -17,6 +17,7 @@ type PlaybackContext = { playVideo: () => void; pauseVideo: () => void; checkVideoPlaying: (statusCallback: StatusCallback) => void; + setCurrentlyPlayingURL: React.Dispatch>; }; type VolumeContext = { diff --git a/src/hooks/useDisableModalDismissOnEscape.ts b/src/hooks/useDisableModalDismissOnEscape.ts new file mode 100644 index 000000000000..c24323265565 --- /dev/null +++ b/src/hooks/useDisableModalDismissOnEscape.ts @@ -0,0 +1,9 @@ +import {useEffect} from 'react'; +import * as Modal from '@userActions/Modal'; + +export default function useDisableModalDismissOnEscape() { + useEffect(() => { + Modal.setDisableDismissOnEscape(true); + return () => Modal.setDisableDismissOnEscape(false); + }, []); +} diff --git a/src/hooks/useStyledSafeAreaInsets.ts b/src/hooks/useStyledSafeAreaInsets.ts new file mode 100644 index 000000000000..bfd9c32a46ae --- /dev/null +++ b/src/hooks/useStyledSafeAreaInsets.ts @@ -0,0 +1,37 @@ +// eslint-disable-next-line no-restricted-imports +import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import useStyleUtils from './useStyleUtils'; + +/** + * Custom hook to get the styled safe area insets. + * This hook utilizes the `SafeAreaInsetsContext` to retrieve the current safe area insets + * and applies styling adjustments using the `useStyleUtils` hook. + * + * @returns An object containing the styled safe area insets and additional styles. + * @returns .paddingTop The top padding adjusted for safe area. + * @returns .paddingBottom The bottom padding adjusted for safe area. + * @returns .insets The safe area insets object or undefined if not available. + * @returns .safeAreaPaddingBottomStyle An object containing the bottom padding style adjusted for safe area. + * + * @example + * // How to use this hook in a component + * function MyComponent() { + * const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets(); + * + * // Use these values to style your component accordingly + * } + */ +function useStyledSafeAreaInsets() { + const StyleUtils = useStyleUtils(); + const insets = useSafeAreaInsets(); + + const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets ?? undefined); + return { + paddingTop, + paddingBottom, + insets: insets ?? undefined, + safeAreaPaddingBottomStyle: {paddingBottom}, + }; +} + +export default useStyledSafeAreaInsets; diff --git a/src/hooks/useViewportOffsetTop/index.ts b/src/hooks/useViewportOffsetTop/index.ts index da2325a7e13f..6f617aa38121 100644 --- a/src/hooks/useViewportOffsetTop/index.ts +++ b/src/hooks/useViewportOffsetTop/index.ts @@ -18,7 +18,7 @@ export default function useViewportOffsetTop(shouldAdjustScrollView = false): nu if (Browser.isMobileSafari() && shouldAdjustScrollView && window.visualViewport) { const clientHeight = document.body.clientHeight; - const adjustScrollY = Math.round(clientHeight - window.visualViewport.height); + const adjustScrollY = clientHeight - window.visualViewport.height; if (cachedDefaultOffsetTop.current === 0) { cachedDefaultOffsetTop.current = targetOffsetTop; } @@ -43,7 +43,7 @@ export default function useViewportOffsetTop(shouldAdjustScrollView = false): nu if (!shouldAdjustScrollView) { return; } - window.scrollTo({top: viewportOffsetTop, behavior: 'instant'}); + window.scrollTo({top: viewportOffsetTop, behavior: 'smooth'}); }, [shouldAdjustScrollView, viewportOffsetTop]); return viewportOffsetTop; diff --git a/src/languages/en.ts b/src/languages/en.ts index 7fc9b0b0ad26..9451407c822f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -593,18 +593,18 @@ export default { addReceipt: 'Add receipt', }, quickAction: { - scanReceipt: 'Scan Receipt', - recordDistance: 'Record Distance', - requestMoney: 'Request Money', - splitBill: 'Split Bill', - splitScan: 'Split Receipt', - splitDistance: 'Split Distance', - trackManual: 'Track Expense', - trackScan: 'Track Receipt', - trackDistance: 'Track Distance', - sendMoney: 'Send Money', - assignTask: 'Assign Task', - shortcut: 'Shortcut', + scanReceipt: 'Scan receipt', + recordDistance: 'Record distance', + requestMoney: 'Request money', + splitBill: 'Split bill', + splitScan: 'Split receipt', + splitDistance: 'Split distance', + sendMoney: 'Send money', + assignTask: 'Assign task', + header: 'Quick action', + trackManual: 'Track manual', + trackScan: 'Track scan', + trackDistance: 'Track distance', }, iou: { amount: 'Amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index e86e1fc5f62d..a56c8ac2739d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -589,18 +589,18 @@ export default { addReceipt: 'Añadir recibo', }, quickAction: { - scanReceipt: 'Escanear Recibo', - recordDistance: 'Grabar Distancia', - requestMoney: 'Solicitar Dinero', - splitBill: 'Dividir Cuenta', - splitScan: 'Dividir Recibo', - splitDistance: 'Dividir Distancia', - sendMoney: 'Enviar Dinero', - assignTask: 'Assignar Tarea', - shortcut: 'Acceso Directo', - trackManual: 'Crear Gasto', - trackScan: 'Crear Recibo', - trackDistance: 'Crear Gasto por desplazamiento', + scanReceipt: 'Escanear recibo', + recordDistance: 'Grabar distancia', + requestMoney: 'Solicitar dinero', + splitBill: 'Dividir cuenta', + splitScan: 'Dividir recibo', + splitDistance: 'Dividir distancia', + sendMoney: 'Enviar dinero', + assignTask: 'Assignar tarea', + header: 'Acción rápida', + trackManual: 'Crear gasto', + trackScan: 'Crear gasto por recibo', + trackDistance: 'Crear gasto por desplazamiento', }, iou: { amount: 'Importe', diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index ffac214c154d..3407656c5e94 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -36,6 +36,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = ], [SCREENS.SETTINGS.SECURITY]: [SCREENS.SETTINGS.TWO_FACTOR_AUTH, SCREENS.SETTINGS.CLOSE], [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS], + [SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER], [SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE], }; diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts index a349a3425e29..747f6e38968b 100755 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -10,6 +10,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = { SCREENS.SETTINGS.WALLET.ROOT, SCREENS.SETTINGS.ABOUT, SCREENS.SETTINGS.WORKSPACES, + SCREENS.SETTINGS.SAVE_THE_WORLD, SCREENS.SETTINGS.TROUBLESHOOT, ], }; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b8e09af02d7a..936a5362044e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -398,6 +398,7 @@ type MoneyRequestNavigatorParamList = { transactionID: string; reportID: string; backTo: Routes; + currency?: string; }; [SCREENS.MONEY_REQUEST.STEP_TAG]: { action: ValueOf; @@ -459,6 +460,7 @@ type MoneyRequestNavigatorParamList = { // for IOURequestStepDistance and IOURequestStepAmount components backTo: never; action: never; + currency: never; }; [SCREENS.MONEY_REQUEST.START]: { iouType: ValueOf; @@ -472,6 +474,7 @@ type MoneyRequestNavigatorParamList = { transactionID: string; backTo: Routes; action: ValueOf; + currency?: string; }; [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: { iouType: ValueOf; @@ -501,6 +504,7 @@ type MoneyRequestNavigatorParamList = { reportID: string; pageIndex?: string; backTo?: Routes; + currency?: string; }; }; diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 4e028ad82392..d45d076a8adb 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -64,7 +64,7 @@ function pushNotificationEventCallback(eventType: EventType, notification: PushP */ function refreshNotificationOptInStatus() { Airship.push.getNotificationStatus().then((notificationStatus) => { - const isOptedIn = notificationStatus.airshipOptIn && notificationStatus.systemEnabled; + const isOptedIn = notificationStatus.isOptedIn && notificationStatus.areNotificationsAllowed; if (isOptedIn === isUserOptedInToPushNotifications) { return; } @@ -94,7 +94,7 @@ const init: Init = () => { }); // Keep track of which users have enabled push notifications via an NVP. - Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus); + Airship.addListener(EventType.PushNotificationStatusChangedStatus, refreshNotificationOptInStatus); ForegroundNotifications.configureForegroundNotifications(); }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index b2e243053bbc..aa16d7b2dc5a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2220,6 +2220,18 @@ function formatSectionsFromSearchTerm( }; } +/** + * Helper method to get the `keyForList` for the first option in the OptionsList + */ +function getFirstKeyForList(data?: Option[] | null) { + if (!data?.length) { + return ''; + } + + const firstNonEmptyDataObj = data[0]; + + return firstNonEmptyDataObj.keyForList ? firstNonEmptyDataObj.keyForList : ''; +} /** * Filters options based on the search input value */ @@ -2341,6 +2353,7 @@ export { createOptionFromReport, getReportOption, getTaxRatesSection, + getFirstKeyForList, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e339dce5b81b..e8f2189f5f7d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -191,7 +191,7 @@ function isThreadParentMessage(reportAction: OnyxEntry, reportID: * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getParentReportAction(report: OnyxEntry | EmptyObject): ReportAction | Record { +function getParentReportAction(report: OnyxEntry | EmptyObject): ReportAction | EmptyObject { if (!report?.parentReportID || !report.parentReportActionID) { return {}; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1b60cd611d57..f7b160bd67e2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2925,6 +2925,22 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco return roomName ? `${verb} ${users} ${preposition} ${roomName}` : `${verb} ${users}`; } +/** + * Get the report action message for a report action. + */ +function getReportActionMessage(reportAction: ReportAction | EmptyObject, parentReportID?: string) { + if (isEmptyObject(reportAction)) { + return ''; + } + if (ReportActionsUtils.isApprovedOrSubmittedReportAction(reportAction)) { + return ReportActionsUtils.getReportActionMessageText(reportAction); + } + if (ReportActionsUtils.isReimbursementQueuedAction(reportAction)) { + return getReimbursementQueuedActionMessage(reportAction, getReport(parentReportID), false); + } + return reportAction?.message?.[0]?.text ?? ''; +} + /** * Get the title for a report. */ @@ -2945,11 +2961,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); - const parentReportActionMessage = ( - ReportActionsUtils.isApprovedOrSubmittedReportAction(parentReportAction) - ? ReportActionsUtils.getReportActionMessageText(parentReportAction) - : parentReportAction?.message?.[0]?.text ?? '' - ).replace(/(\r\n|\n|\r)/gm, ' '); + const parentReportActionMessage = getReportActionMessage(parentReportAction, report?.parentReportID).replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cd0264ddb6ea..85f4b74f3436 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -371,11 +371,7 @@ function startMoneyRequest(iouType: ValueOf, reportID: st } // eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amount: number, currency: string, removeOriginalCurrency = false) { - if (removeOriginalCurrency) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency, originalCurrency: null}); - return; - } +function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amount: number, currency: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency}); } @@ -385,20 +381,11 @@ function setMoneyRequestCreated(transactionID: string, created: string, isDraft: } // eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestCurrency_temporaryForRefactor(transactionID: string, currency: string, removeOriginalCurrency = false, isEditing = false) { +function setMoneyRequestCurrency_temporaryForRefactor(transactionID: string, currency: string, isEditing = false) { const fieldToUpdate = isEditing ? 'modifiedCurrency' : 'currency'; - if (removeOriginalCurrency) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {[fieldToUpdate]: currency, originalCurrency: null}); - return; - } Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {[fieldToUpdate]: currency}); } -// eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestOriginalCurrency_temporaryForRefactor(transactionID: string, originalCurrency: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {originalCurrency}); -} - function setMoneyRequestDescription(transactionID: string, comment: string, isDraft: boolean) { Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {comment: {comment: comment.trim()}}); } @@ -5567,7 +5554,6 @@ export { setMoneyRequestCreated, setMoneyRequestCurrency_temporaryForRefactor, setMoneyRequestDescription, - setMoneyRequestOriginalCurrency_temporaryForRefactor, setMoneyRequestParticipants_temporaryForRefactor, setMoneyRequestPendingFields, setMoneyRequestReceipt, diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 6351d0165544..7de4548b92c9 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -69,6 +69,13 @@ function setModalVisibility(isVisible: boolean) { Onyx.merge(ONYXKEYS.MODAL, {isVisible}); } +/** + * Allows other parts of the app to set whether modals should be dismissable using the Escape key + */ +function setDisableDismissOnEscape(disableDismissOnEscape: boolean) { + Onyx.merge(ONYXKEYS.MODAL, {disableDismissOnEscape}); +} + /** * Allows other parts of app to know that an alert modal is about to open. * This will trigger as soon as a modal is opened but not yet visible while animation is running. @@ -78,4 +85,4 @@ function willAlertModalBecomeVisible(isVisible: boolean, isPopover = false) { Onyx.merge(ONYXKEYS.MODAL, {willAlertModalBecomeVisible: isVisible, isPopover}); } -export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, closeTop}; +export {setCloseModal, close, onModalDidClose, setModalVisibility, willAlertModalBecomeVisible, setDisableDismissOnEscape, closeTop}; diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index fc72733b190c..87762bc856ca 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -9,7 +9,6 @@ let hasSelectedPurpose: boolean | undefined; let hasProvidedPersonalDetails: boolean | undefined; let isFirstTimeNewExpensifyUser: boolean | undefined; let hasDismissedModal: boolean | undefined; -let hasSelectedChoice: boolean | undefined; let isLoadingReportData = true; type DetermineOnboardingStatusProps = { @@ -89,7 +88,7 @@ function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOn * - Whether we have loaded all reports the server knows about */ function checkOnReady() { - const hasRequiredOnyxKeysBeenLoaded = [isFirstTimeNewExpensifyUser, hasSelectedChoice, hasDismissedModal].every((value) => value !== undefined); + const hasRequiredOnyxKeysBeenLoaded = [isFirstTimeNewExpensifyUser, hasDismissedModal].every((value) => value !== undefined); if (isLoadingReportData || !hasRequiredOnyxKeysBeenLoaded) { return; diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 06bd47f3b39b..d9893d93d2f6 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -1,4 +1,7 @@ +import Str from 'expensify-common/lib/str'; import {Alert, Linking, Platform} from 'react-native'; +import ImageSize from 'react-native-image-size'; +import type {FileObject} from '@components/AttachmentModal'; import DateUtils from '@libs/DateUtils'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; @@ -238,6 +241,17 @@ function base64ToFile(base64: string, filename: string): File { return file; } +function validateImageForCorruption(file: FileObject): Promise { + if (!Str.isImage(file.name ?? '')) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + ImageSize.getSize(file.uri ?? '') + .then(() => resolve()) + .catch(() => reject(new Error('Error reading file: The file is corrupted'))); + }); +} + export { showGeneralErrorAlert, showSuccessAlert, @@ -250,4 +264,5 @@ export { appendTimeToFileName, readFileAsync, base64ToFile, + validateImageForCorruption, }; diff --git a/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx b/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx index 52b0617361f9..4c006abacfc7 100644 --- a/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx +++ b/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx @@ -1,17 +1,9 @@ import React from 'react'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; -import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; function ChatFinderPageFooter() { - const themeStyles = useThemeStyles(); - - return ( - - ); + return ; } ChatFinderPageFooter.displayName = 'ChatFinderPageFooter'; diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index eac69b8af644..50228f1c23aa 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -54,9 +54,9 @@ const ChatFinderPageFooterInstance = ; function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); + const themeStyles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const themeStyles = useThemeStyles(); const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: isScreenTransitionEnd, }); diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 7711501ccde3..8137bb0e8515 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,20 +1,27 @@ +import isEmpty from 'lodash/isEmpty'; +import reject from 'lodash/reject'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import {useOptionsList} from '@components/OptionListContextProvider'; -import OptionsSelector from '@components/OptionsSelector'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import {PressableWithFeedback} from '@components/Pressable'; +import ReferralProgramCTA from '@components/ReferralProgramCTA'; +import SelectCircle from '@components/SelectCircle'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import doInteractionTask from '@libs/DoInteractionTask'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -32,268 +39,279 @@ type NewChatPageProps = { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -function NewChatPage({isGroupChat}: NewChatPageProps) { - const [dismissedReferralBanners] = useOnyx(ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS); +function useOptions({isGroupChat}: NewChatPageProps) { + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [selectedOptions, setSelectedOptions] = useState>([]); + const [betas] = useOnyx(ONYXKEYS.BETAS); const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT); + const personalData = useCurrentUserPersonalDetails(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + const {options: listOptions, areOptionsInitialized} = useOptionsList({ + shouldInitialize: didScreenTransitionEnd, + }); - const {translate} = useLocalize(); + const options = useMemo(() => { + const filteredOptions = OptionsListUtils.getFilteredOptions( + listOptions.reports ?? [], + listOptions.personalDetails ?? [], + betas ?? [], + debouncedSearchTerm, + selectedOptions, + isGroupChat ? excludedGroupEmails : [], + false, + true, + false, + {}, + [], + false, + {}, + [], + true, + ); + const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; + + const headerMessage = OptionsListUtils.getHeaderMessage( + filteredOptions.personalDetails.length + filteredOptions.recentReports.length !== 0, + Boolean(filteredOptions.userToInvite), + debouncedSearchTerm.trim(), + maxParticipantsReached, + selectedOptions.some((participant) => participant?.searchText?.toLowerCase?.().includes(debouncedSearchTerm.trim().toLowerCase())), + ); + return {...filteredOptions, headerMessage, maxParticipantsReached}; + }, [betas, debouncedSearchTerm, isGroupChat, listOptions.personalDetails, listOptions.reports, selectedOptions]); - const styles = useThemeStyles(); + useEffect(() => { + if (!debouncedSearchTerm.length || options.maxParticipantsReached) { + return; + } - const personalData = useCurrentUserPersonalDetails(); + Report.searchInServer(debouncedSearchTerm); + }, [debouncedSearchTerm, options.maxParticipantsReached]); - const getGroupParticipants = () => { + useEffect(() => { if (!newGroupDraft?.participants) { - return []; + return; } const selectedParticipants = newGroupDraft.participants.filter((participant) => participant.accountID !== personalData.accountID); const newSelectedOptions = selectedParticipants.map((participant): OptionData => { const baseOption = OptionsListUtils.getParticipantsOption({accountID: participant.accountID, login: participant.login, reportID: ''}, personalDetails); - return {...baseOption, reportID: baseOption.reportID ?? ''}; + return {...baseOption, reportID: baseOption.reportID ?? '', isSelected: true}; }); - return newSelectedOptions; - }; - - const [searchTerm, setSearchTerm] = useState(''); - const [filteredRecentReports, setFilteredRecentReports] = useState([]); - const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); - const [filteredUserToInvite, setFilteredUserToInvite] = useState(); - const [selectedOptions, setSelectedOptions] = useState(getGroupParticipants); + setSelectedOptions(newSelectedOptions); + }, [newGroupDraft, personalData, personalDetails]); + + return {...options, searchTerm, debouncedSearchTerm, setSearchTerm, areOptionsInitialized: areOptionsInitialized && didScreenTransitionEnd, selectedOptions, setSelectedOptions}; +} + +function NewChatPage({isGroupChat}: NewChatPageProps) { + const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: didScreenTransitionEnd, - }); - - const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; - const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); + const styles = useThemeStyles(); + const personalData = useCurrentUserPersonalDetails(); + const {insets} = useStyledSafeAreaInsets(); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); - const headerMessage = OptionsListUtils.getHeaderMessage( - filteredPersonalDetails.length + filteredRecentReports.length !== 0, - Boolean(filteredUserToInvite), - searchTerm.trim(), + const { + headerMessage, maxParticipantsReached, - selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())), - ); + searchTerm, + debouncedSearchTerm, + setSearchTerm, + selectedOptions, + setSelectedOptions, + recentReports, + personalDetails, + userToInvite, + areOptionsInitialized, + } = useOptions({ + isGroupChat, + }); - const sections = useMemo((): OptionsListUtils.CategorySection[] => { + const [sections, firstKeyForList] = useMemo(() => { const sectionsList: OptionsListUtils.CategorySection[] = []; + let firstKey = ''; - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions, recentReports, personalDetails, maxParticipantsReached); sectionsList.push(formatResults.section); + if (!firstKey) { + firstKey = OptionsListUtils.getFirstKeyForList(formatResults.section.data); + } + if (maxParticipantsReached) { - return sectionsList; + return [sectionsList, firstKey]; } sectionsList.push({ title: translate('common.recents'), - data: filteredRecentReports, - shouldShow: filteredRecentReports.length > 0, + data: recentReports, + shouldShow: !isEmpty(recentReports), }); + if (!firstKey) { + firstKey = OptionsListUtils.getFirstKeyForList(recentReports); + } sectionsList.push({ title: translate('common.contacts'), - data: filteredPersonalDetails, - shouldShow: filteredPersonalDetails.length > 0, + data: personalDetails, + shouldShow: !isEmpty(personalDetails), }); + if (!firstKey) { + firstKey = OptionsListUtils.getFirstKeyForList(personalDetails); + } - if (filteredUserToInvite) { + if (userToInvite) { sectionsList.push({ title: undefined, - data: [filteredUserToInvite], + data: [userToInvite], shouldShow: true, }); + if (!firstKey) { + firstKey = OptionsListUtils.getFirstKeyForList([userToInvite]); + } } - return sectionsList; - }, [translate, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, maxParticipantsReached, selectedOptions, searchTerm]); - - /** - * Removes a selected option from list if already selected. If not already selected add this option to the list. - */ - const toggleOption = (option: OptionData) => { - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - - let newSelectedOptions; - - if (isOptionInList) { - newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); - } else { - newSelectedOptions = [...selectedOptions, option]; - } - - const { - recentReports, - personalDetails: newChatPersonalDetails, - userToInvite, - } = OptionsListUtils.getFilteredOptions( - options.reports ?? [], - options.personalDetails ?? [], - betas ?? [], - searchTerm, - newSelectedOptions, - isGroupChat ? excludedGroupEmails : [], - false, - true, - false, - {}, - [], - false, - {}, - [], - true, - ); - setSelectedOptions(newSelectedOptions); - setFilteredRecentReports(recentReports); - setFilteredPersonalDetails(newChatPersonalDetails); - setFilteredUserToInvite(userToInvite); - }; + return [sectionsList, firstKey]; + }, [debouncedSearchTerm, selectedOptions, recentReports, personalDetails, maxParticipantsReached, translate, userToInvite]); /** * Creates a new 1:1 chat with the option and the current user, * or navigates to the existing chat if one with those participants already exists. */ - const createChat = (option: OptionData) => { - let login = ''; + const createChat = useCallback( + (option?: OptionsListUtils.Option) => { + let login = ''; + + if (option?.login) { + login = option.login; + } else if (selectedOptions.length === 1) { + login = selectedOptions[0].login ?? ''; + } + if (!login) { + Log.warn('Tried to create chat with empty login'); + return; + } + Report.navigateToAndOpenReport([login]); + }, + [selectedOptions], + ); - if (option.login) { - login = option.login; - } else if (selectedOptions.length === 1) { - login = selectedOptions[0].login ?? ''; - } + const itemRightSideComponent = useCallback( + (item: ListItem & OptionsListUtils.Option) => { + /** + * Removes a selected option from list if already selected. If not already selected add this option to the list. + * @param option + */ + function toggleOption(option: ListItem & Partial) { + const isOptionInList = !!option.isSelected; - if (!login) { - Log.warn('Tried to create chat with empty login'); - return; - } + let newSelectedOptions; - Report.navigateToAndOpenReport([login]); - }; - /** - * Navigates to create group confirm page - */ - const navigateToConfirmPage = () => { - if (!personalData || !personalData.login || !personalData.accountID) { - return; - } - const selectedParticipants: SelectedParticipant[] = selectedOptions.map((option: OptionData) => ({login: option.login ?? '', accountID: option.accountID ?? -1})); - const logins = [...selectedParticipants, {login: personalData.login, accountID: personalData.accountID}]; - Report.setGroupDraft({participants: logins}); - Navigation.navigate(ROUTES.NEW_CHAT_CONFIRM); - }; - - const updateOptions = useCallback(() => { - const { - recentReports, - personalDetails: newChatPersonalDetails, - userToInvite, - } = OptionsListUtils.getFilteredOptions( - options.reports ?? [], - options.personalDetails ?? [], - betas ?? [], - searchTerm, - selectedOptions, - isGroupChat ? excludedGroupEmails : [], - false, - true, - false, - {}, - [], - false, - {}, - [], - true, - ); + if (isOptionInList) { + newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); + } else { + newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID ?? ''}]; + } - setFilteredRecentReports(recentReports); - setFilteredPersonalDetails(newChatPersonalDetails); - setFilteredUserToInvite(userToInvite); - // props.betas is not added as dependency since it doesn't change during the component lifecycle - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [options, searchTerm]); + setSelectedOptions(newSelectedOptions); + } - useEffect(() => { - const interactionTask = doInteractionTask(() => { - setDidScreenTransitionEnd(true); - }); + if (item.isSelected) { + return ( + toggleOption(item)} + disabled={item.isDisabled} + role={CONST.ACCESSIBILITY_ROLE.CHECKBOX} + accessibilityLabel={CONST.ACCESSIBILITY_ROLE.CHECKBOX} + style={[styles.flexRow, styles.alignItemsCenter, styles.ml3]} + > + + + ); + } + + return ( +