diff --git a/src/Expensify.js b/src/Expensify.js index fd269fbc27f4..50bab06874ef 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -26,7 +26,7 @@ Onyx.init({ [ONYXKEYS.SESSION]: {loading: false, shouldShowComposeInput: true}, [ONYXKEYS.ACCOUNT]: CONST.DEFAULT_ACCOUNT_DATA, [ONYXKEYS.NETWORK]: {isOffline: false}, - [ONYXKEYS.IOU]: {loading: false}, + [ONYXKEYS.IOU]: {loading: false, error: false, creatingIOUTransaction: false}, }, registerStorageEventListener: (onStorageEvent) => { listenToStorageEvents(onStorageEvent); diff --git a/src/ROUTES.js b/src/ROUTES.js index 939787218e17..886816e14cfc 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -26,6 +26,9 @@ export default { getIouRequestRoute: reportID => `iou/request/${reportID}`, IOU_BILL: 'iou/split/:reportID', getIouSplitRoute: reportID => `iou/split/${reportID}`, + IOU_DETAILS: 'iou/details', + IOU_DETAILS_WITH_IOU_REPORT_ID: 'iou/details/:chatReportID/:iouReportID/', + getIouDetailsRoute: (chatReportID, iouReportID) => `iou/details/${chatReportID}/${iouReportID}`, SEARCH: 'search', SET_PASSWORD_WITH_VALIDATE_CODE: 'setpassword/:accountID/:validateCode', DETAILS: 'details', diff --git a/src/components/ReportActionItemIOUAction.js b/src/components/ReportActionItemIOUAction.js new file mode 100644 index 000000000000..ca6564e68ae5 --- /dev/null +++ b/src/components/ReportActionItemIOUAction.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../ONYXKEYS'; +import ReportActionItemIOUQuote from './ReportActionItemIOUQuote'; +import ReportActionPropTypes from '../pages/home/report/ReportActionPropTypes'; +import ReportActionItemIOUPreview from './ReportActionItemIOUPreview'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; + +const propTypes = { + /** All the data of the action */ + action: PropTypes.shape(ReportActionPropTypes).isRequired, + + /** The associated chatReport */ + chatReportID: PropTypes.number.isRequired, + + /** Should render the preview Component? */ + shouldDisplayPreview: PropTypes.bool.isRequired, + + /* Onyx Props */ + /** ChatReport associated with iouReport */ + chatReport: PropTypes.shape({ + /** The participants of this report */ + participants: PropTypes.arrayOf(PropTypes.string), + }), +}; + +const defaultProps = { + chatReport: {}, +}; + +const ReportActionItemIOUAction = ({ + action, + chatReportID, + shouldDisplayPreview, + chatReport, +}) => { + const launchDetailsModal = () => { + Navigation.navigate(ROUTES.getIouDetailsRoute(chatReportID, action.originalMessage.IOUReportID)); + }; + const hasMultipleParticipants = chatReport.participants.length >= 2; + return ( + <> + + {shouldDisplayPreview && ( + + )} + + ); +}; + +ReportActionItemIOUAction.propTypes = propTypes; +ReportActionItemIOUAction.defaultProps = defaultProps; +ReportActionItemIOUAction.displayName = 'ReportActionItemIOUAction'; + +export default withOnyx({ + chatReport: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, + }, +})(ReportActionItemIOUAction); diff --git a/src/components/ReportActionItemIOUPreview.js b/src/components/ReportActionItemIOUPreview.js index 3851145d3f9f..fb89bf51dadd 100644 --- a/src/components/ReportActionItemIOUPreview.js +++ b/src/components/ReportActionItemIOUPreview.js @@ -1,26 +1,25 @@ import React from 'react'; -import {View, TouchableOpacity} from 'react-native'; +import {View, TouchableOpacity, Text} from 'react-native'; import PropTypes from 'prop-types'; +import Str from 'expensify-common/lib/str'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import lodashGet from 'lodash/get'; -import Str from 'expensify-common/lib/str'; +import compose from '../libs/compose'; +import styles from '../styles/styles'; import ONYXKEYS from '../ONYXKEYS'; -import ReportActionItemIOUQuote from './ReportActionItemIOUQuote'; -import ReportActionPropTypes from '../pages/home/report/ReportActionPropTypes'; -import Text from './Text'; import MultipleAvatars from './MultipleAvatars'; -import styles from '../styles/styles'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; const propTypes = { - /** All the data of the action */ - action: PropTypes.shape(ReportActionPropTypes).isRequired, + /** Additional logic for displaying the pay button */ + shouldHidePayButton: PropTypes.bool, - /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, + /** Callback for the Pay/Settle button */ + onPayButtonPressed: PropTypes.func, - /** Whether there is an outstanding amount in IOU */ - hasOutstandingIOU: PropTypes.bool.isRequired, + /** The active IOUReport, used for Onyx subscription */ + /* eslint-disable-next-line react/no-unused-prop-types */ + iouReportID: PropTypes.number, /* Onyx Props */ @@ -34,6 +33,9 @@ const propTypes = { /** Outstanding amount of this transaction */ cachedTotal: PropTypes.string, + + /** Does the report have an outstanding IOU that needs to be paid? */ + hasOutstandingIOU: PropTypes.bool, }), /** All of the personal details for everyone */ @@ -48,20 +50,30 @@ const propTypes = { /** Currently logged in user email */ email: PropTypes.string, }).isRequired, + + ...withLocalizePropTypes, }; const defaultProps = { iou: {}, + iouReportID: undefined, + shouldHidePayButton: false, + onPayButtonPressed: null, }; const ReportActionItemIOUPreview = ({ - action, - isMostRecentIOUReportAction, - hasOutstandingIOU, iou, personalDetails, session, + shouldHidePayButton, + onPayButtonPressed, + translate, }) => { + const sessionEmail = lodashGet(session, 'email', null); + + // Pay button should only be visible to the manager of the report. + const isCurrentUserManager = iou.managerEmail === sessionEmail; + const managerName = lodashGet( personalDetails, [iou.managerEmail, 'displayName'], @@ -74,51 +86,40 @@ const ReportActionItemIOUPreview = ({ ); const managerAvatar = lodashGet(personalDetails, [iou.managerEmail, 'avatar'], ''); const ownerAvatar = lodashGet(personalDetails, [iou.ownerEmail, 'avatar'], ''); - const sessionEmail = lodashGet(session, 'email', null); const cachedTotal = iou.cachedTotal ? iou.cachedTotal.replace(/[()]/g, '') : ''; - // Pay button should be visible to manager person in the report - // Check if the currently logged in user is the manager. - const isCurrentUserManager = iou.managerEmail === sessionEmail; - return ( - - - {isMostRecentIOUReportAction - && hasOutstandingIOU - && !_.isEmpty(iou) && ( - - - - {cachedTotal} - - {managerName} - {' owes '} - {ownerName} - - - - - - - {isCurrentUserManager && ( - - - Pay - - - )} - + + + + {cachedTotal} + + {iou.hasOutstandingIOU + ? translate('iou.owes', {manager: managerName, owner: ownerName}) + : translate('iou.paid', {manager: managerName, owner: ownerName})} + + + + + + + {isCurrentUserManager && !shouldHidePayButton && ( + + + {translate('iou.pay')} + + )} ); @@ -128,14 +129,17 @@ ReportActionItemIOUPreview.propTypes = propTypes; ReportActionItemIOUPreview.defaultProps = defaultProps; ReportActionItemIOUPreview.displayName = 'ReportActionItemIOUPreview'; -export default withOnyx({ - iou: { - key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportID}`, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS, - }, - session: { - key: ONYXKEYS.SESSION, - }, -})(ReportActionItemIOUPreview); +export default compose( + withLocalize, + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, + iou: { + key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportID}`, + }, + session: { + key: ONYXKEYS.SESSION, + }, + }), +)(ReportActionItemIOUPreview); diff --git a/src/components/ReportActionItemIOUQuote.js b/src/components/ReportActionItemIOUQuote.js index 1b391e68395e..02ab47b860bb 100644 --- a/src/components/ReportActionItemIOUQuote.js +++ b/src/components/ReportActionItemIOUQuote.js @@ -1,29 +1,58 @@ import React from 'react'; -import {View} from 'react-native'; +import {View, Text} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import styles from '../styles/styles'; import ReportActionPropTypes from '../pages/home/report/ReportActionPropTypes'; -import RenderHTML from './RenderHTML'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; const propTypes = { /** All the data of the action */ action: PropTypes.shape(ReportActionPropTypes).isRequired, + + /** Should the View Details link be displayed? */ + shouldShowViewDetailsLink: PropTypes.bool, + + /** Callback invoked when View Details is pressed */ + onViewDetailsPressed: PropTypes.func, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + shouldShowViewDetailsLink: false, + onViewDetailsPressed: () => {}, }; -const ReportActionItemIOUQuote = ({action}) => ( +const ReportActionItemIOUQuote = ({ + action, + shouldShowViewDetailsLink, + onViewDetailsPressed, + translate, +}) => ( - {_.map(action.message, (fragment, index) => { - const viewDetails = '
View Details'; - const html = `
${fragment.text}${viewDetails}
`; - return ( - - ); - })} + {_.map(action.message, (fragment, index) => ( + + + + {fragment.text} + + {shouldShowViewDetailsLink && ( + + {translate('iou.viewDetails')} + + )} + + + ))}
); ReportActionItemIOUQuote.propTypes = propTypes; +ReportActionItemIOUQuote.defaultProps = defaultProps; ReportActionItemIOUQuote.displayName = 'ReportActionItemIOUQuote'; -export default ReportActionItemIOUQuote; +export default withLocalize(ReportActionItemIOUQuote); diff --git a/src/components/ReportTransaction.js b/src/components/ReportTransaction.js new file mode 100644 index 000000000000..747982060c5d --- /dev/null +++ b/src/components/ReportTransaction.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Text} from 'react-native'; +import styles from '../styles/styles'; +import ReportActionPropTypes from '../pages/home/report/ReportActionPropTypes'; +import ReportActionItemSingle from '../pages/home/report/ReportActionItemSingle'; + +const propTypes = { + /** The chatReport which the transaction is associated with */ + /* eslint-disable-next-line react/no-unused-prop-types */ + chatReportID: PropTypes.number.isRequired, + + /** ID for the IOU report */ + /* eslint-disable-next-line react/no-unused-prop-types */ + iouReportID: PropTypes.number.isRequired, + + /** The report action which we are displaying */ + action: PropTypes.shape(ReportActionPropTypes).isRequired, +}; + +const ReportTransaction = ({ + action, +}) => ( + + + {action.message[0].text} + + +); + +ReportTransaction.displayName = 'ReportTransaction'; +ReportTransaction.propTypes = propTypes; +export default ReportTransaction; diff --git a/src/languages/en.js b/src/languages/en.js index 4dbe13891733..8a84e7440af6 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -26,6 +26,7 @@ export default { phoneNumber: 'Phone Number', email: 'Email', and: 'and', + details: 'Details', }, attachmentPicker: { cameraPermissionRequired: 'Camera Permission Required', @@ -99,7 +100,12 @@ export default { confirm: 'Confirm', splitBill: 'Split Bill', requestMoney: 'Request Money', + pay: 'Pay', + viewDetails: 'View Details', + settleElsewhere: 'I\'ll settle up elsewhere', request: ({amount}) => `Request ${amount}`, + owes: ({manager, owner}) => `${manager} owes ${owner}`, + paid: ({owner, manager}) => `${manager} paid ${owner}`, }, loginField: { addYourPhoneToSettleViaVenmo: 'Add your phone number to settle up via Venmo.', @@ -198,7 +204,6 @@ export default { resendLink: 'Resend Link', }, detailsPage: { - details: 'Details', localTime: 'Local Time', }, newGroupPage: { diff --git a/src/libs/API.js b/src/libs/API.js index 5d53b436a17a..a6770801a185 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -468,6 +468,18 @@ function Graphite_Timer(parameters) { return Network.post(commandName, parameters); } +/** + * @param {Object} parameters + * @param {Number} parameters.reportID + * @param {String} parameters.paymentMethodType + * @returns {Promise} + */ +function PayIOU(parameters) { + const commandName = 'PayIOU'; + requireParameters(['reportID', 'paymentMethodType'], parameters, commandName); + return Network.post(commandName, parameters); +} + /** * @param {Object} parameters * @param {String} parameters.emailList @@ -787,6 +799,7 @@ export { Graphite_Timer, Log, Mobile_GetConstants, + PayIOU, PersonalDetails_GetForEmails, PersonalDetails_Update, Plaid_GetLinkToken, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index ad5dfbfc65f1..f9c8e072fd75 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -41,6 +41,7 @@ import ValidateLoginPage from '../../../pages/ValidateLoginPage'; import { IOUBillStackNavigator, IOURequestModalStackNavigator, + IOUDetailsModalStackNavigator, DetailsModalStackNavigator, ReportParticipantsModalStackNavigator, SearchModalStackNavigator, @@ -249,6 +250,11 @@ class AuthScreens extends React.Component { component={IOUBillStackNavigator} listeners={modalScreenListeners} /> + Onyx.merge(ONYXKEYS.IOU, {loading: false, creatingIOUTransaction: false, error: true})) + .catch(() => Onyx.merge(ONYXKEYS.IOU, {error: true})) .finally(() => Onyx.merge(ONYXKEYS.IOU, {loading: false, creatingIOUTransaction: false})); } @@ -111,8 +111,40 @@ function createIOUSplit(params) { }); } +/** + * Pays an IOU Report and then retrieves the iou and chat reports to trigger updates to the UI. + */ +function payIOUReport({ + chatReportID, reportID, paymentMethodType, +}) { + Onyx.merge(ONYXKEYS.IOU, {loading: true, error: false}); + API.PayIOU({ + reportID, + paymentMethodType, + }) + .then((response) => { + if (response.jsonCode !== 200) { + throw new Error(response.message); + } + fetchChatReportsByIDs([chatReportID]); + + // If an iouReport is open (has an IOU, but is not yet paid) then we sync the chatReport's 'iouReportID' + // field in Onyx, simplifying IOU data retrieval and reducing necessary API calls when displaying IOU + // components. If we didn't sync the reportIDs, the paid IOU would still be shown to users as unpaid. The + // iouReport being fetched here must be open, because only an open iouReoport can be paid. + // Therefore, we should also sync the chatReport after fetching the iouReport. + fetchIOUReportByIDAndUpdateChatReport(reportID, chatReportID); + }) + .catch((error) => { + console.error(`Error Paying iouReport: ${error}`); + Onyx.merge(ONYXKEYS.IOU, {error: true}); + }) + .finally(() => Onyx.merge(ONYXKEYS.IOU, {loading: false})); +} + export { getPreferredCurrency, createIOUTransaction, createIOUSplit, + payIOUReport, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index cf1817775bae..b3175177cfd1 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -183,7 +183,7 @@ function getSimplifiedReportObject(report) { * Get a simplified version of an IOU report * * @param {Object} reportData - * @param {Number} reportData.transactionID + * @param {String} reportData.transactionID * @param {Number} reportData.amount * @param {String} reportData.currency * @param {String} reportData.created @@ -202,7 +202,8 @@ function getSimplifiedIOUReport(reportData, chatReportID) { currency: transaction.currency, created: transaction.created, comment: transaction.comment, - })); + })).reverse(); // `transactionList` data is returned ordered by desc creation date, they are changed to asc order + // because we must instead display them in the order that they were created (asc). return { reportID: reportData.reportID, @@ -243,7 +244,8 @@ function fetchIOUReport(iouReportID, chatReportID) { } const iouReportData = response.reports[iouReportID]; if (!iouReportData) { - console.error(`No iouReportData found for reportID ${iouReportID}`); + // IOU data for a report will be missing when the IOU report has already been paid. + // This is expected and we return early as no further processing can be done. return; } return getSimplifiedIOUReport(iouReportData, chatReportID); @@ -362,26 +364,16 @@ function fetchChatReportsByIDs(chatList) { } /** - * Given IOU object and chat report ID save the data to Onyx. + * Given IOU object, save the data to Onyx. * * @param {Object} iouReportObject * @param {Number} iouReportObject.stateNum * @param {Number} iouReportObject.total * @param {Number} iouReportObject.reportID - * @param {Number} chatReportID */ -function setLocalIOUReportData(iouReportObject, chatReportID) { - const chatReportObject = { - hasOutstandingIOU: iouReportObject.stateNum === 1 && iouReportObject.total !== 0, - iouReportID: iouReportObject.reportID, - }; - if (!chatReportObject.hasOutstandingIOU) { - chatReportObject.iouReportID = null; - } +function setLocalIOUReportData(iouReportObject) { const iouReportKey = `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportObject.reportID}`; - const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`; Onyx.merge(iouReportKey, iouReportObject); - Onyx.merge(reportKey, chatReportObject); } /** @@ -425,6 +417,56 @@ function removeOptimisticActions(reportID) { }); } +/** + * Fetch the iouReport and persist the data to Onyx. + * + * @param {Number} iouReportID - ID of the report we are fetching + * @param {Number} chatReportID - associated chatReportID, set as an iouReport field + * @returns {Promise} + */ +function fetchIOUReportByID(iouReportID, chatReportID) { + return fetchIOUReport(iouReportID, chatReportID) + .then((iouReportObject) => { + setLocalIOUReportData(iouReportObject); + return iouReportObject; + }); +} + +/** + * If an iouReport is open (has an IOU, but is not yet paid) then we sync the reportIDs of both chatReport and + * iouReport in Onyx, simplifying IOU data retrieval and reducing necessary API calls when displaying IOU components: + * - chatReport: {id: 123, iouReportID: 987, ...} + * - iouReport: {id: 987, chatReportID: 123, ...} + * + * The reports must remain in sync when the iouReport is modified. This function ensures that we sync reportIds after + * fetching the iouReport and therefore should only be called if we are certain that the fetched iouReport is currently + * open - else we would overwrite the existing open iouReportID with a closed iouReportID. + * + * Examples of usage include 'receieving a push notification', or 'paying an IOU', because both of these cases can only + * occur for an iouReport that is currently open (notifications are not sent for closed iouReports, and you cannot pay a + * closed IOU). + * + * @param {Number} iouReportID - ID of the report we are fetching + * @param {Number} chatReportID - associated chatReportID, used to sync the reports + */ +function fetchIOUReportByIDAndUpdateChatReport(iouReportID, chatReportID) { + fetchIOUReportByID(iouReportID, chatReportID) + .then((iouReportObject) => { + // Now sync the chatReport data to ensure it has a reference to the updated iouReportID + const chatReportObject = { + hasOutstandingIOU: iouReportObject.stateNum === 1 && iouReportObject.total !== 0, + iouReportID: iouReportObject.reportID, + }; + + if (!chatReportObject.hasOutstandingIOU) { + chatReportObject.iouReportID = null; + } + + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`; + Onyx.merge(reportKey, chatReportObject); + }); +} + /** * @param {Number} reportID * @param {Number} sequenceNumber @@ -507,16 +549,13 @@ function updateReportWithNewAction(reportID, reportAction) { // If chat report receives an action with IOU, update IOU object if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - const chatReport = lodashGet(allReports, reportID); - const iouReportID = lodashGet(chatReport, 'iouReportID'); - if (iouReportID) { - fetchIOUReport(iouReportID, reportID) - .then(iouReportObject => setLocalIOUReportData(iouReportObject, reportID)); - } else if (!chatReport || chatReport.participants.length === 1) { - fetchIOUReportID(chatReport ? chatReport.participants[0] : reportAction.actorEmail) - .then(iouID => fetchIOUReport(iouID, reportID)) - .then(iouReportObject => setLocalIOUReportData(iouReportObject, reportID)); - } + const iouReportID = reportAction.originalMessage.IOUReportID; + + // We know this iouReport is open because reportActions of type CONST.REPORT.ACTIONS.TYPE.IOU can only be + // triggered for an open iouReport (an open iouReport has an IOU, but is not yet paid). After fetching the + // iouReport we must update the chatReport with the correct iouReportID. If we don't, then new IOUs would not + // be displayed and paid IOUs would show as unpaid. + fetchIOUReportByIDAndUpdateChatReport(iouReportID, reportID); } if (!ActiveClientManager.isClientTheLeader()) { @@ -1138,6 +1177,9 @@ export { fetchAllReports, fetchActions, fetchOrCreateChatReport, + fetchChatReportsByIDs, + fetchIOUReportByID, + fetchIOUReportByIDAndUpdateChatReport, addAction, updateLastReadActionID, setNewMarkerPosition, diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 0193bca74df2..ae13e9271c6f 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -48,7 +48,7 @@ const DetailsPage = ({personalDetails, route, translate}) => { return ( Navigation.dismissModal()} diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index f211ea82a1d6..08785faeeb04 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -84,7 +84,7 @@ const ReportParticipantsPage = ({ return ( ); } else { diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index eba9b164697b..5d60f327a811 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -19,18 +19,23 @@ const propTypes = { /** All of the personalDetails */ personalDetails: PropTypes.objectOf(personalDetailsPropType), + /** Styles for the outermost View */ + wrapperStyles: PropTypes.arrayOf(PropTypes.object), + /** Children view component for this action item */ children: PropTypes.node.isRequired, }; const defaultProps = { personalDetails: {}, + wrapperStyles: [styles.chatItem], }; const ReportActionItemSingle = ({ action, personalDetails, children, + wrapperStyles, }) => { const {avatar, displayName} = personalDetails[action.actorEmail] || {}; const avatarUrl = action.automatic @@ -44,7 +49,7 @@ const ReportActionItemSingle = ({ // we should stop referring to the report history items entirely for this information. const personArray = displayName ? [{type: 'TEXT', text: displayName}] : action.person; return ( - + + + {reportIsLoading ? : ( + + + + + + {(this.props.iouReport.hasOutstandingIOU + && this.props.iouReport.managerEmail === sessionEmail && ( + + + + ))} + + )} + + ); + } +} + +IOUDetailsModal.propTypes = propTypes; +IOUDetailsModal.defaultProps = defaultProps; + +export default compose( + withLocalize, + withOnyx({ + iou: { + key: ONYXKEYS.IOU, + }, + iouReport: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IOUS}${route.params.iouReportID}`, + }, + session: { + key: ONYXKEYS.SESSION, + }, + }), +)(IOUDetailsModal); diff --git a/src/pages/iou/IOUTransactions.js b/src/pages/iou/IOUTransactions.js new file mode 100644 index 000000000000..b01323d87e6e --- /dev/null +++ b/src/pages/iou/IOUTransactions.js @@ -0,0 +1,88 @@ +import React, {Component} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import styles from '../../styles/styles'; +import ONYXKEYS from '../../ONYXKEYS'; +import ReportActionPropTypes from '../home/report/ReportActionPropTypes'; +import iouTransactionPropTypes from './iouTransactionPropTypes'; +import ReportTransaction from '../../components/ReportTransaction'; + +const propTypes = { + /** Actions from the ChatReport */ + reportActions: PropTypes.shape(ReportActionPropTypes), + + /** ReportID for the associated chat report */ + chatReportID: PropTypes.number.isRequired, + + /** ReportID for the associated IOU report */ + iouReportID: PropTypes.number.isRequired, + + /** Transactions for this IOU report */ + transactions: PropTypes.arrayOf(PropTypes.shape(iouTransactionPropTypes)), +}; + +const defaultProps = { + reportActions: {}, + transactions: [], +}; + +class IOUTransactions extends Component { + constructor(props) { + super(props); + + this.getActionForTransaction = this.getActionForTransaction.bind(this); + } + + /** + * Given a transaction from an IOU Report, returns the chatReport action with a matching transactionID. Unless + * something has gone wrong with our storing logic, there should always exist an action for each transaction. + * + * @param {Object} transaction + * @returns {Object} action + */ + getActionForTransaction(transaction) { + const matchedAction = _.find(this.props.reportActions, (action) => { + // iouReport.transaction.transactionID is returned as a String, but the originalMessage value is Number + if (action && action.originalMessage && action.originalMessage.IOUTransactionID + && action.originalMessage.IOUTransactionID.toString() === transaction.transactionID) { + return action; + } + return false; + }); + if (!matchedAction) { + throw new Error(`Unable to locate a matching report action for transaction ${transaction.transactionID}!`); + } + + return matchedAction; + } + + render() { + return ( + + {/* For each IOU transaction, get the matching report action */} + {_.map(this.props.transactions, (transaction) => { + const action = this.getActionForTransaction(transaction); + return ( + + ); + })} + + ); + } +} + +IOUTransactions.defaultProps = defaultProps; +IOUTransactions.propTypes = propTypes; +export default withOnyx({ + reportActions: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + canEvict: false, + }, +})(IOUTransactions); diff --git a/src/pages/iou/iouTransactionPropTypes.js b/src/pages/iou/iouTransactionPropTypes.js new file mode 100644 index 000000000000..51664bfd8559 --- /dev/null +++ b/src/pages/iou/iouTransactionPropTypes.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; + +export default { + /** The transaction currency code */ + currency: PropTypes.string, + + /** The transaction amount */ + amount: PropTypes.number, + + /** The transaction comment */ + comment: PropTypes.string, + + /** Date that the transaction was created */ + created: PropTypes.string, + + /** The ID of this report transaction */ + transactionID: PropTypes.string, +}; diff --git a/src/styles/styles.js b/src/styles/styles.js index 4da46e69f9b3..fc0dad2c1929 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -766,6 +766,13 @@ const styles = { opacity: 0.6, }, + chatItemMessageLink: { + color: colors.blue, + fontSize: variables.fontSizeNormal, + fontFamily: fontFamily.GTA, + lineHeight: 20, + }, + chatItemCompose: { minHeight: 65, marginBottom: 5, @@ -1145,6 +1152,12 @@ const styles = { ...{borderRadius: variables.componentBorderRadiusSmall}, }, + reportTransaction: { + paddingVertical: 8, + display: 'flex', + flexDirection: 'row', + }, + settingsPageBackground: { flexDirection: 'column', width: '100%', @@ -1346,6 +1359,12 @@ const styles = { marginRight: -10, }, + iouDetailsContainer: { + flexGrow: 1, + paddingStart: 20, + paddingEnd: 20, + }, + noScrollbars: { scrollbarWidth: 'none', }, @@ -1397,6 +1416,13 @@ const styles = { transform: 'translateX(-100%)', }, + blockquote: { + borderLeftColor: themeColors.border, + borderLeftWidth: 4, + paddingLeft: 12, + marginVertical: 4, + }, + cursorDisabled: { cursor: 'not-allowed', },