diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index 1cb7934cfb9..d84c6df1a0d 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12738,6 +12738,10 @@ function promiseWhile(condition, action) { else { const actionResult = action?.(); console.info('[promiseWhile] promiseWhile() actionResult', actionResult); + if (!actionResult) { + resolve(); + return; + } Promise.resolve(actionResult).then(loop).catch(reject); } }; diff --git a/.github/libs/promiseWhile.ts b/.github/libs/promiseWhile.ts index 2fc53c34fec..01c061096d6 100644 --- a/.github/libs/promiseWhile.ts +++ b/.github/libs/promiseWhile.ts @@ -13,6 +13,12 @@ function promiseWhile(condition: () => boolean, action: (() => Promise) | } else { const actionResult = action?.(); console.info('[promiseWhile] promiseWhile() actionResult', actionResult); + + if (!actionResult) { + resolve(); + return; + } + Promise.resolve(actionResult).then(loop).catch(reject); } }; diff --git a/__mocks__/react-native-onyx.ts b/__mocks__/react-native-onyx.ts index 253e3db47a9..9c6a36380c0 100644 --- a/__mocks__/react-native-onyx.ts +++ b/__mocks__/react-native-onyx.ts @@ -5,7 +5,7 @@ /* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ import type {ConnectOptions, OnyxKey} from 'react-native-onyx'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; let connectCallbackDelay = 0; function addDelayToConnectCallback(delay: number) { @@ -40,4 +40,4 @@ const reactNativeOnyxMock: ReactNativeOnyxMock = { }; export default reactNativeOnyxMock; -export {withOnyx}; +export {withOnyx, useOnyx}; diff --git a/android/app/build.gradle b/android/app/build.gradle index aaf2dc81b3f..1b651bd29de 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 1001046204 - versionName "1.4.62-4" + versionCode 1001046206 + versionName "1.4.62-6" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/simple-illustrations/simple-illustration__lightbulb.svg b/assets/images/simple-illustrations/simple-illustration__lightbulb.svg new file mode 100644 index 00000000000..1dc35976414 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__lightbulb.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + 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 00000000000..0e195d5e3f1 --- /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/bank-accounts-and-payments/Global-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md new file mode 100644 index 00000000000..2ff74760b37 --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md @@ -0,0 +1,106 @@ +--- +title: International Reimbursements +description: International Reimbursements +--- +# Overview + +If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you now have the option to send direct reimbursements to nearly any country across the globe! +The process to enable global reimbursements is dependent on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below. + +# How to request international reimbursements + +## The reimbursement account is in USD + +If your reimbursement bank account is in USD, the first step is connecting the bank account to Expensify. +The individual who plans on sending reimbursements internationally should head to **Settings > Account > Payments > Add Verified Bank Account**. From there, you will provide company details, input personal information, and upload a copy of your ID. + +Once the USD bank account is verified (or if you already had a USD business bank account connected), click the support icon in your Expensify account to inform your Setup Specialist, Account Manager, or Concierge that you’d like to enable international reimbursements. From there, Expensify will ask you to confirm the currencies of the reimbursement and employee bank accounts. + +Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. + +## The reimbursement account is in AUD, CAD, GBP, EUR + +To request international reimbursements, contact Expensify Support to make that request. + +You can do this by clicking on the support icon and informing your Setup Specialist, Account Manager, or Concierge that you’d like to set up global reimbursements on your account. +From there, Expensify will ask you to confirm both the currencies of the reimbursement and employee bank accounts. + +Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. + +# How to verify the bank account for sending international payments + +Once international payments are enabled on your Expensify account, the next step is verifying the bank account to send the reimbursements. + +## The reimbursement account is in USD + +First, confirm the workspace settings are set up correctly by doing the following: +1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reports** and check that the workspace currency is USD +2. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the reimbursement method to direct +3. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the USD bank account to the default account + +Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account). + +From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. + +## The reimbursement account is in AUD, CAD, GBP, EUR + +First, confirm the workspace currency corresponds with the currency of the reimbursement bank account. You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reports**. It should be AUD, CAD, GBP, or EUR. + +Next, add the bank account to Expensify: +1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** and set the reimbursement method to direct (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account) +2. Click **Add Business Bank Account** +3. If the incorrect country shows as the default, click **Switch Country** to select the correct country +4. Enter the bank account details +5. Click **Save & Continue** + +From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. + +# How to start reimbursing internationally + +After the bank account is verified for international payments, set the correct bank account as the reimbursement account. + +You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** by selecting the reimbursement account as the default account. + +Finally, have your employees add their deposit-only bank accounts. They can do this by logging into their Expensify accounts, heading to **Settings > Account > Payments**, and clicking **Add Deposit-Only Bank Account**. + +# Deep Dive + +## Documents requested + +Our Compliance Team may ask for additional information depending on who initiates the verification or what information you provide on the DocuSign form. + +Examples of additional requested information: +- The reimburser’s proof of address and ID +- Company directors’ proofs of address and IDs +- An authorization letter +- An independently certified documentation such as shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company + +{% include faq-begin.md %} + +## How many people can send reimbursements internationally? + +Once your company is authorized to send global payments, the individual who verified the bank account can share it with additional admins on the workspace. That way, multiple workspace members can send international reimbursements. + +## How long does it take to verify an account for international payments? + +It varies! The verification process can take a few business days to several weeks. It depends on whether or not the information in the DocuSign form is correct if our Compliance Team requires any additional information, and how responsive the employee verifying the company’s details is to our requests. + +## If I already have a USD bank account connected to Expensify, do I need to go through the verification process again to enable international payments? + +If you’ve already connected a US business bank account, you can request to enable global reimbursements by contacting Expensify Support immediately. However, additional steps are required to verify the bank account for international payments. + +## My employee says they don’t have the option to add their non-USD bank account as a deposit account – what should they do? + +Have the employee double-check that their default workspace is set as the workspace that's connected to the bank you're using to send international payments. + +An employee can confirm their default workspace is under **Settings > Workspaces > Group**. The default workspace has a green checkmark next to it. They can change their default workspace by clicking **Default Workspace** on the correct workspace. + +## Who is the “Authorized User” on the International Reimbursement DocuSign form? + +This is the person who will process international reimbursements. The authorized user should be the same person who manages the bank account connection in Expensify. + +## Who should I enter as the “User” on the International Reimbursement form? + +You can leave this form section blank since the “User” is Expensify. + +{% include faq-end.md %} diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 9d09b4dc04f..7f50db5da85 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 8b25084df43..f0bb2ace4f4 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.62.4 + 1.4.62.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 96d8c25d570..b8c2140b854 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.62.4 + 1.4.62.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 0781526824b..0d7c6d12f53 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.62 CFBundleVersion - 1.4.62.4 + 1.4.62.6 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f9244c515a2..634aeca6465 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) @@ -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,7 +2507,7 @@ 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 diff --git a/package-lock.json b/package-lock.json index 439b14837b1..374d08c69bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.62-4", + "version": "1.4.62-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.62-4", + "version": "1.4.62-6", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -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", @@ -13336,8 +13336,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" }, diff --git a/package.json b/package.json index 34a15e9e540..f04895c3e99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.62-4", + "version": "1.4.62-6", "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.", @@ -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", 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 1ab11c1f744..00000000000 --- 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/CONST.ts b/src/CONST.ts index 39c3d9d3109..74e722cdba5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -683,6 +683,7 @@ const CONST = { UNAPPROVED: 'UNAPPROVED', // OldDot Action UNHOLD: 'UNHOLD', UNSHARE: 'UNSHARE', // OldDot Action + UPDATEGROUPCHATMEMBERROLE: 'UPDATEGROUPCHATMEMBERROLE', POLICYCHANGELOG: { ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE', ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET', @@ -749,6 +750,7 @@ const CONST = { UPDATE_TAG_NAME: 'POLICYCHANGELOG_UPDATE_TAG_NAME', UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED', UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE', + LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY', }, ROOMCHANGELOG: { INVITE_TO_ROOM: 'INVITETOROOM', @@ -1862,17 +1864,17 @@ const CONST = { MAX_THREAD_REPLIES_PREVIEW: 99, + // Character Limits FORM_CHARACTER_LIMIT: 50, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, - TAG_NAME_LIMIT: 256, - + REPORT_NAME_LIMIT: 256, TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, - WORKSPACE_NAME_CHARACTER_LIMIT: 80, + AVATAR_CROP_MODAL: { // The next two constants control what is min and max value of the image crop scale. // Values define in how many times the image can be bigger than its container. @@ -4313,6 +4315,9 @@ const CONST = { SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', }, + + DOT_SEPARATOR: '•', + DEFAULT_TAX: { defaultExternalID: 'id_TAX_EXEMPT', defaultValue: '0%', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1eafd9d898e..cda74da86a5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -290,6 +290,9 @@ const ONYXKEYS = { /** Indicates whether an forced upgrade is required */ UPDATE_REQUIRED: 'updateRequired', + /** Indicates whether an forced reset is required. Used in emergency situations where we must completely erase the Onyx data in the client because it is in a bad state. This will clear Oynx data without signing the user out. */ + RESET_REQUIRED: 'resetRequired', + /** Stores the logs of the app for debugging purposes */ LOGS: 'logs', @@ -453,6 +456,8 @@ const ONYXKEYS = { WORKSPACE_TAX_NAME_FORM_DRAFT: 'workspaceTaxNameFormDraft', WORKSPACE_TAX_VALUE_FORM: 'workspaceTaxValueForm', WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft', + NEW_CHAT_NAME_FORM: 'newChatNameForm', + NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', }, } as const; @@ -508,6 +513,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; + [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; }; type OnyxFormDraftValuesMapping = { @@ -642,6 +648,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.UPDATE_REQUIRED]: boolean; + [ONYXKEYS.RESET_REQUIRED]: boolean; [ONYXKEYS.PLAID_CURRENT_EVENT]: string; [ONYXKEYS.LOGS]: OnyxTypes.CapturedLogs; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7fee9b5497c..8e4c84f2496 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -190,6 +190,7 @@ const ROUTES = { NEW: 'new', NEW_CHAT: 'new/chat', NEW_CHAT_CONFIRM: 'new/chat/confirm', + NEW_CHAT_EDIT_NAME: 'new/chat/confirm/name/edit', NEW_ROOM: 'new/room', REPORT: 'r', @@ -226,6 +227,18 @@ const ROUTES = { route: 'r/:reportID/participants', getRoute: (reportID: string) => `r/${reportID}/participants` as const, }, + REPORT_PARTICIPANTS_INVITE: { + route: 'r/:reportID/participants/invite', + getRoute: (reportID: string) => `r/${reportID}/participants/invite` as const, + }, + REPORT_PARTICIPANTS_DETAILS: { + route: 'r/:reportID/participants/:accountID', + getRoute: (reportID: string, accountID: number) => `r/${reportID}/participants/${accountID}` as const, + }, + REPORT_PARTICIPANTS_ROLE_SELECTION: { + route: 'r/:reportID/participants/:accountID/role', + getRoute: (reportID: string, accountID: number) => `r/${reportID}/participants/${accountID}/role` as const, + }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), @@ -238,6 +251,10 @@ const ROUTES = { route: 'r/:reportID/settings/room-name', getRoute: (reportID: string) => `r/${reportID}/settings/room-name` as const, }, + REPORT_SETTINGS_GROUP_NAME: { + route: 'r/:reportID/settings/group-name', + getRoute: (reportID: string) => `r/${reportID}/settings/group-name` as const, + }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences` as const, @@ -382,7 +399,7 @@ const ROUTES = { }, MONEY_REQUEST_STEP_WAYPOINT: { route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', - getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID?: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`${action}/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo), }, // This URL is used as a redirect to one of the create tabs below. This is so that we can message users with a link diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b3c2012e90d..96372d5bbab 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -172,6 +172,7 @@ const SCREENS = { REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', ROOM_NAME: 'Report_Settings_Room_Name', + GROUP_NAME: 'Report_Settings_Group_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', VISIBILITY: 'Report_Settings_Visibility', @@ -273,6 +274,7 @@ const SCREENS = { ROOT: 'NewChat_Root', NEW_CHAT: 'chat', NEW_CHAT_CONFIRM: 'NewChat_Confirm', + NEW_CHAT_EDIT_NAME: 'NewChat_Edit_Name', NEW_ROOM: 'room', }, @@ -309,7 +311,12 @@ const SCREENS = { PROFILE_ROOT: 'Profile_Root', PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root', REPORT_DESCRIPTION_ROOT: 'Report_Description_Root', - REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root', + REPORT_PARTICIPANTS: { + ROOT: 'ReportParticipants_Root', + INVITE: 'ReportParticipants_Invite', + DETAILS: 'ReportParticipants_Details', + ROLE: 'ReportParticipants_Role', + }, ROOM_MEMBERS_ROOT: 'RoomMembers_Root', ROOM_INVITE_ROOT: 'RoomInvite_Root', SEARCH_ROOT: 'Search_Root', diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index d803aa97fd0..5fa17e726c6 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -117,6 +117,12 @@ type AvatarWithImagePickerProps = { /** Allows to open an image without Attachment Picker. */ enablePreview?: boolean; + + /** Hard disables the "View photo" option */ + shouldDisableViewPhoto?: boolean; + + /** Optionally override the default "Edit" icon */ + editIcon?: IconAsset; }; function AvatarWithImagePicker({ @@ -144,6 +150,8 @@ function AvatarWithImagePicker({ disabled = false, onViewPhotoPress, enablePreview = false, + shouldDisableViewPhoto = false, + editIcon = Expensicons.Pencil, }: AvatarWithImagePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -332,7 +340,7 @@ function AvatarWithImagePicker({ {!disabled && ( { const menuItems = createMenuItems(openPicker); - // If the current avatar isn't a default avatar, allow the "View Photo" option - if (!isUsingDefaultAvatar) { + // If the current avatar isn't a default avatar and we are not overriding this behavior allow the "View Photo" option + if (!shouldDisableViewPhoto && !isUsingDefaultAvatar) { menuItems.push({ icon: Expensicons.Eye, text: translate('avatarWithImagePicker.viewPhoto'), diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 3b05d22fe6c..68f1aac41a5 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -289,6 +289,10 @@ function Button( currentTarget?.blur(); } + if (event?.type === 'keyup') { + return; + } + if (shouldEnableHapticFeedback) { HapticFeedback.press(); } diff --git a/src/components/ChatDetailsQuickActionsBar.tsx b/src/components/ChatDetailsQuickActionsBar.tsx new file mode 100644 index 00000000000..f15fc31aec4 --- /dev/null +++ b/src/components/ChatDetailsQuickActionsBar.tsx @@ -0,0 +1,64 @@ +import React, {useState} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Report from '@userActions/Report'; +import type {Report as OnyxReportType} from '@src/types/onyx'; +import Button from './Button'; +import ConfirmModal from './ConfirmModal'; +import * as Expensicons from './Icon/Expensicons'; + +type ChatDetailsQuickActionsBarProps = { + report: OnyxReportType; +}; + +function ChatDetailsQuickActionsBar({report}: ChatDetailsQuickActionsBarProps) { + const styles = useThemeStyles(); + const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false); + const {translate} = useLocalize(); + const isPinned = !!report.isPinned; + return ( + + + { + setIsLastMemberLeavingGroupModalVisible(false); + Report.leaveGroupChat(report.reportID); + }} + onCancel={() => setIsLastMemberLeavingGroupModalVisible(false)} + prompt={translate('groupChat.lastMemberWarning')} + confirmText={translate('common.leave')} + cancelText={translate('common.cancel')} + /> +