diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index c7b8a1bb7e01..380b874c6c95 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -113,6 +113,8 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode + with: + IS_DESKTOP_BUILD: 'true' - name: Make zip directory for everything to send to AWS Device Farm run: mkdir zip diff --git a/Mobile-Expensify b/Mobile-Expensify index 75361a078c87..1f1c233f53e0 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 75361a078c8792ae3a4f7b145c77d32357297ed2 +Subproject commit 1f1c233f53e03d601039a5ed41ca84ee9ae8a1e8 diff --git a/android/app/build.gradle b/android/app/build.gradle index ddbc3ceb9033..7e8b398f1268 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009011207 - versionName "9.1.12-7" + versionCode 1009011300 + versionName "9.1.13-0" // 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/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt b/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt index 5c566df606eb..dd02d8aba1bd 100644 --- a/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt +++ b/android/app/src/main/java/com/expensify/chat/navbar/NavBarManagerModule.kt @@ -1,10 +1,12 @@ package com.expensify.chat.navbar +import android.content.res.Resources import androidx.core.view.WindowInsetsControllerCompat +import com.expensify.chat.R import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.UiThreadUtil class NavBarManagerModule( private val mReactContext: ReactApplicationContext, @@ -24,4 +26,18 @@ class NavBarManagerModule( } } } + + @ReactMethod + fun getType(): String { + val resources = mReactContext.resources + val resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android"); + if (resourceId > 0) { + val navBarInteractionMode = resources.getInteger(resourceId) + when (navBarInteractionMode) { + 0, 1 -> return "soft-keys" + 2 -> return "gesture-bar" + } + } + return "soft-keys"; + } } diff --git a/assets/images/product-illustrations/emptystate__receiptfairy.svg b/assets/images/product-illustrations/emptystate__receiptfairy.svg new file mode 100644 index 000000000000..ccdeda5926f8 --- /dev/null +++ b/assets/images/product-illustrations/emptystate__receiptfairy.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index 469281d3e86b..be769faf568e 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -68,7 +68,7 @@ Browsers use the name prop to autofill information into the input. Here's a [ref ```jsx ``` @@ -118,7 +118,7 @@ Once a user has “touched” an input, i.e. blurred the input, we will also sta All form fields will additionally be validated when the form is submitted. Although we are validating on blur this additional step is necessary to cover edge cases where forms are auto-filled or when a form is submitted by pressing enter (i.e. there will be only a ‘submit’ event and no ‘blur’ event to hook into). -The Form component takes care of validation internally and the only requirement is that we pass a validate callback prop. The validate callback takes in the input values as argument and should return an object with shape `{[inputID]: errorMessage}`. +The Form component takes care of validation internally and the only requirement is that we pass a validate callback prop. The validate callback takes in the input values as argument and should return an object with shape `{[inputID]: errorMessage}`. Here's an example for a form that has two inputs, `routingNumber` and `accountNumber`: @@ -332,10 +332,10 @@ An example of this can be seen in the [ACHContractStep](https://github.com/Expen ### Safe Area Padding Any `FormProvider.tsx` that has a button at the bottom. If the `` is inside a ``, the bottom safe area inset is handled automatically (`includeSafeAreaPaddingBottom` needs to be set to `true`, but its the default). -If you have custom requirements and can't use ``, you can use the `useStyledSafeAreaInsets()` hook: +If you have custom requirements and can't use ``, you can use the `useSafeAreaPaddings()` hook: ```jsx -const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets(); +const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useSafeAreaPaddings(); diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments.md deleted file mode 100644 index 7d318fd35143..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Third-Party Payments -description: Reimburse reports and pay bills using PayPal or Venmo. ---- -Expensify integrates with PayPal and Venmo, which can be used to reimburse employees or pay bills. Some of the key benefits of using a third-party payment provider are: -- Faster Reimbursements: Expedite the reimbursement process and reduce the time it takes for employees to receive their funds. -- Secure Transactions: Benefit from the security features and protocols provided by trusted payment providers. -- Centralized Expense Management: Consolidate all your expenses and payments within Expensify for a more efficient financial workflow. - -# Connect a third-party payment option - -To connect a third-party payment platform to Expensify: -1. Log into your Expensify web account -2. Head to **Settings > Account > Payments > Alternative Payment Accounts** -3. Choose PayPal or Venmo - - **PayPal**: Enter your username in the `paypal.me/` field - - **Venmo**: Receive invoices via Venmo by adding your mobile phone number as a Secondary Login - -{% include faq-begin.md %} - -## Can I use multiple third-party payment providers with Expensify? - -Yes, you can link both your Venmo and PayPal accounts to Expensify if you'd like. - -## Is there a limit on the amount I can reimburse using third party payments? - -The payment limit is dependent on the settings configured within your Expensify account as well as the limits imposed by the third-party payment provider. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md index 02ee7b7ce04a..72ba03e600ee 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements.md @@ -1,81 +1,192 @@ --- title: Enable Global Reimbursements description: Send international payments +keywords: Expensify Classic, foreign bank validation, global reimbursement, Canada, Europe, Singapore, Australia, United Kingdom, international reimbursements --- -
-Enabling global reimbursements allows you to send direct reimbursements to countries worldwide if your company’s bank account is in the US, UK, Canada, Europe, or Australia. +If your company’s business bank account is in the US, Canada, the UK, Europe, Australia, or Singapore, you can send direct reimbursements to nearly any country worldwide! -# For USD accounts +The process to enable international reimbursements depends on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below. -{% include info.html %} -Before you can complete this process, you must first connect a **verified** U.S. bank account, and your employees receiving payments from this account must also connect their **deposit-only** U.S. bank account. -{% include end-info.html %} +--- + +# If the Reimbursement Account is in the U.S. (USD) + +Before you begin, ensure you have a verified U.S. business bank account connected. Once global reimbursement is enabled on your account, employees receiving payments will connect their **non-USD bank account**. -## Step 1: Request global reimbursements +## Step 1: Connect a U.S Business Bank Account -Once your verified U.S. bank account has been added and verified, you can request that global reimbursements be enabled on your account. +If you haven't already, follow the instructions to [Connect a US Business Bank Account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account). -## Step 2: Re-verify the bank account +## Step 2: Verify the Bank Account for Global Reimbursements 1. Hover over **Settings**, then click **Workspaces**. -2. Select the workspace. 3. Click the **Reports** tab on the left. 4. Ensure that the workspace currency is set to **USD**. 5. Click the **Reimbursements** tab on the left. -6. Ensure that the reimbursement method is set to **Direct** and that the right bank account is selected. +6. Ensure that the reimbursement method is set to **Direct** and that the correct bank account is selected. 7. Click the **Payments** tab on the left. 8. Click **Enable Global Reimbursement** next to the bank account. -9. Complete the International Reimbursement DocuSign form. +9. Complete the Global Reimbursement DocuSign form. +10. Once the form is complete, it is automatically sent to our Compliance Team for review. -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, which may include: +**Our Support Team will contact you with more details if additional information is required, which may include:** - An authorization letter - Proof of address and ID for the reimburser and/or company directors - Independently certified documentation, such as a shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company -# For AUD, CAD, GBP, and EUR accounts +## Step 3: Start Reimbursing Internationally + +After the bank account is verified for international payments, set the correct bank account as the reimbursement account: + +1. Under **Settings** > **Workspaces** > **Group** > **[Workspace Name]** > **Reimbursements** +2. Select the reimbursement account as the default account. +3. Ask your employees to add their deposit-only bank account. + - They can do this by logging into their Expensify accounts, heading to **Settings** > **Account** > **Payments**, and clicking **Add Deposit-Only Bank Account**. + +--- + +# If the Bank Account is Outside of the U.S (AUD, CAD, GBP, EUR, and SGD Currencies) + +**Overview of the International Bank Account Setup:** +1. Connect the reimbursement account to Expensify. +2. Complete DocuSign Form. +3. Expensify's Compliance Team Reviews the Application: Depending on the country, the team approves the request or requests additional documentation. +4. Expensify sends test deposits to the provided bank account: Expensify asks the customer to confirm test deposit amounts. +5. Admin Actions Required: + - Set the reimbursement account under **Settings > Workspaces > [Workspace Name] > Reimbursements**. + - Ensure employees add their deposit-only bank accounts under **Settings > Account > Payments > Add Deposit-Only Bank Account**. -## Step 1: Add the bank account +## Step 1: Connect the Bank Account 1. Hover over **Settings**, then click **Workspaces**. -2. Select the workspace. -3. Click the **Reports** tab on the left. -4. Ensure that the selected workspace currency matches your reimbursement bank account currency. -5. Click the **Reimbursements** tab on the left. -6. Set the reimbursement method to **Direct**. -7. Click **Add Business Bank Account**. -8. If necessary, click **Switch Country** to select the correct country if not automatically selected. -9. Enter the bank account details, then click **Save & Continue**. -10. Complete the International Reimbursement DocuSign form. +2. Click the **Reports** tab on the left. +3. Ensure that the selected workspace currency matches your reimbursement bank account currency. +4. Click the **Reimbursements** tab on the left. +5. Set the reimbursement method to **Direct**. +6. Click **Add Business Bank Account**. +7. Click **Switch Country** and adjust the **Currency** so the information is correct. +8. Click **Save & Continue**. +9. Enter the details of your bank account and click **Save & Continue**. -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, which may include: -- An authorization letter -- Proof of address and ID for the reimburser and/or company directors -- Independently certified documentation, such as a shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company +## Step 2: Verify the Bank Account for Global Reimbursements + +1. Complete the Global Reimbursement DocuSign form. +2. Once the form is complete, it is automatically sent to our Compliance Team for review. +3. See the **Requirements for Global Reimbursement** section below to determine which additional information is required to set up international reimbursements for your entity. +4. If prompted, upload one of the following: + - **Voided check** with business name and account details + - **Bank statement** showing the business name and account information + - **Bank letter** confirming the account details + +**Our Support Team will contact you via Concierge with more details if additional information is required.** + + +## Step 3: Start Reimbursing Internationally -{% include faq-begin.md %} +After the bank account is verified for international payments, set the correct bank account as the reimbursement account: -**Can multiple people send reimbursements internationally?** +1. Under **Settings** > **Workspaces** > **Group** > **[Workspace Name]** > **Reimbursements** +2. Select the reimbursement account as the default account. +3. Ask your employees to add their deposit-only bank account. + - They can do this by logging into their Expensify accounts, heading to **Settings** > **Account** > **Payments**, and clicking **Add Deposit-Only Bank Account**. + +--- + +# Requirements for International Reimbursement + +## U.S. +**Requirements:** +- ✅ Partner Application Form +- ✅ Address Verification Document (e.g., utility bill, lease agreement) +- ✅ Bank Statement for Direct Debit Setup + +**Direct Debit Timeline:** +⏳ Takes 1 day to set up once onboarding is complete and the bank account is verified. + +## Canada +**Requirements:** +- ✅ Partner Application Form +- ✅ Address Verification Document +- ✅ Bank Statement for Direct Debit Setup +- ✅ Proof of Beneficial Ownership + +**Direct Debit Timeline:** +⏳ Takes 1 day to set up once onboarding is complete and the bank account is verified. + +## United Kingdom +**Requirements:** +- ✅ Partner Application Form +- ✅ Address Verification Document +- ✅ Bank Statement for Direct Debit Setup +- ✅ Proof of Beneficial Ownership +- ✅ Copy of ID and Proof of Address for Signatories + +**Direct Debit Timeline:** +⏳ Can only be set up after onboarding is complete. + +## European Union +**Requirements:** +- ✅ Partner Application Form +- ✅ Address Verification Document +- ✅ Bank Statement for Direct Debit Setup +- ✅ Proof of Beneficial Ownership +- ✅ Certified Copies of ID & Address for UBOs (Only if the company is in Jersey) + +**Special Notes:** +🇮🇹 Italy: Requires "Codice Fiscale" (Tax Identification Number). +🇪🇸 Spain: Driver's license not accepted as ID. + +**Direct Debit Timeline:** +⏳ Can only be set up after onboarding is complete. + +## Australia +**Requirements:** +- ✅ Partner Application Form +- ✅ Address Verification Document +- ✅ Bank Statement for Direct Debit Setup +- ✅ Proof of Beneficial Ownership +- ✅ Copy of ID & Proof of Address for all Signatories + +**Special Notes:** +Expensify's Compliance Team must send a PDS FSG to company directors before onboarding. + +**Direct Debit Timeline:** +⏳ Takes 1 day to set up once onboarding is complete and the bank account is verified. + +## Singapore +**Requirements:** +- ✅ Partner Application Form +- ✅ Address Verification Document +- ✅ Proof of Beneficial Ownership +- ✅ Director/Board Resolution +- ✅ Memorandum & Articles of Association +- ✅ Certified Copies of ID & Address for UBOs + +**Direct Debit Timeline:** +⏳ Takes 3 weeks to set up. Clients are encouraged to pre-fund the account while waiting. + +--- -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. This will enable them to be able to send global reimbursements. +# FAQ -**How long does it take to verify an account for global payments?** +## Can multiple people send reimbursements internationally? -The verification process can take anywhere from a few business days to several weeks depending on the information provided in the DocuSign form, if additional information is required for compliance. +Yes, once your company is authorized to send international payments, the individual who verified the bank account can share it with additional admins on the workspace, enabling them to send global reimbursements. -**My employee doesn’t have the option to add their non-USD bank account as a deposit account. What should they do?** +## How long does it take to verify an account for global payments? -Have the employee double-check that their [default workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Navigate-multiple-workspaces) is set as the workspace that’s connected to the bank you’re using to send global payments. +The verification process can take anywhere from a few business days to several weeks, depending on the information provided in the DocuSign form and if additional information is required for compliance. -**Who is the “Authorized User” and the “User” on the International Reimbursement DocuSign form?** +## Why can't my employee add a non-USD bank account? -- **Authorized User**: The person who will process global reimbursements. The Authorized User should be the same person who manages the bank account connection in Expensify. -- **User**: You can leave this section blank because the “User” is Expensify. +Make sure your employee's default workspace is set to the one linked to the bank you're using for global payments. They can confirm their default workspace by following [these steps](https://help.expensify.com/articles/expensify-classic/workspaces/Navigate-multiple-workspaces). -**Does Global Reimbursement support Sepa in the EU?** +## Who are the “Authorized User” and the “User” on the International Reimbursement DocuSign form? -Global Reimbursement uses Sepa B2B to facilitate payments from EU-based accounts. Sepa Core is not supported. +- **Authorized User**: The person who will process international reimbursements. The Authorized User should be the same person who manages the bank account connection in Expensify. +- **User**: Expensify (leave blank). -{% include faq-end.md %} +## Does Global Reimbursement in Expensify support SEPA (Single Euro Payments Area) in the EU? -
+Global Reimbursement uses SEPA B2B to facilitate payments from EU-based accounts. SEPA Core is not supported. diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Get-reimbursed-faster-as-a-non-US-employee.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Get-reimbursed-faster-as-a-non-US-employee.md deleted file mode 100644 index 30dea99bbfde..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Get-reimbursed-faster-as-a-non-US-employee.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Get Reimbursed Faster as a Non-US Employee -description: How to use Wise to get paid faster ---- - -If you are an overseas employee who works for a US-based company, you can use Wise to be reimbursed for expenses just as quickly as your US-based colleagues. Wise (formerly TransferWise) is an FCA-regulated global money transfer service. - -Here’s how it works: - -1. When you sign up for a Wise account, you are provided with a USD checking account number and a routing number to use as your Expensify bank account. -2. Once you receive a reimbursement, it will be deposited directly into your Wise account. -3. You can then convert your funds into 40+ different currencies and withdraw them to your local bank account. If you live in the UK or EU, you can also get a debit card to spend money directly from your Wise account. - -## Set up reimbursements through Wise - -1. Check with your company to see if you can submit your expenses in USD. -2. Sign up for a Wise Borderless Account and get verified (verification can take up to 3 days). -3. In Expensify, [add a deposit-only bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account) with your Wise USD account and ACH routing numbers (NOT the wire transfer routing number). - -{% include info.html %} -Do not include spaces in the Wise account number, which should be 16 digits. -{% include end-info.html %} - -If your expenses are not in USD, Expensify will automatically convert them to USD when they are added to your expense report. Once you submit your expenses to your company’s USD workspace and they are approved, you will receive the reimbursement for the approved report total in USD in your Wise account. diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md index 00fb236e1763..c036c316896d 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md @@ -1,36 +1,82 @@ --- -title: Receive payments -description: Receive reimbursements from an employer +title: Receive Payments +description: Learn how to receive reimbursements through Expensify, including ACH, Wise, PayPal, and Venmo options. +keywords: [reimbursement, ACH, bank account, Wise, PayPal, Venmo, Rapid Reimbursement] --- -
-To get paid after submitting a report for reimbursement, you must first connect a personal U.S. bank account or a personal Australian bank account. Then once your employer approves your report or invoice, the reimbursement will be paid directly to your bank account. -Funds for U.S. and global payments are generally deposited within a maximum of four to five business days: 2 days for the funds to be debited from the business bank account, and 2-3 business days for the ACH or wire to deposit into the employee account. +To receive a reimbursement after submitting an expense report, you must first [connect a personal bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-Bank-Account) to Expensify. Once your employer approves your report or invoice, the reimbursement is deposited directly into your bank account. -However, banks only process ACH transactions before the daily cutoff (generally 3 p.m. PST) and only on business weekdays that are not bank holidays. This may affect when the payment is disbursed. If the payment qualifies for Rapid Reimbursement, you may receive the payment sooner. +**Processing Times:** +- U.S. and global payments generally take **4-5 business days**: + - **2 days** for the business account to process the debit + - **2-3 business days** for ACH or wire transfer to your account +- Payments initiated **after 3 p.m. PST** or on **weekends/bank holidays** may be delayed. +- If your payment qualifies for **Rapid Reimbursement**, you may receive it sooner. -{% include info.html %} -Companies also have the option to submit payments outside of Expensify via check, cash, or a third-party payment processor. Check with your Workspace Admin to know how you will be reimbursed. -{% include end-info.html %} - -# Rapid Reimbursement (U.S. only) +Your company or client also has the option to pay you via **check, a third-party payment processor, or outside of Expensify**. Check with your **Workspace Admin** or payor to confirm the payment method. -With Expensify’s ACH reimbursement, payments may be eligible for reimbursement by the next business day with Rapid Reimbursement if they meet the following qualifications: -- **Deposit-only accounts**: Payment must not exceed $100 -- **Verified business bank accounts**: The account does not disburse more than $10,000 within a 24-hour time period. +--- + +# Rapid Reimbursement (U.S. Only) + +Expensify’s **Rapid Reimbursement** allows eligible ACH payments to process **within one business day** if they meet the following conditions: + +- **Deposit-only accounts:** Payment must not exceed **$100**. +- **Verified business bank accounts:** Total disbursements must not exceed **$10,000** within 24 hours. + +If payments exceed these limits, they follow the standard **4-5 business day ACH timeframe**. + +--- + +# Global Reimbursements via Wise + +For employees outside the U.S., Expensify partners with **Wise (formerly TransferWise)** to enable **faster global reimbursements**. -If the payment amount exceeds the limit, funds will be deposited within the typical ACH time frame of four to five business days. +## How It Works +1. Sign up for a **Wise account** to receive a USD checking account number and ACH routing number. +2. Add these details as a **deposit-only bank account** in Expensify. +3. When your reimbursement is approved, it is deposited into your Wise account. +4. Convert funds into **40+ currencies** or withdraw them to your local bank. **UK and EU users can also request a Wise debit card**. -{% include faq-begin.md %} +## Setting Up Wise for Expensify +1. Confirm with your employer if **USD expenses** can be submitted. +2. Open a **Wise Borderless Account** and complete **verification** (may take up to **3 days**). +3. Add your **Wise USD account and ACH routing number** to Expensify. -**Is there a way I can track my payment?** +**Important:** Use the **ACH routing number**, *not* the wire transfer routing number. + +Expensify will **automatically convert non-USD expenses** to USD before submission. + +--- + +# Third-Party Payment Options + +Expensify integrates with **PayPal** and **Venmo** to provide alternative reimbursement options. + +## Benefits of Third-Party Payments +- **Faster reimbursements** compared to standard ACH transfers. +- **Enhanced security** via trusted payment platforms. +- **Streamlined expense management** within Expensify. + +## Connecting a Third-Party Payment Option +1. Log into your **Expensify account**. +2. Navigate to **Settings > Account > Payments > Alternative Payment Accounts**. +3. Select **PayPal** or **Venmo**: + - **PayPal**: Enter your **PayPal.Me** username. + - **Venmo**: Enter your **mobile number** (must match your Venmo account). + +--- -For U.S. ACH payments and global reimbursements, the expected date of reimbursement is provided at the top of the report and in the comments section of the report. Funds will be deposited within the typical ACH time frame of four to five business days unless the payment is eligible for Rapid Reimbursement. +# FAQ -**For global payments, what currency is the payment provided in?** +## Can I link multiple third-party payment providers? +Yes, you can link both **PayPal and Venmo** to your Expensify account. -Global payments are reimbursed in the recipient's currency. +## Is there a limit on third-party reimbursements? +Limits depend on **Expensify settings** and **third-party provider restrictions**. -{% include faq-end.md %} +## How can I track my reimbursement? +For **ACH and global payments**, Expensify displays the **expected deposit date** at the top of your expense report and in the report comments. -
+## What currency will my global reimbursement be in? +Expensify will reimburse you in your local currency, based on the conversion at the time of payout. diff --git a/docs/articles/expensify-classic/expensify-partner-program/Card-Revenue-Share.md b/docs/articles/expensify-classic/expensify-partner-program/Card-Revenue-Share.md index 663a5e3cd9c8..96d961c479a9 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Card-Revenue-Share.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Card-Revenue-Share.md @@ -1,27 +1,42 @@ --- -title: Expensify Card revenue share for ExpensifyApproved! partners -description: Earn money when your clients adopt the Expensify Card +title: Expensify-Card-Revenue-Share.md +description: Learn how ExpensifyApproved! partner accountants can earn revenue by helping clients adopt the Expensify Card. +keywords: [Expensify Card, revenue share, ExpensifyApproved! partner, accountant earnings] --- -You can now earn additional income for your firm every time your client uses their Expensify Card. In short, your firm gets 0.5% of your clients’ total Expensify Card spend as cash back. The more your clients spend, the more cashback your firm receives! -This program is currently only available to US-based ExpensifyApproved! partner accountants. +ExpensifyApproved! Accounting Partners in the US can now earn additional income through the Expensify Card. Your firm will receive **0.5% of your clients’ total Expensify Card spend** as cashback -- More client spending means more revenue for your firm! -# Become a Domain Admin -To benefit from this program, you or a member of your firm must be a domain admin on the client’s domain in Expensify: -1. Head to *Settings > Domains* -2. Click the client's domain - - If you can click on the domain and access the domain settings, you are a Domain Admin - - If you’re not a Domain Admin, your client can add you as one by heading to **Settings > Domains > [Client's Domain] > Domain Admins > Add Admin**. +**Note:** This program is currently available only to **US-based ExpensifyApproved! partner accountants**. -_**Note:** You can view all domain admins under Settings > Domains > [Client's Domain] > Domain Admins._ +--- + +# How to Earn Revenue + +## 1. Become a Domain Admin +To qualify, you or a member of your firm must be a **Domain Admin** on the client’s domain in Expensify. + +Steps to becoming a Domain Admin: +1. Go to **Settings > Domains**. +2. Click on your client’s domain. + - If you can access the domain settings, you are a Domain Admin. + - If you are not a Domain Admin, ask your client to add you: + - Navigate to **Settings > Domains > [Client's Domain] > Domain Admins > Add Admin**. + +**Tip:** You can view all Domain Admins under **Settings > Domains > [Client's Domain] > Domain Admins**. -# Connect a deposit-only business bank account -[Follow these instructions](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account#connect-a-business-deposit-only-account) to connect a deposit-only business bank account. +--- + +## 2. Connect a Deposit-Only Business Bank Account +To receive your revenue share, connect a **deposit-only business bank account**: + +[Follow these instructions](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account#connect-a-business-deposit-only-account) to complete the setup. -Once that's complete, any revenue earned will be deposited directly into that bank account. +Once connected, all earned revenue will be deposited directly into this account. + +--- -{% include faq-begin.md %} +# FAQ -## What if my firm is not permitted to accept revenue share from our clients? +## What if my firm cannot accept revenue share from clients? -We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools. +We understand that some firms have restrictions on receiving revenue share. If your firm cannot accept the cashback, you can choose to pass the 0.5% cashback directly to your client using your internal payment tools. diff --git a/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md b/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md deleted file mode 100644 index 3153e6e217fa..000000000000 --- a/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Refer a Friend to New Expensify! -description: Share your invite link with a friend, start a chat with a coworker or request money from your boss ---- - - -# About - -New Expensify's referral program is currently paused. Please check back later. - -[New Expensify](https://new.expensify.com/) is growing thanks to members like you who love it so much that they tell their friends, family, colleagues, managers, and fellow business founders to use it, too. - -# How to refer a friend to New Expensify - -1. There are a bunch of different ways to refer someone to New Expensify: - - Start a chat - - Submit an expense to them - - Split an expense with them - - Pay someone (them) - - Assign them a task - - @ mention them - - Invite them to a room - - Add them to a workspace - - -{% include faq-begin.md %} - -- **Where can I find my referral link?** - -In New Expensify, go to **Settings** > **Share code** > **Get $250** to retrieve your invite link. -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md index 8243833dcc23..a8c54842e13e 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md @@ -1,77 +1,82 @@ --- title: Expensify Partner Support -description: Understanding support for our partners +description: Learn about Expensify's Partner Support program, including training, dedicated partner managers, and real-time chat support. +keywords: [ExpensifyApproved, partner support, training, partner manager, onboarding, concierge] --- -# Overview -As an ExpensifyApproved! Partner, your firm gains access to a specialized suite of support services. We're here to ensure your clients have the tools and resources they need, every step of the way. -Our well-rounded support methodology is designed to provide comprehensive assistance, ensuring that both you and your clients have a seamless experience. Have questions or want to delve deeper? Don't hesitate to reach out to your dedicated Partner Manager to get started! +As an **ExpensifyApproved! Partner**, your firm gains access to exclusive support services designed to enhance your experience and help your clients succeed. This guide outlines the key support resources available to you. +# ExpensifyApproved! University -## 1. ExpensifyApproved! University -**Purpose:** Equip your team with a comprehensive understanding of Expensify. +**Purpose:** Train and certify your team on Expensify. **Benefits:** -- Foundation-level knowledge about the platform. -- 3 CPE credits upon successful completion (US-only). -- Unlock exclusive partner perks, including tickets to ExpensiCon! -- Visit university.Expensify.com to access our comprehensive training program. +- Gain foundational knowledge of Expensify. +- Earn **3 CPE credits** (US-only). +- Unlock partner perks, including tickets to **ExpensiCon**. +- Access training at [university.expensify.com](https://university.expensify.com). -## 2. Partner Manager -**Role:** -A Partner Manager is a dedicated point of contact for your firm Partner Managers support our accounting partners by providing recommendations for client’s accounts, assisting with firm-wide training, and ensuring partners receive the full benefits of our partnership program. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency. +--- + +# Partner Manager +Your **Partner Manager** is your dedicated contact for strategic support. -**Key Responsibilities:** -- Handle any escalations promptly. +## Key Responsibilities: +- Handle escalations promptly. - Organize firm-wide training sessions. -- Assist with strategic planning and the introduction of new features. -- Once you've completed the ExpensifyApproved! University, log in to your Expensify account. Click on the "Support" option to connect with your dedicated Partner Manager. +- Provide recommendations to optimize your clients' accounts. +- Assist with strategic planning and feature adoption. -**How do I know if I have a Partner Manager?** +## How to Connect with Your Partner Manager +To be assigned a **Partner Manager**, you must complete the **ExpensifyApproved! University** training. Once completed, check your **Expensify account**: +- Look for the **ExpensifyApproved!** logo in the bottom left-hand corner. +- Navigate to `Support` to message your **Partner Manager**. -For your firm to be assigned a Partner Manager, you must complete the ExpensifyApproved! University training course. Every external accountant or bookkeeper who completes the training is automatically enrolled in our program and receives all the benefits, including access to the Partner Manager. So everyone at your firm must complete the training to receive the maximum benefit. +**Contact Options:** +1. Log in to [new.expensify.com](https://new.expensify.com) and search for your **Partner Manager**. +2. Reply to any email from your **Partner Manager**. -You can check to see if you’ve completed the course and enrolled in the ExpensifyApproved! Accountants program simply by logging into your Expensify account. In the bottom left-hand corner of the website, you will see the ExpensifyApproved! logo. +**Checking Availability:** +- Your **Partner Manager's** status in **New Expensify** will show as **"Online"** or display their working hours. -**How do I contact my Partner Manager?** -1. Signing in to new.expensify.com and searching for your Partner Manager -2. Replying to or clicking the chat link on any email you get from your Partner Manager +**Scheduling a Call:** +- You can request a call for discussions on client onboarding, training, and best practices. +- For general support questions, use **Concierge chat** for immediate assistance. -**How do I know if my Partner Manager is online?** +--- -You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours. +# Client Setup Specialist -**Can I get on a call with my Partner Manager?** +**Purpose:** Ensure seamless onboarding for your referred clients. -Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm-wide training, and client setups. +**Duties:** +- Assist with Expensify setup and **accounting integrations**. +- Ensure clients have the necessary tools and knowledge. +- To connect with a **Client Setup Specialist**, log in and navigate to `Support`. -We recommend continuing to work with Concierge for general support questions, as this team is always online and available to help immediately. +--- -## 3. Client Setup Specialist -**Purpose:** Ensure smooth onboarding for every client you refer. +# Client Account Manager -**Duties:** -- Comprehensive assistance with setting up Expensify. -- Help with configuring accounting integrations. -- Ensure clients have the tools and knowledge they need to thrive. -- After logging into Expensify, click on the "Support" option in the left-hand navigation pane. This will connect you directly to your assigned Client Onboarding Manager. - -## 4. Client Account Manager -**Role:** Dedicated support for ongoing client needs. +**Role:** Provide ongoing support for client accounts. **Responsibilities:** -- Address day-to-day product inquiries. -- Assist clients in navigating and optimizing their use of Expensify. -- After logging into Expensify, click on the "Support" option in the left-hand navigation pane. This will connect you directly to your assigned Account Manager. +- Address daily product-related inquiries. +- Guide clients in optimizing their **Expensify experience**. +- To contact your **Client Account Manager**, log in and go to `Support`. + +--- -## 5. Concierge chat support -**Availability:** Real-time support for any urgent inquiries. +# Concierge Chat Support + +**Availability:** Real-time support for immediate inquiries. **Features:** -- Immediate assistance with an average response time of under two minutes. -- Available to both accountants and clients for all product-related questions. -- For instant assistance, click on the chat bubble located at the bottom right-hand corner of your screen when logged in. +- **Response time under 2 minutes**. +- Available to **both accountants and clients**. +- Click the **chat bubble** in the bottom right-hand corner of Expensify for instant help. +--- -Our well-rounded support methodology is designed to provide comprehensive assistance, ensuring that both you and your clients have a seamless experience. Have questions or want to delve deeper? Don't hesitate to reach out to your dedicated Partner Manager. You can find your partner manager by logging into Expensify and clicking the "Support" button in the left hand navigator. +Expensify's Partner Support ensures you and your clients receive elevated support in Expensify. If you need further guidance, reach out via `Support` in your Expensify account or connect directly with your Partner Manager! diff --git a/docs/articles/new-expensify/getting-started/Using-Reports-in-New-Expensify.md b/docs/articles/new-expensify/getting-started/Using-Reports-in-New-Expensify.md index 7c2016b4e212..bc4afbe957a7 100644 --- a/docs/articles/new-expensify/getting-started/Using-Reports-in-New-Expensify.md +++ b/docs/articles/new-expensify/getting-started/Using-Reports-in-New-Expensify.md @@ -24,6 +24,8 @@ Expensify's Reports feature introduces a powerful way to access and manage finan - **Cross-platform consistency** - Enjoy a seamless experience across desktop and mobile platforms. - **Saved reports** - Save and revisit frequently used report queries for recurring tasks. +![A photo of the Reports page]({{site.url}}/assets/images/ExpensifyHelp-Reports-1-v2.png){:width="100%"} + --- # Report Filters @@ -37,6 +39,8 @@ Expensify’s report filters help users narrow down results to find specific dat - Trips: Drafts, Upcoming, In Progress, Past - **Advanced filters** - Enable precise reports using query syntax (e.g., `type:expenses status:approved`). +![A photo of common report filter]({{site.url}}/assets/images/ExpensifyHelp-SearchFormat.png){:width="100%"} + --- # Search Format diff --git a/docs/redirects.csv b/docs/redirects.csv index 26d902954652..cf7fd558e5cf 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -663,3 +663,6 @@ https://help.expensify.com/articles/expensify-classic/reports/Add-comments-and-a https://help.expensify.com/articles/expensify-classic/expensify-card/Statements,https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Statements https://help.expensify.com/articles/expensify-classic/expensify-billing/Out-of-date-Billing,https://help.expensify.com/articles/expensify-classic/expensify-billing/Out-of-Date-Billing https://help.expensify.com/articles/expensify-classic/settings/Set-time-zone,https://help.expensify.com/articles/expensify-classic/settings/Set-Time-Zone +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Get-reimbursed-faster-as-a-non-US-employee,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments +https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Referral-Program,https://use.expensify.com/accountants-program \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4a7b8f7a1e3a..5259b0cfc5ef 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.1.12 + 9.1.13 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.1.12.7 + 9.1.13.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d7e45b02f7d2..6c877542b6f2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.1.12 + 9.1.13 CFBundleSignature ???? CFBundleVersion - 9.1.12.7 + 9.1.13.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 664e379b3d79..2c9338f0d407 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.1.12 + 9.1.13 CFBundleVersion - 9.1.12.7 + 9.1.13.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 4d5098f0c62c..0b70c75704bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.1.12-7", + "version": "9.1.13-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.1.12-7", + "version": "9.1.13-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d51c0ad899d3..dbcb50f41f0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.1.12-7", + "version": "9.1.13-0", "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.", diff --git a/src/App.tsx b/src/App.tsx index 8dd2631a6b7d..3e6fc9ac2a27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import InitialURLContextProvider from './components/InitialURLContextProvider'; import {InputBlurContextProvider} from './components/InputBlurContext'; import KeyboardProvider from './components/KeyboardProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; +import NavigationBar from './components/NavigationBar'; import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; import {ProductTrainingContextProvider} from './components/ProductTrainingContext'; @@ -118,6 +119,7 @@ function App({url, hybridAppSettings, timestamp}: AppProps) { + diff --git a/src/CONST.ts b/src/CONST.ts index 1d4a682fa262..a5542620fdf9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -781,6 +781,7 @@ const CONST = { NEWDOT_INTERNATIONAL_DEPOSIT_BANK_ACCOUNT: 'newDotInternationalDepositBankAccount', NSQS: 'nsqs', CUSTOM_RULES: 'customRules', + TABLE_REPORT_VIEW: 'tableReportView', }, BUTTON_STATES: { DEFAULT: 'default', @@ -1026,7 +1027,7 @@ const CONST = { GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', ADD_SECONDARY_LOGIN_URL: encodeURI('settings?param={"section":"account","openModal":"secondaryLogin"}'), MANAGE_CARDS_URL: 'domain_companycards', - FEES_URL: `${USE_EXPENSIFY_URL}/fees`, + FEES_URL: `${EXPENSIFY_URL}/fees`, SAVE_WITH_EXPENSIFY_URL: `${USE_EXPENSIFY_URL}/savings-calculator`, CFPB_PREPAID_URL: 'https://cfpb.gov/prepaid', STAGING_NEW_EXPENSIFY_URL: 'https://staging.new.expensify.com', @@ -1311,6 +1312,7 @@ const CONST = { CORPORATE_UPGRADE: 'POLICYCHANGELOG_CORPORATE_UPGRADE', TEAM_DOWNGRADE: 'POLICYCHANGELOG_TEAM_DOWNGRADE', }, + RESOLVED_DUPLICATES: 'RESOLVEDDUPLICATES', ROOM_CHANGE_LOG: { INVITE_TO_ROOM: 'INVITETOROOM', REMOVE_FROM_ROOM: 'REMOVEFROMROOM', @@ -1531,6 +1533,20 @@ const CONST = { LIGHT: 'light', DARK: 'dark', }, + NAVIGATION_BAR_TYPE: { + // We consider there to be no navigation bar in one of these cases: + // 1. The device has physical navigation buttons + // 2. The device uses gesture navigation without a gesture bar. + // 3. The device uses hidden (auto-hiding) soft keys. + NONE: 'none', + SOFT_KEYS: 'soft-keys', + GESTURE_BAR: 'gesture-bar', + }, + // Currently, in Android there is no native API to detect the type of navigation bar (soft keys vs. gesture). + // The navigation bar on (standard) Android devices is around 30-50dpi tall. (Samsung: 40dpi, Huawei: ~34dpi) + // To leave room to detect soft-key navigation bars on non-standard Android devices, + // we set this height threshold to 30dpi, since gesture bars will never be taller than that. (Samsung & Huawei: ~14-15dpi) + NAVIGATION_BAR_ANDROID_SOFT_KEYS_MINIMUM_HEIGHT_THRESHOLD: 30, TRANSACTION: { DEFAULT_MERCHANT: 'Expense', UNKNOWN_MERCHANT: 'Unknown Merchant', @@ -1928,6 +1944,7 @@ const CONST = { AUTO_CREATE_VENDOR: 'autoCreateVendor', REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID', COLLECTION_ACCOUNT_ID: 'collectionAccountID', + ACCOUNTING_METHOD: 'accountingMethod', }, XERO_CONFIG: { @@ -2460,7 +2477,7 @@ const CONST = { BANCORP_WALLET_PROGRAM_ID: '660', PROGRAM_ISSUERS: { EXPENSIFY_PAYMENTS: 'Expensify Payments LLC', - BANCORP_BANK: 'The Bancorp Bank', + BANCORP_BANK: 'The Bancorp Bank, N.A.', }, }, @@ -3363,7 +3380,7 @@ const CONST = { `(? { - return `search?q=${encodeURIComponent(query)}${groupBy ? `&groupBy=${groupBy}` : ''}${name ? `&name=${name}` : ''}` as const; + getRoute: ({query, name}: {query: SearchQueryString; name?: string}) => { + return `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const; }, }, SEARCH_SAVED_SEARCH_RENAME: { @@ -79,9 +79,9 @@ const ROUTES = { }, }, SEARCH_MONEY_REQUEST_REPORT: { - route: 'search/report/:reportID', + route: 'search/r/:reportID', getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { - const baseRoute = `search/view/${reportID}` as const; + const baseRoute = `search/r/${reportID}` as const; return getUrlWithBackToParam(baseRoute, backTo); }, }, @@ -310,6 +310,7 @@ const ROUTES = { NEW_CHAT_EDIT_NAME: 'new/chat/confirm/name/edit', NEW_ROOM: 'new/room', + NEW_REPORT_WORKSPACE_SELECTION: 'new-report-workspace-selection', REPORT: 'r', REPORT_WITH_ID: { route: 'r/:reportID?/:reportActionID?', @@ -405,6 +406,10 @@ const ROUTES = { route: 'r/:reportID/details/export/:connectionName', getRoute: (reportID: string, connectionName: ConnectionName, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/export/${connectionName as string}` as const, backTo), }, + REPORT_WITH_ID_CHANGE_WORKSPACE: { + route: 'r/:reportID/change-workspace', + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-workspace` as const, backTo), + }, REPORT_SETTINGS: { route: 'r/:reportID/settings', getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings` as const, backTo), @@ -1171,11 +1176,19 @@ const ROUTES = { }, WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/account-selector', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/account-selector` as const, + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/accounting/quickbooks-online/account-selector` as const, }, WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const, + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const, + }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_AUTO_SYNC: { + route: 'settings/workspaces/:policyID/connections/quickbooks-online/advanced/autosync', + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/connections/quickbooks-online/advanced/autosync` as const, + }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_ACCOUNTING_METHOD: { + route: 'settings/workspaces/:policyID/connections/quickbooks-online/advanced/autosync/accounting-method', + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/connections/quickbooks-online/advanced/autosync/accounting-method` as const, }, WORKSPACE_ACCOUNTING_CARD_RECONCILIATION: { route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation', @@ -1680,6 +1693,10 @@ const ROUTES = { route: 'hold-expense-educational', getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo), }, + CHANGE_POLICY_EDUCATIONAL: { + route: 'change-workspace-educational', + getRoute: (backTo?: string) => getUrlWithBackToParam('change-workspace-educational', backTo), + }, TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: { route: 'travel/terms/:domain/accept', @@ -2074,11 +2091,11 @@ const ROUTES = { }, POLICY_ACCOUNTING_NETSUITE_AUTO_SYNC: { route: 'settings/workspaces/:policyID/connections/netsuite/advanced/autosync', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/autosync` as const, + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/connections/netsuite/advanced/autosync` as const, }, POLICY_ACCOUNTING_NETSUITE_ACCOUNTING_METHOD: { route: 'settings/workspaces/:policyID/connections/netsuite/advanced/autosync/accounting-method', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/autosync/accounting-method` as const, + getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/connections/netsuite/advanced/autosync/accounting-method` as const, }, POLICY_ACCOUNTING_NSQS_SETUP: { route: 'settings/workspaces/:policyID/accounting/nsqs/setup', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 0825b540244f..759755a04617 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -164,7 +164,9 @@ const SCREENS = { NEW_CHAT: 'NewChat', DETAILS: 'Details', PROFILE: 'Profile', + NEW_REPORT_WORKSPACE_SELECTION: 'New_Report_Workspace_Selection', REPORT_DETAILS: 'Report_Details', + REPORT_CHANGE_WORKSPACE: 'ReportChangeWorkspace', WORKSPACE_CONFIRMATION: 'Workspace_Confirmation', REPORT_SETTINGS: 'Report_Settings', REPORT_DESCRIPTION: 'Report_Description', @@ -327,12 +329,20 @@ const SCREENS = { EDIT: 'PrivateNotes_Edit', }, + NEW_REPORT_WORKSPACE_SELECTION: { + ROOT: 'NewReportWorkspaceSelection_Root', + }, + REPORT_DETAILS: { ROOT: 'Report_Details_Root', SHARE_CODE: 'Report_Details_Share_Code', EXPORT: 'Report_Details_Export', }, + REPORT_CHANGE_WORKSPACE: { + ROOT: 'ReportChangeWorkspace_Root', + }, + WORKSPACE_CONFIRMATION: {ROOT: 'Workspace_Confirmation_Root'}, WORKSPACE: { @@ -361,6 +371,8 @@ const SCREENS = { QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Classes_Displayed_As', QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Customers_Displayed_As', QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Locations_Displayed_As', + QUICKBOOKS_ONLINE_AUTO_SYNC: 'Policy_Accounting_Quickbooks_Online_Auto_Sync', + QUICKBOOKS_ONLINE_ACCOUNTING_METHOD: 'Policy_Accounting_Quickbooks_Online_Accounting_Method', QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense_Account_Select', QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense_Select', QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense', @@ -646,6 +658,7 @@ const SCREENS = { DETAILS_ROOT: 'Details_Root', PROFILE_ROOT: 'Profile_Root', PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root', + CHANGE_POLICY_EDUCATIONAL_ROOT: 'ChangePolicyEducational_Root', REPORT_DESCRIPTION_ROOT: 'Report_Description_Root', REPORT_PARTICIPANTS: { ROOT: 'ReportParticipants_Root', diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 2c07863ea2e8..93729a8e0da3 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -14,7 +14,7 @@ import Text from '@components/Text'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -116,7 +116,7 @@ function AttachmentView({ const {updateCurrentlyPlayingURL} = usePlaybackContext(); const theme = useTheme(); - const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets(); + const {safeAreaPaddingBottomStyle} = useSafeAreaPaddings(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 8d91525c25bc..a459cc193b40 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -64,7 +64,7 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu const StyleUtils = useStyleUtils(); const insets = useSafeAreaInsets(); const {keyboardHeight, isKeyboardAnimatingRef} = useKeyboardState(); - const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); + const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getPlatformSafeAreaPadding(insets ?? undefined); useEffect(() => { const container = containerRef.current; @@ -103,8 +103,20 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu let bottomValue = windowHeight - (cursorCoordinates.y - scrollValue + y) - keyboardHeight; const widthValue = shouldUseNarrowLayout ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; - const isEnoughSpaceToRenderMenuAboveForBig = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMaxHeight, topInset}); - const isEnoughSpaceToRenderMenuAboveForSmall = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMinHeight, topInset}); + const isEnoughSpaceToRenderMenuAboveForBig = isEnoughSpaceToRenderMenuAboveCursor({ + y, + cursorCoordinates, + scrollValue, + contentHeight: contentMaxHeight, + topInset, + }); + const isEnoughSpaceToRenderMenuAboveForSmall = isEnoughSpaceToRenderMenuAboveCursor({ + y, + cursorCoordinates, + scrollValue, + contentHeight: contentMinHeight, + topInset, + }); const newLeftOffset = shouldUseNarrowLayout ? x : bigScreenLeftOffset; // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup @@ -139,9 +151,11 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu }); }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, shouldUseNarrowLayout, suggestionsLength, bottomInset, topInset, isKeyboardAnimatingRef]); - if ((containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) || (containerState.cursorCoordinates.x === 0 && containerState.cursorCoordinates.y === 0)) { + // Prevent rendering if container dimensions are not set or if we have no suggestions + if ((containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) || !suggestionsLength) { return null; } + return ( ; + + /** Whether to add bottom safe area padding to the view. */ + addBottomSafeAreaPadding?: boolean; }; type BlockingViewIconProps = { @@ -94,7 +98,8 @@ function BlockingView({ animationWebStyle = {}, CustomSubtitle, contentFitImage, - containerStyle, + containerStyle: containerStyleProp, + addBottomSafeAreaPadding = false, }: BlockingViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -132,6 +137,8 @@ function BlockingView({ ); }, [styles, subtitleText, shouldEmbedLinkWithSubtitle, CustomSubtitle]); + const containerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding, style: containerStyleProp}); + return ( {!!animation && ( diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index d751d8bc666b..b9afe868878c 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -50,6 +50,9 @@ type FullPageNotFoundViewProps = { /** Whether we should display the button that opens new SearchRouter */ shouldDisplaySearchRouter?: boolean; + + /** Whether to add bottom safe area padding to the view. */ + addBottomSafeAreaPadding?: boolean; }; // eslint-disable-next-line rulesdir/no-negated-variables @@ -67,6 +70,7 @@ function FullPageNotFoundView({ shouldForceFullScreen = false, subtitleStyle, shouldDisplaySearchRouter, + addBottomSafeAreaPadding = true, }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -93,6 +97,7 @@ function FullPageNotFoundView({ shouldShowLink={shouldShowLink} onLinkPress={onLinkPress} subtitleStyle={subtitleStyle} + addBottomSafeAreaPadding={addBottomSafeAreaPadding} /> diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx index 787752dd4e72..dc5c210b3178 100644 --- a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx +++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx @@ -6,7 +6,12 @@ import useTheme from '@hooks/useTheme'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import BlockingView from './BlockingView'; -function FullPageOfflineBlockingView({children}: ChildrenProps) { +type FullPageOfflineBlockingViewProps = ChildrenProps & { + /** Whether to add bottom safe area padding to the view. */ + addBottomSafeAreaPadding?: boolean; +}; + +function FullPageOfflineBlockingView({children, addBottomSafeAreaPadding = true}: FullPageOfflineBlockingViewProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -19,6 +24,7 @@ function FullPageOfflineBlockingView({children}: ChildrenProps) { iconColor={theme.offline} title={translate('common.youAppearToBeOffline')} subtitle={translate('common.thisFeatureRequiresInternet')} + addBottomSafeAreaPadding={addBottomSafeAreaPadding} /> ); } diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 5baba39107ac..fe3e28dcf50c 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; import React, {useCallback, useMemo, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {ActivityIndicator, View} from 'react-native'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -152,6 +152,12 @@ type ButtonProps = Partial & { /** The text displays under the first line */ secondLineText?: string; + + /** + * Whether the button should have a background layer in the color of theme.appBG. + * This is needed for buttons that allow content to display under them. + */ + shouldBlendOpacity?: boolean; }; type KeyboardShortcutComponentProps = Pick; @@ -255,6 +261,7 @@ function Button( isPressOnEnterActive, isNested = false, secondLineText = '', + shouldBlendOpacity = false, ...rest }: ButtonProps, ref: ForwardedRef, @@ -356,6 +363,57 @@ function Button( return textComponent; }; + const buttonStyles = useMemo>( + () => [ + styles.button, + StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, !!icon, !!(text?.length > 0), shouldShowRightIcon), + success ? styles.buttonSuccess : undefined, + danger ? styles.buttonDanger : undefined, + isDisabled ? styles.buttonOpacityDisabled : undefined, + isDisabled && !danger && !success ? styles.buttonDisabled : undefined, + shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, + shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, + text && shouldShowRightIcon ? styles.alignItemsStretch : undefined, + innerStyles, + link && styles.bgTransparent, + ], + [ + StyleUtils, + danger, + icon, + innerStyles, + isDisabled, + large, + link, + medium, + shouldRemoveLeftBorderRadius, + shouldRemoveRightBorderRadius, + shouldShowRightIcon, + small, + styles, + success, + text, + ], + ); + + const buttonContainerStyles = useMemo>( + () => [buttonStyles, shouldBlendOpacity && styles.buttonBlendContainer], + [buttonStyles, shouldBlendOpacity, styles.buttonBlendContainer], + ); + + const buttonBlendForegroundStyle = useMemo>(() => { + if (!shouldBlendOpacity) { + return undefined; + } + + const {backgroundColor, opacity} = StyleSheet.flatten(buttonStyles); + + return { + backgroundColor, + opacity, + }; + }, [buttonStyles, shouldBlendOpacity]); + return ( <> {pressOnEnter && ( @@ -402,6 +460,7 @@ function Button( onPressIn={onPressIn} onPressOut={onPressOut} onMouseDown={onMouseDown} + shouldBlendOpacity={shouldBlendOpacity} disabled={isLoading || isDisabled} wrapperStyle={[ isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {}, @@ -410,19 +469,7 @@ function Button( shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, style, ]} - style={[ - styles.button, - StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, !!icon, !!(text?.length > 0), shouldShowRightIcon), - success ? styles.buttonSuccess : undefined, - danger ? styles.buttonDanger : undefined, - isDisabled ? styles.buttonOpacityDisabled : undefined, - isDisabled && !danger && !success ? styles.buttonDisabled : undefined, - shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, - shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, - text && shouldShowRightIcon ? styles.alignItemsStretch : undefined, - innerStyles, - link && styles.bgTransparent, - ]} + style={buttonContainerStyles} isNested={isNested} hoverStyle={[ shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined, @@ -439,6 +486,7 @@ function Button( onHoverIn={() => setIsHovered(true)} onHoverOut={() => setIsHovered(false)} > + {shouldBlendOpacity && } {renderContent()} {isLoading && ( + {changeWorkspaceMenuSections.map((section) => ( + + + + ${convertToLTR(translate(section.titleTranslationKey))}`} /> + + + ))} + + ); +} + +ChangeWorkspaceMenuSectionList.displayName = 'ChangeWorkspaceMenuSectionList'; +export default ChangeWorkspaceMenuSectionList; diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index fd0d4306f7c6..1052082480d3 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -18,6 +18,7 @@ function CurrencySelectionList({ canSelectMultiple = false, recentlyUsedCurrencies, excludedCurrencies = [], + ...restProps }: CurrencySelectionListProps) { const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [searchValue, setSearchValue] = useState(''); @@ -91,6 +92,8 @@ function CurrencySelectionList({ return ( > & { /** Label for the search text input */ searchInputLabel: string; diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 058b2e564f89..d3a715ad8748 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -29,6 +29,7 @@ function EmptyStateComponent({ lottieWebViewStyles, showsVerticalScrollIndicator, minModalHeight = 400, + addBottomSafeAreaPadding = false, }: EmptyStateComponentProps) { const styles = useThemeStyles(); const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO); @@ -88,6 +89,7 @@ function EmptyStateComponent({ showsVerticalScrollIndicator={showsVerticalScrollIndicator} contentContainerStyle={[{minHeight: minModalHeight}, styles.flexGrow1, styles.flexShrink0, containerStyles]} style={styles.flex1} + addBottomSafeAreaPadding={addBottomSafeAreaPadding} > = { lottieWebViewStyles?: React.CSSProperties | undefined; minModalHeight?: number; showsVerticalScrollIndicator?: boolean; + + /** Whether to add bottom safe area padding to the view. */ + addBottomSafeAreaPadding?: boolean; }; type MediaType = SharedProps & { diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 613d786230f2..0307fdb90284 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -115,6 +115,12 @@ type FeatureTrainingModalSVGProps = { /** Determines how the image should be resized to fit its container */ contentFitImage?: ImageContentFit; + + /** The width of the image */ + imageWidth?: number; + + /** The height of the image */ + imageHeight?: number; }; // This page requires either an icon or a video/animation, but not both @@ -143,6 +149,8 @@ function FeatureTrainingModal({ contentInnerContainerStyles, contentOuterContainerStyles, modalInnerContainerStyle, + imageWidth, + imageHeight, isModalDisabled = true, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); @@ -211,6 +219,8 @@ function FeatureTrainingModal({ )} {!!videoURL && videoStatus === 'video' && ( @@ -241,6 +251,8 @@ function FeatureTrainingModal({ ); }, [ image, + imageHeight, + imageWidth, contentFitImage, illustrationAspectRatio, styles.w100, diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 90adaec7a27e..fe9179fa88b4 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -1,7 +1,8 @@ import type {ReactNode} from 'react'; -import React from 'react'; +import React, {useMemo} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; type FixedFooterProps = { @@ -10,16 +11,41 @@ type FixedFooterProps = { /** Styles to be assigned to Container */ style?: StyleProp; + + /** Whether to add bottom safe area padding to the content. */ + addBottomSafeAreaPadding?: boolean; + + /** Whether to stick the footer to the bottom of the screen. */ + shouldStickToBottom?: boolean; }; -function FixedFooter({style, children}: FixedFooterProps) { +function FixedFooter({style, children, addBottomSafeAreaPadding = false, shouldStickToBottom = false}: FixedFooterProps) { const styles = useThemeStyles(); + const {paddingBottom} = useSafeAreaPaddings(true); + + const footerStyle = useMemo>(() => { + const totalPaddingBottom = styles.pb5.paddingBottom + paddingBottom; + + // If the footer should stick to the bottom, we use absolute positioning instead of flex. + // In this case, we need to use style.bottom instead of style.paddingBottom. + if (shouldStickToBottom) { + return {position: 'absolute', left: 0, right: 0, bottom: addBottomSafeAreaPadding ? totalPaddingBottom : styles.pb5.paddingBottom}; + } + + // If the footer should not stick to the bottom, we use flex and add the safe area padding in styles.paddingBottom. + if (addBottomSafeAreaPadding) { + return {paddingBottom: totalPaddingBottom}; + } + + // Otherwise, we just use the default bottom padding. + return styles.pb5; + }, [addBottomSafeAreaPadding, paddingBottom, shouldStickToBottom, styles.pb5]); if (!children) { return null; } - return {children}; + return {children}; } FixedFooter.displayName = 'FixedFooter'; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index bf3746b61776..6c8d3f989e16 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -75,6 +75,12 @@ type FormProviderProps = FormProps(null); const formContentRef = useRef(null); @@ -103,13 +110,78 @@ function FormWrapper({ focusInput?.focus?.(); }, [errors, formState?.errorFields, inputRefs]); + const {paddingBottom} = useSafeAreaPaddings(true); + const SubmitButton = useMemo( + () => + isSubmitButtonVisible && ( + + ), + [ + disablePressOnEnter, + enabledWhenOffline, + errorMessage, + errors, + footerContent, + formState?.errorFields, + formState?.isLoading, + isLoading, + isSubmitActionDangerous, + isSubmitButtonVisible, + isSubmitDisabled, + onFixTheErrorsLinkPressed, + onSubmit, + paddingBottom, + shouldHideFixErrorsAlert, + shouldSubmitButtonStickToBottom, + style, + styles.flex1, + styles.mh0, + styles.mt5, + styles.pb5.paddingBottom, + submitButtonStyles, + submitButtonText, + submitFlexEnabled, + ], + ); + const scrollViewContent = useCallback( () => ( { if (!shouldScrollToEnd) { return; @@ -122,77 +194,50 @@ function FormWrapper({ }} > {children} - {isSubmitButtonVisible && ( - - )} + {!shouldSubmitButtonStickToBottom && SubmitButton} ), - [ - formID, - style, - safeAreaInsetPaddingBottom, - styles.pb5.paddingBottom, - styles.mh0, - styles.mt5, - styles.flex1, - children, - isSubmitButtonVisible, - submitButtonText, - isSubmitDisabled, - errors, - formState?.errorFields, - formState?.isLoading, - shouldHideFixErrorsAlert, - errorMessage, - isLoading, - onSubmit, - footerContent, - onFixTheErrorsLinkPressed, - submitFlexEnabled, - submitButtonStyles, - enabledWhenOffline, - isSubmitActionDangerous, - disablePressOnEnter, - shouldScrollToEnd, - ], + [formID, style, styles.pb5, children, shouldSubmitButtonStickToBottom, SubmitButton, shouldScrollToEnd], ); if (!shouldUseScrollView) { + if (shouldSubmitButtonStickToBottom) { + return ( + <> + {scrollViewContent()} + {SubmitButton} + + ); + } + return scrollViewContent(); } - return scrollContextEnabled ? ( - - {scrollViewContent()} - - ) : ( - - {scrollViewContent()} - + return ( + + {scrollContextEnabled ? ( + + {scrollViewContent()} + + ) : ( + + {scrollViewContent()} + + )} + {shouldSubmitButtonStickToBottom && SubmitButton} + ); } diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index fabb5e54cb60..3eb0b0abd508 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -62,6 +62,12 @@ type FormAlertWithSubmitButtonProps = { /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ enterKeyEventListenerPriority?: number; + + /** + * Whether the button should have a background layer in the color of theme.appBG. + * This is needed for buttons that allow content to display under them. + */ + shouldBlendOpacity?: boolean; }; function FormAlertWithSubmitButton({ @@ -83,6 +89,7 @@ function FormAlertWithSubmitButton({ useSmallerSubmitButtonSize = false, errorMessageStyle, enterKeyEventListenerPriority = 0, + shouldBlendOpacity = false, }: FormAlertWithSubmitButtonProps) { const styles = useThemeStyles(); const style = [!footerContent ? {} : styles.mb3, buttonStyles]; @@ -107,6 +114,7 @@ function FormAlertWithSubmitButton({ {isOffline && !enabledWhenOffline ? (