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',
},