diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index 1e63fdcb2d52..b3adf0f59b9c 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -80,6 +80,10 @@ "/": "/search/*", "comment": "Search" }, + { + "/": "/send/*", + "comment": "Send money" + }, { "/": "/money2020/*", "comment": "Money 2020" diff --git a/android/app/build.gradle b/android/app/build.gradle index b8f59db4aecf..508e464c5c91 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001038601 - versionName "1.3.86-1" + versionCode 1001038603 + versionName "1.3.86-3" } flavorDimensions "default" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7419d5b1e1a7..74e91caa91d5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -70,6 +70,7 @@ + @@ -88,6 +89,7 @@ + diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index c6733ac11715..84735e95e0e9 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -44,16 +44,21 @@ platforms: icon: /assets/images/hand-card.svg description: Explore how the Expensify Card combines convenience and security to enhance everyday business transactions. Discover how to apply for, oversee, and maximize your card perks here. - - href: exports - title: Exports - icon: /assets/images/monitor.svg - description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. + - href: expensify-partner-program + title: Expensify Partner Program + icon: /assets/images/handshake.svg + description: Discover how to get the most out of Expensify as an ExpensifyApproved! accountant partner. Learn how to set up your clients, receive CPE credits, and take advantage of your partner discount. - href: get-paid-back title: Get Paid Back icon: /assets/images/money-into-wallet.svg description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. + - href: insights-and-custom-reporting + title: Insights & Custom Reporting + icon: /assets/images/monitor.svg + description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. + - href: integrations title: Integrations icon: /assets/images/workflow.svg @@ -64,15 +69,15 @@ platforms: icon: /assets/images/envelope-receipt.svg description: Master the art of overseeing employees and reports by utilizing Expensify’s automation features and approval workflows. - - href: policy-and-domain-settings - title: Policy & Domain Settings - icon: /assets/images/shield.svg - description: Discover how to set up and manage policies, define user permissions, and implement compliance rules to maintain a secure and compliant financial management landscape. - - href: send-payments title: Send Payments icon: /assets/images/money-wings.svg description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. + + - href: workspace-and-domain-settings + title: Workspace & Domain Settings + icon: /assets/images/shield.svg + description: Discover how to set up and manage workspace, define user permissions, and implement compliance rules to maintain a secure and compliant financial management landscape. - href: new-expensify title: New Expensify @@ -113,16 +118,21 @@ platforms: icon: /assets/images/hand-card.svg description: Explore how the Expensify Card combines convenience and security to enhance everyday business transactions. Discover how to apply for, oversee, and maximize your card perks here. - - href: exports - title: Exports - icon: /assets/images/monitor.svg - description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. + - href: expensify-partner-program + title: Expensify Partner Program + icon: /assets/images/handshake.svg + description: Discover how to get the most out of Expensify as an ExpensifyApproved! accountant partner. Learn how to set up your clients, receive CPE credits, and take advantage of your partner discount. - href: get-paid-back title: Get Paid Back icon: /assets/images/money-into-wallet.svg description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. + - href: insights-and-custom-reporting + title: Insights & Custom Reporting + icon: /assets/images/monitor.svg + description: From exporting reports to creating custom templates, here is where you can learn more about Expensify's versatile export options. + - href: integrations title: Integrations icon: /assets/images/workflow.svg diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Add-a-Business-Bank-Account-(AUD).md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Add-a-Business-Bank-Account-(AUD).md deleted file mode 100644 index 1fa5734293ac..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Add-a-Business-Bank-Account-(AUD).md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Add-a-Business-Bank-Account-(AUD).md -description: This article provides insight on setting up and using an Australian Business Bank account in Expensify. ---- - -# How to add an Australian business bank account (for admins) -A withdrawal account is the business bank account that you want to use to pay your employee reimbursements. - -_Your policy currency must be set to AUD and reimbursement setting set to Indirect to continue. If your main policy is used for something other than AUD, then you will need to create a new one and set that policy to AUD._ - -To set this up, you’ll run through the following steps: - -1. Go to **Settings > Your Account > Payments** and click **Add Verified Bank Account** -![Click the Verified Bank Account button in the bottom right-hand corner of the screen](https://help.expensify.com/assets/images/add-vba-australian-account.png){:width="100%"} - -2. Enter the required information to connect to your business bank account. If you don't know your Bank User ID/Direct Entry ID/APCA Number, please contact your bank and they will be able to provide this. -![Enter your information in each of the required fields](https://help.expensify.com/assets/images/add-vba-australian-account-modal.png){:width="100%"} - -3. Link the withdrawal account to your policy by heading to **Settings > Policies > Group > [Policy name] > Reimbursement** -4. Click **Direct reimbursement** -5. Set the default withdrawal account for processing reimbursements -6. Tell your employees to add their deposit accounts and start reimbursing. - -# How to delete a bank account -If you’re no longer using a bank account you previously connected to Expensify, you can delete it by doing the following: - -1. Navigate to Settings > Accounts > Payments -2. Click **Delete** -![Click the Delete button](https://help.expensify.com/assets/images/delete-australian-bank-account.png){:width="100%"} - -You can complete this process either via the web app (on a computer), or via the mobile app. - -# Deep Dive -## Bank-specific batch payment support - -If you are new to using Batch Payments in Australia, to reimburse your staff or process payroll, you may want to check out these bank-specific instructions for how to upload your .aba file: - -- ANZ Bank - [Import a file for payroll payments](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/) -- CommBank - [Importing and using
 Direct Entry (EFT) files](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) -- Westpac - [Importing Payment Files](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/) -- NAB - [Quick Reference Guide - Upload a payment file](https://www.nab.com.au/business/online-banking/nab-connect/help) -- Bendigo Bank - [Bulk payments user guide](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf) -- Bank of Queensland - [Payments file upload facility FAQ](https://www.boq.com.au/help-and-support/online-banking/ob-faqs-and-support/faq-pfuf) - -**Note:** Some financial institutions require an ABA file to include a *self-balancing transaction*. If you are unsure, please check with your bank to ensure whether to tick this option or not, as selecting an incorrect option will result in the ABA file not working with your bank's internet banking platform. - -## Enable Global Reimbursement - -If you have employees in other countries outside of Australia, you can now reimburse them directly using Global Reimbursement. - -To do this, you’ll first need to delete any existing Australian business bank accounts. Then, you’ll want to follow the instructions to enable Global Reimbursements diff --git a/docs/articles/new-expensify/exports/Coming-Soon.md b/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md similarity index 100% rename from docs/articles/new-expensify/exports/Coming-Soon.md rename to docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md diff --git a/docs/articles/expensify-classic/exports/Custom-Templates.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates.md similarity index 100% rename from docs/articles/expensify-classic/exports/Custom-Templates.md rename to docs/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates.md diff --git a/docs/articles/expensify-classic/exports/Default-Export-Templates.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md similarity index 100% rename from docs/articles/expensify-classic/exports/Default-Export-Templates.md rename to docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md diff --git a/docs/articles/expensify-classic/exports/Insights.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md similarity index 100% rename from docs/articles/expensify-classic/exports/Insights.md rename to docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md diff --git a/docs/articles/expensify-classic/exports/Other-Export-Options.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md similarity index 100% rename from docs/articles/expensify-classic/exports/Other-Export-Options.md rename to docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Categories.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Categories.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Domains-Overview.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Expenses.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Invoicing.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Invoicing.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Invoicing.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Invoicing.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Per-Diem.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Per-Diem.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Reimbursement.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Reimbursement.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/SAML-SSO.md b/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/SAML-SSO.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Tags.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/Tags.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md b/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Currency.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/reports/Currency.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit.md diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md b/docs/articles/expensify-classic/workspace-and-domain-settings/tax-tracking.md similarity index 100% rename from docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md rename to docs/articles/expensify-classic/workspace-and-domain-settings/tax-tracking.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Grab.md b/docs/articles/new-expensify/expensify-partner-program/Coming-Soon.md similarity index 67% rename from docs/articles/expensify-classic/integrations/travel-integrations/Grab.md rename to docs/articles/new-expensify/expensify-partner-program/Coming-Soon.md index 3ee1c8656b4b..6b85bb0364b5 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Grab.md +++ b/docs/articles/new-expensify/expensify-partner-program/Coming-Soon.md @@ -2,4 +2,3 @@ title: Coming Soon description: Coming Soon --- -## Resource Coming Soon! diff --git a/docs/articles/new-expensify/insights-and-custom-reporting/Coming-Soon.md b/docs/articles/new-expensify/insights-and-custom-reporting/Coming-Soon.md new file mode 100644 index 000000000000..6b85bb0364b5 --- /dev/null +++ b/docs/articles/new-expensify/insights-and-custom-reporting/Coming-Soon.md @@ -0,0 +1,4 @@ +--- +title: Coming Soon +description: Coming Soon +--- diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_1.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_1.png new file mode 100644 index 000000000000..d4e73beb16b3 Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_1.png differ diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_2.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_2.png new file mode 100644 index 000000000000..45956a586d98 Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_2.png differ diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_3.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_3.png new file mode 100644 index 000000000000..32aae12d3687 Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_3.png differ diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_4.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_4.png new file mode 100644 index 000000000000..ccd9335025bf Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_4.png differ diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_5.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_5.png new file mode 100644 index 000000000000..5363935f0ab5 Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_5.png differ diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_6.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_6.png new file mode 100644 index 000000000000..739446de8383 Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_6.png differ diff --git a/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_7.png b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_7.png new file mode 100644 index 000000000000..21a1d3416858 Binary files /dev/null and b/docs/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_7.png differ diff --git a/docs/assets/images/handshake.svg b/docs/assets/images/handshake.svg new file mode 100644 index 000000000000..04872bd3a88b --- /dev/null +++ b/docs/assets/images/handshake.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/expensify-classic/hubs/expensify-partner-program/index.html b/docs/expensify-classic/hubs/expensify-partner-program/index.html new file mode 100644 index 000000000000..c0a192c6e916 --- /dev/null +++ b/docs/expensify-classic/hubs/expensify-partner-program/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Expensify Partner Program +--- + +{% include hub.html %} diff --git a/docs/expensify-classic/hubs/exports/index.html b/docs/expensify-classic/hubs/insights-and-custom-reporting/index.html similarity index 100% rename from docs/expensify-classic/hubs/exports/index.html rename to docs/expensify-classic/hubs/insights-and-custom-reporting/index.html diff --git a/docs/expensify-classic/hubs/policy-and-domain-settings/index.html b/docs/expensify-classic/hubs/workspace-and-domain-settings/index.html similarity index 100% rename from docs/expensify-classic/hubs/policy-and-domain-settings/index.html rename to docs/expensify-classic/hubs/workspace-and-domain-settings/index.html diff --git a/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html b/docs/expensify-classic/hubs/workspace-and-domain-settings/reports.html similarity index 100% rename from docs/expensify-classic/hubs/policy-and-domain-settings/reports.html rename to docs/expensify-classic/hubs/workspace-and-domain-settings/reports.html diff --git a/docs/new-expensify/hubs/expensify-partner-program/index.html b/docs/new-expensify/hubs/expensify-partner-program/index.html new file mode 100644 index 000000000000..c0a192c6e916 --- /dev/null +++ b/docs/new-expensify/hubs/expensify-partner-program/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Expensify Partner Program +--- + +{% include hub.html %} diff --git a/docs/new-expensify/hubs/exports/index.html b/docs/new-expensify/hubs/insights-and-custom-reporting/index.html similarity index 100% rename from docs/new-expensify/hubs/exports/index.html rename to docs/new-expensify/hubs/insights-and-custom-reporting/index.html diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 32d356a96cf8..33079da4c508 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.86.1 + 1.3.86.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7fa3d841d5d8..659006d0017b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.86.1 + 1.3.86.3 diff --git a/package-lock.json b/package-lock.json index b575c151364d..adf12552b48a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.86-1", + "version": "1.3.86-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.86-1", + "version": "1.3.86-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 993b8d165ed0..6b605cd1dbc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.86-1", + "version": "1.3.86-3", "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/CONST.ts b/src/CONST.ts index e2f3fea08215..d239f1004835 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1098,7 +1098,7 @@ const CONST = { EXPENSIFY: 'Expensify', VBBA: 'ACH', }, - MONEY_REQUEST_TYPE: { + TYPE: { SEND: 'send', SPLIT: 'split', REQUEST: 'request', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index c3003699378c..ad8b60700e39 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -299,6 +299,7 @@ const ONYXKEYS = { PRIVATE_NOTES_FORM: 'privateNotesForm', I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm', INTRO_SCHOOL_PRINCIPAL_FORM: 'introSchoolPrincipalForm', + REPORT_PHYSICAL_CARD_FORM: 'requestPhysicalCardForm', REPORT_VIRTUAL_CARD_FRAUD: 'reportVirtualCardFraudForm', }, } as const; @@ -389,7 +390,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; - [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportAction; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: string; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; @@ -430,6 +431,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_AFTER_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a677b7192fac..3b7bc2546fe2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -90,6 +90,10 @@ export default { }, SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', + SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { + route: '/settings/wallet/card/:domain/report-card-lost-or-damaged', + getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged`, + }, SETTINGS_WALLET_CARD_ACTIVATE: { route: 'settings/wallet/card/:domain/activate', getRoute: (domain: string) => `settings/wallet/card/${domain}/activate`, @@ -210,7 +214,7 @@ export default { getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`, }, - // To see the available iouType, please refer to CONST.IOU.MONEY_REQUEST_TYPE + // To see the available iouType, please refer to CONST.IOU.TYPE MONEY_REQUEST: { route: ':iouType/new/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}`, diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index 13abf057e4b1..e7f68e7011fc 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -63,7 +63,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC return categoryInitialFocusedIndex; }, [selectedCategory, searchValue, isCategoriesCountBelowThreshold, sections]); - const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, searchValue); + const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, searchValue); const shouldShowTextInput = !isCategoriesCountBelowThreshold; return ( diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 7c7837b8413d..f866de0b885e 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -31,7 +31,7 @@ const defaultProps = { function DistanceEReceipt({transaction}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename) : {}; + const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : translate('common.tbd'); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index 3023a9abf95c..0dc967d257d2 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -28,12 +28,18 @@ function EmojiPickerButtonDropdown(props) { const emojiPopoverAnchor = useRef(null); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); - const onPress = () => + const onPress = () => { + if (EmojiPickerAction.isEmojiPickerVisible()) { + EmojiPickerAction.hideEmojiPicker(); + return; + } + EmojiPickerAction.showEmojiPicker(props.onModalHide, (emoji) => props.onInputChange(emoji), emojiPopoverAnchor.current, { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, shiftVertical: 4, }); + }; return ( @@ -44,7 +50,7 @@ function EmojiPickerButtonDropdown(props) { onPress={onPress} nativeID="emojiDropdownButton" accessibilityLabel="statusEmoji" - accessibilityRole="text" + accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} > {({hovered, pressed}) => ( diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index fefacc385116..5ca08bf82f89 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -171,7 +171,7 @@ const defaultProps = { onConfirm: () => {}, onSendMoney: () => {}, onSelectParticipant: () => {}, - iouType: CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, + iouType: CONST.IOU.TYPE.REQUEST, iouCategory: '', iouTag: '', iouIsBillable: false, @@ -208,9 +208,9 @@ function MoneyRequestConfirmationList(props) { const {translate, toLocaleDigit} = useLocalize(); const transaction = props.isEditingSplitBill ? props.draftTransaction || props.transaction : props.transaction; - const isTypeRequest = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST; - const isSplitBill = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT; - const isTypeSend = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND; + const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; + const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND; const isSplitWithScan = isSplitBill && props.isScanRequest; @@ -445,7 +445,7 @@ function MoneyRequestConfirmationList(props) { return; } - if (props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND) { + if (props.iouType === CONST.IOU.TYPE.SEND) { if (!paymentMethod) { return; } @@ -491,7 +491,7 @@ function MoneyRequestConfirmationList(props) { return; } - const shouldShowSettlementButton = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND; + const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; const shouldDisableButton = selectedParticipants.length === 0; const button = shouldShowSettlementButton ? ( @@ -536,7 +536,8 @@ function MoneyRequestConfirmationList(props) { ); }, [confirm, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions, translate, formError]); - const {image: receiptImage, thumbnail: receiptThumbnail} = props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; + const {image: receiptImage, thumbnail: receiptThumbnail} = + props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; return ( {}, withoutOverlay: false, + shouldSetModalVisibility: true, }; function PopoverMenu(props) { @@ -89,6 +93,7 @@ function PopoverMenu(props) { disableAnimation={props.disableAnimation} fromSidebarMediumScreen={props.fromSidebarMediumScreen} withoutOverlay={props.withoutOverlay} + shouldSetModalVisibility={props.shouldSetModalVisibility} > {!_.isEmpty(props.headerText) && {props.headerText}} diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js index 7c8444a5d5b9..23a27682a7d4 100644 --- a/src/components/ReportWelcomeText.js +++ b/src/components/ReportWelcomeText.js @@ -133,7 +133,7 @@ function ReportWelcomeText(props) { ))} )} - {(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)) && ( + {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && ( {props.translate('reportActionsView.usePlusButton')} )} diff --git a/src/components/SelectCircle.js b/src/components/SelectCircle.js index 93cf285eab59..55e410f8baa1 100644 --- a/src/components/SelectCircle.js +++ b/src/components/SelectCircle.js @@ -9,15 +9,20 @@ import themeColors from '../styles/themes/default'; const propTypes = { /** Should we show the checkmark inside the circle */ isChecked: PropTypes.bool, + + /** Additional styles to pass to SelectCircle */ + // eslint-disable-next-line react/forbid-prop-types + styles: PropTypes.arrayOf(PropTypes.object), }; const defaultProps = { isChecked: false, + styles: [], }; function SelectCircle(props) { return ( - + {props.isChecked && ( {}, +}; + +function SingleOptionSelector({options, selectedOptionKey, onSelectOption, translate}) { + return ( + + {_.map(options, (option) => ( + + onSelectOption(option)} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityState={{checked: selectedOptionKey === option.key}} + aria-checked={selectedOptionKey === option.key} + accessibilityLabel={option.label} + > + + {translate(option.label)} + + + ))} + + ); +} + +SingleOptionSelector.propTypes = propTypes; +SingleOptionSelector.defaultProps = defaultProps; +SingleOptionSelector.displayName = 'SingleOptionSelector'; + +export default withLocalize(SingleOptionSelector); diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js index 4efa033c60d0..3483ec10f804 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.js @@ -1,5 +1,5 @@ import {View} from 'react-native'; -import React from 'react'; +import React, {useMemo, useState} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import * as Expensicons from '../Icon/Expensicons'; @@ -53,7 +53,7 @@ const getIconAndTitle = (route, translate) => { } }; -const getOpacity = (position, routesLength, tabIndex, active) => { +const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => { const activeValue = active ? 1 : 0; const inactiveValue = active ? 0 : 1; @@ -62,19 +62,19 @@ const getOpacity = (position, routesLength, tabIndex, active) => { return position.interpolate({ inputRange, - outputRange: _.map(inputRange, (i) => (i === tabIndex ? activeValue : inactiveValue)), + outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), }); } return activeValue; }; -const getBackgroundColor = (position, routesLength, tabIndex) => { +const getBackgroundColor = (position, routesLength, tabIndex, affectedTabs) => { if (routesLength > 1) { const inputRange = Array.from({length: routesLength}, (v, i) => i); return position.interpolate({ inputRange, - outputRange: _.map(inputRange, (i) => (i === tabIndex ? themeColors.border : themeColors.appBG)), + outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? themeColors.border : themeColors.appBG)), }); } return themeColors.border; @@ -82,12 +82,23 @@ const getBackgroundColor = (position, routesLength, tabIndex) => { function TabSelector({state, navigation, onTabPress, position}) { const {translate} = useLocalize(); + + const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]); + const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs); + + React.useEffect(() => { + // It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition. + setTimeout(() => { + setAffectedAnimatedTabs(defaultAffectedAnimatedTabs); + }, CONST.ANIMATED_TRANSITION); + }, [defaultAffectedAnimatedTabs, state.index]); + return ( {_.map(state.routes, (route, index) => { - const activeOpacity = getOpacity(position, state.routes.length, index, true); - const inactiveOpacity = getOpacity(position, state.routes.length, index, false); - const backgroundColor = getBackgroundColor(position, state.routes.length, index); + const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs); + const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs); + const backgroundColor = getBackgroundColor(position, state.routes.length, index, affectedAnimatedTabs); const isFocused = index === state.index; const {icon, title} = getIconAndTitle(route.name, translate); @@ -96,6 +107,8 @@ function TabSelector({state, navigation, onTabPress, position}) { return; } + setAffectedAnimatedTabs([state.index, index]); + const event = navigation.emit({ type: 'tabPress', target: route.key, diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js index 8e7cf11f7e5a..05eca664bd0f 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.js @@ -53,7 +53,7 @@ function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubm [searchValue, selectedOptions, policyTagList, policyRecentlyUsedTagsList], ); - const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, ''); + const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, ''); return ( { + /* Keep the focus state on mWeb like we did on the native apps. */ + if (!Browser.isMobile()) { + return; + } + e.preventDefault(); + }} ref={buttonRef} style={[styles.touchableButtonImage, ...iconStyles]} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} @@ -111,6 +123,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me onItemSelected={hidePopoverMenu} menuItems={menuItems} withoutOverlay={!shouldOverlay} + shouldSetModalVisibility={shouldSetModalVisibility} anchorRef={buttonRef} /> diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index f052116697b3..d89c9bc7a953 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -118,7 +118,6 @@ function BaseVideoChatButtonAndMenu(props) { left: videoChatIconPosition.x - 150, top: videoChatIconPosition.y + 40, }} - shouldSetModalVisibility={false} withoutOverlay anchorRef={videoChatButtonRef} > diff --git a/src/languages/en.ts b/src/languages/en.ts index e7f71e755dd8..c7295b523010 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1874,6 +1874,20 @@ export default { selectSuggestedAddress: 'Please select a suggested address or use current location', }, }, + reportCardLostOrDamaged: { + report: 'Report physical card loss / damage', + screenTitle: 'Report card lost or damaged', + nextButtonLabel: 'Next', + reasonTitle: 'Why do you need a new card?', + cardDamaged: 'My card was damaged', + cardLostOrStolen: 'My card was lost or stolen', + confirmAddressTitle: "Please confirm the address below is where you'd like us to send your new card.", + currentCardInfo: 'Your current card will be permanently deactivated as soon as your order is placed. Most cards arrive in a few business days.', + address: 'Address', + deactivateCardButton: 'Deactivate card', + addressError: 'Address is required', + reasonError: 'Reason is required', + }, eReceipt: { guaranteed: 'Guaranteed eReceipt', transactionDate: 'Transaction date', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6020ded30b92..4a24b2243e03 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2359,6 +2359,20 @@ export default { selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.', }, }, + reportCardLostOrDamaged: { + report: 'Notificar la pérdida / daño de la tarjeta física', + screenTitle: 'Notificar la pérdida o deterioro de la tarjeta', + nextButtonLabel: 'Siguiente', + reasonTitle: '¿Por qué necesitas una tarjeta nueva?', + cardDamaged: 'Mi tarjeta está dañada', + cardLostOrStolen: 'He perdido o me han robado la tarjeta', + confirmAddressTitle: 'Confirma que la dirección que aparece a continuación es a la que deseas que te enviemos tu nueva tarjeta.', + currentCardInfo: 'La tarjeta actual se desactivará permanentemente en cuanto se realice el pedido. La mayoría de las tarjetas llegan en unos pocos días laborables.', + address: 'Dirección', + deactivateCardButton: 'Desactivar tarjeta', + addressError: 'La dirección es obligatoria', + reasonError: 'Se requiere justificación', + }, eReceipt: { guaranteed: 'eRecibo garantizado', transactionDate: 'Fecha de transacción', diff --git a/src/languages/types.ts b/src/languages/types.ts index 9560cd41b25f..5f8093e96520 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -1,3 +1,4 @@ +import {ReportAction} from '../types/onyx'; import en from './en'; type AddressLineParams = { @@ -42,15 +43,15 @@ type LocalTimeParams = { }; type EditActionParams = { - action: NonNullable; + action: ReportAction | null; }; type DeleteActionParams = { - action: NonNullable; + action: ReportAction | null; }; type DeleteConfirmationParams = { - action: NonNullable; + action: ReportAction | null; }; type BeginningOfChatHistoryDomainRoomPartOneParams = { diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index c8ea03cc86c0..eb9a85697137 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -4,6 +4,7 @@ import CONST from '../CONST'; import * as Localize from './Localize'; import * as OnyxTypes from '../types/onyx'; import ONYXKEYS, {OnyxValues} from '../ONYXKEYS'; +import {Card} from '../types/onyx'; let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {}; Onyx.connect({ @@ -88,4 +89,13 @@ function maskCard(lastFour = ''): string { return maskedString.replace(/(.{4})/g, '$1 ').trim(); } -export {isExpensifyCard, getDomainCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard, getCardDescription}; +/** + * Finds physical card in a list of cards + * + * @returns a physical card object (or undefined if none is found) + */ +function findPhysicalCard(cards: Card[]) { + return cards.find((card) => !card.isVirtual); +} + +export {isExpensifyCard, getDomainCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard, getCardDescription, findPhysicalCard}; diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js index a44a69f087ab..344d0c3bd397 100644 --- a/src/libs/EmojiUtils.js +++ b/src/libs/EmojiUtils.js @@ -110,6 +110,23 @@ function trimEmojiUnicode(emojiCode) { return emojiCode.replace(/(fe0f|1f3fb|1f3fc|1f3fd|1f3fe|1f3ff)$/, '').trim(); } +/** + * Validates first character is emoji in text string + * + * @param {String} message + * @returns {Boolean} + */ +function isFirstLetterEmoji(message) { + const trimmedMessage = Str.replaceAll(message.replace(/ /g, ''), '\n', ''); + const match = trimmedMessage.match(CONST.REGEX.EMOJIS); + + if (!match) { + return false; + } + + return trimmedMessage.indexOf(match[0]) === 0; +} + /** * Validates that this message contains only emojis * @@ -497,4 +514,5 @@ export { replaceAndExtractEmojis, extractEmojis, getAddedEmojis, + isFirstLetterEmoji, }; diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 2425211d16bc..d8a916d0dfb0 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -77,7 +77,7 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { * Checks if the iou type is one of request, send, or split. */ function isValidMoneyRequestType(iouType: string): boolean { - const moneyRequestType: string[] = [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, CONST.IOU.MONEY_REQUEST_TYPE.SPLIT, CONST.IOU.MONEY_REQUEST_TYPE.SEND]; + const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND]; return moneyRequestType.includes(iouType); } diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index ffa765621110..420184973a34 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -78,8 +78,8 @@ function replaceAllDigits(text: string, convertFn: (char: string) => string): st /** * Check if distance request or not */ -function isDistanceRequest(iouType: ValueOf, selectedTab: ValueOf): boolean { - return iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST && selectedTab === CONST.TAB.DISTANCE; +function isDistanceRequest(iouType: ValueOf, selectedTab: ValueOf): boolean { + return iouType === CONST.IOU.TYPE.REQUEST && selectedTab === CONST.TAB.DISTANCE; } /** diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 54c7b9b8396e..7c8403cc9534 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -169,6 +169,7 @@ const SettingsModalStackNavigator = createModalStackNavigator({ ReimbursementAccount: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, GetAssistance: () => require('../../../pages/GetAssistancePage').default, Settings_TwoFactorAuth: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default, + Settings_ReportCardLostOrDamaged: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default, KeyboardShortcuts: () => require('../../../pages/KeyboardShortcutsPage').default, }); diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 8a68ec9c0d07..c578e2097175 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -93,6 +93,10 @@ export default { path: ROUTES.SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT, exact: true, }, + Settings_ReportCardLostOrDamaged: { + path: ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.route, + exact: true, + }, Settings_Wallet_Card_Activate: { path: ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.route, exact: true, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 886f6862f4a9..82714dbcbe11 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1528,6 +1528,20 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma return ''; } +/** + * Helper method for non-user lists (eg. categories and tags) that returns the text to be used for the header's message and title (if any) + * + * @param {Boolean} hasSelectableOptions + * @param {String} searchValue + * @return {String} + */ +function getHeaderMessageForNonUserList(hasSelectableOptions, searchValue) { + if (searchValue && !hasSelectableOptions) { + return Localize.translate(preferredLocale, 'common.noResultsFound'); + } + return ''; +} + /** * Helper method to check whether an option can show tooltip or not * @param {Object} option @@ -1547,6 +1561,7 @@ export { getShareDestinationOptions, getMemberInviteOptions, getHeaderMessage, + getHeaderMessageForNonUserList, getPersonalDetailsForAccountIDs, getIOUConfirmationOptionsFromPayeePersonalDetail, getIOUConfirmationOptionsFromParticipants, diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 9fa7ebdc6559..13e8a195cccb 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -24,12 +24,14 @@ type FileNameAndExtension = { * Grab the appropriate receipt image and thumbnail URIs based on file type * * @param transaction + * @param receiptPath + * @param receiptFileName */ -function getThumbnailAndImageURIs(transaction: Transaction): ThumbnailAndImageURI { +function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg - const path = transaction?.receipt?.source ?? ''; + const path = transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI - const filename = transaction?.filename ?? ''; + const filename = transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); const hasEReceipt = transaction?.hasEReceipt; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.ts similarity index 51% rename from src/libs/ReportActionsUtils.js rename to src/libs/ReportActionsUtils.ts index d0f0b35d5f9a..1f71b290e386 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.ts @@ -1,16 +1,23 @@ -/* eslint-disable rulesdir/prefer-underscore-method */ -import lodashGet from 'lodash/get'; -import _ from 'underscore'; -import {max, parseISO, isEqual} from 'date-fns'; +import {isEqual, max, parseISO} from 'date-fns'; import lodashFindLast from 'lodash/findLast'; -import Onyx from 'react-native-onyx'; -import * as CollectionUtils from './CollectionUtils'; +import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; +import ReportAction, {ReportActions} from '../types/onyx/ReportAction'; +import Report from '../types/onyx/Report'; +import {ActionName} from '../types/onyx/OriginalMessage'; +import * as CollectionUtils from './CollectionUtils'; import Log from './Log'; import isReportMessageAttachment from './isReportMessageAttachment'; -const allReports = {}; +type LastVisibleMessage = { + lastMessageTranslationKey?: string; + lastMessageText: string; + lastMessageHtml?: string; +}; + +const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { @@ -23,7 +30,7 @@ Onyx.connect({ }, }); -const allReportActions = {}; +const allReportActions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { @@ -39,134 +46,85 @@ Onyx.connect({ let isNetworkOffline = false; Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: (val) => (isNetworkOffline = lodashGet(val, 'isOffline', false)), + callback: (val) => (isNetworkOffline = val?.isOffline ?? false), }); -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isCreatedAction(reportAction) { - return lodashGet(reportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; +function isCreatedAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isDeletedAction(reportAction) { +function isDeletedAction(reportAction: OnyxEntry): boolean { // A deleted comment has either an empty array or an object with html field with empty string as value - const message = lodashGet(reportAction, 'message', []); - return message.length === 0 || lodashGet(message, [0, 'html']) === ''; + const message = reportAction?.message ?? []; + return message.length === 0 || message[0]?.html === ''; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isDeletedParentAction(reportAction) { - return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false) && lodashGet(reportAction, 'childVisibleActionCount', 0) > 0; +function isDeletedParentAction(reportAction: OnyxEntry): boolean { + return (reportAction?.message?.[0]?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isReversedTransaction(reportAction) { - return lodashGet(reportAction, ['message', 0, 'isReversedTransaction'], false) && lodashGet(reportAction, 'childVisibleActionCount', 0) > 0; +function isReversedTransaction(reportAction: OnyxEntry) { + return (reportAction?.message?.[0].isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isPendingRemove(reportAction) { - return lodashGet(reportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; +function isPendingRemove(reportAction: OnyxEntry): boolean { + return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isMoneyRequestAction(reportAction) { - return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.IOU; +function isMoneyRequestAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isReportPreviewAction(reportAction) { - return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; +function isReportPreviewAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; } -/** - * @param {Object} reportAction - * @returns {Boolean} - */ -function isModifiedExpenseAction(reportAction) { - return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; +function isModifiedExpenseAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; } -function isWhisperAction(action) { - return (action.whisperedToAccountIDs || []).length > 0; +function isWhisperAction(reportAction: OnyxEntry): boolean { + return (reportAction?.whisperedToAccountIDs ?? []).length > 0; } /** * Returns whether the comment is a thread parent message/the first message in a thread - * - * @param {Object} reportAction - * @param {String} reportID - * @returns {Boolean} */ -function isThreadParentMessage(reportAction = {}, reportID) { - const {childType, childVisibleActionCount = 0, childReportID} = reportAction; +function isThreadParentMessage(reportAction: OnyxEntry, reportID: string): boolean { + const {childType, childVisibleActionCount = 0, childReportID} = reportAction ?? {}; return childType === CONST.REPORT.TYPE.CHAT && (childVisibleActionCount > 0 || String(childReportID) === reportID); } /** * Returns the parentReportAction if the given report is a thread/task. * - * @param {Object} report - * @param {Object} [allReportActionsParam] - * @returns {Object} * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getParentReportAction(report, allReportActionsParam = undefined) { - if (!report || !report.parentReportID || !report.parentReportActionID) { +function getParentReportAction(report: OnyxEntry, allReportActionsParam?: OnyxCollection): ReportAction | Record { + if (!report?.parentReportID || !report.parentReportActionID) { return {}; } - return lodashGet(allReportActionsParam || allReportActions, [report.parentReportID, report.parentReportActionID], {}); + return (allReportActionsParam ?? allReportActions)?.[report.parentReportID]?.[report.parentReportActionID] ?? {}; } /** * Determines if the given report action is sent money report action by checking for 'pay' type and presence of IOUDetails object. - * - * @param {Object} reportAction - * @returns {Boolean} */ -function isSentMoneyReportAction(reportAction) { +function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { return ( - reportAction && - reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.PAY && - _.has(reportAction.originalMessage, 'IOUDetails') + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!reportAction?.originalMessage?.IOUDetails ); } /** * Returns whether the thread is a transaction thread, which is any thread with IOU parent * report action from requesting money (type - create) or from sending money (type - pay with IOUDetails field) - * - * @param {Object} parentReportAction - * @returns {Boolean} */ -function isTransactionThread(parentReportAction) { - const originalMessage = lodashGet(parentReportAction, 'originalMessage', {}); +function isTransactionThread(parentReportAction: OnyxEntry): boolean { return ( - parentReportAction && - parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'))) + parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || + (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!parentReportAction.originalMessage.IOUDetails)) ); } @@ -174,67 +132,56 @@ function isTransactionThread(parentReportAction) { * Sort an array of reportActions by their created timestamp first, and reportActionID second * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * - * @param {Array} reportActions - * @param {Boolean} shouldSortInDescendingOrder - * @returns {Array} */ -function getSortedReportActions(reportActions, shouldSortInDescendingOrder = false) { - if (!_.isArray(reportActions)) { +function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] { + if (!Array.isArray(reportActions)) { throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`); } const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1; - return _.chain(reportActions) - .compact() - .sort((first, second) => { - // First sort by timestamp - if (first.created !== second.created) { - return (first.created < second.created ? -1 : 1) * invertedMultiplier; - } - - // Then by action type, ensuring that `CREATED` actions always come first if they have the same timestamp as another action type - if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || second.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) && first.actionName !== second.actionName) { - return (first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED ? -1 : 1) * invertedMultiplier; - } - // Ensure that `REPORTPREVIEW` actions always come after if they have the same timestamp as another action type - if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW || second.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) && first.actionName !== second.actionName) { - return (first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? 1 : -1) * invertedMultiplier; - } - - // Then fallback on reportActionID as the final sorting criteria. It is a random number, - // but using this will ensure that the order of reportActions with the same created time and action type - // will be consistent across all users and devices - return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; - }) - .value(); + + return reportActions.filter(Boolean).sort((first, second) => { + // First sort by timestamp + if (first.created !== second.created) { + return (first.created < second.created ? -1 : 1) * invertedMultiplier; + } + + // Then by action type, ensuring that `CREATED` actions always come first if they have the same timestamp as another action type + if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || second.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) && first.actionName !== second.actionName) { + return (first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED ? -1 : 1) * invertedMultiplier; + } + // Ensure that `REPORTPREVIEW` actions always come after if they have the same timestamp as another action type + if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW || second.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) && first.actionName !== second.actionName) { + return (first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? 1 : -1) * invertedMultiplier; + } + + // Then fallback on reportActionID as the final sorting criteria. It is a random number, + // but using this will ensure that the order of reportActions with the same created time and action type + // will be consistent across all users and devices + return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; + }); } /** * Finds most recent IOU request action ID. - * - * @param {Array} reportActions - * @returns {String} */ -function getMostRecentIOURequestActionID(reportActions) { - const iouRequestTypes = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; - const iouRequestActions = _.filter(reportActions, (action) => iouRequestTypes.includes(lodashGet(action, 'originalMessage.type'))); +function getMostRecentIOURequestActionID(reportActions: ReportAction[] | null): string | null { + const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; + const iouRequestActions = reportActions?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && iouRequestTypes.includes(action.originalMessage.type)) ?? []; - if (_.isEmpty(iouRequestActions)) { + if (iouRequestActions.length === 0) { return null; } const sortedReportActions = getSortedReportActions(iouRequestActions); - return _.last(sortedReportActions).reportActionID; + return sortedReportActions.at(-1)?.reportActionID ?? null; } /** * Returns array of links inside a given report action - * - * @param {Object} reportAction - * @returns {Array} */ -function extractLinksFromMessageHtml(reportAction) { - const htmlContent = lodashGet(reportAction, ['message', 0, 'html']); +function extractLinksFromMessageHtml(reportAction: OnyxEntry): string[] { + const htmlContent = reportAction?.message?.[0]?.html; // Regex to get link in href prop inside of component const regex = /]*?\s+)?href="([^"]*)"/gi; @@ -243,16 +190,19 @@ function extractLinksFromMessageHtml(reportAction) { return []; } - return _.map([...htmlContent.matchAll(regex)], (match) => match[1]); + return [...htmlContent.matchAll(regex)].map((match) => match[1]); } /** * Returns the report action immediately before the specified index. - * @param {Array} reportActions - all actions - * @param {Number} actionIndex - index of the action - * @returns {Object|null} + * @param reportActions - all actions + * @param actionIndex - index of the action */ -function findPreviousAction(reportActions, actionIndex) { +function findPreviousAction(reportActions: ReportAction[] | null, actionIndex: number): OnyxEntry { + if (!reportActions) { + return null; + } + for (let i = actionIndex + 1; i < reportActions.length; i++) { // Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list. // If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete. @@ -260,6 +210,7 @@ function findPreviousAction(reportActions, actionIndex) { return reportActions[i]; } } + return null; } @@ -267,13 +218,11 @@ function findPreviousAction(reportActions, actionIndex) { * Returns true when the report action immediately before the specified index is a comment made by the same actor who who is leaving a comment in the action at the specified index. * Also checks to ensure that the comment is not too old to be shown as a grouped comment. * - * @param {Array} reportActions - * @param {Number} actionIndex - index of the comment item in state to check - * @returns {Boolean} + * @param actionIndex - index of the comment item in state to check */ -function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { +function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] | null, actionIndex: number): boolean { const previousAction = findPreviousAction(reportActions, actionIndex); - const currentAction = reportActions[actionIndex]; + const currentAction = reportActions?.[actionIndex]; // It's OK for there to be no previous action, and in that case, false will be returned // so that the comment isn't grouped @@ -306,12 +255,8 @@ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { /** * Checks if a reportAction is deprecated. - * - * @param {Object} reportAction - * @param {String} key - * @returns {Boolean} */ -function isReportActionDeprecated(reportAction, key) { +function isReportActionDeprecated(reportAction: OnyxEntry, key: string): boolean { if (!reportAction) { return true; } @@ -329,12 +274,12 @@ function isReportActionDeprecated(reportAction, key) { /** * Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid * and supported type, it's not deleted and also not closed. - * - * @param {Object} reportAction - * @param {String} key - * @returns {Boolean} */ -function shouldReportActionBeVisible(reportAction, key) { +function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string): boolean { + if (!reportAction) { + return false; + } + if (isReportActionDeprecated(reportAction, key)) { return false; } @@ -343,8 +288,11 @@ function shouldReportActionBeVisible(reportAction, key) { return false; } + const {POLICYCHANGELOG: policyChangelogTypes, ...otherActionTypes} = CONST.REPORT.ACTIONS.TYPE; + const supportedActionTypes: ActionName[] = [...Object.values(otherActionTypes), ...Object.values(policyChangelogTypes)]; + // Filter out any unsupported reportAction types - if (!Object.values(CONST.REPORT.ACTIONS.TYPE).includes(reportAction.actionName) && !Object.values(CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG).includes(reportAction.actionName)) { + if (!supportedActionTypes.includes(reportAction.actionName)) { return false; } @@ -366,16 +314,13 @@ function shouldReportActionBeVisible(reportAction, key) { /** * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. - * - * @param {Object} reportAction - * @returns {Boolean} */ -function shouldReportActionBeVisibleAsLastAction(reportAction) { +function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxEntry): boolean { if (!reportAction) { return false; } - if (!_.isEmpty(reportAction.errors)) { + if (Object.keys(reportAction.errors ?? {}).length > 0) { return false; } @@ -388,42 +333,32 @@ function shouldReportActionBeVisibleAsLastAction(reportAction) { ); } -/** - * @param {String} reportID - * @param {Object} [actionsToMerge] - * @return {Object} - */ -function getLastVisibleAction(reportID, actionsToMerge = {}) { - const updatedActionsToMerge = {}; +function getLastVisibleAction(reportID: string, actionsToMerge: ReportActions = {}): OnyxEntry { + const updatedActionsToMerge: ReportActions = {}; if (actionsToMerge && Object.keys(actionsToMerge).length !== 0) { Object.keys(actionsToMerge).forEach( - (actionToMergeID) => (updatedActionsToMerge[actionToMergeID] = {...allReportActions[reportID][actionToMergeID], ...actionsToMerge[actionToMergeID]}), + (actionToMergeID) => (updatedActionsToMerge[actionToMergeID] = {...allReportActions?.[reportID]?.[actionToMergeID], ...actionsToMerge[actionToMergeID]}), ); } const actions = Object.values({ - ...allReportActions[reportID], + ...allReportActions?.[reportID], ...updatedActionsToMerge, }); const visibleActions = actions.filter((action) => shouldReportActionBeVisibleAsLastAction(action)); if (visibleActions.length === 0) { - return {}; + return null; } const maxDate = max(visibleActions.map((action) => parseISO(action.created))); const maxAction = visibleActions.find((action) => isEqual(parseISO(action.created), maxDate)); - return maxAction; + return maxAction ?? null; } -/** - * @param {String} reportID - * @param {Object} [actionsToMerge] - * @return {Object} - */ -function getLastVisibleMessage(reportID, actionsToMerge = {}) { +function getLastVisibleMessage(reportID: string, actionsToMerge: ReportActions = {}): LastVisibleMessage { const lastVisibleAction = getLastVisibleAction(reportID, actionsToMerge); - const message = lodashGet(lastVisibleAction, ['message', 0], {}); + const message = lastVisibleAction?.message?.[0]; - if (isReportMessageAttachment(message)) { + if (message && isReportMessageAttachment(message)) { return { lastMessageTranslationKey: CONST.TRANSLATION_KEYS.ATTACHMENT, lastMessageText: CONST.ATTACHMENT_MESSAGE_TEXT, @@ -437,7 +372,7 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) { }; } - const messageText = lodashGet(message, 'text', ''); + const messageText = message?.text ?? ''; return { lastMessageText: String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(), }; @@ -445,12 +380,11 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) { /** * A helper method to filter out report actions keyed by sequenceNumbers. - * - * @param {Object} reportActions - * @returns {Array} */ -function filterOutDeprecatedReportActions(reportActions) { - return _.filter(reportActions, (reportAction, key) => !isReportActionDeprecated(reportAction, key)); +function filterOutDeprecatedReportActions(reportActions: ReportActions | null): ReportAction[] { + return Object.entries(reportActions ?? {}) + .filter(([key, reportAction]) => !isReportActionDeprecated(reportAction, key)) + .map((entry) => entry[1]); } /** @@ -458,12 +392,11 @@ function filterOutDeprecatedReportActions(reportActions) { * The report actions need to be sorted by created timestamp first, and reportActionID second * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. - * - * @param {Object} reportActions - * @returns {Array} */ -function getSortedReportActionsForDisplay(reportActions) { - const filteredReportActions = _.filter(reportActions, (reportAction, key) => shouldReportActionBeVisible(reportAction, key)); +function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { + const filteredReportActions = Object.entries(reportActions ?? {}) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) + .map((entry) => entry[1]); return getSortedReportActions(filteredReportActions, true); } @@ -472,78 +405,66 @@ function getSortedReportActionsForDisplay(reportActions) { * This method returns the last closed report action so we can always show the correct archived report reason. * Additionally, archived #admins and #announce do not have the closed report action so we will return null if none is found. * - * @param {Object} reportActions - * @returns {Object|null} */ -function getLastClosedReportAction(reportActions) { +function getLastClosedReportAction(reportActions: ReportActions | null): OnyxEntry { // If closed report action is not present, return early - if (!_.some(reportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED)) { + if (!Object.values(reportActions ?? {}).some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED)) { return null; } + const filteredReportActions = filterOutDeprecatedReportActions(reportActions); const sortedReportActions = getSortedReportActions(filteredReportActions); - return lodashFindLast(sortedReportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED); + return lodashFindLast(sortedReportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) ?? null; } /** - * @param {Array} onyxData - * @returns {Object} The latest report action in the `onyxData` or `null` if one couldn't be found + * @returns The latest report action in the `onyxData` or `null` if one couldn't be found */ -function getLatestReportActionFromOnyxData(onyxData) { - const reportActionUpdate = _.find(onyxData, (onyxUpdate) => onyxUpdate.key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)); +function getLatestReportActionFromOnyxData(onyxData: OnyxUpdate[] | null): OnyxEntry { + const reportActionUpdate = onyxData?.find((onyxUpdate) => onyxUpdate.key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)); if (!reportActionUpdate) { return null; } - const reportActions = _.values(reportActionUpdate.value); + const reportActions = Object.values((reportActionUpdate.value as ReportActions) ?? {}); const sortedReportActions = getSortedReportActions(reportActions); - return _.last(sortedReportActions); + return sortedReportActions.at(-1) ?? null; } /** * Find the transaction associated with this reportAction, if one exists. - * - * @param {String} reportID - * @param {String} reportActionID - * @returns {String|null} */ -function getLinkedTransactionID(reportID, reportActionID) { - const reportAction = lodashGet(allReportActions, [reportID, reportActionID]); +function getLinkedTransactionID(reportID: string, reportActionID: string): string | null { + const reportAction = allReportActions?.[reportID]?.[reportActionID]; if (!reportAction || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return null; } - return reportAction.originalMessage.IOUTransactionID; + return reportAction.originalMessage.IOUTransactionID ?? null; } -/** - * - * @param {String} reportID - * @param {String} reportActionID - * @returns {Object} - */ -function getReportAction(reportID, reportActionID) { - return lodashGet(allReportActions, [reportID, reportActionID], {}); +function getReportAction(reportID: string, reportActionID: string): OnyxEntry { + return allReportActions?.[reportID]?.[reportActionID] ?? null; } -/** - * @returns {string} - */ -function getMostRecentReportActionLastModified() { +function getMostRecentReportActionLastModified(): string { // Start with the oldest date possible let mostRecentReportActionLastModified = new Date(0).toISOString(); // Flatten all the actions // Loop over them all to find the one that is the most recent - const flatReportActions = _.flatten(_.map(allReportActions, (actions) => _.values(actions))); - _.each(flatReportActions, (action) => { + const flatReportActions = Object.values(allReportActions ?? {}) + .flatMap((actions) => (actions ? Object.values(actions) : [])) + .filter(Boolean); + flatReportActions.forEach((action) => { // Pending actions should not be counted here as a user could create a comment or some other action while offline and the server might know about // messages they have not seen yet. - if (!_.isEmpty(action.pendingAction)) { + if (action.pendingAction) { return; } - const lastModified = action.lastModified || action.created; + const lastModified = action.lastModified ?? action.created; + if (lastModified < mostRecentReportActionLastModified) { return; } @@ -553,8 +474,8 @@ function getMostRecentReportActionLastModified() { // We might not have actions so we also look at the report objects to see if any have a lastVisibleActionLastModified that is more recent. We don't need to get // any reports that have been updated before either a recently updated report or reportAction as we should be up to date on these - _.each(allReports, (report) => { - const reportLastVisibleActionLastModified = report.lastVisibleActionLastModified || report.lastVisibleActionCreated; + Object.values(allReports ?? {}).forEach((report) => { + const reportLastVisibleActionLastModified = report?.lastVisibleActionLastModified ?? report?.lastVisibleActionCreated; if (!reportLastVisibleActionLastModified || reportLastVisibleActionLastModified < mostRecentReportActionLastModified) { return; } @@ -566,66 +487,47 @@ function getMostRecentReportActionLastModified() { } /** - * @param {*} chatReportID - * @param {*} iouReportID - * @returns {Object} The report preview action or `null` if one couldn't be found + * @returns The report preview action or `null` if one couldn't be found */ -function getReportPreviewAction(chatReportID, iouReportID) { - return _.find( - allReportActions[chatReportID], - (reportAction) => reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lodashGet(reportAction, 'originalMessage.linkedReportID') === iouReportID, +function getReportPreviewAction(chatReportID: string, iouReportID: string): OnyxEntry { + return ( + Object.values(allReportActions?.[chatReportID] ?? {}).find( + (reportAction) => reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && reportAction.originalMessage.linkedReportID === iouReportID, + ) ?? null ); } /** * Get the iouReportID for a given report action. - * - * @param {Object} reportAction - * @returns {String} */ -function getIOUReportIDFromReportActionPreview(reportAction) { - return lodashGet(reportAction, 'originalMessage.linkedReportID', ''); +function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? reportAction.originalMessage.linkedReportID : ''; } -function isCreatedTaskReportAction(reportAction) { - return reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && _.has(reportAction.originalMessage, 'taskReportID'); +function isCreatedTaskReportAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !!reportAction.originalMessage?.taskReportID; } /** * A helper method to identify if the message is deleted or not. - * - * @param {Object} reportAction - * @returns {Boolean} */ -function isMessageDeleted(reportAction) { - return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false); +function isMessageDeleted(reportAction: OnyxEntry): boolean { + return reportAction?.message?.[0]?.isDeletedParentAction ?? false; } /** * Returns the number of money requests associated with a report preview - * - * @param {Object|null} reportPreviewAction - * @returns {Number} */ -function getNumberOfMoneyRequests(reportPreviewAction) { - return lodashGet(reportPreviewAction, 'childMoneyRequestCount', 0); +function getNumberOfMoneyRequests(reportPreviewAction: OnyxEntry): number { + return reportPreviewAction?.childMoneyRequestCount ?? 0; } -/** - * @param {*} reportAction - * @returns {Boolean} - */ -function isSplitBillAction(reportAction) { - return lodashGet(reportAction, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; +function isSplitBillAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } -/** - * - * @param {*} reportAction - * @returns {Boolean} - */ -function isTaskAction(reportAction) { - const reportActionName = lodashGet(reportAction, 'actionName', ''); +function isTaskAction(reportAction: OnyxEntry): boolean { + const reportActionName = reportAction?.actionName; return ( reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || @@ -633,67 +535,76 @@ function isTaskAction(reportAction) { ); } -/** - * @param {*} reportID - * @returns {[Object]} - */ -function getAllReportActions(reportID) { - return lodashGet(allReportActions, reportID, []); +function getAllReportActions(reportID: string): ReportActions { + return allReportActions?.[reportID] ?? {}; } /** * Check whether a report action is an attachment (a file, such as an image or a zip). * - * @param {Object} reportAction report action - * @returns {Boolean} */ -function isReportActionAttachment(reportAction) { - const message = _.first(lodashGet(reportAction, 'message', [{}])); - return _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : isReportMessageAttachment(message); +function isReportActionAttachment(reportAction: OnyxEntry): boolean { + const message = reportAction?.message?.[0]; + + if (reportAction && 'isAttachment' in reportAction) { + return reportAction.isAttachment ?? false; + } + + if (message) { + return isReportMessageAttachment(message); + } + + return false; } // eslint-disable-next-line rulesdir/no-negated-variables -function isNotifiableReportAction(reportAction) { - return reportAction && _.contains([CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE], reportAction.actionName); +function isNotifiableReportAction(reportAction: OnyxEntry): boolean { + if (!reportAction) { + return false; + } + + const actions: ActionName[] = [CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE]; + + return actions.includes(reportAction.actionName); } export { - getSortedReportActions, - getLastVisibleAction, - getLastVisibleMessage, - getMostRecentIOURequestActionID, extractLinksFromMessageHtml, - isCreatedAction, - isDeletedAction, - shouldReportActionBeVisible, - shouldReportActionBeVisibleAsLastAction, - isReportActionDeprecated, - isConsecutiveActionMadeByPreviousActor, - getSortedReportActionsForDisplay, + getAllReportActions, + getIOUReportIDFromReportActionPreview, getLastClosedReportAction, + getLastVisibleAction, + getLastVisibleMessage, getLatestReportActionFromOnyxData, - isMoneyRequestAction, - isThreadParentMessage, getLinkedTransactionID, + getMostRecentIOURequestActionID, getMostRecentReportActionLastModified, + getNumberOfMoneyRequests, + getParentReportAction, + getReportAction, getReportPreviewAction, + getSortedReportActions, + getSortedReportActionsForDisplay, + isConsecutiveActionMadeByPreviousActor, + isCreatedAction, isCreatedTaskReportAction, - getParentReportAction, - isTransactionThread, - isSentMoneyReportAction, + isDeletedAction, isDeletedParentAction, - isReversedTransaction, - isReportPreviewAction, - isModifiedExpenseAction, - getIOUReportIDFromReportActionPreview, isMessageDeleted, - isWhisperAction, + isModifiedExpenseAction, + isMoneyRequestAction, + isNotifiableReportAction, isPendingRemove, - getReportAction, - getNumberOfMoneyRequests, + isReversedTransaction, + isReportActionAttachment, + isReportActionDeprecated, + isReportPreviewAction, + isSentMoneyReportAction, isSplitBillAction, isTaskAction, - getAllReportActions, - isReportActionAttachment, - isNotifiableReportAction, + isThreadParentMessage, + isTransactionThread, + isWhisperAction, + shouldReportActionBeVisible, + shouldReportActionBeVisibleAsLastAction, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index eb512b7927a9..4e351d2dc5e3 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3520,17 +3520,17 @@ function getMoneyRequestOptions(report, reportParticipants) { (hasMultipleParticipants && !isPolicyExpenseChat(report) && !isMoneyRequestReport(report)) || (isControlPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat) ) { - return [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT]; + return [CONST.IOU.TYPE.SPLIT]; } // DM chats that only have 2 people will see the Send / Request money options. // IOU and open or processing expense reports should show the Request option. // Workspace chats should only see the Request money option or Split option in case of Control policies return [ - ...(canRequestMoney(report, participants) ? [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST] : []), + ...(canRequestMoney(report, participants) ? [CONST.IOU.TYPE.REQUEST] : []), // Send money option should be visible only in DMs - ...(isChatReport(report) && !isPolicyExpenseChat(report) && hasSingleParticipantInReport ? [CONST.IOU.MONEY_REQUEST_TYPE.SEND] : []), + ...(isChatReport(report) && !isPolicyExpenseChat(report) && hasSingleParticipantInReport ? [CONST.IOU.TYPE.SEND] : []), ]; } diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index a060c1bc67fa..92b23e2103ee 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -43,6 +43,51 @@ function reportVirtualExpensifyCardFraud(cardID) { ); } +/** + * Call the API to deactivate the card and request a new one + * @param {String} cardId - id of the card that is going to be replaced + * @param {String} reason - reason for replacement ('damaged' | 'stolen') + */ +function requestReplacementExpensifyCard(cardId, reason) { + API.write( + 'RequestReplacementExpensifyCard', + { + cardId, + reason, + }, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ], + }, + ); +} + /** * Activates the physical Expensify card based on the last four digits of the card number * @@ -101,4 +146,4 @@ function clearCardListErrors(cardID) { Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}}); } -export {reportVirtualExpensifyCardFraud, activatePhysicalExpensifyCard, clearCardListErrors}; +export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud}; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b3fa78d07614..07e814f92884 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1106,7 +1106,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco oneOnOneIOUReport.reportID, comment, '', - CONST.IOU.MONEY_REQUEST_TYPE.SPLIT, + CONST.IOU.TYPE.SPLIT, splitTransaction.transactionID, undefined, undefined, @@ -1641,7 +1641,7 @@ function completeSplitBill(chatReportID, reportAction, updatedTransaction, sessi oneOnOneIOUReport.reportID, updatedTransaction.comment.comment, updatedTransaction.modifiedCreated, - CONST.IOU.MONEY_REQUEST_TYPE.SPLIT, + CONST.IOU.TYPE.SPLIT, transactionID, updatedTransaction.modifiedMerchant, {...updatedTransaction.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, @@ -2006,7 +2006,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView } updatedIOUReport.lastMessageText = iouReportLastMessageText; - updatedIOUReport.lastVisibleActionCreated = lastVisibleAction.created; + updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); updatedReportPreviewAction = {...reportPreviewAction}; const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); @@ -2068,7 +2068,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView hasOutstandingIOU: false, iouReportID: null, lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).lastMessageText, - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).created, + lastVisibleActionCreated: lodashGet(ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}), 'created'), }, }, ] diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index c9f3ba6318db..f0977345cdea 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -375,8 +375,8 @@ function addActions(reportID, text = '', file) { const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(reportID); if (lastMessageText || lastMessageTranslationKey) { const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID); - const lastVisibleActionCreated = lastVisibleAction.created; - const lastActorAccountID = lastVisibleAction.actorAccountID; + const lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); + const lastActorAccountID = lodashGet(lastVisibleAction, 'actorAccountID'); failureReport = { lastMessageTranslationKey, lastMessageText, @@ -1057,8 +1057,8 @@ function deleteReportComment(reportID, reportAction) { const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions); if (lastMessageText || lastMessageTranslationKey) { const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions); - const lastVisibleActionCreated = lastVisibleAction.created; - const lastActorAccountID = lastVisibleAction.actorAccountID; + const lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); + const lastActorAccountID = lodashGet(lastVisibleAction, 'actorAccountID'); optimisticReport = { lastMessageTranslationKey, lastMessageText, @@ -1239,7 +1239,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { ]; const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions); - if (reportActionID === lastVisibleAction.reportActionID) { + if (reportActionID === lodashGet(lastVisibleAction, 'reportActionID')) { const lastMessageText = ReportUtils.formatReportLastMessageText(reportComment); const optimisticReport = { lastMessageTranslationKey: '', diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts index c257e1db4191..8d1112261d1f 100644 --- a/src/libs/isReportMessageAttachment.ts +++ b/src/libs/isReportMessageAttachment.ts @@ -1,10 +1,5 @@ import CONST from '../CONST'; - -type IsReportMessageAttachmentParams = { - text: string; - html: string; - translationKey: string; -}; +import {Message} from '../types/onyx/ReportAction'; /** * Check whether a report action is Attachment or not. @@ -12,7 +7,7 @@ type IsReportMessageAttachmentParams = { * * @param reportActionMessage report action's message as text, html and translationKey */ -export default function isReportMessageAttachment({text, html, translationKey}: IsReportMessageAttachmentParams): boolean { +export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean { if (!text || !html) { return false; } diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js index f5beba5fdcfd..4eb7340dd410 100644 --- a/src/pages/EditRequestDistancePage.js +++ b/src/pages/EditRequestDistancePage.js @@ -29,7 +29,7 @@ const propTypes = { /** Parameters the route gets */ params: PropTypes.shape({ /** Type of IOU */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.MONEY_REQUEST_TYPE)), + iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)), /** Id of the report on which the distance request is being created */ reportID: PropTypes.string, diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 5e68e852c60b..8ddbf066a774 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -255,6 +255,7 @@ function HeaderView(props) { )} diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 4024cbd7a2c8..6522bedc825a 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -126,15 +126,15 @@ function AttachmentPickerWithMenuItems({ */ const moneyRequestOptions = useMemo(() => { const options = { - [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT]: { + [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, text: translate('iou.splitBill'), }, - [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST]: { + [CONST.IOU.TYPE.REQUEST]: { icon: Expensicons.MoneyCircle, text: translate('iou.requestMoney'), }, - [CONST.IOU.MONEY_REQUEST_TYPE.SEND]: { + [CONST.IOU.TYPE.SEND]: { icon: Expensicons.Send, text: translate('iou.sendMoney'), }, diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index 24501e307759..0b6333e31ef8 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -18,6 +18,7 @@ import CONST from '../../../CONST'; import editedLabelStyles from '../../../styles/editedLabelStyles'; import UserDetailsTooltip from '../../../components/UserDetailsTooltip'; import avatarPropTypes from '../../../components/avatarPropTypes'; +import * as Browser from '../../../libs/Browser'; const propTypes = { /** Users accountID */ @@ -66,6 +67,9 @@ const propTypes = { /** localization props */ ...withLocalizePropTypes, + + /** Should the comment have the appearance of being grouped with the previous comment? */ + displayAsGroup: PropTypes.bool, }; const defaultProps = { @@ -82,9 +86,28 @@ const defaultProps = { delegateAccountID: 0, actorIcon: {}, isThreadParentMessage: false, + displayAsGroup: false, }; function ReportActionItemFragment(props) { + /** + * Checks text element for presence of emoji as first character + * and insert Zero-Width character to avoid selection issue + * mentioned here https://github.com/Expensify/App/issues/29021 + * + * @param {String} text + * @param {Boolean} displayAsGroup + * @returns {ReactNode | null} Text component with zero width character + */ + + const checkForEmojiForSelection = (text, displayAsGroup) => { + const firstLetterIsEmoji = EmojiUtils.isFirstLetterEmoji(text); + if (firstLetterIsEmoji && !displayAsGroup && !Browser.isMobile()) { + return ; + } + return null; + }; + switch (props.fragment.type) { case 'COMMENT': { const {html, text} = props.fragment; @@ -116,6 +139,7 @@ function ReportActionItemFragment(props) { return ( + {checkForEmojiForSelection(text, props.displayAsGroup)} )) ) : ( diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index f76f884dca52..1d0f918de4f7 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -461,6 +461,7 @@ function ReportActionItemMessageEdit(props) { setHasExceededMaxCommentLength(hasExceeded)} /> diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index c87d4f06e1f4..02f1856c7bf5 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -199,12 +199,12 @@ function FloatingActionButtonAndPopover(props) { { icon: Expensicons.MoneyCircle, text: props.translate('iou.requestMoney'), - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)), + onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST)), }, { icon: Expensicons.Send, text: props.translate('iou.sendMoney'), - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.SEND)), + onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), }, ...(Permissions.canUseTasks(props.betas) ? [ diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index cd14dcd25f11..fd173dfa5b93 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -1,4 +1,5 @@ import React, {useState, useMemo, useCallback, useRef} from 'react'; +import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -72,12 +73,14 @@ function IOUCurrencySelection(props) { const [searchValue, setSearchValue] = useState(''); const optionsSelectorRef = useRef(); const selectedCurrencyCode = (lodashGet(props.route, 'params.currency', props.iou.currency) || CONST.CURRENCY.USD).toUpperCase(); - const iouType = lodashGet(props.route, 'params.iouType', CONST.IOU.MONEY_REQUEST_TYPE.REQUEST); + const iouType = lodashGet(props.route, 'params.iouType', CONST.IOU.TYPE.REQUEST); const reportID = lodashGet(props.route, 'params.reportID', ''); const confirmCurrencySelection = useCallback( (option) => { const backTo = lodashGet(props.route, 'params.backTo', ''); + Keyboard.dismiss(); + // When we refresh the web, the money request route gets cleared from the navigation stack. // Navigating to "backTo" will result in forward navigation instead, causing disruption to the currency selection. // To prevent any negative experience, we have made the decision to simply close the currency selection page. diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 6570cffb58d4..cfdbb60b4f0d 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -45,11 +45,12 @@ const propTypes = { }).isRequired, /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ - selectedTab: PropTypes.oneOf([CONST.TAB.DISTANCE, CONST.TAB.MANUAL, CONST.TAB.SCAN]).isRequired, + selectedTab: PropTypes.oneOf(['', CONST.TAB.DISTANCE, CONST.TAB.MANUAL, CONST.TAB.SCAN]), }; const defaultProps = { iou: iouDefaultProps, + selectedTab: '', }; function MoneyRequestDescriptionPage({iou, route, selectedTab}) { diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index d006e3480a4e..0786faa3841b 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -56,9 +56,9 @@ function MoneyRequestSelectorPage(props) { const {translate} = useLocalize(); const title = { - [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST]: translate('iou.requestMoney'), - [CONST.IOU.MONEY_REQUEST_TYPE.SEND]: translate('iou.sendMoney'), - [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT]: translate('iou.splitBill'), + [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), + [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), + [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), }; const isFromGlobalCreate = !reportID; const isExpenseRequest = ReportUtils.isPolicyExpenseChat(props.report); @@ -99,7 +99,7 @@ function MoneyRequestSelectorPage(props) { title={title[iouType]} onBackButtonPress={Navigation.dismissModal} /> - {iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST || iouType === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT ? ( + {iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.SPLIT ? ( ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(props.report)), [props.report]); - const isManualRequestDM = props.selectedTab === CONST.TAB.MANUAL && iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST; + const isManualRequestDM = props.selectedTab === CONST.TAB.MANUAL && iouType.current === CONST.IOU.TYPE.REQUEST; useEffect(() => { IOU.resetMoneyRequestCategory(); @@ -211,7 +211,7 @@ function MoneyRequestConfirmPage(props) { const trimmedComment = props.iou.comment.trim(); // If we have a receipt let's start the split bill by creating only the action, the transaction, and the group DM if needed - if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT && props.iou.receiptPath) { + if (iouType.current === CONST.IOU.TYPE.SPLIT && props.iou.receiptPath) { const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID.current) ? reportID.current : ''; FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename).then((receipt) => { IOU.startSplitBill( @@ -228,7 +228,7 @@ function MoneyRequestConfirmPage(props) { // IOUs created from a group report will have a reportID param in the route. // Since the user is already viewing the report, we don't need to navigate them to the report - if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT && CONST.REGEX.NUMBER.test(reportID.current)) { + if (iouType.current === CONST.IOU.TYPE.SPLIT && CONST.REGEX.NUMBER.test(reportID.current)) { IOU.splitBill( selectedParticipants, props.currentUserPersonalDetails.login, @@ -243,7 +243,7 @@ function MoneyRequestConfirmPage(props) { } // If the request is created from the global create menu, we also navigate the user to the group report - if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT) { + if (iouType.current === CONST.IOU.TYPE.SPLIT) { IOU.splitBillAndOpenReport( selectedParticipants, props.currentUserPersonalDetails.login, @@ -312,11 +312,11 @@ function MoneyRequestConfirmPage(props) { return props.translate('common.distance'); } - if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT) { + if (iouType.current === CONST.IOU.TYPE.SPLIT) { return props.translate('iou.split'); } - if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SEND) { + if (iouType.current === CONST.IOU.TYPE.SEND) { return props.translate('common.send'); } @@ -345,7 +345,7 @@ function MoneyRequestConfirmPage(props) { /> { @@ -121,7 +121,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { participants={iou.participants} onAddParticipants={IOU.setMoneyRequestParticipants} navigateToRequest={() => navigateToConfirmationStep(iouType.current)} - navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)} + navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.TYPE.SPLIT)} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} iouType={iouType.current} isDistanceRequest={isDistanceRequest} diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 547d2b7c363a..4571af34ddee 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -223,7 +223,7 @@ function MoneyRequestParticipantsSelector({ // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, + iouType === CONST.IOU.TYPE.REQUEST, // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. !isDistanceRequest, @@ -240,7 +240,7 @@ function MoneyRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowConfirmButton = !(participants.length > 1 && hasPolicyExpenseChatParticipant); - const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.MONEY_REQUEST_TYPE.SEND; + const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; return ( 0 ? safeAreaPaddingBottomStyle : {}]}> diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index c9ee7ece8fa9..c7a178134139 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -18,6 +18,7 @@ import * as Expensicons from '../../../components/Icon/Expensicons'; import * as CardUtils from '../../../libs/CardUtils'; import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; +import MenuItem from '../../../components/MenuItem'; import CONST from '../../../CONST'; import assignedCardPropTypes from './assignedCardPropTypes'; @@ -120,12 +121,20 @@ function ExpensifyCardPage({ )} {!_.isEmpty(physicalCard) && ( - + <> + + Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain))} + /> + )} {physicalCard.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED && ( diff --git a/src/pages/settings/Wallet/ReportCardLostPage.js b/src/pages/settings/Wallet/ReportCardLostPage.js new file mode 100644 index 000000000000..29a588916326 --- /dev/null +++ b/src/pages/settings/Wallet/ReportCardLostPage.js @@ -0,0 +1,227 @@ +import React, {useState, useEffect} from 'react'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import styles from '../../../styles/styles'; +import ONYXKEYS from '../../../ONYXKEYS'; +import SingleOptionSelector from '../../../components/SingleOptionSelector'; +import useLocalize from '../../../hooks/useLocalize'; +import Text from '../../../components/Text'; +import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; +import usePrivatePersonalDetails from '../../../hooks/usePrivatePersonalDetails'; +import assignedCardPropTypes from './assignedCardPropTypes'; +import * as CardUtils from '../../../libs/CardUtils'; +import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; +import NotFoundPage from '../../ErrorPage/NotFoundPage'; +import usePrevious from '../../../hooks/usePrevious'; +import * as FormActions from '../../../libs/actions/FormActions'; +import * as CardActions from '../../../libs/actions/Card'; +import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton'; + +/** Options for reason selector */ +const OPTIONS = [ + { + key: 'damaged', + label: 'reportCardLostOrDamaged.cardDamaged', + }, + { + key: 'stolen', + label: 'reportCardLostOrDamaged.cardLostOrStolen', + }, +]; + +const propTypes = { + /** Onyx form data */ + formData: PropTypes.shape({ + isLoading: PropTypes.bool, + }), + /** User's private personal details */ + privatePersonalDetails: PropTypes.shape({ + /** User's home address */ + address: PropTypes.shape({ + street: PropTypes.string, + city: PropTypes.string, + state: PropTypes.string, + zip: PropTypes.string, + country: PropTypes.string, + }), + }), + /** User's cards list */ + cardList: PropTypes.objectOf(assignedCardPropTypes), + route: PropTypes.shape({ + /** Each parameter passed via the URL */ + params: PropTypes.shape({ + /** Domain string */ + domain: PropTypes.string, + }), + }).isRequired, +}; + +const defaultProps = { + formData: {}, + privatePersonalDetails: { + address: { + street: '', + street2: '', + city: '', + state: '', + zip: '', + country: '', + }, + }, + cardList: {}, +}; + +function ReportCardLostPage({ + privatePersonalDetails, + cardList, + route: { + params: {domain}, + }, + formData, +}) { + usePrivatePersonalDetails(); + + const domainCards = CardUtils.getDomainCards(cardList)[domain]; + const physicalCard = CardUtils.findPhysicalCard(domainCards); + + const {translate} = useLocalize(); + + const [reason, setReason] = useState(); + const [isReasonConfirmed, setIsReasonConfirmed] = useState(false); + const [shouldShowAddressError, setShouldShowAddressError] = useState(false); + const [shouldShowReasonError, setShouldShowReasonError] = useState(false); + + const prevIsLoading = usePrevious(formData.isLoading); + + const formattedAddress = PersonalDetailsUtils.getFormattedAddress(privatePersonalDetails); + + useEffect(() => { + if (!_.isEmpty(physicalCard.errors) || !(prevIsLoading && !formData.isLoading)) { + return; + } + + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARDS.getRoute(domain)); + }, [domain, formData.isLoading, prevIsLoading, physicalCard.errors]); + + useEffect(() => { + if (formData.isLoading && _.isEmpty(physicalCard.errors)) { + return; + } + + FormActions.setErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, physicalCard.errors); + }, [formData.isLoading, physicalCard.errors]); + + if (_.isEmpty(physicalCard)) { + return ; + } + + const handleSubmitFirstStep = () => { + if (!reason) { + setShouldShowReasonError(true); + return; + } + + setIsReasonConfirmed(true); + setShouldShowAddressError(false); + setShouldShowReasonError(false); + }; + + const handleSubmitSecondStep = () => { + if (!formattedAddress) { + setShouldShowAddressError(true); + return; + } + + CardActions.requestReplacementExpensifyCard(physicalCard.cardID, reason); + }; + + const handleOptionSelect = (option) => { + setReason(option); + setShouldShowReasonError(false); + }; + + const handleBackButtonPress = () => { + if (isReasonConfirmed) { + setIsReasonConfirmed(false); + return; + } + + Navigation.goBack(ROUTES.SETTINGS_WALLET); + }; + + return ( + + + + {isReasonConfirmed ? ( + <> + + {translate('reportCardLostOrDamaged.confirmAddressTitle')} + Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS)} + numberOfLinesTitle={2} + /> + {translate('reportCardLostOrDamaged.currentCardInfo')} + + + + ) : ( + <> + + {translate('reportCardLostOrDamaged.reasonTitle')} + + + + + )} + + + ); +} + +ReportCardLostPage.propTypes = propTypes; +ReportCardLostPage.defaultProps = defaultProps; +ReportCardLostPage.displayName = 'ReportCardLostPage'; + +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + cardList: { + key: ONYXKEYS.CARD_LIST, + }, + formData: { + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + }, +})(ReportCardLostPage); diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 7b61df8b936d..01ae79f0169b 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -3669,6 +3669,7 @@ const styles = (theme: ThemeDefault) => paddingRight: 4, marginBottom: 32, alignSelf: 'flex-start', + ...userSelect.userSelectNone, }, emojiPickerButtonDropdownIcon: { @@ -3703,8 +3704,7 @@ const styles = (theme: ThemeDefault) => reportActionItemImages: { flexDirection: 'row', - borderWidth: 4, - borderColor: theme.transparent, + margin: 4, borderTopLeftRadius: variables.componentBorderRadiusLarge, borderTopRightRadius: variables.componentBorderRadiusLarge, overflow: 'hidden', @@ -3888,6 +3888,13 @@ const styles = (theme: ThemeDefault) => ...objectFit.oFCover, }, + singleOptionSelectorRow: { + ...flex.flexRow, + ...flex.alignItemsCenter, + gap: 12, + marginBottom: 16, + }, + globalNavigation: { width: variables.globalNavigationWidth, backgroundColor: theme.highlightBG, @@ -3959,6 +3966,10 @@ const styles = (theme: ThemeDefault) => checkboxWithLabelCheckboxStyle: { marginLeft: -2, }, + + singleOptionSelectorCircle: { + borderColor: theme.icon, + }, } satisfies Styles); // For now we need to export the styles function that takes the theme as an argument diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index e03775ee114e..26ccb59f4f46 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -1,5 +1,19 @@ import {ValueOf} from 'type-fest'; import CONST from '../../CONST'; +import DeepValueOf from '../utils/DeepValueOf'; + +type ActionName = DeepValueOf; + +type OriginalMessageApproved = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.APPROVED; + originalMessage: unknown; +}; + +type IOUDetails = { + amount: number; + comment?: string; + currency: string; +}; type OriginalMessageIOU = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.IOU; @@ -8,23 +22,38 @@ type OriginalMessageIOU = { IOUTransactionID?: string; IOUReportID?: number; + + /** Only exists when we are sending money */ + IOUDetails?: IOUDetails; + amount: number; comment?: string; currency: string; lastModified?: string; participantAccountIDs?: number[]; - type: string; + type: ValueOf; }; }; -type FlagSeverityName = 'spam' | 'inconsiderate' | 'bullying' | 'intimidation' | 'harassment' | 'assault'; +type FlagSeverityName = ValueOf< + Pick< + typeof CONST.MODERATION, + 'FLAG_SEVERITY_SPAM' | 'FLAG_SEVERITY_INCONSIDERATE' | 'FLAG_SEVERITY_INTIMIDATION' | 'FLAG_SEVERITY_BULLYING' | 'FLAG_SEVERITY_HARASSMENT' | 'FLAG_SEVERITY_ASSAULT' + > +>; type FlagSeverity = { accountID: number; timestamp: string; }; +type DecisionName = ValueOf< + Pick< + typeof CONST.MODERATION, + 'MODERATOR_DECISION_PENDING' | 'MODERATOR_DECISION_PENDING_HIDE' | 'MODERATOR_DECISION_PENDING_REMOVE' | 'MODERATOR_DECISION_APPROVED' | 'MODERATOR_DECISION_HIDDEN' + > +>; type Decision = { - decision: string; + decision: DecisionName; timestamp: string; }; @@ -62,7 +91,7 @@ type OriginalMessageClosed = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.CLOSED; originalMessage: { policyName: string; - reason: 'default' | 'accountClosed' | 'accountMerged' | 'removedPolicy' | 'policyDeleted'; + reason: ValueOf; lastModified?: string; }; }; @@ -128,7 +157,18 @@ type OriginalMessagePolicyTask = { originalMessage: unknown; }; +type OriginalMessageModifiedExpense = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; + originalMessage: unknown; +}; + +type OriginalMessageReimbursementQueued = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED; + originalMessage: unknown; +}; + type OriginalMessage = + | OriginalMessageApproved | OriginalMessageIOU | OriginalMessageAddComment | OriginalMessageSubmitted @@ -138,7 +178,9 @@ type OriginalMessage = | OriginalMessageChronosOOOList | OriginalMessageReportPreview | OriginalMessagePolicyChangeLog - | OriginalMessagePolicyTask; + | OriginalMessagePolicyTask + | OriginalMessageModifiedExpense + | OriginalMessageReimbursementQueued; export default OriginalMessage; -export type {Reaction, ChronosOOOEvent}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index ca81e2c2946a..8587cf9b7cd5 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -57,6 +57,7 @@ type Report = { /** The report type */ type?: string; + lastMessageTranslationKey?: string; parentReportID?: string; parentReportActionID?: string; isOptimisticReport?: boolean; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 5cbb02f08722..fbe4c02c4072 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,10 +1,13 @@ -import OriginalMessage, {Reaction} from './OriginalMessage'; import * as OnyxCommon from './OnyxCommon'; +import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; type Message = { /** The type of the action item fragment. Used to render a corresponding component */ type: string; + /** The html content of the fragment. */ + html: string; + /** The text content of the fragment. */ text: string; @@ -37,6 +40,9 @@ type Message = { isReversedTransaction?: boolean; whisperedTo: number[]; reactions: Reaction[]; + + moderationDecision?: Decision; + translationKey?: string; }; type Person = { @@ -47,7 +53,10 @@ type Person = { type ReportActionBase = { /** The ID of the reportAction. It is the string representation of the a 64-bit integer. */ - reportActionID?: string; + reportActionID: string; + + /** @deprecated Used in old report actions before migration. Replaced by reportActionID. */ + sequenceNumber?: number; /** The ID of the previous reportAction on the report. It is a string represenation of a 64-bit integer (or null for CREATED actions). */ previousReportActionID?: string; @@ -58,7 +67,7 @@ type ReportActionBase = { person?: Person[]; /** ISO-formatted datetime */ - created?: string; + created: string; /** report action message */ message?: Message[]; @@ -83,10 +92,23 @@ type ReportActionBase = { childCommenterCount?: number; childLastVisibleActionCreated?: string; childVisibleActionCount?: number; + childMoneyRequestCount?: number; + + /** ISO-formatted datetime */ + lastModified?: string; pendingAction?: OnyxCommon.PendingAction; + delegateAccountID?: string; + + /** Server side errors keyed by microtime */ + errors?: OnyxCommon.Errors; + + isAttachment?: boolean; }; type ReportAction = ReportActionBase & OriginalMessage; +type ReportActions = Record; + export default ReportAction; +export type {ReportActions, Message}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 571c2e04a390..4603c4579343 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -37,7 +37,7 @@ import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; import Report from './Report'; import ReportMetadata from './ReportMetadata'; -import ReportAction from './ReportAction'; +import ReportAction, {ReportActions} from './ReportAction'; import ReportActionReactions from './ReportActionReactions'; import SecurityGroup from './SecurityGroup'; import Transaction from './Transaction'; @@ -88,6 +88,7 @@ export type { Report, ReportMetadata, ReportAction, + ReportActions, ReportActionReactions, SecurityGroup, Transaction, diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index f530e5892e94..0ee3b0c0b357 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -1105,9 +1105,9 @@ describe('actions/IOU', () => { `Split bill with ${RORY_EMAIL}, ${CARLOS_EMAIL}, ${JULES_EMAIL}, and ${VIT_EMAIL} [${DateUtils.getDBTime().slice(0, 10)}]`, ); - expect(carlosTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); - expect(julesTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); - expect(vitTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); + expect(carlosTransaction.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + expect(julesTransaction.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + expect(vitTransaction.comment.source).toBe(CONST.IOU.TYPE.SPLIT); expect(carlosTransaction.comment.originalTransactionID).toBe(groupTransaction.transactionID); expect(julesTransaction.comment.originalTransactionID).toBe(groupTransaction.transactionID); diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index c6afde7d9161..6d3aba1fd9ad 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -408,7 +408,7 @@ describe('ReportUtils', () => { chatType, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, participantsAccountIDs[0]], []); - return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); + return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); }, ); expect(onlyHaveSplitOption).toBe(true); @@ -421,7 +421,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, ...participantsAccountIDs], []); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); it('user has send money permission', () => { @@ -431,7 +431,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, ...participantsAccountIDs], [CONST.BETAS.IOU_SEND]); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); it("it's a group chat report", () => { @@ -442,7 +442,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, ...participantsAccountIDs], [CONST.BETAS.IOU_SEND]); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); }); }); @@ -455,7 +455,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, ...participantsAccountIDs], [CONST.BETAS.IOU_SEND]); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); it("it is an expense report tied to user's own policy expense chat", () => { @@ -471,7 +471,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID], [CONST.BETAS.IOU_SEND]); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); }); @@ -485,7 +485,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, participantsAccountIDs[0]], []); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); it('it is an IOU report in submitted state even with send money permissions', () => { @@ -498,7 +498,7 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, participantsAccountIDs[0]], [CONST.BETAS.IOU_SEND]); expect(moneyRequestOptions.length).toBe(1); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); }); @@ -509,8 +509,8 @@ describe('ReportUtils', () => { }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, [currentUserAccountID, participantsAccountIDs[0]], [CONST.BETAS.IOU_SEND]); expect(moneyRequestOptions.length).toBe(2); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)).toBe(true); - expect(moneyRequestOptions.includes(CONST.IOU.MONEY_REQUEST_TYPE.SEND)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); }); });