diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 1f80908b02b5..0951b194430b 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -24,8 +24,16 @@ jobs: - name: Check for new JavaScript files run: | git fetch origin main --no-tags --depth=1 - count_new_js=$(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/libs/*.js' 'src/hooks/*.js' 'src/styles/*.js' 'src/languages/*.js' | wc -l) + + # Explanation: + # - comm is used to get the intersection between two bash arrays + # - git diff is used to see the files that were added on this branch + # - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main + # - wc counts the words in the result of the intersection + count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/libs/*.js' 'src/hooks/*.js' 'src/styles/*.js' 'src/languages/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) if [ "$count_new_js" -gt "0" ]; then echo "ERROR: Found new JavaScript files in the /src/libs, /src/hooks, /src/styles, or /src/languages directories; use TypeScript instead." exit 1 fi + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/android/app/build.gradle b/android/app/build.gradle index f7ca133b213b..e11f938a9a8e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001040200 - versionName "1.4.2-0" + versionCode 1001040306 + versionName "1.4.3-6" } flavorDimensions "default" diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md index 3ee1c8656b4b..32ce41d3cbf3 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Approving-Reports.md @@ -1,5 +1,189 @@ --- -title: Coming Soon -description: Coming Soon +title: How To Manage Employees and Reports > Approving Reports +description: This page will help you understand the lifecycle of a report and how to approve reports that are submitted to you. --- -## Resource Coming Soon! +# About +This article provides a comprehensive guide on report management within our platform. From viewing, editing, and submitting your employees' Open reports to handling rejections and unapproving, as well as harnessing the power of our "Guided Review" feature. Additionally, we'll delve into best practices for Concierge, offering insights on how to streamline and automate your report approval processes for maximum efficiency. +Let's dive in! + + +# How-to manage reports +This section covers the most essential information a user needs to operate a feature i.e. what to click on. We’ll go over any action the user might take when configuring or using the feature, starting from configuration and moving to usage. + + +What options does a user have when configuring this feature? + + +What options does a user have then interacting with this feature? + + +What elements of this feature are pay-walled vs. free? + + +As a Workspace admin, you have the ability to view, edit, and submit your employees' Open reports. + + +We recommend beginning this process from the web version of Expensify because it offers more functionality compared to the mobile app. Here's how to get started: +Click on the "Reports" tab. +Select the "All Submitters" and "Open" filters. +This will display all employee reports on your Workspaces that have not yet been submitted. +​ +## Viewing Employee Reports +Viewing employee reports can vary depending on whether you're using the web or mobile versions of Expensify. We generally recommend using the web version for this purpose, as it offers the following advantages: + + +You will only receive reports directly submitted to you when using the mobile app. + + +The option to filter reports via the Reports page is exclusively available in the web version, making it more convenient when reviewing multiple reports during a session. +​ +## Viewing employee reports on the mobile app +When using the mobile app to view reports, please note the following: + + +Tapping on the Reports list will only display your own reports; you won't see reports from other Workspace members. + + +To view another Workspace member's report in the Expensify app, it must be submitted directly to you, and you must access it through a link from an email or via Home. + + +When you access a report in this manner, you will have the option to approve/reject it or go through the review process if there are expenses that require your attention. + + +Once you've approved or rejected the report, it won't be accessible in the app anymore. To view it again, please visit the website and follow the steps mentioned above. +​ +## Editing employee reports +If a report has been submitted directly to you, follow these steps to edit the expense details. Please note that you cannot change the expense amount; to make changes affecting the report total, you must reject it and return it to the employee. + + +Here's what to do: +- Click on any expense within the report to edit the expense details. +- Remember that you cannot modify the expense amount directly. To make changes affecting the report total, reject the report, and it will be sent back to the employee for revisions. +- If you're a Workspace admin and need to edit a report that wasn't submitted directly to you, use the "Take Control" button at the top of the report. Keep in mind that taking control of a report will disrupt the approval workflow. +​ +Additionally, here are some other editing options for Admins: +- Undelete deleted company card expenses via the Reconciliation Dashboard (requires Domain Admin privileges). +- Add unreported company card expenses to an existing Open (unsubmitted) report or create a new report via the Reconciliation Dashboard (requires Domain Admin privileges). +- Add or modify expense coding, including Category, Tag(s), and Attendees. +- Attach a receipt to an expense that doesn't have one. +- Move card expenses between two Open (unsubmitted) reports. +- Merge duplicate expenses (only applicable if they are not card transactions). +- Change the Workspace associated with a report.​ + + +## Submitting Employee Reports +As a Workspace Admin, you have the option to submit any of your employee's Open reports. If an employee is unable to do it themselves, a Workspace admin can submit an expense report on their behalf to initiate the approval process. Follow these steps: +Click the "Submit" button located at the top of the report. + + +## Report History and Comments +Please keep in mind that any changes made by the admin are tracked under "Report History and Comments." If you change the reimbursable status of an expense (e.g., from Reimbursable to Non-Reimbursable), an email notification will be sent to the employee to notify them of this change. + + +## Rejecting or Unapproving a Report +If you need to reject a report that has been submitted to you or Unapprove a report that has already been approved. + + +To reject the report, click Reject rather than beginning the Review process. If there are multiple approvers involved, you can choose how far back to reject the report. + + +Rejecting a report will return the report back to the submitter in an Open status or, in the case of a multi-level approval workflow, back to the previous approver in a Processing status (awaiting their approval). You may need to do this if the submitter is not ready to submit the report, or perhaps the report as a whole needs to be rejected based on the configuration of your organization's expense Workspace. + + +## Unapprove a Report +You can click the red Unapprove button at the top of the report to undo approving a report. Keep in mind that you'll only see the Unapprove button if you're a report approver on an admin that has taken control of the report.​ + + +## Marking a Report as Reimbursed Outside of Expensify +If you are reimbursing reports via paper check, through payroll or any other method that takes place outside of Expensify, you'll want to keep track of which reports have been taken care of by marking reports as reimbursed. + + +1. Log into your Expensify account using your preferred web browser, (ie: Chrome or Safari) +2. Head to your Reports page and locate the report +3. Click the report name to open it +4. Click on Reimburse +5. Choose "I'll do it manually - just mark it as reimbursed". This will change the report status to Reimbursed +6. The submitter can then go into the report and confirm that they received the reimbursement by clicking the button at the top of the report. +7. This will change the report status to Reimbursed: CONFIRMED + + +# How to Use Guided Review to Approve Reports +Guided Review helps alert you to what might be out-of-Workspace for an Expense Report. You'll be guided through all report violations and warnings and given the option to Reject or Edit items that need review prior to approving a report. + + +Guided Review helps approvers quickly identify reports that need more attention so they can pass over reports that can be quickly approved. Both Submitters and Approvers have actionable notifications for the following: violations, warnings, and notices. These notifications are important since they will be included in “review mode” for the approver to make clear approve or reject decisions. + + +Via the Website:​ +1. Simply click Review at the top left of the report and the system will begin to walk you through the entire report. +2. Choose to Reject, View, or skip over an item needing review. If you wish to stop the process at any time, click the X in the progress bar in the top right corner. +Reject: This will remove the expense from the report and send it back to the submitter. An email will be sent to the submitter explaining this expense has been rejected. +View: This will allow you to open the expense so you can view and fix any incorrect data. +Next: This will allow you to skip over the current item and move forward to review the rest of the report. +Finish: Click this to finish reviewing the report! +3. Click the Finish button if you are done reviewing, or reject/edit the last item to finish the review process. +4. Approve the report! Approve and Forward the report if there is another person who needs to review the report in your approval workflow, or you can Final Approve if you are the final approver. Note: When in Guided Review, you'll automatically Approve the report adhering to your Company's Approval Workflow once you Approve the final expense on the report. You'll then be immediately taken to the next report requiring your attention - making Approving multiple expenses a quick and painless process! + + + + +Via the Mobile App:​ +1. From Home, under Reports that need your attention, click Begin Review, and the system will bring you to the first expense on the oldest report in Home. +2. Edit the expense: Make any necessary edits to the expense by tapping the corresponding field. Be sure to address any Violations and Notes on the expense! Notes are indicated at the top of the expense with a yellow exclamation point, while violations appear on the expense with a red exclamation point: +3. Choose Reject or Accept at the top of the expense. + + +Reject: This will remove the expense from the report and send it back to the submitter. An email will be sent to the submitter explaining this expense has been rejected, and a comment will be added to the report it was rejected from. If this is the only expense on the report, the entire report will be rejected (and the expense will remain on the report). + + +If Scheduled Submit is being used, rejected expenses will auto-report to the next Open report on the Workspace (as if it were a new expense). If an open report doesn't exist, Concierge will create a new one. +​ + + +If Scheduled Submit is not being used, any rejected expenses will be Unreported in the submitter's account and need to be manually applied to a new report. +​ +Accept: This will move to the next expense on the report, leaving behind any outstanding violations or notes. If this is the last expense on the report, you'll be all done! +Once you've made it through all of the expenses on the report, you'll be all set! + + +# Deep Dive +## Concierge Report Management + + +Concierge report approval removes the need for you to manually click "Approve" on endless reports! Instead, you can set up your group Workspace to capture all the requirements you have for your team's expenses. As long as all the rules have been followed and Concierge's additional audit is passed (more below), we will automatically approve such reports on behalf of the approver after submission. +​ +Before you start: +Ensure are a Workspace admin on a group Workspace +Set your workflow to Submit-and-Approve or Advanced Approval workflow​ + + +## Then follow these steps: +Set up your group Workspace so that all of your expense requirements are defined. Setting automatic categories for employees and category rules (e.g., maximum amounts, receipt requirements, etc.) are great examples! + + +Navigate to Settings > Workspaces > Group > [Workspace Name] > Members.​ + + +Scroll down to Approval Mode and select either Submit-and-Approve or Advanced Approval. + + +Under Expense Approvals, select a Manual Approval Threshold greater than $0.​ + + +With this setup, manual approval will only be required: +- For reports that fail audit (i.e. there is at least one Workspace violation on the report) +- For reports that contain at least one expense over the Manual Approval Threshold +- For any percentage of reports that you'd like to spot-check (this is set at 5% or 1 in 20 by default). +- If the report meets all of the requirements you specify in your Workspace settings and all expenses are under the Manual Approval Threshold, then Concierge will automatically move your report through each step of your designated approval workflow (unless it's routed for a spot-check).​ + + + + +## Concierge Receipt Audit +Concierge Receipt Audit is a real-time audit and compliance of receipts submitted by employees and Workspace users. Concierge checks every receipt for accuracy and compliance, flagging any expenses that seem fishy before expense reports are even submitted for approval. All risky expenses are highlighted for manual review, leaving you with more control over and visibility into employee expenses. + + +1. Concierge will SmartScan every receipt to verify the data input by the user matches the currency, date, and amount on the physical receipt. +2. After the report is submitted for approval, Concierge highlights any differences between the SmartScanned values and the employee's chosen input. +3. Each receipt that has been verified will show the "Verified" logo. + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 24298c4ff064..2d89178b271b 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.2.0 + 1.4.3.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 519c58663534..2f053575ff3b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleSignature ???? CFBundleVersion - 1.4.2.0 + 1.4.3.6 diff --git a/package-lock.json b/package-lock.json index 7f2c585bf748..9dd6974b12c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.2-0", + "version": "1.4.3-6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.2-0", + "version": "1.4.3-6", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -53,7 +53,6 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", "fbjs": "^3.0.2", - "focus-trap-react": "^10.2.1", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-when": "^3.5.2", @@ -30788,28 +30787,6 @@ "readable-stream": "^2.3.6" } }, - "node_modules/focus-trap": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz", - "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==", - "dependencies": { - "tabbable": "^6.2.0" - } - }, - "node_modules/focus-trap-react": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz", - "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==", - "dependencies": { - "focus-trap": "^7.5.2", - "tabbable": "^6.2.0" - }, - "peerDependencies": { - "prop-types": "^15.8.1", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, "node_modules/follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", @@ -49067,11 +49044,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" - }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -75103,23 +75075,6 @@ "readable-stream": "^2.3.6" } }, - "focus-trap": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz", - "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==", - "requires": { - "tabbable": "^6.2.0" - } - }, - "focus-trap-react": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz", - "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==", - "requires": { - "focus-trap": "^7.5.2", - "tabbable": "^6.2.0" - } - }, "follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", @@ -88110,11 +88065,6 @@ "version": "2.0.15", "dev": true }, - "tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" - }, "table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", diff --git a/package.json b/package.json index 787e30b17d5c..eb1b725eea10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.2-0", + "version": "1.4.3-6", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -100,7 +100,6 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", "fbjs": "^3.0.2", - "focus-trap-react": "^10.2.1", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-when": "^3.5.2", diff --git a/src/CONST.ts b/src/CONST.ts index 47ab30589612..f1364ebbb5bf 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -248,88 +248,9 @@ const CONST = { BETAS: { ALL: 'all', CHRONOS_IN_CASH: 'chronosInCash', - PAY_WITH_EXPENSIFY: 'payWithExpensify', - FREE_PLAN: 'freePlan', DEFAULT_ROOMS: 'defaultRooms', - BETA_EXPENSIFY_WALLET: 'expensifyWallet', BETA_COMMENT_LINKING: 'commentLinking', - INTERNATIONALIZATION: 'internationalization', POLICY_ROOMS: 'policyRooms', - PASSWORDLESS: 'passwordless', - TASKS: 'tasks', - THREADS: 'threads', - CUSTOM_STATUS: 'customStatus', - NEW_DOT_SAML: 'newDotSAML', - PDF_META_STORE: 'pdfMetaStore', - REPORT_ACTION_CONTEXT_MENU: 'reportActionContextMenu', - SUBMIT_POLICY: 'submitPolicy', - ATTENDEES: 'attendees', - AUTO_EXPORT: 'autoExport', - AUTO_EXPORT_INTACCT: 'autoExportIntacct', - AUTO_EXPORT_QBO: 'autoExportQbo', - AUTO_EXPORT_XERO: 'autoExportXero', - AUTO_JOIN_POLICY: 'autoJoinPolicy', - AUTOMATED_TAX_EXEMPTION: 'automatedTaxExemption', - BILL_PAY: 'billPay', - CATEGORY_DEFAULT_TAX: 'categoryDefaultTax', - COLLECTABLE_DEPOSIT_ACCOUNTS: 'collectableDepositAccounts', - CONCIERGE_TRAVEL: 'conciergeTravel', - CONNECTED_CARDS: 'connectedCards', - DISCREPANCY: 'discrepancy', - DOMAIN_CONTACT_BILLING: 'domainContactBilling', - DOMAIN_TWO_FACTOR_AUTH: 'domainTwoFactorAuth', - DUPLICATE_DETECTION: 'duplicateDetection', - EMAIL_SUPPRESSION_BETA: 'emailSuppressionBeta', - EXPENSES_V2: 'expensesV2', - EXPENSIFY_CARD: 'expensifyCard', - EXPENSIFY_CARD_INTACCT_RECONCILIATION: 'expensifyCardIntacctReconciliation', - EXPENSIFY_CARD_NETSUITE_RECONCILIATION: 'expensifyCardNetSuiteReconciliation', - EXPENSIFY_CARD_QBO_RECONCILIATION: 'expensifyCardQBOReconciliation', - EXPENSIFY_CARD_RAPID_INCREASE_FRAUD: 'expensifyCardRapidIncreaseFraud', - EXPENSIFY_CARD_XERO_RECONCILIATION: 'expensifyCardXeroReconciliation', - EXPENSIFY_ORG: 'expensifyOrg', - FIX_VIOLATION_PUSH_NOTIFICATION: 'fixViolationPushNotification', - FREE_PLAN_FULL_LAUNCH: 'freePlanFullLaunch', - FREE_PLAN_SOFT_LAUNCH: 'freePlanSoftLaunch', - GUSTO: 'gusto', - INBOX_CACHE: 'inboxCache', - INBOX_HIDDEN_TASKS: 'inboxHiddenTasks', - INDIRECT_INTEGRATION_SETUP: 'indirectIntegrationSetup', - IOU: 'IOU', - JOIN_POLICY: 'joinPolicy', - LOAD_POLICY_ASYNC: 'loadPolicyAsync', - MAP_RECEIPT: 'mapReceipt', - MERGE_API: 'mergeAPI', - MOBILE_REALTIME_REPORT_COMMENTS: 'mobileRealtimeReportComments', - MOBILE_SECURE_RECEIPTS: 'mobileSecureReceipts', - MONTHLY_SETTLEMENT: 'monthlySettlement', - NAMES_AND_AVATARS: 'namesAndAvatars', - NATIVE_CHAT: 'nativeChat', - NEW_PRICING: 'newPricing', - NEWSLETTER_THREE: 'newsletterThree', - NEXT_STEPS: 'nextSteps', - OPEN_FACE_HAMBURGER: 'openFaceHamburger', - PER_DIEM: 'perDiem', - PER_DIEM_INTERNATIONAL: 'perDiemInternational', - PRICING_COPY_CHANGES: 'pricingCopyChanges', - QBO_INVOICES: 'qboInvoices', - QUICKBOOKS_DESKTOP_V2: 'quickbooksDesktopV2', - REALTIME_REPORT_COMMENTS: 'realtimeReportComments', - S2W_ANNOUNCEMENT: 's2wAnnouncement', - SCHEDULED_AUTO_REPORTING: 'scheduledAutoReporting', - SECURE_RECEIPTS: 'secureReceipts', - SECURE_RECEIPTS_REPORTS: 'secureReceiptsReports', - SELF_SERVICE_HARD_LAUNCH: 'selfServiceHardLaunch', - SEND_MONEY: 'sendMoney', - SMART_SCAN_USER_DISPUTES: 'smartScanUserDisputes', - SMS_SIGN_UP: 'smsSignUp', - STRIPE_CONNECT: 'stripeConnect', - SUMMARY_EMAIL: 'summaryEmail', - SWIPE_TO_WIN: 'swipeToWin', - TAX_FOR_MILEAGE: 'taxForMileage', - TWO_FACTOR_AUTH: 'twoFactorAuth', - VENMO_INTEGRATION: 'venmoIntegration', - ZENEFITS_INTEGRATION: 'zenefitsIntegration', VIOLATIONS: 'violations', }, BUTTON_STATES: { @@ -574,6 +495,7 @@ const CONST = { CREATED: 'CREATED', IOU: 'IOU', MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', + MOVED: 'MOVED', REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', RENAMED: 'RENAMED', REPORTPREVIEW: 'REPORTPREVIEW', @@ -1188,7 +1110,8 @@ const CONST = { PAYMENT_METHODS: { DEBIT_CARD: 'debitCard', - BANK_ACCOUNT: 'bankAccount', + PERSONAL_BANK_ACCOUNT: 'bankAccount', + BUSINESS_BANK_ACCOUNT: 'businessBankAccount', }, PAYMENT_METHOD_ID_KEYS: { @@ -1278,7 +1201,11 @@ const CONST = { TYPE: { FREE: 'free', PERSONAL: 'personal', + + // Often referred to as "control" workspaces CORPORATE: 'corporate', + + // Often referred to as "collect" workspaces TEAM: 'team', }, ROLE: { diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 252c8380b062..ce26985932d6 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -1,15 +1,18 @@ +import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; -import Permissions from '@libs/Permissions'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import iouReportPropTypes from '@pages/iouReportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; import refPropTypes from './refPropTypes'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; import withWindowDimensions from './withWindowDimensions'; const propTypes = { @@ -19,6 +22,12 @@ const propTypes = { /** Callback to execute when the component closes. */ onClose: PropTypes.func.isRequired, + /** Callback to execute when the payment method is selected. */ + onItemSelected: PropTypes.func.isRequired, + + /** The IOU/Expense report we are paying */ + iouReport: iouReportPropTypes, + /** Anchor position for the AddPaymentMenu. */ anchorPosition: PropTypes.shape({ horizontal: PropTypes.number, @@ -31,51 +40,66 @@ const propTypes = { vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), }), - /** List of betas available to current user */ - betas: PropTypes.arrayOf(PropTypes.string), - /** Popover anchor ref */ anchorRef: refPropTypes, - ...withLocalizePropTypes, + /** Session info for the currently logged in user. */ + session: PropTypes.shape({ + /** Currently logged in user accountID */ + accountID: PropTypes.number, + }), }; const defaultProps = { + iouReport: {}, anchorPosition: {}, anchorAlignment: { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, - betas: [], anchorRef: () => {}, + session: {}, }; -function AddPaymentMethodMenu(props) { +function AddPaymentMethodMenu({isVisible, onClose, anchorPosition, anchorAlignment, anchorRef, iouReport, onItemSelected, session}) { + const {translate} = useLocalize(); + return ( { - props.onItemSelected(CONST.PAYMENT_METHODS.BANK_ACCOUNT); - }, - }, - ...(Permissions.canUseWallet(props.betas) + ...(ReportUtils.isIOUReport(iouReport) ? [ { - text: props.translate('common.debitCard'), - icon: Expensicons.CreditCard, - onSelected: () => props.onItemSelected(CONST.PAYMENT_METHODS.DEBIT_CARD), + text: translate('common.personalBankAccount'), + icon: Expensicons.Bank, + onSelected: () => { + onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); + }, }, ] : []), + ...(!ReportActionsUtils.hasRequestFromCurrentAccount(lodashGet(iouReport, 'reportID', 0), lodashGet(session, 'accountID', 0)) + ? [ + { + text: translate('common.businessBankAccount'), + icon: Expensicons.Building, + onSelected: () => onItemSelected(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT), + }, + ] + : []), + ...[ + { + text: translate('common.debitCard'), + icon: Expensicons.CreditCard, + onSelected: () => onItemSelected(CONST.PAYMENT_METHODS.DEBIT_CARD), + }, + ], ]} withoutOverlay /> @@ -88,10 +112,9 @@ AddPaymentMethodMenu.displayName = 'AddPaymentMethodMenu'; export default compose( withWindowDimensions, - withLocalize, withOnyx({ - betas: { - key: ONYXKEYS.BETAS, + session: { + key: ONYXKEYS.SESSION, }, }), )(AddPaymentMethodMenu); diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 4ab81ae462c9..d346f271b36d 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -128,6 +128,8 @@ function AttachmentModal(props) { const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); const {windowWidth} = useWindowDimensions(); + const isOverlayModalVisible = (isAttachmentReceipt && isDeleteReceiptConfirmModalVisible) || (!isAttachmentReceipt && isAttachmentInvalid); + const [file, setFile] = useState( props.originalFileName ? { @@ -406,7 +408,7 @@ function AttachmentModal(props) { { diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx similarity index 77% rename from src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js rename to src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index ec53507d4d8e..3e5e7b4fdd9a 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,5 +1,6 @@ import {FlashList} from '@shopify/flash-list'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useRef} from 'react'; +import {View} from 'react-native'; // We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another import {ScrollView} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; @@ -7,14 +8,10 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import {propTypes} from './autoCompleteSuggestionsPropTypes'; +import viewForwardedRef from '@src/types/utils/viewForwardedRef'; +import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; -/** - * @param {Number} numRows - * @param {Boolean} isSuggestionPickerLarge - * @returns {Number} - */ -const measureHeightOfSuggestionRows = (numRows, isSuggestionPickerLarge) => { +const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: boolean): number => { if (isSuggestionPickerLarge) { if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available @@ -29,28 +26,26 @@ const measureHeightOfSuggestionRows = (numRows, isSuggestionPickerLarge) => { return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; }; -function BaseAutoCompleteSuggestions({ - highlightedSuggestionIndex, - onSelect, - renderSuggestionMenuItem, - suggestions, - accessibilityLabelExtractor, - keyExtractor, - isSuggestionPickerLarge, - forwardedRef, -}) { +function BaseAutoCompleteSuggestions( + { + highlightedSuggestionIndex, + onSelect, + accessibilityLabelExtractor, + renderSuggestionMenuItem, + suggestions, + isSuggestionPickerLarge, + keyExtractor, + }: AutoCompleteSuggestionsProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); const rowHeight = useSharedValue(0); - const scrollRef = useRef(null); + const scrollRef = useRef>(null); /** * Render a suggestion menu item component. - * @param {Object} params - * @param {Object} params.item - * @param {Number} params.index - * @returns {JSX.Element} */ const renderItem = useCallback( - ({item, index}) => ( + ({item, index}: RenderSuggestionMenuItemProps): ReactElement => ( StyleUtils.getAutoCompleteSuggestionItemStyle(highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} hoverDimmingValue={1} @@ -84,7 +79,7 @@ function BaseAutoCompleteSuggestions({ return ( @@ -104,17 +99,6 @@ function BaseAutoCompleteSuggestions({ ); } -BaseAutoCompleteSuggestions.propTypes = propTypes; BaseAutoCompleteSuggestions.displayName = 'BaseAutoCompleteSuggestions'; -const BaseAutoCompleteSuggestionsWithRef = React.forwardRef((props, ref) => ( - -)); - -BaseAutoCompleteSuggestionsWithRef.displayName = 'BaseAutoCompleteSuggestionsWithRef'; - -export default BaseAutoCompleteSuggestionsWithRef; +export default forwardRef(BaseAutoCompleteSuggestions); diff --git a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js deleted file mode 100644 index 8c6dca1902c5..000000000000 --- a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js +++ /dev/null @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Array of suggestions */ - // eslint-disable-next-line react/forbid-prop-types - suggestions: PropTypes.arrayOf(PropTypes.object).isRequired, - - /** Function used to render each suggestion, returned JSX will be enclosed inside a Pressable component */ - renderSuggestionMenuItem: PropTypes.func.isRequired, - - /** Create unique keys for each suggestion item */ - keyExtractor: PropTypes.func.isRequired, - - /** The index of the highlighted suggestion */ - highlightedSuggestionIndex: PropTypes.number.isRequired, - - /** Fired when the user selects a suggestion */ - onSelect: PropTypes.func.isRequired, - - /** Show that we can use large auto-complete suggestion picker. - * Depending on available space and whether the input is expanded, we can have a small or large mention suggester. - * When this value is false, the suggester will have a height of 2.5 items. When this value is true, the height can be up to 5 items. */ - isSuggestionPickerLarge: PropTypes.bool.isRequired, - - /** create accessibility label for each item */ - accessibilityLabelExtractor: PropTypes.func.isRequired, - - /** Meaures the parent container's position and dimensions. */ - measureParentContainer: PropTypes.func, -}; - -const defaultProps = { - measureParentContainer: () => {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/AutoCompleteSuggestions/index.native.js b/src/components/AutoCompleteSuggestions/index.native.tsx similarity index 61% rename from src/components/AutoCompleteSuggestions/index.native.js rename to src/components/AutoCompleteSuggestions/index.native.tsx index 439fa45eae78..fbfa7d953581 100644 --- a/src/components/AutoCompleteSuggestions/index.native.js +++ b/src/components/AutoCompleteSuggestions/index.native.tsx @@ -1,18 +1,17 @@ import {Portal} from '@gorhom/portal'; import React from 'react'; -import {propTypes} from './autoCompleteSuggestionsPropTypes'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; +import type {AutoCompleteSuggestionsProps} from './types'; -function AutoCompleteSuggestions({measureParentContainer, ...props}) { +function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { return ( {/* eslint-disable-next-line react/jsx-props-no-spreading */} - + {...props} /> ); } -AutoCompleteSuggestions.propTypes = propTypes; AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/index.js b/src/components/AutoCompleteSuggestions/index.tsx similarity index 76% rename from src/components/AutoCompleteSuggestions/index.js rename to src/components/AutoCompleteSuggestions/index.tsx index 30654caf5708..24b846c265a9 100644 --- a/src/components/AutoCompleteSuggestions/index.js +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -4,8 +4,8 @@ import {View} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as StyleUtils from '@styles/StyleUtils'; -import {propTypes} from './autoCompleteSuggestionsPropTypes'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; +import type {AutoCompleteSuggestionsProps} from './types'; /** * On the mobile-web platform, when long-pressing on auto-complete suggestions, @@ -14,8 +14,8 @@ import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; * On the native platform, tapping on auto-complete suggestions will not blur the main input. */ -function AutoCompleteSuggestions({measureParentContainer, ...props}) { - const containerRef = React.useRef(null); +function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, @@ -25,7 +25,7 @@ function AutoCompleteSuggestions({measureParentContainer, ...props}) { React.useEffect(() => { const container = containerRef.current; if (!container) { - return; + return () => {}; } container.onpointerdown = (e) => { if (DeviceCapabilities.hasHoverSupport()) { @@ -44,20 +44,20 @@ function AutoCompleteSuggestions({measureParentContainer, ...props}) { }, [measureParentContainer, windowHeight, windowWidth]); const componentToRender = ( - // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={containerRef} /> ); + const bodyElement = document.querySelector('body'); + return ( - Boolean(width) && - ReactDOM.createPortal({componentToRender}, document.querySelector('body')) + !!width && bodyElement && ReactDOM.createPortal({componentToRender}, bodyElement) ); } -AutoCompleteSuggestions.propTypes = propTypes; AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts new file mode 100644 index 000000000000..9130f5139d71 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -0,0 +1,38 @@ +import {ReactElement} from 'react'; + +type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; + +type RenderSuggestionMenuItemProps = { + item: TSuggestion; + index: number; +}; + +type AutoCompleteSuggestionsProps = { + /** Array of suggestions */ + suggestions: TSuggestion[]; + + /** Function used to render each suggestion, returned JSX will be enclosed inside a Pressable component */ + renderSuggestionMenuItem: (item: TSuggestion, index: number) => ReactElement; + + /** Create unique keys for each suggestion item */ + keyExtractor: (item: TSuggestion, index: number) => string; + + /** The index of the highlighted suggestion */ + highlightedSuggestionIndex: number; + + /** Fired when the user selects a suggestion */ + onSelect: (index: number) => void; + + /** Show that we can use large auto-complete suggestion picker. + * Depending on available space and whether the input is expanded, we can have a small or large mention suggester. + * When this value is false, the suggester will have a height of 2.5 items. When this value is true, the height can be up to 5 items. */ + isSuggestionPickerLarge: boolean; + + /** create accessibility label for each item */ + accessibilityLabelExtractor: (item: TSuggestion, index: number) => string; + + /** Meaures the parent container's position and dimensions. */ + measureParentContainer?: (callback: MeasureParentContainerCallback) => void; +}; + +export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/components/Banner.js b/src/components/Banner.tsx similarity index 61% rename from src/components/Banner.js rename to src/components/Banner.tsx index 2fcb866334e0..1c92208a7aa2 100644 --- a/src/components/Banner.js +++ b/src/components/Banner.tsx @@ -1,7 +1,6 @@ -import PropTypes from 'prop-types'; import React, {memo} from 'react'; -import {View} from 'react-native'; -import compose from '@libs/compose'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; import getButtonState from '@libs/getButtonState'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; @@ -13,54 +12,41 @@ import PressableWithFeedback from './Pressable/PressableWithFeedback'; import RenderHTML from './RenderHTML'; import Text from './Text'; import Tooltip from './Tooltip'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -const propTypes = { +type BannerProps = { /** Text to display in the banner. */ - text: PropTypes.string.isRequired, + text: string; /** Should this component render the left-aligned exclamation icon? */ - shouldShowIcon: PropTypes.bool, + shouldShowIcon?: boolean; /** Should this component render a close button? */ - shouldShowCloseButton: PropTypes.bool, + shouldShowCloseButton?: boolean; /** Should this component render the text as HTML? */ - shouldRenderHTML: PropTypes.bool, + shouldRenderHTML?: boolean; /** Callback called when the close button is pressed */ - onClose: PropTypes.func, + onClose?: () => void; /** Callback called when the message is pressed */ - onPress: PropTypes.func, + onPress?: () => void; /** Styles to be assigned to the Banner container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), + containerStyles?: StyleProp; /** Styles to be assigned to the Banner text */ - // eslint-disable-next-line react/forbid-prop-types - textStyles: PropTypes.arrayOf(PropTypes.object), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - shouldRenderHTML: false, - shouldShowIcon: false, - shouldShowCloseButton: false, - onClose: undefined, - onPress: undefined, - containerStyles: [], - textStyles: [], + textStyles?: StyleProp; }; -function Banner(props) { +function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); + return ( {(isHovered) => { - const isClickable = props.onClose || props.onPress; + const isClickable = onClose ?? onPress; const shouldHighlight = isClickable && isHovered; return ( - {props.shouldShowIcon && ( + {shouldShowIcon && ( )} - {props.shouldRenderHTML ? ( - + {shouldRenderHTML ? ( + ) : ( - {props.text} + {text} )} - {props.shouldShowCloseButton && ( - + {shouldShowCloseButton && !!onClose && ( + @@ -113,8 +99,6 @@ function Banner(props) { ); } -Banner.propTypes = propTypes; -Banner.defaultProps = defaultProps; Banner.displayName = 'Banner'; -export default compose(withLocalize, memo)(Banner); +export default memo(Banner); diff --git a/src/components/Button/index.js b/src/components/Button/index.tsx similarity index 63% rename from src/components/Button/index.js rename to src/components/Button/index.tsx index b9aaf8868924..02f743b6a1b6 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.tsx @@ -1,212 +1,167 @@ -import {useIsFocused} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {ActivityIndicator, View} from 'react-native'; +import React, {ForwardedRef, useCallback} from 'react'; +import {ActivityIndicator, GestureResponderEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import refPropTypes from '@components/refPropTypes'; import Text from '@components/Text'; import withNavigationFallback from '@components/withNavigationFallback'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import HapticFeedback from '@libs/HapticFeedback'; -import * as StyleUtils from '@styles/StyleUtils'; +import themeColors from '@styles/themes/default'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import validateSubmitShortcut from './validateSubmitShortcut'; -const propTypes = { - /** Should the press event bubble across multiple instances when Enter key triggers it. */ - allowBubble: PropTypes.bool, - +type ButtonWithText = { /** The text for the button label */ - text: PropTypes.string, + text: string; /** Boolean whether to display the right icon */ - shouldShowRightIcon: PropTypes.bool, + shouldShowRightIcon?: boolean; /** The icon asset to display to the left of the text */ - icon: PropTypes.func, + icon?: React.FC | null; +}; + +type ButtonProps = (ButtonWithText | ChildrenProps) & { + /** Should the press event bubble across multiple instances when Enter key triggers it. */ + allowBubble?: boolean; /** The icon asset to display to the right of the text */ - iconRight: PropTypes.func, + iconRight?: React.FC; /** The fill color to pass into the icon. */ - iconFill: PropTypes.string, + iconFill?: string; /** Any additional styles to pass to the left icon container. */ - // eslint-disable-next-line react/forbid-prop-types - iconStyles: PropTypes.arrayOf(PropTypes.object), + iconStyles?: StyleProp; /** Any additional styles to pass to the right icon container. */ - // eslint-disable-next-line react/forbid-prop-types - iconRightStyles: PropTypes.arrayOf(PropTypes.object), + iconRightStyles?: StyleProp; /** Small sized button */ - small: PropTypes.bool, + small?: boolean; /** Large sized button */ - large: PropTypes.bool, + large?: boolean; - /** medium sized button */ - medium: PropTypes.bool, + /** Medium sized button */ + medium?: boolean; /** Indicates whether the button should be disabled and in the loading state */ - isLoading: PropTypes.bool, + isLoading?: boolean; /** Indicates whether the button should be disabled */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** A function that is called when the button is clicked on */ - onPress: PropTypes.func, + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; /** A function that is called when the button is long pressed */ - onLongPress: PropTypes.func, + onLongPress?: (event?: GestureResponderEvent) => void; /** A function that is called when the button is pressed */ - onPressIn: PropTypes.func, + onPressIn?: () => void; /** A function that is called when the button is released */ - onPressOut: PropTypes.func, + onPressOut?: () => void; /** Callback that is called when mousedown is triggered. */ - onMouseDown: PropTypes.func, + onMouseDown?: () => void; /** Call the onPress function when Enter key is pressed */ - pressOnEnter: PropTypes.bool, + pressOnEnter?: boolean; /** The priority to assign the enter key event listener. 0 is the highest priority. */ - enterKeyEventListenerPriority: PropTypes.number, + enterKeyEventListenerPriority?: number; /** Additional styles to add after local styles. Applied to Pressable portion of button */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style?: StyleProp; - /** Additional button styles. Specific to the OpacityView of button */ - // eslint-disable-next-line react/forbid-prop-types - innerStyles: PropTypes.arrayOf(PropTypes.object), + /** Additional button styles. Specific to the OpacityView of the button */ + innerStyles?: StyleProp; /** Additional text styles */ - // eslint-disable-next-line react/forbid-prop-types - textStyles: PropTypes.arrayOf(PropTypes.object), + textStyles?: StyleProp; /** Whether we should use the default hover style */ - shouldUseDefaultHover: PropTypes.bool, + shouldUseDefaultHover?: boolean; /** Whether we should use the success theme color */ - success: PropTypes.bool, + success?: boolean; /** Whether we should use the danger theme color */ - danger: PropTypes.bool, - - /** Children to replace all inner contents of button */ - children: PropTypes.node, + danger?: boolean; /** Should we remove the right border radius top + bottom? */ - shouldRemoveRightBorderRadius: PropTypes.bool, + shouldRemoveRightBorderRadius?: boolean; /** Should we remove the left border radius top + bottom? */ - shouldRemoveLeftBorderRadius: PropTypes.bool, + shouldRemoveLeftBorderRadius?: boolean; /** Should enable the haptic feedback? */ - shouldEnableHapticFeedback: PropTypes.bool, + shouldEnableHapticFeedback?: boolean; /** Id to use for this button */ - id: PropTypes.string, + id?: string; /** Accessibility label for the component */ - accessibilityLabel: PropTypes.string, - - /** A ref to forward the button */ - forwardedRef: refPropTypes, -}; - -const defaultProps = { - allowBubble: false, - text: '', - shouldShowRightIcon: false, - icon: null, - iconRight: Expensicons.ArrowRight, - iconFill: undefined, - iconStyles: [], - iconRightStyles: [], - isLoading: false, - isDisabled: false, - small: false, - large: false, - medium: false, - onPress: () => {}, - onLongPress: () => {}, - onPressIn: () => {}, - onPressOut: () => {}, - onMouseDown: undefined, - pressOnEnter: false, - enterKeyEventListenerPriority: 0, - style: [], - innerStyles: [], - textStyles: [], - shouldUseDefaultHover: true, - success: false, - danger: false, - children: null, - shouldRemoveRightBorderRadius: false, - shouldRemoveLeftBorderRadius: false, - shouldEnableHapticFeedback: false, - id: '', - accessibilityLabel: '', - forwardedRef: undefined, + accessibilityLabel?: string; + isFocused: boolean; }; -function Button({ - allowBubble, - text, - shouldShowRightIcon, - - icon, - iconRight, - iconFill, - iconStyles, - iconRightStyles, - - small, - large, - medium, - - isLoading, - isDisabled, - - onPress, - onLongPress, - onPressIn, - onPressOut, - onMouseDown, - - pressOnEnter, - enterKeyEventListenerPriority, - - style, - innerStyles, - textStyles, - - shouldUseDefaultHover, - success, - danger, - children, - - shouldRemoveRightBorderRadius, - shouldRemoveLeftBorderRadius, - shouldEnableHapticFeedback, - - id, - accessibilityLabel, - forwardedRef, -}) { +function Button( + { + allowBubble = false, + + iconRight = Expensicons.ArrowRight, + iconFill = themeColors.textLight, + iconStyles = [], + iconRightStyles = [], + + small = false, + large = false, + medium = false, + + isLoading = false, + isDisabled = false, + + onPress = () => {}, + onLongPress = () => {}, + onPressIn = () => {}, + onPressOut = () => {}, + onMouseDown = undefined, + + pressOnEnter = false, + enterKeyEventListenerPriority = 0, + + style = [], + innerStyles = [], + textStyles = [], + + shouldUseDefaultHover = true, + success = false, + danger = false, + + shouldRemoveRightBorderRadius = false, + shouldRemoveLeftBorderRadius = false, + shouldEnableHapticFeedback = false, + isFocused, + + id = '', + accessibilityLabel = '', + ...rest + }: ButtonProps, + ref: ForwardedRef, +) { const theme = useTheme(); const styles = useThemeStyles(); - const isFocused = useIsFocused(); const keyboardShortcutCallback = useCallback( - (event) => { + (event?: GestureResponderEvent | KeyboardEvent) => { if (!validateSubmitShortcut(isFocused, isDisabled, isLoading, event)) { return; } @@ -223,10 +178,12 @@ function Button({ }); const renderContent = () => { - if (children) { - return children; + if ('children' in rest) { + return rest.children; } + const {text = '', icon = null, shouldShowRightIcon = false} = rest; + const textComponent = ( @@ -248,12 +205,13 @@ function Button({ ); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (icon || shouldShowRightIcon) { return ( {icon && ( - + {shouldShowRightIcon && ( - + { - if (event && event.type === 'click') { - event.currentTarget.blur(); + if (event?.type === 'click') { + const currentTarget = event?.currentTarget as HTMLElement; + currentTarget?.blur(); } if (shouldEnableHapticFeedback) { @@ -307,7 +266,7 @@ function Button({ styles.buttonContainer, shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, - ...StyleUtils.parseStyleAsArray(style), + style, ]} style={[ styles.button, @@ -320,8 +279,9 @@ function Button({ isDisabled && !danger && !success ? styles.buttonDisabled : undefined, shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, - icon || shouldShowRightIcon ? styles.alignItemsStretch : undefined, - ...innerStyles, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'text' in rest && (rest?.icon || rest?.shouldShowRightIcon) ? styles.alignItemsStretch : undefined, + innerStyles, ]} hoverStyle={[ shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined, @@ -344,18 +304,6 @@ function Button({ ); } -Button.propTypes = propTypes; -Button.defaultProps = defaultProps; Button.displayName = 'Button'; -const ButtonWithRef = React.forwardRef((props, ref) => ( -