diff --git a/android/app/build.gradle b/android/app/build.gradle index 46d9b09fe89f..ce5c92aec37f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006503 - versionName "9.0.65-3" + versionCode 1009006505 + versionName "9.0.65-5" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md deleted file mode 100644 index b231984f61e2..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Create and Pay Bills -description: Expensify bill management and payment methods. ---- -Streamline your operations by receiving and paying vendor or supplier bills directly in Expensify. Vendors can send bills even if they don't have an Expensify account, and you can manage payments seamlessly. - -# Receive Bills in Expensify -You can receive bills in three ways: -- Directly from Vendors: Provide your Expensify billing email to vendors. -- Forwarding Emails: Forward bills received in your email to Expensify. -- Manual Upload: For physical bills, create a Bill in Expensify from the Reports page. - -# Bill Pay Workflow -1. When a vendor or supplier sends a bill to Expensify, the document is automatically SmartScanned, and a Bill is created. This Bill is managed by the primary domain contact, who can view it on the Reports page within their default group workspace. - -2. Once the Bill is ready for processing, it follows the established approval workflow. As each person approves it, the Bill appears in the next approver’s Inbox. The final approver will pay the Bill using one of the available payment methods. - -3. During this process, the Bill is coded with the appropriate GL codes from your connected accounting software. After completing the approval workflow, the Bill can be exported back to your accounting system. - -# Payment Methods -There are multiple ways to pay Bills in Expensify. Let’s go over each method below. - -## ACH bank-to-bank transfer - -To use this payment method, you must have a [business bank account connected to your Expensify account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account). - -**To pay with an ACH bank-to-bank transfer:** -1. Sign in to your [Expensify web account](www.expensify.com). -2. Go to the Home or Reports page and locate the Bill that needs to be paid. -3. Click the Pay button to be redirected to the Bill. -4. Choose the ACH option from the drop-down list. - -**Fees:** None - -## Credit or Debit Card -This option is available to all US and International customers receiving a bill from a US vendor with a US business bank account. - -**To pay with a credit or debit card:** -1. Sign in to your [Expensify web account](www.expensify.com). -2. Click on the Bill you’d like to pay to see the details. -3. Click the Pay button. -4. Enter your credit card or debit card details. - -**Fees:** 2.9% of the total amount paid. - -## Venmo -If both you and the vendor must have Venmo connected to Expensify, you can pay the bill by following the steps outlined [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments). - -**Fees:** Venmo charges a 3% sender’s fee. - - -## Pay outside of Expensify -If you are unable to pay using one of the above methods, you can still mark the Bill as paid. This will update its status to indicate that the payment was made outside Expensify. - -**To mark a Bill as paid outside of Expensify:** -1. Sign in to your [Expensify web account](www.expensify.com). -2. Click on the Bill you’d like to pay to see the details. -3. Click the Reimburse button. -4. Choose **I’ll do it manually**. - -**Fees:** None. - -{% include faq-begin.md %} - -## Who receives vendor bills in Expensify? -Bills are sent to the Primary Contact listed under **Settings > Domains > [Domain Name] > Domain Admins**. - -## Who can view and pay a Bill? -Only the primary domain contact can view and pay a Bill. - -## How can others access Bills? -The primary contact can share Bills or grant Copilot access for others to manage payments. - -## Is Bill Pay supported internationally? -Currently, payments are only supported in USD. - -## What's the difference between a Bill and an Invoice in Expensify? -A Bill represents a payable amount owed to a vendor, while an Invoice is a receivable amount owed to you. -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md new file mode 100644 index 000000000000..328b7f2051bc --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md @@ -0,0 +1,111 @@ +--- +title: Receive and Pay Bills +description: Expensify bill management and payment methods. +--- + +Easily receive and pay vendor or supplier bills directly in Expensify. Your vendors don’t even need an Expensify account! Manage everything seamlessly in one place. + +# Receiving Bills + +Expensify makes it easy to receive bills in three simple ways: + +### 1. Directly from Vendors +Share your Expensify billing email with vendors to receive bills automatically. + +- Set a Primary Contact under **Settings > Domains > Domain Admins**. +- Ask vendors to email bills to your billing address: `domainname@expensify.cash` (e.g., for *expensify.com*, use `expensify@expensify.cash`). +- Once emailed, the bill is automatically created in Expensify, ready for payment. + +### 2. Forwarding Emails +Received a bill in your email? Forward it to Expensify. + +- Ensure your Primary Contact is set under **Settings > Domains > Domain Admins**. +- Forward bills to `domainname@expensify.cash`. Example: `domainname@expensify.cash` (e.g., for *expensify.com*, use `expensify@expensify.cash`). +- Expensify will create a bill automatically, ready for payment. + +### 3. Manual Upload +Got a paper bill? Create a bill manually in [Expensify](https://www.expensify.com/): + +1. Log in to [Expensify](https://www.expensify.com). +2. Go to **Reports > New Report > Bill**. +3. Enter the invoice details: sender’s email, merchant name, amount, and date. +4. Upload the invoice as a receipt. + + +# Paying Bills in Expensify + +Expensify makes it easy to manage and pay vendor bills with a straightforward workflow and flexible payment options. Here’s how it works: + +## Bill Pay Workflow + +1. **SmartScan & Create**: When a vendor sends a bill, Expensify automatically SmartScans the document and creates a bill. +2. **Submission to Primary Contact**: The bill is submitted to the primary contact, who can review it on the Reports page under their default group policy. +3. **Communication**: If the approver needs clarification, they can communicate directly with the sender via the invoice linked to the bill. +4. **Approval Workflow**: Once reviewed, the bill follows your workspace’s approval process. The final approver handles the payment. +5. **Accounting Integration**: During approval, the bill is coded with the correct GL codes from your connected accounting software. Once approved, it can be exported back to your accounting system. + +## Payment Methods + +Expensify offers several ways to pay bills. Choose the method that works best for you: + +### 1. ACH Bank-to-Bank Transfer + +Fast and fee-free, this method requires a connected [business bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account). + +**How to Pay via ACH:** +1. Log in to your [Expensify web account](https://www.expensify.com/). +2. Find the bill on the Home or Reports page. +3. Click **Pay** and select the ACH option. + +**Fees:** None. + +--- + +### 2. Credit or Debit Card + +Pay vendors using a credit or debit card. This option is available for US and international customers paying US vendors with a US business bank account. + +**How to Pay with a Card:** +1. Log in to your [Expensify web account](https://www.expensify.com/). +2. Open the bill details and click **Pay**. +3. Enter your card information to complete the payment. + +**Fees:** 2.9% of the total amount paid. + +--- + +### 3. Venmo + +If both you and the vendor have Venmo accounts connected to Expensify, you can pay through Venmo. Learn how to set up Venmo [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments). + +**Fees:** Venmo charges a 3% sender’s fee. + +--- + +### 4. Pay Outside Expensify + +If you prefer to pay outside Expensify, you can still track the payment within the platform. + +**How to Mark as Paid Outside Expensify:** +1. Log in to your [Expensify web account](https://www.expensify.com/). +2. Open the bill details and click **Pay**. +3. Select **Mark as Paid** to update its status. + +**Fees:** None. +{% include faq-begin.md %} + +## Who receives vendor bills in Expensify? +bills are sent to the Primary Contact listed under **Settings > Domains > [Domain Name] > Domain Admins**. + +## Who can view and pay a bill? +Only the primary domain contact can view and pay a bill. + +## How can others access bills? +The primary contact can share bills or grant Copilot access for others to manage payments. + +## Is bill Pay supported internationally? +Currently, payments are only supported in USD. + +## What's the difference between a bill and an Invoice in Expensify? +A bill represents a payable amount owed to a vendor, while an Invoice is a receivable amount owed to you. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md index 516497c9dce7..4a4ec72a671a 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md +++ b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md @@ -80,6 +80,9 @@ Wait until the end of the second business day. If you still don’t see them, pl Once that's all set, make sure to contact your account manager or concierge, and our team will be able to re-trigger those three test transactions! +## Is my data safe? + +We take security seriously. Our measures align with what banks use to protect sensitive financial data. We regularly test and update our security to stay ahead of any threats. Plus, we’re checked daily by McAfee for extra reassurance against hackers. You can verify our security strength below or on the McAfee SECURE site. Discover how Expensify safeguards your information [here](https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security). {% include faq-end.md %} diff --git a/docs/redirects.csv b/docs/redirects.csv index 5c83d510ccb8..e1c0e12eb070 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -603,3 +603,4 @@ https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-T https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates,https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports/ https://help.expensify.com/articles/expensify-classic/spending-insights/Other-Export-Options,https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports/ https://help.expensify.com/articles/expensify-classic/travel/Edit-or-cancel-travel-arrangements,https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify-Travel +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills diff --git a/help/README.md b/help/README.md index d62513f07f53..c0fb4dbf524a 100644 --- a/help/README.md +++ b/help/README.md @@ -48,7 +48,7 @@ Every PR pushed by an authorized Expensify employee or representative will autom 3. Install Ruby and Jekyll 4. Build the entire site using Jekyll 5. Create a "preview" of the newly built site in Cloudflare -6. Record a link to that preview in the PR. +6. Record a link to that preview in the PR ## How to deploy the site for real Whenever a PR that touches the `/help` directory is merged, it will re-run the build just like before. However, it will detect that this build is being run from the `main` branch, and thus push the changes to the `production` Cloudflare environment -- meaning, it will replace the contents hosted at https://newhelp.expensify.com diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7f65a14b4d6e..48ab1740093e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.65.3 + 9.0.65.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 38d7dea5a9ce..c283e62e44af 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.65.3 + 9.0.65.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index b53c5eb457a1..7db594f06494 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.65 CFBundleVersion - 9.0.65.3 + 9.0.65.5 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 01aefab63378..5ea5b19896e4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2661,7 +2661,7 @@ PODS: - RNSound/Core (= 0.11.2) - RNSound/Core (0.11.2): - React-Core - - RNSVG (15.6.0): + - RNSVG (15.9.0): - DoubleConversion - glog - hermes-engine @@ -2681,9 +2681,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.6.0) + - RNSVG/common (= 15.9.0) - Yoga - - RNSVG/common (15.6.0): + - RNSVG/common (15.9.0): - DoubleConversion - glog - hermes-engine @@ -3295,7 +3295,7 @@ SPEC CHECKSUMS: RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 - RNSVG: 1079f96b39a35753d481a20e30603fd6fc4f6fa9 + RNSVG: b2fbe96b2bb3887752f8abc1f495953847e90384 SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c diff --git a/package-lock.json b/package-lock.json index 60c3db01596c..318b08eaf217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.65-3", + "version": "9.0.65-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.65-3", + "version": "9.0.65-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -111,7 +111,7 @@ "react-native-screens": "3.34.0", "react-native-share": "11.0.2", "react-native-sound": "^0.11.2", - "react-native-svg": "15.6.0", + "react-native-svg": "15.9.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", @@ -36031,9 +36031,10 @@ } }, "node_modules/react-native-svg": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.6.0.tgz", - "integrity": "sha512-TUtR+h+yi1ODsd8FHdom1TpjfWOmnaK5pri5rnSBXnMqpzq8o2zZfonHTjPX+nS3wb/Pu2XsoARgYaHNjVWXhQ==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.9.0.tgz", + "integrity": "sha512-pwo7hteAM0P8jNpPGQtiSd0SnbBhE8tNd94LT8AcZcbnH5AJdXBIcXU4+tWYYeGUjiNAH2E5d0T5XIfnvaz1gA==", + "license": "MIT", "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", diff --git a/package.json b/package.json index 05fe606b9f91..57de821f096e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.65-3", + "version": "9.0.65-5", "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.", @@ -168,7 +168,7 @@ "react-native-screens": "3.34.0", "react-native-share": "11.0.2", "react-native-sound": "^0.11.2", - "react-native-svg": "15.6.0", + "react-native-svg": "15.9.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", diff --git a/src/CONST.ts b/src/CONST.ts index 06feb863a80a..496b7d8b28ec 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2843,6 +2843,7 @@ const CONST = { AMEX_CUSTOM_FEED: { CORPORATE: 'American Express Corporate Cards', BUSINESS: 'American Express Business Cards', + PERSONAL: 'American Express Personal Cards', }, DELETE_TRANSACTIONS: { RESTRICT: 'corporate', @@ -2969,8 +2970,8 @@ const CONST = { // eslint-disable-next-line max-len, no-misleading-character-class EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, - // eslint-disable-next-line max-len, no-misleading-character-class - EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu, + // eslint-disable-next-line max-len, no-misleading-character-class, no-empty-character-class + EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/du, // eslint-disable-next-line max-len, no-misleading-character-class EMOJI_SKIN_TONES: /[\u{1f3fb}-\u{1f3ff}]/gu, @@ -3007,6 +3008,10 @@ const CONST = { return new RegExp(`[\\n\\s]|${this.SPECIAL_CHAR.source}|${this.EMOJI.source}`, 'gu'); }, + get ALL_EMOJIS() { + return new RegExp(this.EMOJIS, this.EMOJIS.flags.concat('g')); + }, + MERGED_ACCOUNT_PREFIX: /^(MERGED_\d+@)/, ROUTES: { VALIDATE_LOGIN: /\/v($|(\/\/*))/, @@ -5959,6 +5964,9 @@ const CONST = { ACTION_TYPES: { VIEW: 'view', REVIEW: 'review', + SUBMIT: 'submit', + APPROVE: 'approve', + PAY: 'pay', DONE: 'done', PAID: 'paid', }, @@ -6294,10 +6302,17 @@ const CONST = { }, DEBUG: { + FORMS: { + REPORT: 'report', + REPORT_ACTION: 'reportAction', + TRANSACTION: 'transaction', + TRANSACTION_VIOLATION: 'transactionViolation', + }, DETAILS: 'details', JSON: 'json', REPORT_ACTIONS: 'actions', REPORT_ACTION_PREVIEW: 'preview', + TRANSACTION_VIOLATIONS: 'violations', }, REPORT_IN_LHN_REASONS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b4510a2faeed..f97edbd744eb 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -716,10 +716,6 @@ const ONYXKEYS = { RULES_MAX_EXPENSE_AMOUNT_FORM_DRAFT: 'rulesMaxExpenseAmountFormDraft', RULES_MAX_EXPENSE_AGE_FORM: 'rulesMaxExpenseAgeForm', RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft', - DEBUG_REPORT_PAGE_FORM: 'debugReportPageForm', - DEBUG_REPORT_PAGE_FORM_DRAFT: 'debugReportPageFormDraft', - DEBUG_REPORT_ACTION_PAGE_FORM: 'debugReportActionPageForm', - DEBUG_REPORT_ACTION_PAGE_FORM_DRAFT: 'debugReportActionPageFormDraft', DEBUG_DETAILS_FORM: 'debugDetailsForm', DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft', }, @@ -814,9 +810,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AMOUNT_FORM]: FormTypes.RulesMaxExpenseAmountForm; [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm; [ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm; - [ONYXKEYS.FORMS.DEBUG_REPORT_PAGE_FORM]: FormTypes.DebugReportForm; - [ONYXKEYS.FORMS.DEBUG_REPORT_ACTION_PAGE_FORM]: FormTypes.DebugReportActionForm; - [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm; + [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bdf4d4774ec1..d8f8b0f91105 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1295,6 +1295,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/per-diem', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem` as const, }, + WORKSPACE_PER_DIEM_SETTINGS: { + route: 'settings/workspaces/:policyID/per-diem/settings', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, @@ -1777,13 +1781,46 @@ const ROUTES = { getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/preview` as const, }, DETAILS_CONSTANT_PICKER_PAGE: { - route: 'debug/details/constant/:fieldName', - getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/constant/${fieldName}?fieldValue=${fieldValue}`, backTo), + route: 'debug/:formType/details/constant/:fieldName', + getRoute: (formType: string, fieldName: string, fieldValue?: string, policyID?: string, backTo?: string) => + getUrlWithBackToParam(`debug/${formType}/details/constant/${fieldName}?fieldValue=${fieldValue}&policyID=${policyID}`, backTo), }, DETAILS_DATE_TIME_PICKER_PAGE: { route: 'debug/details/datetime/:fieldName', getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/datetime/${fieldName}?fieldValue=${fieldValue}`, backTo), }, + DEBUG_TRANSACTION: { + route: 'debug/transaction/:transactionID', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}` as const, + }, + DEBUG_TRANSACTION_TAB_DETAILS: { + route: 'debug/transaction/:transactionID/details', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/details` as const, + }, + DEBUG_TRANSACTION_TAB_JSON: { + route: 'debug/transaction/:transactionID/json', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/json` as const, + }, + DEBUG_TRANSACTION_TAB_VIOLATIONS: { + route: 'debug/transaction/:transactionID/violations', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/violations` as const, + }, + DEBUG_TRANSACTION_VIOLATION_CREATE: { + route: 'debug/transaction/:transactionID/violations/create', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/violations/create` as const, + }, + DEBUG_TRANSACTION_VIOLATION: { + route: 'debug/transaction/:transactionID/violations/:index', + getRoute: (transactionID: string, index: string) => `debug/transaction/${transactionID}/violations/${index}` as const, + }, + DEBUG_TRANSACTION_VIOLATION_TAB_DETAILS: { + route: 'debug/transaction/:transactionID/violations/:index/details', + getRoute: (transactionID: string, index: string) => `debug/transaction/${transactionID}/violations/${index}/details` as const, + }, + DEBUG_TRANSACTION_VIOLATION_TAB_JSON: { + route: 'debug/transaction/:transactionID/violations/:index/json', + getRoute: (transactionID: string, index: string) => `debug/transaction/${transactionID}/violations/${index}/json` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 543e8708fea3..5fd64b0fc0d0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -546,6 +546,7 @@ const SCREENS = { RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age', RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default', PER_DIEM: 'Per_Diem', + PER_DIEM_SETTINGS: 'Per_Diem_Settings', }, EDIT_REQUEST: { @@ -620,6 +621,9 @@ const SCREENS = { REPORT_ACTION_CREATE: 'Debug_Report_Action_Create', DETAILS_CONSTANT_PICKER_PAGE: 'Debug_Details_Constant_Picker_Page', DETAILS_DATE_TIME_PICKER_PAGE: 'Debug_Details_Date_Time_Picker_Page', + TRANSACTION: 'Debug_Transaction', + TRANSACTION_VIOLATION_CREATE: 'Debug_Transaction_Violation_Create', + TRANSACTION_VIOLATION: 'Debug_Transaction_Violation', }, } as const; diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 9a90de17595d..ed2eae7a0a4c 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -10,6 +10,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate'; +import * as EmojiUtils from '@libs/EmojiUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import variables from '@styles/variables'; @@ -46,6 +47,7 @@ function AccountSwitcher() { const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate; + const processedTextArray = EmojiUtils.splitTextWithEmojis(currentUserPersonalDetails?.displayName); const createBaseMenuItem = ( personalDetails: PersonalDetails | undefined, @@ -149,7 +151,9 @@ function AccountSwitcher() { numberOfLines={1} style={[styles.textBold, styles.textLarge, styles.flexShrink1]} > - {currentUserPersonalDetails?.displayName} + {processedTextArray.length !== 0 + ? EmojiUtils.getProcessedText(processedTextArray, styles.initialSettingsUsernameEmoji) + : currentUserPersonalDetails?.displayName} {!!canSwitchAccounts && ( diff --git a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx b/src/components/CategorySelector/CategorySelectorModal.tsx similarity index 100% rename from src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx rename to src/components/CategorySelector/CategorySelectorModal.tsx diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/components/CategorySelector/index.tsx similarity index 100% rename from src/pages/workspace/distanceRates/CategorySelector/index.tsx rename to src/components/CategorySelector/index.tsx diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index be875790d75e..e71ade65e66d 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -19,6 +19,7 @@ import * as Browser from '@libs/Browser'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; const excludeNoStyles: Array = []; @@ -70,6 +71,7 @@ function Composer( start: selectionProp.start, end: selectionProp.end, }); + const [hasMultipleLines, setHasMultipleLines] = useState(false); const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); @@ -328,10 +330,10 @@ function Composer( scrollStyleMemo, StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), isComposerFullSize ? {height: '100%', maxHeight: 'none'} : undefined, - textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}, + textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}, ], - [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], + [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, hasMultipleLines, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); return ( @@ -350,6 +352,9 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} + onContentSizeChange={(e) => { + setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge); + }} disabled={isDisabled} onKeyPress={handleKeyPress} addAuthTokenToImageURLCallback={addEncryptedAuthTokenToURL} diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 201ed7bab730..1e8b5294286f 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -1,6 +1,6 @@ import {Str} from 'expensify-common'; import React, {useCallback, useMemo, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import SelectableListItem from '@components/SelectionList/SelectableListItem'; @@ -8,17 +8,17 @@ import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; +import type {CurrencyListItem, CurrencySelectionListProps} from './types'; function CurrencySelectionList({ searchInputLabel, initiallySelectedCurrencyCode, onSelect, - currencyList, selectedCurrencies = [], canSelectMultiple = false, recentlyUsedCurrencies, }: CurrencySelectionListProps) { + const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); const getUnselectedOptions = useCallback((options: CurrencyListItem[]) => options.filter((option) => !option.isSelected), []); @@ -107,8 +107,4 @@ function CurrencySelectionList({ CurrencySelectionList.displayName = 'CurrencySelectionList'; -const CurrencySelectionListWithOnyx = withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, -})(CurrencySelectionList); - -export default CurrencySelectionListWithOnyx; +export default CurrencySelectionList; diff --git a/src/components/CurrencySelectionList/types.ts b/src/components/CurrencySelectionList/types.ts index 3001b0ceeaab..5cfef604ab94 100644 --- a/src/components/CurrencySelectionList/types.ts +++ b/src/components/CurrencySelectionList/types.ts @@ -1,18 +1,11 @@ -import type {OnyxEntry} from 'react-native-onyx'; import type {ListItem} from '@components/SelectionList/types'; -import type {CurrencyList} from '@src/types/onyx'; type CurrencyListItem = ListItem & { currencyName: string; currencyCode: string; }; -type CurrencySelectionListOnyxProps = { - /** List of available currencies */ - currencyList: OnyxEntry; -}; - -type CurrencySelectionListProps = CurrencySelectionListOnyxProps & { +type CurrencySelectionListProps = { /** Label for the search text input */ searchInputLabel: string; @@ -32,4 +25,4 @@ type CurrencySelectionListProps = CurrencySelectionListOnyxProps & { canSelectMultiple?: boolean; }; -export type {CurrencyListItem, CurrencySelectionListProps, CurrencySelectionListOnyxProps}; +export type {CurrencyListItem, CurrencySelectionListProps}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index 31d092800d20..879684210825 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -1,11 +1,22 @@ -import React from 'react'; +import React, {useMemo} from 'react'; +import type {TextStyle} from 'react-native'; import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; import EmojiWithTooltip from '@components/EmojiWithTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; function EmojiRenderer({tnode, style: styleProp}: CustomRendererProps) { const styles = useThemeStyles(); - const style = {...styleProp, ...('islarge' in tnode.attributes ? styles.onlyEmojisText : {})}; + const style = useMemo(() => { + if ('islarge' in tnode.attributes) { + return [styleProp as TextStyle, styles.onlyEmojisText]; + } + + if ('ismedium' in tnode.attributes) { + return [styleProp as TextStyle, styles.emojisWithTextFontSize, styles.verticalAlignTopText]; + } + + return null; + }, [tnode.attributes, styles, styleProp]); return ( { const lineWidth = getLinedWidth(itemIndex); diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index b59d1604a5aa..a98683b68b41 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -7,6 +7,7 @@ import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types' const PopoverContext = createContext({ onOpen: () => {}, popover: null, + popoverAnchor: null, close: () => {}, isOpen: false, }); @@ -21,6 +22,7 @@ function elementContains(ref: RefObject | undefined, function PopoverContextProvider(props: PopoverContextProps) { const [isOpen, setIsOpen] = useState(false); const activePopoverRef = useRef(null); + const [activePopoverAnchor, setActivePopoverAnchor] = useState(null); const closePopover = useCallback((anchorRef?: RefObject): boolean => { if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) { @@ -30,6 +32,7 @@ function PopoverContextProvider(props: PopoverContextProps) { activePopoverRef.current.close(); activePopoverRef.current = null; setIsOpen(false); + setActivePopoverAnchor(null); return true; }, []); @@ -108,6 +111,7 @@ function PopoverContextProvider(props: PopoverContextProps) { closePopover(activePopoverRef.current.anchorRef); } activePopoverRef.current = popoverParams; + setActivePopoverAnchor(popoverParams.anchorRef.current); setIsOpen(true); }, [closePopover], @@ -119,9 +123,10 @@ function PopoverContextProvider(props: PopoverContextProps) { close: closePopover, // eslint-disable-next-line react-compiler/react-compiler popover: activePopoverRef.current, + popoverAnchor: activePopoverAnchor, isOpen, }), - [onOpen, closePopover, isOpen], + [onOpen, closePopover, isOpen, activePopoverAnchor], ); return {props.children}; diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index b3d21e9ed5d9..04b0b8f90305 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -9,6 +9,7 @@ type PopoverContextProps = { type PopoverContextValue = { onOpen?: (popoverParams: AnchorRef) => void; popover?: AnchorRef | null; + popoverAnchor?: AnchorRef['anchorRef']['current']; close: (anchorRef?: RefObject) => void; isOpen: boolean; }; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index dfc88840446f..de20575aeef4 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -122,7 +122,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo return ( !!report) as string[]) ?? []; + const reportIDList = selectedReports?.filter((report) => !!report) ?? []; SearchActions.exportSearchItemsToCSV( {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => { diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 74bf7b16d020..130ad7ae6f6e 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -24,6 +24,13 @@ type SelectedTransactionInfo = { /** Model of selected results */ type SelectedTransactions = Record; +/** Model of payment data used by Search bulk actions */ +type PaymentData = { + reportID: string; + amount: number; + paymentType: ValueOf; +}; + type SortOrder = ValueOf; type SearchColumnType = ValueOf; type ExpenseSearchStatus = ValueOf; @@ -117,5 +124,6 @@ export type { TripSearchStatus, ChatSearchStatus, SearchAutocompleteResult, + PaymentData, SearchAutocompleteQueryRange, }; diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 66d648f1b472..0e12e993cc79 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -487,7 +487,8 @@ function BaseSelectionList( const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { const normalizedIndex = index + (section?.indexOffset ?? 0); const isDisabled = !!section.isDisabled || item.isDisabled; - const isItemFocused = (!isDisabled || item.isSelected) && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? '')); + const isItemFocused = (!isDisabled || item.isSelected) && focusedIndex === normalizedIndex; + const isItemHighlighted = !!itemsToHighlight?.has(item.keyForList ?? ''); // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = shouldShowTooltips && normalizedIndex < 10; @@ -495,7 +496,10 @@ function BaseSelectionList( onItemLayout(event, item?.keyForList)}> ( * @param timeout - The timeout in milliseconds before removing the highlight. */ const scrollAndHighlightItem = useCallback( - (items: string[], timeout: number) => { + (items: string[]) => { const newItemsToHighlight = new Set(); items.forEach((item) => { newItemsToHighlight.add(item); }); const index = flattenedSections.allOptions.findIndex((option) => newItemsToHighlight.has(option.keyForList ?? '')); - updateAndScrollToFocusedIndex(index); + scrollToIndex(index); setItemsToHighlight(newItemsToHighlight); if (itemFocusTimeoutRef.current) { clearTimeout(itemFocusTimeoutRef.current); } + const duration = + CONST.ANIMATED_HIGHLIGHT_ENTRY_DELAY + + CONST.ANIMATED_HIGHLIGHT_ENTRY_DURATION + + CONST.ANIMATED_HIGHLIGHT_START_DELAY + + CONST.ANIMATED_HIGHLIGHT_START_DURATION + + CONST.ANIMATED_HIGHLIGHT_END_DELAY + + CONST.ANIMATED_HIGHLIGHT_END_DURATION; itemFocusTimeoutRef.current = setTimeout(() => { - setFocusedIndex(-1); setItemsToHighlight(null); - }, timeout); + }, duration); }, - [flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex], + [flattenedSections.allOptions, scrollToIndex], ); /** diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index faafa6159dc1..0a360a96e7c7 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -4,6 +4,7 @@ import Badge from '@components/Badge'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +16,9 @@ import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; const actionTranslationsMap: Record = { view: 'common.view', review: 'common.review', + submit: 'common.submit', + approve: 'iou.approve', + pay: 'iou.pay', done: 'common.done', paid: 'iou.settledExpensify', }; @@ -26,13 +30,23 @@ type ActionCellProps = { goToItem: () => void; isChildListItem?: boolean; parentAction?: string; + isLoading?: boolean; }; -function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false, goToItem, isChildListItem = false, parentAction = ''}: ActionCellProps) { +function ActionCell({ + action = CONST.SEARCH.ACTION_TYPES.VIEW, + isLargeScreenWidth = true, + isSelected = false, + goToItem, + isChildListItem = false, + parentAction = '', + isLoading = false, +}: ActionCellProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {isOffline} = useNetwork(); const text = translate(actionTranslationsMap[action]); @@ -61,9 +75,8 @@ function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth ); } - const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {}; - if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) { + const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {}; return isLargeScreenWidth ? (