From 3f4df08e9622fa2c535bd15d2d5e2577738d115c Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 22 Nov 2023 13:08:28 +0100 Subject: [PATCH 001/672] Add reason interstatial --- src/ROUTES.ts | 4 + src/components/MoneyRequestHeader.js | 9 ++ .../AppNavigator/ModalStackNavigators.js | 1 + src/libs/Navigation/linkingConfig.js | 1 + src/pages/iou/HoldReasonPage.js | 114 ++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 src/pages/iou/HoldReasonPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 57d4eb8187ec..dd04b1844317 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -254,6 +254,10 @@ export default { route: ':iouType/new/category/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}`, }, + MONEY_REQUEST_HOLD_REASON: { + route: ':iouType/edit/reason/:transactionID?', + getRoute: (iouType: string, transactionID: string) => `${iouType}/edit/reason/${transactionID}`, + }, MONEY_REQUEST_TAG: { route: ':iouType/new/tag/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`, diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 178163f6569f..82df425ee858 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -87,6 +87,10 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); + const holdMoneyRequest = () => { + Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(transaction.type, lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'))) + }; + useEffect(() => { if (canModifyRequest) { return; @@ -103,6 +107,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, onSelected: () => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)), }); } + threeDotsMenuItems.push({ + icon: Expensicons.Hourglass, + text: "Hold request", + onSelected: () => holdMoneyRequest(), + }); threeDotsMenuItems.push({ icon: Expensicons.Trashcan, text: translate('reportActionContextMenu.deleteAction', {action: parentReportAction}), diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index a2f9bdd7a903..4f8d83dec3bd 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -47,6 +47,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ Money_Request_Date: () => require('../../../pages/iou/MoneyRequestDatePage').default, Money_Request_Description: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default, Money_Request_Category: () => require('../../../pages/iou/MoneyRequestCategoryPage').default, + Money_Request_Hold_Reason: () => require('../../../pages/iou/HoldReasonPage').default, Money_Request_Tag: () => require('../../../pages/iou/MoneyRequestTagPage').default, Money_Request_Merchant: () => require('../../../pages/iou/MoneyRequestMerchantPage').default, IOU_Send_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 44473998ac62..77006ad1415d 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -366,6 +366,7 @@ export default { Money_Request_Currency: ROUTES.MONEY_REQUEST_CURRENCY.route, Money_Request_Description: ROUTES.MONEY_REQUEST_DESCRIPTION.route, Money_Request_Category: ROUTES.MONEY_REQUEST_CATEGORY.route, + Money_Request_Hold_Reason: ROUTES.MONEY_REQUEST_HOLD_REASON.route, Money_Request_Tag: ROUTES.MONEY_REQUEST_TAG.route, Money_Request_Merchant: ROUTES.MONEY_REQUEST_MERCHANT.route, Money_Request_Waypoint: ROUTES.MONEY_REQUEST_WAYPOINT.route, diff --git a/src/pages/iou/HoldReasonPage.js b/src/pages/iou/HoldReasonPage.js new file mode 100644 index 000000000000..c45d5fbb73ae --- /dev/null +++ b/src/pages/iou/HoldReasonPage.js @@ -0,0 +1,114 @@ +// import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import React, {useCallback, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import compose from '@libs/compose'; +import Navigation from '@libs/Navigation/Navigation'; +import useThemeStyles from '@styles/useThemeStyles'; +import * as IOU from '@userActions/IOU'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Form from "@components/Form"; +import {View} from "react-native"; +import TextInput from "@components/TextInput"; +import CONST from "@src/CONST"; +import lodashGet from "lodash/get"; +import _ from "underscore"; +import * as API from '@libs/API'; + +const propTypes = { + /** Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + /** Route specific parameters used on this screen via route :iouType/new/category/:reportID? */ + params: PropTypes.shape({ + /** The type of IOU report, i.e. bill, request, send */ + iouType: PropTypes.string, + + /** The report ID of the IOU */ + reportID: PropTypes.string, + }), + }).isRequired +}; + +function HoldReasonPage({route}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const reasonRef = useRef(); + + const transactionID = lodashGet(route, 'params.transactionID', ''); + // const iouType = lodashGet(route, 'params.iouType', ''); + + const navigateBack = () => { + Navigation.goBack(); + }; + + const onSubmit = (values) => { + // eslint-disable-next-line rulesdir/no-api-in-views + API.write('HoldRequest', { + transactionID, + comment: values.reason + }) + } + + const validate = useCallback((value) => { + const errors = {}; + + if (_.isEmpty(value.reason)) { + errors.reason = 'common.error.fieldRequired'; + } + + return errors; + }, []); + + return ( + + +
+ Explain why you're holding this request + + (reasonRef.current = e)} + autoFocus + /> + +
+
+ ); +} + +HoldReasonPage.displayName = 'MoneyRequestHoldReasonPage'; +HoldReasonPage.propTypes = propTypes; + +export default compose( + withOnyx({ + iou: { + key: ONYXKEYS.IOU, + }, + }), + // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file + withOnyx({ + report: { + key: ({route, iou}) => { + const reportID = IOU.getIOUReportID(iou, route); + + return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; + }, + }, + }), +)(HoldReasonPage); From f4297cd218cb7860b606d93b53edbaf8e9a6edd5 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Thu, 23 Nov 2023 12:01:21 +0100 Subject: [PATCH 002/672] Hold banner --- assets/images/stopwatch.svg | 3 + src/components/Icon/Expensicons.ts | 2 + src/components/MoneyRequestHeader.js | 23 +++++--- .../ReportActionItem/MoneyRequestView.js | 1 + src/components/TextPill.tsx | 22 ++++++++ src/languages/en.ts | 11 ++++ src/languages/es.ts | 11 ++++ src/libs/ReportUtils.js | 12 ++++ src/libs/TransactionUtils.ts | 9 +++ src/pages/iou/HoldReasonPage.js | 55 ++++++++++--------- 10 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 assets/images/stopwatch.svg create mode 100644 src/components/TextPill.tsx diff --git a/assets/images/stopwatch.svg b/assets/images/stopwatch.svg new file mode 100644 index 000000000000..d27d6b0b7c36 --- /dev/null +++ b/assets/images/stopwatch.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 3d4f0edb1656..f96ceb30f9c5 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -106,6 +106,7 @@ import Rotate from '@assets/images/rotate-image.svg'; import RotateLeft from '@assets/images/rotate-left.svg'; import Send from '@assets/images/send.svg'; import Shield from '@assets/images/shield.svg'; +import Stopwatch from '@assets/images/stopwatch.svg'; import AppleLogo from '@assets/images/signIn/apple-logo.svg'; import GoogleLogo from '@assets/images/signIn/google-logo.svg'; import Facebook from '@assets/images/social-facebook.svg'; @@ -240,6 +241,7 @@ export { RotateLeft, Send, Shield, + Stopwatch, Sync, Task, ThreeDots, diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 82df425ee858..f55f93112365 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import Text from '@components/Text'; +import TextPill from '@components/TextPill'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; @@ -72,6 +74,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const moneyRequestReport = parentReport; const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); + const isHold = true; const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); // Only the requestor can take delete the request, admins can only edit it. @@ -87,8 +90,10 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); + // console.log('MYTRANSACTION', transaction); + const holdMoneyRequest = () => { - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(transaction.type, lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'))) + Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(transaction.type, lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'))); }; useEffect(() => { @@ -100,6 +105,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, }, [canModifyRequest]); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; if (canModifyRequest) { + threeDotsMenuItems.push({ + icon: Expensicons.Stopwatch, + text: !isHold ? translate('iou.holdRequest') : translate('iou.unholdRequest'), + onSelected: () => holdMoneyRequest(), + }); if (!TransactionUtils.hasReceipt(transaction)) { threeDotsMenuItems.push({ icon: Expensicons.Receipt, @@ -107,11 +117,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, onSelected: () => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)), }); } - threeDotsMenuItems.push({ - icon: Expensicons.Hourglass, - text: "Hold request", - onSelected: () => holdMoneyRequest(), - }); threeDotsMenuItems.push({ icon: Expensicons.Trashcan, text: translate('reportActionContextMenu.deleteAction', {action: parentReportAction}), @@ -123,7 +128,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, <> )} + + {translate('iou.hold')} + {translate('iou.requestOnHold')} + {children}; +} + +TextPill.displayName = 'TextPill'; + +export default TextPill; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4c6ea25eb2c8..5c9e2d7b27e3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -599,6 +599,17 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', + hold: 'Hold', + holdRequest: 'Hold Request', + unholdRequest: 'Unhold Request', + explainHold: "Explain why you're holding this request.", + reason: 'Reason', + holdReasonRequired: 'A reason is required when holding.', + requestOnHold: 'This request was put on hold. Review the comments for next steps.', + confirmApprove: 'Confirm what to approve', + confirmApprovalAmount: 'Approve the entire report total or only the amount not on hold.', + confirmPay: 'Confirm what to pay', + confirmPayAmount: 'Pay all out-of-pocket spend or only the amount not on hold.', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 85eab5c3f14d..90876a3fbc6d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -593,6 +593,17 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', + hold: 'En espera', + holdRequest: 'Solicitud de retención', + unholdRequest: 'Solicitud de cancelación de retención', + explainHold: 'Explique por qué mantiene esta solicitud.', + reason: 'Razón', + holdReasonRequired: 'Se requiere una razón al sostener.', + requestOnHold: 'Esta solicitud quedó en suspenso. Revise los comentarios para los próximos pasos.', + confirmApprove: 'Confirmar qué aprobar', + confirmApprovalAmount: 'Aprobar el total del informe completo o solo el monto no retenido.', + confirmPay: 'Confirmar que pagar', + confirmPayAmount: 'Pague todos los gastos de bolsillo o solo el monto no retenido.', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index db836549234d..ed3c6ad3e244 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4315,6 +4315,18 @@ function shouldDisableWelcomeMessage(report, policy) { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } +/** + * Put money request on HOLD + * @param transactionID + * @param comment + */ +function putOnHold(transactionID, comment) { + API.write('HoldRequest', { + transactionID, // the money request being held + comment, // the reason given for the hold + }); +} + export { getReportParticipantsTitle, isReportMessageAttachment, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 00ce8c55dbd7..efa1fe84382c 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -450,6 +450,14 @@ function getRecentTransactions(transactions: Record, size = 2): .slice(0, size); } +/** + * Check if transaction is on hold + */ +function isOnHold(transaction: Transaction): boolean { + // TODO - add logic + return !!transaction; +} + export { buildOptimisticTransaction, getUpdatedTransaction, @@ -477,6 +485,7 @@ export { isExpensifyCardTransaction, isPending, isPosted, + isOnHold, getWaypoints, isAmountMissing, isMerchantMissing, diff --git a/src/pages/iou/HoldReasonPage.js b/src/pages/iou/HoldReasonPage.js index c45d5fbb73ae..449672ddc1ff 100644 --- a/src/pages/iou/HoldReasonPage.js +++ b/src/pages/iou/HoldReasonPage.js @@ -1,23 +1,22 @@ // import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useRef} from 'react'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; +import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import Form from "@components/Form"; -import {View} from "react-native"; -import TextInput from "@components/TextInput"; -import CONST from "@src/CONST"; -import lodashGet from "lodash/get"; -import _ from "underscore"; -import * as API from '@libs/API'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -30,7 +29,7 @@ const propTypes = { /** The report ID of the IOU */ reportID: PropTypes.string, }), - }).isRequired + }).isRequired, }; function HoldReasonPage({route}) { @@ -38,7 +37,7 @@ function HoldReasonPage({route}) { const {translate} = useLocalize(); const reasonRef = useRef(); - const transactionID = lodashGet(route, 'params.transactionID', ''); + // const transactionID = lodashGet(route, 'params.transactionID', ''); // const iouType = lodashGet(route, 'params.iouType', ''); const navigateBack = () => { @@ -46,17 +45,15 @@ function HoldReasonPage({route}) { }; const onSubmit = (values) => { + // TODO - add a helper function for API call // eslint-disable-next-line rulesdir/no-api-in-views - API.write('HoldRequest', { - transactionID, - comment: values.reason - }) - } + console.log(values); + }; const validate = useCallback((value) => { const errors = {}; - if (_.isEmpty(value.reason)) { + if (_.isEmpty(value.comment)) { errors.reason = 'common.error.fieldRequired'; } @@ -70,24 +67,32 @@ function HoldReasonPage({route}) { testID={HoldReasonPage.displayName} > -
- Explain why you're holding this request + + {translate('iou.explainHold')} - (reasonRef.current = e)} autoFocus /> - +
); } From 317f5fc08df54b987c534aad6c4dbcf2e77c097e Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 5 Dec 2023 16:47:44 +0100 Subject: [PATCH 003/672] Add new translations --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 5c9e2d7b27e3..9b3dd9f1944f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -610,6 +610,8 @@ export default { confirmApprovalAmount: 'Approve the entire report total or only the amount not on hold.', confirmPay: 'Confirm what to pay', confirmPayAmount: 'Pay all out-of-pocket spend or only the amount not on hold.', + payOnly: 'Pay only', + approveOnly: 'Approve only' }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 90876a3fbc6d..f112aeb8a5fc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -604,6 +604,8 @@ export default { confirmApprovalAmount: 'Aprobar el total del informe completo o solo el monto no retenido.', confirmPay: 'Confirmar que pagar', confirmPayAmount: 'Pague todos los gastos de bolsillo o solo el monto no retenido.', + payOnly: 'Paga solo', + approveOnly: 'Aprobar sólo' }, notificationPreferencesPage: { header: 'Preferencias de avisos', From da7e0a69921289525d8e5398f2c9265d40341cbb Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 5 Dec 2023 16:48:15 +0100 Subject: [PATCH 004/672] Change Header styles --- src/components/Header.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 46fe1a25c920..b48883e25175 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,5 @@ import React, {ReactElement} from 'react'; -import {StyleProp, TextStyle, View} from 'react-native'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import EnvironmentBadge from './EnvironmentBadge'; import Text from './Text'; @@ -16,12 +16,15 @@ type HeaderProps = { /** Additional text styles */ textStyles?: StyleProp; + + /** Additional text styles */ + containerStyles?: StyleProp; }; -function Header({title = '', subtitle = '', textStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) { const styles = useThemeStyles(); return ( - + {typeof title === 'string' ? Boolean(title) && ( From d38c0f69f3f059809d0021d3bc66f004f9a8b6cc Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 5 Dec 2023 17:15:59 +0100 Subject: [PATCH 005/672] Add full boolean to IOU requests --- src/libs/actions/IOU.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 939a11dad511..54e47fce8705 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2440,9 +2440,10 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType * @param {Object} iouReport * @param {Object} recipient * @param {String} paymentMethodType + * @param {Boolean} full * @returns {Object} */ -function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMethodType) { +function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMethodType, full) { const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, -iouReport.total, @@ -2557,6 +2558,7 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho chatReportID: chatReport.reportID, reportActionID: optimisticIOUReportAction.reportActionID, paymentMethodType, + full }, optimisticData, successData, @@ -2600,7 +2602,7 @@ function sendMoneyWithWallet(report, amount, currency, comment, managerID, recip Report.notifyNewAction(params.chatReportID, managerID); } -function approveMoneyRequest(expenseReport) { +function approveMoneyRequest(expenseReport, full) { const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); const optimisticReportActionsData = { @@ -2650,7 +2652,7 @@ function approveMoneyRequest(expenseReport) { }, ]; - API.write('ApproveMoneyRequest', {reportID: expenseReport.reportID, approvedReportActionID: optimisticApprovedReportAction.reportActionID}, {optimisticData, successData, failureData}); + API.write('ApproveMoneyRequest', {reportID: expenseReport.reportID, approvedReportActionID: optimisticApprovedReportAction.reportActionID, full}, {optimisticData, successData, failureData}); } /** @@ -2730,11 +2732,11 @@ function submitReport(expenseReport) { * @param {String} paymentType * @param {Object} chatReport * @param {Object} iouReport - * @param {String} reimbursementBankAccountState + * @param {Boolean} full */ -function payMoneyRequest(paymentType, chatReport, iouReport) { +function payMoneyRequest(paymentType, chatReport, iouReport, full) { const recipient = {accountID: iouReport.ownerAccountID}; - const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType); + const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full); // For now we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with // Expensify Wallets. From 818a84187c1cf33d3be5e063cee4c50479b05e73 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 5 Dec 2023 17:16:21 +0100 Subject: [PATCH 006/672] Mock Report & Transaction Utils --- src/libs/ReportUtils.js | 19 +++++++++++++------ src/libs/TransactionUtils.ts | 3 +-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index ed3c6ad3e244..bbed6796eea3 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4317,14 +4317,19 @@ function shouldDisableWelcomeMessage(report, policy) { /** * Put money request on HOLD - * @param transactionID - * @param comment + * @param {string} transactionID + * @param {string} comment */ function putOnHold(transactionID, comment) { - API.write('HoldRequest', { - transactionID, // the money request being held - comment, // the reason given for the hold - }); + return; +} + +/** + * Check if Report has any held expenses + * @param {Object} report + */ +function hasHeldExpenses(report) { + return true; } export { @@ -4492,4 +4497,6 @@ export { getRoom, shouldDisableWelcomeMessage, canEditWriteCapability, + hasHeldExpenses, + putOnHold }; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index efa1fe84382c..3df67742ea88 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -454,8 +454,7 @@ function getRecentTransactions(transactions: Record, size = 2): * Check if transaction is on hold */ function isOnHold(transaction: Transaction): boolean { - // TODO - add logic - return !!transaction; + return true; } export { From ed3fe5eca317c348752aa82a8c2676fbc1526bbb Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Thu, 14 Dec 2023 10:15:46 +0100 Subject: [PATCH 007/672] Add Offline Support --- src/ROUTES.ts | 2 +- src/components/DecisionModal.js | 102 +++++++++++++++ src/components/Icon/Expensicons.ts | 2 +- src/components/MoneyReportHeader.js | 47 ++++++- src/components/MoneyRequestHeader.js | 30 +++-- src/components/ProcessMoneyRequestHoldMenu.js | 92 ++++++++++++++ src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/libs/ReportUtils.js | 31 +++-- src/libs/TransactionUtils.ts | 5 +- src/libs/actions/IOU.js | 116 +++++++++++++++++- src/pages/iou/HoldReasonPage.js | 15 +-- src/types/onyx/Transaction.ts | 1 + 13 files changed, 408 insertions(+), 39 deletions(-) create mode 100644 src/components/DecisionModal.js create mode 100644 src/components/ProcessMoneyRequestHoldMenu.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 23da11673499..ad606cebad3c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -272,7 +272,7 @@ export default { }, MONEY_REQUEST_HOLD_REASON: { route: ':iouType/edit/reason/:transactionID?', - getRoute: (iouType: string, transactionID: string) => `${iouType}/edit/reason/${transactionID}`, + getRoute: (iouType: string, transactionID: string, reportID: string, backTo: string) => `${iouType}/edit/reason/${transactionID}?reportID=${reportID}&backTo=${backTo}`, }, MONEY_REQUEST_TAG: { route: ':iouType/new/tag/:reportID?', diff --git a/src/components/DecisionModal.js b/src/components/DecisionModal.js new file mode 100644 index 000000000000..52c1974e6af9 --- /dev/null +++ b/src/components/DecisionModal.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import Header from '@components/Header'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Modal from '@components/Modal'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; + +const propTypes = { + /** Title describing purpose of modal */ + title: PropTypes.string.isRequired, + + /** Modal subtitle/description */ + prompt: PropTypes.string, + + /** Text content used in first button */ + firstOptionText: PropTypes.string.isRequired, + + /** Text content used in second button */ + secondOptionText: PropTypes.string.isRequired, + + /** onSubmit callback fired after clicking on first button */ + onFirstOptionSubmit: PropTypes.func.isRequired, + + /** onSubmit callback fired after clicking on */ + onSecondOptionSubmit: PropTypes.func.isRequired, + + /** Is the window width narrow, like on a mobile device? */ + isSmallScreenWidth: PropTypes.bool.isRequired, + + /** Callback for closing modal */ + onClose: PropTypes.func.isRequired, + + /** Whether modal is visible */ + isVisible: PropTypes.bool.isRequired, +}; + +const defaultProps = { + prompt: '', +}; + +function DecisionModal({title, prompt, firstOptionText, secondOptionText, onFirstOptionSubmit, onSecondOptionSubmit, isSmallScreenWidth, onClose, isVisible}) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + return ( + + + + +
+ + + + + + + + {prompt} + + )} @@ -535,49 +579,46 @@ function ReportActionItem(props) { for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} + {actionableItemButtons.length > 0 && } ) : ( )} ); } - const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); + const numberOfThreadReplies = action.childVisibleActionCount ?? 0; - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); - const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); - const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, report.reportID); + const oldestFourAccountIDs = + action.childOldestFourAccountIDs + ?.split(',') + .map((accountID) => Number(accountID)) + .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; + const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {}; return ( <> {children} - {Permissions.canUseLinkPreviews() && !isHidden && !_.isEmpty(props.action.linkMetadata) && ( - - !_.isEmpty(item))} /> + {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( + + !isEmptyObject(item))} /> )} - {!ReportActionsUtils.isMessageDeleted(props.action) && ( + {!ReportActionsUtils.isMessageDeleted(action) && ( { if (Session.isAnonymousUser()) { @@ -598,9 +639,9 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( { + const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => { const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors); - if (!_.isUndefined(props.draftMessage)) { + if (draftMessage !== undefined) { return {content}; } - if (!props.displayAsGroup) { + if (!displayAsGroup) { return ( item === moderationDecision) && + !ReportActionsUtils.isPendingRemove(action) } > {content} @@ -648,23 +689,23 @@ function ReportActionItem(props) { return {content}; }; - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; if (ReportActionsUtils.isTransactionThread(parentReportAction)) { const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction); if (ReportActionsUtils.isDeletedParentAction(parentReportAction) || isReversedTransaction) { return ( - + - - + + ${props.translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} + html={`${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} /> @@ -676,26 +717,26 @@ function ReportActionItem(props) { return ( ); } - if (ReportUtils.isTaskReport(props.report)) { - if (ReportUtils.isCanceledTaskReport(props.report, parentReportAction)) { + if (ReportUtils.isTaskReport(report)) { + if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { return ( - + - - + + - ${props.translate('parentReportAction.deletedTask')}`} /> + ${translate('parentReportAction.deletedTask')}`} /> @@ -704,44 +745,44 @@ function ReportActionItem(props) { ); } return ( - + - + ); } - if (ReportUtils.isExpenseReport(props.report) || ReportUtils.isIOUReport(props.report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { return ( - - {transactionThreadReport && !_.isEmpty(transactionThreadReport) ? ( + + {transactionThreadReport && !lodashIsEmpty(transactionThreadReport) ? ( <> - {transactionCurrency !== props.report.currency && ( + {transactionCurrency !== report.currency && ( )} ) : ( )} @@ -750,96 +791,94 @@ function ReportActionItem(props) { return ( ); } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; } - if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ( ); } // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet - if ( - props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - lodashGet(props.report, 'isWaitingOnBankAccount', false) && - originalMessage && - originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && - !isSendingMoney - ) { + if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { return null; } // if action is actionable mention whisper and resolved by user, then we don't want to render anything - if (ReportActionsUtils.isActionableMentionWhisper(props.action) && lodashGet(props.action, 'originalMessage.resolution', null)) { + if (ReportActionsUtils.isActionableMentionWhisper(action) && (action.originalMessage.resolution ?? null)) { return null; } // We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them. // This is a temporary solution needed for comment-linking. // The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt. - if (ReportActionsUtils.isWhisperActionTargetedToOthers(props.action)) { + if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) { return null; } - const hasErrors = !_.isEmpty(props.action.errors); - const whisperedToAccountIDs = props.action.whisperedToAccountIDs || []; + const hasErrors = !isEmptyObject(action.errors); + const whisperedToAccountIDs = action.whisperedToAccountIDs ?? []; const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : []; + const whisperedToPersonalDetails = isWhisper + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPress={onPress} + style={[action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? styles.pointerEventsNone : styles.pointerEventsAuto]} + onPressIn={() => isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onSecondaryInteraction={showPopover} - preventDefaultContextMenu={_.isUndefined(props.draftMessage) && !hasErrors} + preventDefaultContextMenu={draftMessage === undefined && !hasErrors} withoutFocusOnSecondaryInteraction - accessibilityLabel={props.translate('accessibilityHints.chatMessage')} + accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessible > {(hovered) => ( - {props.shouldDisplayNewMarker && } + {shouldDisplayNewMarker && } - + ReportActions.clearReportActionErrors(props.report.reportID, props.action)} + onClose={() => ReportActions.clearReportActionErrors(report.reportID, action)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') + draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} - errors={ErrorUtils.getLatestErrorMessageField(props.action)} + shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} + errors={ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} shouldDisableStrikeThrough > {isWhisper && ( @@ -852,11 +891,11 @@ function ReportActionItem(props) { /> - {props.translate('reportActionContextMenu.onlyVisible')} + {translate('reportActionContextMenu.onlyVisible')}   )} - {renderReportActionItem(hovered || isReportActionLinked, isWhisper, hasErrors)} + {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} )} - + {/* @ts-expect-error TODO check if there is a field on the reportAction object */} + ); } -ReportActionItem.propTypes = propTypes; -ReportActionItem.defaultProps = defaultProps; - -export default compose( - withWindowDimensions, - withLocalize, - withNetwork(), - withBlockedFromConcierge({propName: 'blockedFromConcierge'}), - withReportActionsDrafts({ - propName: 'draftMessage', - transformValue: (drafts, props) => { - const originalReportID = ReportUtils.getOriginalReportID(props.report.reportID, props.action); - const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`; - return lodashGet(drafts, [draftKey, props.action.reportActionID, 'message']); - }, - }), - withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, - }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - iouReport: { - key: ({action}) => { - const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); - return iouReportID ? `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}` : undefined; - }, - initialValue: {}, - }, - policyReportFields: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined), - initialValue: [], +export default withOnyx({ + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, + }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + iouReport: { + key: ({action}) => { + const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); + return `${ONYXKEYS.COLLECTION.REPORT}${iouReportID ?? ''}`; }, - policy: { - key: ({report}) => (report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}` : undefined), - initialValue: {}, - }, - emojiReactions: { - key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, - initialValue: {}, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || 0}`, - canEvict: false, - }, - reportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || 0}`, - canEvict: false, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - }), -)( + initialValue: {} as OnyxTypes.Report, + }, + policyReportFields: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID ?? ''}`, + initialValue: {}, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID ?? ''}`, + initialValue: {} as OnyxTypes.Policy, + }, + emojiReactions: { + key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`, + initialValue: {}, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? 0}`, + canEvict: false, + }, + reportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || 0}`, + canEvict: false, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, +})( memo(ReportActionItem, (prevProps, nextProps) => { - const prevParentReportAction = prevProps.parentReportActions[prevProps.report.parentReportActionID]; - const nextParentReportAction = nextProps.parentReportActions[nextProps.report.parentReportActionID]; + const prevParentReportAction = prevProps.parentReportActions?.[prevProps.report.parentReportActionID ?? '']; + const nextParentReportAction = nextProps.parentReportActions?.[nextProps.report.parentReportActionID ?? '']; return ( prevProps.displayAsGroup === nextProps.displayAsGroup && - prevProps.draftMessage === nextProps.draftMessage && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - _.isEqual(prevProps.reports, nextProps.reports) && - _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && - _.isEqual(prevProps.action, nextProps.action) && - _.isEqual(prevProps.iouReport, nextProps.iouReport) && - _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && - _.isEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && - _.isEqual(prevProps.report.errorFields, nextProps.report.errorFields) && - lodashGet(prevProps.report, 'statusNum') === lodashGet(nextProps.report, 'statusNum') && - lodashGet(prevProps.report, 'stateNum') === lodashGet(nextProps.report, 'stateNum') && - lodashGet(prevProps.report, 'parentReportID') === lodashGet(nextProps.report, 'parentReportID') && - lodashGet(prevProps.report, 'parentReportActionID') === lodashGet(nextProps.report, 'parentReportActionID') && - prevProps.translate === nextProps.translate && + lodashIsEqual(prevProps.reports, nextProps.reports) && + lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && + lodashIsEqual(prevProps.action, nextProps.action) && + lodashIsEqual(prevProps.iouReport, nextProps.iouReport) && + lodashIsEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + lodashIsEqual(prevProps.report.isDeletedParentAction, nextProps.report.isDeletedParentAction) && + lodashIsEqual(prevProps.report.errorFields, nextProps.report.errorFields) && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum && + prevProps.report?.parentReportID === nextProps.report?.parentReportID && + prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID && // TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) && prevProps.action.actionName === nextProps.action.actionName && @@ -965,15 +986,15 @@ export default compose( ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) && prevProps.report.managerID === nextProps.report.managerID && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && - lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && - lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && + prevProps.report?.total === nextProps.report?.total && + prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) && - _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields) && - _.isEqual(prevProps.policy, nextProps.policy) && - _.isEqual(prevParentReportAction, nextParentReportAction) && - _.isEqual(prevProps.reportActions, nextProps.reportActions) && - _.isEqual(prevProps.transactions, nextProps.transactions) + lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) && + lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) && + lodashIsEqual(prevProps.policy, nextProps.policy) && + lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && + lodashIsEqual(prevProps.transactions, nextProps.transactions) && + lodashIsEqual(prevParentReportAction, nextParentReportAction) ); }), ); diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx index 35141a42b726..a28f2af24448 100644 --- a/src/pages/home/report/ReportActionItemBasicMessage.tsx +++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx @@ -5,7 +5,7 @@ import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type ReportActionItemBasicMessageProps = ChildrenProps & { +type ReportActionItemBasicMessageProps = Partial & { message: string; }; diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 95578c10e816..4fe52f6adf41 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -35,7 +35,7 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { /** The id of the policy */ // eslint-disable-next-line react/no-unused-prop-types - policyID: string; + policyID: string | undefined; }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index e16d94eb7db7..04391bb19cd5 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -70,6 +70,7 @@ const MUTED_ACTIONS = [ CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.MOVED, + CONST.REPORT.ACTIONS.TYPE.ACTIONABLEJOINREQUEST, ] as ActionName[]; function ReportActionItemFragment({ diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 2c9a4cbd21e8..fbf2da69aa31 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -5,6 +5,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -58,7 +59,7 @@ type ReportActionItemMessageEditProps = { shouldDisableEmojiPicker?: boolean; /** Stores user's preferred skin tone */ - preferredSkinTone?: number; + preferredSkinTone?: OnyxEntry; }; // native ids @@ -69,7 +70,7 @@ const isMobileSafari = Browser.isMobileSafari(); function ReportActionItemMessageEdit( {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, - forwardedRef: ForwardedRef, + forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index af1c4e85104e..4a041fc495c0 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -83,7 +83,6 @@ function ReportActionItemParentAction({report, index = 0, shouldHideThreadDivide onClose={() => Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} > Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID))} report={ancestor.report} action={ancestor.reportAction} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 741422cc7e82..696cd7a7d850 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useMemo} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -29,7 +30,7 @@ import ReportActionItemFragment from './ReportActionItemFragment'; type ReportActionItemSingleProps = Partial & { /** All the data of the action */ - action: ReportAction; + action: OnyxEntry; /** Styles for the outermost View */ wrapperStyle?: StyleProp; @@ -38,7 +39,7 @@ type ReportActionItemSingleProps = Partial & { report: Report; /** IOU Report for this action, if any */ - iouReport?: Report; + iouReport?: OnyxEntry; /** Show header for action */ showHeader?: boolean; @@ -77,12 +78,12 @@ function ReportActionItemSingle({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; + const actorAccountID = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action?.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action.actionName, iouReport]); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]); const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); @@ -90,7 +91,7 @@ function ReportActionItemSingle({ displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); - } else if (action.delegateAccountID && personalDetails[action.delegateAccountID]) { + } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. const delegateDetails = personalDetails[action.delegateAccountID]; @@ -141,7 +142,7 @@ function ReportActionItemSingle({ text: displayName, }, ] - : action.person; + : action?.person; const reportID = report?.reportID; const iouReportID = iouReport?.reportID; @@ -155,14 +156,14 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); + showUserDetails(action?.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); } - }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); + }, [isWorkspaceActor, reportID, actorAccountID, action?.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( () => CONST.RESTRICTED_ACCOUNT_IDS.includes(actorAccountID ?? 0) || - (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), + (!isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(action?.delegateAccountID ? Number(action.delegateAccountID) : actorAccountID ?? -1)), [action, isWorkspaceActor, actorAccountID], ); @@ -189,7 +190,7 @@ function ReportActionItemSingle({ return ( @@ -237,13 +238,13 @@ function ReportActionItemSingle({ {personArray?.map((fragment, index) => ( ))} @@ -255,7 +256,7 @@ function ReportActionItemSingle({ >{`${status?.emojiCode}`} )} - + ) : null} {children} diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index f7c7e5fcf91d..c0dbe2a3825d 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; @@ -26,7 +27,7 @@ type ReportActionItemThreadProps = { isHovered: boolean; /** The function that should be called when the thread is LongPressed or right-clicked */ - onSecondaryInteraction: () => void; + onSecondaryInteraction: (event: GestureResponderEvent | MouseEvent) => void; }; function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childReportID, isHovered, onSecondaryInteraction}: ReportActionItemThreadProps) { diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 5e9d863dd62d..ca3ee7d2ab6a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -194,7 +194,7 @@ function ReportActionsView(props) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments - Report.getOlderActions(reportID, oldestReportAction.reportActionID); + Report.getOlderActions(reportID); }, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]); /** @@ -223,10 +223,9 @@ function ReportActionsView(props) { return; } - const newestReportAction = _.first(props.reportActions); - Report.getNewerActions(reportID, newestReportAction.reportActionID); + Report.getNewerActions(reportID); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID, hasNewestReportAction], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, hasNewestReportAction], ); /** diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx index a9e284329421..7151cc84e735 100644 --- a/src/pages/home/sidebar/AllSettingsScreen.tsx +++ b/src/pages/home/sidebar/AllSettingsScreen.tsx @@ -1,11 +1,11 @@ import React, {useMemo} from 'react'; -import {ScrollView} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 8d7272df63e9..62b1adf1fb8c 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -62,6 +63,7 @@ const defaultProps = { function MoneyRequestSelectorPage(props) { const styles = useThemeStyles(); const [isDraggingOver, setIsDraggingOver] = useState(false); + const {canUseP2PDistanceRequests} = usePermissions(); const iouType = lodashGet(props.route, 'params.iouType', ''); const reportID = lodashGet(props.route, 'params.reportID', ''); @@ -75,7 +77,7 @@ function MoneyRequestSelectorPage(props) { const isFromGlobalCreate = !reportID; const isExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const isExpenseReport = ReportUtils.isExpenseReport(props.report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; const resetMoneyRequestInfo = () => { const moneyRequestID = `${iouType}${reportID}`; diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 8e50577ede1f..b1ae257b792f 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -13,6 +13,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -80,6 +81,7 @@ function IOURequestStartPage({ }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); + const {canUseP2PDistanceRequests} = usePermissions(); const isFromGlobalCreate = _.isEmpty(report.reportID); useFocusEffect( @@ -102,12 +104,12 @@ function IOURequestStartPage({ if (transaction.reportID === reportID) { return; } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current); - }, [transaction, reportID, iouType, isFromGlobalCreate]); + IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, transactionRequestType.current); + }, [transaction, policy, reportID, iouType, isFromGlobalCreate]); const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); @@ -124,10 +126,10 @@ function IOURequestStartPage({ if (iouType === CONST.IOU.TYPE.SPLIT && transaction.isFromGlobalCreate) { IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST, newIouType); } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, newIouType); + IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; }, - [previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate], + [policy, previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate], ); if (!transaction.transactionID) { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 95dda131eab7..fb3a4d9457d5 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -90,6 +91,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {canUseP2PDistanceRequests} = usePermissions(); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -120,18 +122,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // sees the option to request money from their admin on their own Workspace Chat. 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. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, {}, [], false, {}, [], - - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); @@ -182,7 +180,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ } return [newSections, chatOptions]; - }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]); + }, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, canUseP2PDistanceRequests, translate]); /** * Adds a single participant to the request @@ -257,7 +255,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // 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 shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; + const isAllowedToSplit = canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index af1de64f8930..1b53dab12fa3 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; import TagPicker from '@components/TagPicker'; @@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import {canEditMoneyRequest} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; @@ -78,10 +80,12 @@ function IOURequestStepTag({ const tag = TransactionUtils.getTag(transaction, tagIndex); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); const parentReportAction = parentReportActions[report.parentReportActionID]; + const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = isEditing && !canEditMoneyRequest(parentReportAction); + const shouldShowNotFoundPage = !shouldShowTag || (isEditing && !canEditMoneyRequest(parentReportAction)); const navigateBack = () => { Navigation.goBack(backTo); diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index cb1f73ae2207..55bf77e9ae88 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -1,11 +1,12 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import type {ValueOf} from 'type-fest'; import BigNumberPad from '@components/BigNumberPad'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; +import ScrollView from '@components/ScrollView'; import TextInputWithCurrencySymbol from '@components/TextInputWithCurrencySymbol'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..1ad6488aeee9 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -94,6 +95,7 @@ function MoneyRequestParticipantsSelector({ const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {canUseP2PDistanceRequests} = usePermissions(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -113,8 +115,7 @@ function MoneyRequestParticipantsSelector({ // sees the option to request money from their admin on their own Workspace Chat. 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, + canUseP2PDistanceRequests || !isDistanceRequest, false, {}, [], @@ -123,7 +124,7 @@ function MoneyRequestParticipantsSelector({ [], // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - !isDistanceRequest, + canUseP2PDistanceRequests || !isDistanceRequest, true, ); return { @@ -131,7 +132,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); + }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]); /** * Returns the sections needed for the OptionsSelector @@ -272,7 +273,7 @@ function MoneyRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; + const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx index 3346b044ceca..0c087b2c93d6 100644 --- a/src/pages/settings/AboutPage/AboutPage.tsx +++ b/src/pages/settings/AboutPage/AboutPage.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useMemo, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, Text as RNText, StyleProp, ViewStyle} from 'react-native'; import DeviceInfo from 'react-native-device-info'; @@ -9,6 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; diff --git a/src/pages/settings/AppDownloadLinks.tsx b/src/pages/settings/AppDownloadLinks.tsx index 352b3772923a..e4165178ff2f 100644 --- a/src/pages/settings/AppDownloadLinks.tsx +++ b/src/pages/settings/AppDownloadLinks.tsx @@ -1,11 +1,11 @@ import React, {useRef} from 'react'; -import {ScrollView} from 'react-native'; import type {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import type {MenuItemProps} from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx index 7459819afd99..14739c4ffc52 100644 --- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx +++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect} from 'react'; -import {View} from 'react-native'; +import {NativeModules, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Icon from '@components//Icon'; @@ -84,6 +84,12 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS text={translate('exitSurvey.goToExpensifyClassic')} onPress={() => { ExitSurvey.switchToOldDot(); + + if (NativeModules.HybridAppModule) { + NativeModules.HybridAppModule.closeReactNativeApp(); + return; + } + Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX); }} isLoading={isLoading ?? false} diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index b29fd600ae16..2f2343027cf0 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,7 +1,7 @@ import {useNavigationState} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; -import {NativeModules, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -174,23 +174,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa ], }; - if (NativeModules.HybridAppModule) { - const hybridAppMenuItems: MenuData[] = [ - { - translationKey: 'initialSettingsPage.returnToClassic' as const, - icon: Expensicons.RotateLeft, - shouldShowRightIcon: true, - iconRight: Expensicons.NewWindow, - action: () => { - NativeModules.HybridAppModule.closeReactNativeApp(); - }, - }, - ...defaultMenu.items, - ].filter((item) => item.translationKey !== 'initialSettingsPage.signOut' && item.translationKey !== 'exitSurvey.goToExpensifyClassic'); - - return {sectionStyle: styles.accountSettingsSectionContainer, sectionTranslationKey: 'initialSettingsPage.account', items: hybridAppMenuItems}; - } - return defaultMenu; }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, signOut]); diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.js index 0fd6121fe512..36a26ccffaa2 100755 --- a/src/pages/settings/Preferences/PreferencesPage.js +++ b/src/pages/settings/Preferences/PreferencesPage.js @@ -1,13 +1,14 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Switch from '@components/Switch'; import Text from '@components/Text'; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 18589beb6353..2ba4fc33580b 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard, ScrollView, View} from 'react-native'; +import {InteractionManager, Keyboard, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -13,6 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 5d150e782c44..3851ef7153fb 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -1,7 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 968d9e502806..2fa133f41616 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -10,6 +10,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemGroup from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; diff --git a/src/pages/settings/Report/ReportSettingsPage.tsx b/src/pages/settings/Report/ReportSettingsPage.tsx index 54057f7c05bb..383cbbcb0833 100644 --- a/src/pages/settings/Report/ReportSettingsPage.tsx +++ b/src/pages/settings/Report/ReportSettingsPage.tsx @@ -1,12 +1,13 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index 8600c9e08471..01563e586792 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -1,11 +1,12 @@ import React, {useMemo} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx index d6c7a1abcd4f..b4c1bc249c81 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import {ActivityIndicator, ScrollView, View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; @@ -7,6 +7,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx index 59c145f9e348..ad9a4060af45 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx @@ -1,8 +1,9 @@ import React, {useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx index d9998c777f3b..58e7d98d69de 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; import Button from '@components/Button'; @@ -8,6 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import {useSession} from '@components/OnyxProvider'; import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle'; import QRCode from '@components/QRCode'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index a8b676f6c379..097b2cf28ed0 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx index 93ead17e9523..85b7bef0550c 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.tsx +++ b/src/pages/settings/Wallet/TransferBalancePage.tsx @@ -1,5 +1,5 @@ import React, {useEffect} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index b9f49049d51a..88236e06f9a9 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -2,7 +2,7 @@ import _ from 'lodash'; import type {ForwardedRef, RefObject} from 'react'; import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; -import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native'; +import {ActivityIndicator, Dimensions, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import Button from '@components/Button'; @@ -18,6 +18,7 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Popover from '@components/Popover'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -74,7 +75,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi }); const addPaymentMethodAnchorRef = useRef(null); - const paymentMethodButtonRef = useRef(null); + const paymentMethodButtonRef = useRef(null); const [anchorPosition, setAnchorPosition] = useState({ anchorPositionHorizontal: 0, anchorPositionVertical: 0, @@ -163,7 +164,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi setShouldShowDefaultDeleteMenu(false); return; } - paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLElement; + paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement; // The delete/default menu if (accountType) { diff --git a/src/pages/signin/SAMLSignInPage/index.tsx b/src/pages/signin/SAMLSignInPage/index.tsx index 701c2917bea6..1ff9d02672be 100644 --- a/src/pages/signin/SAMLSignInPage/index.tsx +++ b/src/pages/signin/SAMLSignInPage/index.tsx @@ -7,7 +7,7 @@ import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types'; function SAMLSignInPage({credentials}: SAMLSignInPageProps) { useEffect(() => { - window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); + window.location.replace(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`); }, [credentials?.login]); return ; diff --git a/src/pages/signin/SignInPageLayout/index.tsx b/src/pages/signin/SignInPageLayout/index.tsx index b65da7eba0a5..3532c17181db 100644 --- a/src/pages/signin/SignInPageLayout/index.tsx +++ b/src/pages/signin/SignInPageLayout/index.tsx @@ -1,8 +1,11 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {ScrollView as RNScrollView} from 'react-native'; +import {View} from 'react-native'; import SignInGradient from '@assets/images/home-fade-gradient.svg'; import ImageSVG from '@components/ImageSVG'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -38,7 +41,7 @@ function SignInPageLayout( const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); const {top: topInsets, bottom: bottomInsets} = useSafeAreaInsets(); - const scrollViewRef = useRef(null); + const scrollViewRef = useRef(null); const prevPreferredLocale = usePrevious(preferredLocale); const {windowHeight, isMediumScreenWidth, isLargeScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js index f77285190e62..352c08115114 100644 --- a/src/pages/tasks/NewTaskPage.js +++ b/src/pages/tasks/NewTaskPage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -155,14 +156,7 @@ function NewTaskPage(props) { Navigation.goBack(ROUTES.NEW_TASK_DETAILS); }} /> - + ; /** Grab the Share destination of the Task */ - task: PropTypes.shape({ - /** Share destination of the Task */ - shareDestination: PropTypes.string, - - /** The task report if it's currently being edited */ - report: reportPropTypes, - }), - - /** The policy of root parent report */ - rootParentReportPolicy: PropTypes.shape({ - /** The role of current user */ - role: PropTypes.string, - }), + task: OnyxEntry; }; -const defaultProps = { - reports: {}, - task: {}, - rootParentReportPolicy: {}, +type UseOptions = { + reports: OnyxCollection; }; -function useOptions({reports}) { +type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; + +function useOptions({reports}: UseOptions) { const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); @@ -78,7 +69,7 @@ function useOptions({reports}) { ); const headerMessage = OptionsListUtils.getHeaderMessage( - (recentReports.length || 0 + personalDetails.length || 0) !== 0 || currentUserOption, + (recentReports?.length || 0) + (personalDetails?.length || 0) !== 0 || Boolean(currentUserOption), Boolean(userToInvite), debouncedSearchValue, ); @@ -99,20 +90,20 @@ function useOptions({reports}) { return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; } -function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { +function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); - const route = useRoute(); + const route = useRoute>(); const {translate} = useLocalize(); const session = useSession(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task}); + const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports}); const onChangeText = (newSearchTerm = '') => { setSearchValue(newSearchTerm); }; - const report = useMemo(() => { - if (!route.params || !route.params.reportID) { + const report: OnyxEntry = useMemo(() => { + if (!route.params?.reportID) { return null; } if (report && !ReportUtils.isTaskReport(report)) { @@ -120,7 +111,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { Navigation.dismissModal(report.reportID); }); } - return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`]; + return reports?.[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID}`] ?? null; }, [reports, route]); const sections = useMemo(() => { @@ -155,17 +146,29 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { if (userToInvite) { sectionsList.push({ + title: '', data: [userToInvite], shouldShow: true, indexOffset, }); } - return sectionsList; - }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]); + return sectionsList.map((section) => ({ + ...section, + data: section.data.map((option) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + })); + }, [currentUserOption, personalDetails, recentReports, translate, userToInvite]); const selectReport = useCallback( - (option) => { + (option: ListItem) => { if (!option) { return; } @@ -173,25 +176,35 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { // Check to see if we're editing a task and if so, update the assignee if (report) { if (option.accountID !== report.managerID) { - const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option)); + const assigneeChatReport = TaskActions.setAssigneeValue( + option?.login ?? '', + option?.accountID ?? -1, + report.reportID, + OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? ''}), + ); // Pass through the selected assignee - Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport); + TaskActions.editTaskAssignee(report, session?.accountID ?? 0, option?.login ?? '', option?.accountID, assigneeChatReport); } Navigation.dismissModal(report.reportID); // If there's no report, we're creating a new task } else if (option.accountID) { - Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option)); + TaskActions.setAssigneeValue( + option?.login ?? '', + option.accountID, + task?.shareDestination ?? '', + OptionsListUtils.isCurrentUser({...option, accountID: option?.accountID ?? -1, login: option?.login ?? undefined}), + ); Navigation.goBack(ROUTES.NEW_TASK); } }, - [session.accountID, task.shareDestination, report], + [session?.accountID, task?.shareDestination, report], ); - const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); + const handleBackButtonPress = useCallback(() => (route.params?.reportID ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]); const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', '')); + const canModifyTask = TaskActions.canModifyTask(report, currentUserPersonalDetails.accountID); const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( @@ -199,7 +212,7 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { includeSafeAreaPaddingBottom={false} testID={TaskAssigneeSelectorModal.displayName} > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( @@ -225,26 +237,14 @@ function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) { } TaskAssigneeSelectorModal.displayName = 'TaskAssigneeSelectorModal'; -TaskAssigneeSelectorModal.propTypes = propTypes; -TaskAssigneeSelectorModal.defaultProps = defaultProps; -export default compose( - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - task: { - key: ONYXKEYS.TASK, - }, - }), - withOnyx({ - rootParentReportPolicy: { - key: ({reports, route}) => { - const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`]; - const rootParentReport = ReportUtils.getRootParentReport(report); - return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`; - }, - selector: (policy) => lodashPick(policy, ['role']), - }, - }), -)(TaskAssigneeSelectorModal); +const TaskAssigneeSelectorModalWithOnyx = withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + task: { + key: ONYXKEYS.TASK, + }, +})(TaskAssigneeSelectorModal); + +export default withCurrentUserPersonalDetails(TaskAssigneeSelectorModalWithOnyx); diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.tsx similarity index 61% rename from src/pages/tasks/TaskDescriptionPage.js rename to src/pages/tasks/TaskDescriptionPage.tsx index b8b48abd09ff..e08d6380bb18 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -2,53 +2,43 @@ import {useFocusEffect} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/EditTaskForm'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - - /* Onyx Props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, -}; +type TaskDescriptionPageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps; const parser = new ExpensiMark(); -function TaskDescriptionPage(props) { + +function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescriptionPageProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); - /** - * @param {Object} values - form input values passed by the Form component - * @returns {Boolean} - */ - const validate = useCallback((values) => { + const validate = useCallback((values: FormOnyxValues): FormInputErrors => { const errors = {}; - if (values.description.length > CONST.DESCRIPTION_LIMIT) { + if (values?.description && values.description?.length > CONST.DESCRIPTION_LIMIT) { ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]); } @@ -56,30 +46,30 @@ function TaskDescriptionPage(props) { }, []); const submit = useCallback( - (values) => { - // props.report.description might contain CRLF from the server - if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(props.report.description)) { + (values: FormOnyxValues) => { + // report.description might contain CRLF from the server + if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(report?.description) && !isEmptyObject(report)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server - Task.editTask(props.report, {description: values.description}); + Task.editTask(report, {description: values.description}); } - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }, - [props], + [report], ); - if (!ReportUtils.isTaskReport(props.report)) { + if (!ReportUtils.isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }); } - const inputRef = useRef(null); - const focusTimeoutRef = useRef(null); + const inputRef = useRef(null); + const focusTimeoutRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(props.report); - const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const isOpen = ReportUtils.isOpenTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); useFocusEffect( useCallback(() => { @@ -104,13 +94,13 @@ function TaskDescriptionPage(props) { testID={TaskDescriptionPage.displayName} > - + @@ -119,14 +109,14 @@ function TaskDescriptionPage(props) { role={CONST.ROLE.PRESENTATION} inputID={INPUT_IDS.DESCRIPTION} name={INPUT_IDS.DESCRIPTION} - label={props.translate('newTaskPage.descriptionOptional')} - accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} - defaultValue={parser.htmlToMarkdown((props.report && parser.replace(props.report.description)) || '')} - ref={(el) => { - if (!el) { + label={translate('newTaskPage.descriptionOptional')} + accessibilityLabel={translate('newTaskPage.descriptionOptional')} + defaultValue={parser.htmlToMarkdown((report && parser.replace(report?.description ?? '')) || '')} + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } - inputRef.current = el; + inputRef.current = element; updateMultilineInputRange(inputRef.current); }} autoGrowHeight @@ -140,17 +130,8 @@ function TaskDescriptionPage(props) { ); } -TaskDescriptionPage.propTypes = propTypes; -TaskDescriptionPage.defaultProps = defaultProps; TaskDescriptionPage.displayName = 'TaskDescriptionPage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - }), -)(TaskDescriptionPage); +const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskDescriptionPage); + +export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails); diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx similarity index 62% rename from src/pages/tasks/TaskShareDestinationSelectorModal.js rename to src/pages/tasks/TaskShareDestinationSelectorModal.tsx index b62440b22967..5b56e58752ac 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -1,9 +1,7 @@ -import keys from 'lodash/keys'; -import reduce from 'lodash/reduce'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,51 +11,45 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Report from '@libs/actions/Report'; +import * as ReportActions from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; -const propTypes = { - /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), - /** Whether or not we are searching for reports on the server */ - isSearchingForReports: PropTypes.bool, -}; +type TaskShareDestinationSelectorModalOnyxProps = { + reports: OnyxCollection; -const defaultProps = { - reports: {}, - isSearchingForReports: false, + isSearchingForReports: OnyxEntry; }; -const selectReportHandler = (option) => { - if (!option || !option.reportID) { +type TaskShareDestinationSelectorModalProps = TaskShareDestinationSelectorModalOnyxProps; + +const selectReportHandler = (option: unknown) => { + const optionItem = option as ReportUtils.OptionData; + + if (!optionItem || !optionItem?.reportID) { return; } - Task.setShareDestinationValue(option.reportID); + Task.setShareDestinationValue(optionItem?.reportID); Navigation.goBack(ROUTES.NEW_TASK); }; -const reportFilter = (reports) => - reduce( - keys(reports), - (filtered, reportKey) => { - const report = reports[reportKey]; - if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { - return {...filtered, [reportKey]: report}; - } - return filtered; - }, - {}, - ); +const reportFilter = (reports: OnyxCollection) => + Object.keys(reports ?? {}).reduce((filtered, reportKey) => { + const report: OnyxEntry = reports?.[reportKey] ?? null; + if (ReportUtils.canUserPerformWriteAction(report) && ReportUtils.canCreateTaskInReport(report) && !ReportUtils.isCanceledTaskReport(report)) { + return {...filtered, [reportKey]: report}; + } + return filtered; + }, {}); -function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { +function TaskShareDestinationSelectorModal({reports, isSearchingForReports}: TaskShareDestinationSelectorModalProps) { const styles = useThemeStyles(); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const {translate} = useLocalize(); @@ -73,13 +65,29 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { const headerMessage = OptionsListUtils.getHeaderMessage(recentReports && recentReports.length !== 0, false, debouncedSearchValue); - const sections = recentReports && recentReports.length > 0 ? [{data: recentReports, shouldShow: true}] : []; + const sections = + recentReports && recentReports.length > 0 + ? [ + { + data: recentReports.map((option) => ({ + ...option, + text: option.text ?? '', + alternateText: option.alternateText ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + shouldShow: true, + }, + ] + : []; return {sections, headerMessage}; }, [personalDetails, reports, debouncedSearchValue]); useEffect(() => { - Report.searchInServer(debouncedSearchValue); + ReportActions.searchInServer(debouncedSearchValue); }, [debouncedSearchValue]); return ( @@ -87,7 +95,7 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { includeSafeAreaPaddingBottom={false} testID="TaskShareDestinationSelectorModal" > - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + {({didScreenTransitionEnd}) => ( <> @@ -115,10 +122,8 @@ function TaskShareDestinationSelectorModal({reports, isSearchingForReports}) { } TaskShareDestinationSelectorModal.displayName = 'TaskShareDestinationSelectorModal'; -TaskShareDestinationSelectorModal.propTypes = propTypes; -TaskShareDestinationSelectorModal.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.tsx similarity index 50% rename from src/pages/tasks/TaskTitlePage.js rename to src/pages/tasks/TaskTitlePage.tsx index 370baab7cd89..009983beac3e 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.tsx @@ -1,98 +1,85 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/EditTaskForm'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, +type TaskTitlePageProps = WithReportOrNotFoundProps & WithCurrentUserPersonalDetailsProps; - /* Onyx Props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, -}; - -function TaskTitlePage(props) { +function TaskTitlePage({report, currentUserPersonalDetails}: TaskTitlePageProps) { const styles = useThemeStyles(); - /** - * @param {Object} values - * @param {String} values.title - * @returns {Object} - An object containing the errors for each inputID - */ - const validate = useCallback((values) => { - const errors = {}; + const {translate} = useLocalize(); + + const validate = useCallback(({title}: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; - if (_.isEmpty(values.title)) { + if (!title) { errors.title = 'newTaskPage.pleaseEnterTaskName'; - } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; }, []); const submit = useCallback( - (values) => { - if (values.title !== props.report.reportName) { + (values: FormOnyxValues) => { + if (values.title !== report?.reportName && !isEmptyObject(report)) { // Set the title of the report in the store and then call EditTask API // to update the title of the report on the server - Task.editTask(props.report, {title: values.title}); + Task.editTask(report, {title: values.title}); } - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }, - [props], + [report], ); - if (!ReportUtils.isTaskReport(props.report)) { + if (!ReportUtils.isTaskReport(report)) { Navigation.isNavigationReady().then(() => { - Navigation.dismissModal(props.report.reportID); + Navigation.dismissModal(report?.reportID); }); } - const inputRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(props.report); - const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(props.report) && (!canModifyTask || !isOpen); + const inputRef = useRef(null); + const isOpen = ReportUtils.isOpenTaskReport(report); + const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); return ( inputRef.current && inputRef.current.focus()} + onEntryTransitionEnd={() => { + inputRef?.current?.focus(); + }} shouldEnableMaxHeight testID={TaskTitlePage.displayName} > {({didScreenTransitionEnd}) => ( - + @@ -101,17 +88,17 @@ function TaskTitlePage(props) { role={CONST.ROLE.PRESENTATION} inputID={INPUT_IDS.TITLE} name={INPUT_IDS.TITLE} - label={props.translate('task.title')} - accessibilityLabel={props.translate('task.title')} - defaultValue={(props.report && props.report.reportName) || ''} - ref={(el) => { - if (!el) { + label={translate('task.title')} + accessibilityLabel={translate('task.title')} + defaultValue={report?.reportName ?? ''} + ref={(element: AnimatedTextInputRef) => { + if (!element) { return; } if (!inputRef.current && didScreenTransitionEnd) { - el.focus(); + element.focus(); } - inputRef.current = el; + inputRef.current = element; }} /> @@ -122,17 +109,8 @@ function TaskTitlePage(props) { ); } -TaskTitlePage.propTypes = propTypes; -TaskTitlePage.defaultProps = defaultProps; TaskTitlePage.displayName = 'TaskTitlePage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, - }, - }), -)(TaskTitlePage); +const ComponentWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(TaskTitlePage); + +export default withReportOrNotFound()(ComponentWithCurrentUserPersonalDetails); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index d2565022075a..c4f4d6399dbd 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useState} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; diff --git a/src/pages/workspace/WorkspaceJoinUserPage.tsx b/src/pages/workspace/WorkspaceJoinUserPage.tsx new file mode 100644 index 000000000000..8167e6fc1ebf --- /dev/null +++ b/src/pages/workspace/WorkspaceJoinUserPage.tsx @@ -0,0 +1,80 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useThemeStyles from '@hooks/useThemeStyles'; +import navigateAfterJoinRequest from '@libs/navigateAfterJoinRequest'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import type {AuthScreensParamList} from '@navigation/types'; +import * as PolicyAction from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; + +type WorkspaceJoinUserPageOnyxProps = { + /** The list of this user's policies */ + policies: OnyxCollection; +}; + +type WorkspaceJoinUserPageRoute = {route: StackScreenProps['route']}; +type WorkspaceJoinUserPageProps = WorkspaceJoinUserPageRoute & WorkspaceJoinUserPageOnyxProps; + +let isJoinLinkUsed = false; + +function WorkspaceJoinUserPage({route, policies}: WorkspaceJoinUserPageProps) { + const styles = useThemeStyles(); + const policyID = route?.params?.policyID; + const inviterEmail = route?.params?.email; + const policy = ReportUtils.getPolicy(policyID); + const isUnmounted = useRef(false); + + useEffect(() => { + if (!isJoinLinkUsed) { + return; + } + Navigation.goBack(undefined, false, true); + }, []); + + useEffect(() => { + if (!policy || !policies || isUnmounted.current || isJoinLinkUsed) { + return; + } + const isPolicyMember = PolicyUtils.isPolicyMember(policyID, policies as Record); + if (isPolicyMember) { + Navigation.goBack(undefined, false, true); + return; + } + PolicyAction.inviteMemberToWorkspace(policyID, inviterEmail); + isJoinLinkUsed = true; + Navigation.isNavigationReady().then(() => { + if (isUnmounted.current) { + return; + } + navigateAfterJoinRequest(); + }); + }, [policy, policyID, policies, inviterEmail]); + + useEffect( + () => () => { + isUnmounted.current = true; + }, + [], + ); + + return ( + + + + ); +} + +WorkspaceJoinUserPage.displayName = 'WorkspaceJoinUserPage'; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(WorkspaceJoinUserPage); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index e47bc4a09be4..42f29f885c00 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -254,6 +254,23 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se [selectedEmployees, addUser, removeUser], ); + /** Opens the member details page */ + const openMemberDetails = useCallback( + (item: MemberOption) => { + if (!isPolicyAdmin) { + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); + return; + } + + if (!PolicyUtils.isPaidGroupPolicy(policy)) { + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(route.params.policyID, item.accountID, Navigation.getActiveRoute())); + }, + [isPolicyAdmin, policy, route.params.policyID], + ); + /** * Dismisses the errors on one item */ @@ -417,22 +434,24 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se }, ]; - if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) { - options.push({ - text: translate('workspace.people.makeMember'), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, - icon: Expensicons.User, - onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER), - }); - } + if (PolicyUtils.isPaidGroupPolicy(policy)) { + if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.ADMIN)) { + options.push({ + text: translate('workspace.people.makeMember'), + value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, + icon: Expensicons.User, + onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER), + }); + } - if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) { - options.push({ - text: translate('workspace.people.makeAdmin'), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, - icon: Expensicons.MakeAdmin, - onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN), - }); + if (selectedEmployees.find((employee) => policyMembers?.[employee]?.role === CONST.POLICY.ROLE.USER)) { + options.push({ + text: translate('workspace.people.makeAdmin'), + value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, + icon: Expensicons.MakeAdmin, + onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN), + }); + } } return options; @@ -463,7 +482,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se onPress={inviteUser} text={translate('workspace.invite.member')} icon={Expensicons.Plus} - iconStyles={{transform: [{scale: 0.6}]}} + iconStyles={StyleUtils.getTransformScaleStyle(0.6)} innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]} style={[isSmallScreenWidth && styles.flexGrow1]} /> @@ -523,13 +542,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} headerContent={getHeaderContent()} - onSelectRow={(item) => { - if (!isPolicyAdmin) { - Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); - return; - } - toggleUser(item.accountID); - }} + onSelectRow={openMemberDetails} + onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))} diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 796f32c343f2..9d90557b1d37 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,15 +1,17 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; -import {Image, ScrollView, StyleSheet, View} from 'react-native'; +import {Image, StyleSheet, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -74,6 +76,19 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi [policy?.avatar, policyName, styles.alignSelfCenter, styles.avatarXLarge], ); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const confirmDeleteAndHideModal = useCallback(() => { + if (!policy?.id || !policyName) { + return; + } + + Policy.deleteWorkspace(policy?.id, policyName); + + PolicyUtils.goBackFromInvalidPolicy(); + + setIsDeleteModalOpen(false); + }, [policy?.id, policyName]); return ( {!readOnly && ( - +