From cbc1fcc05c4c7d2889d287664e95039d7441af64 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Tue, 12 Dec 2017 14:34:40 +0200 Subject: [PATCH] TransactionPage --- .../SaleDetailsPanel/SaleDetailsPanel.js | 4 +- .../SaleDetailsPanel.test.js.snap | 8 +- src/containers/CheckoutPage/CheckoutPage.js | 2 +- .../TransactionPage/TransactionPage.css | 34 ++ .../TransactionPage/TransactionPage.duck.js | 440 ++++++++++++++++++ .../TransactionPage/TransactionPage.js | 313 +++++++++++++ .../TransactionPage/TransactionPage.test.js | 101 ++++ .../TransactionPage.test.js.snap | 373 +++++++++++++++ src/containers/index.js | 1 + src/containers/reducers.js | 2 + src/routeConfiguration.js | 13 +- src/translations/en.json | 7 + 12 files changed, 1284 insertions(+), 14 deletions(-) create mode 100644 src/containers/TransactionPage/TransactionPage.css create mode 100644 src/containers/TransactionPage/TransactionPage.duck.js create mode 100644 src/containers/TransactionPage/TransactionPage.js create mode 100644 src/containers/TransactionPage/TransactionPage.test.js create mode 100644 src/containers/TransactionPage/__snapshots__/TransactionPage.test.js.snap diff --git a/src/components/SaleDetailsPanel/SaleDetailsPanel.js b/src/components/SaleDetailsPanel/SaleDetailsPanel.js index 75cf8454..a3434e6a 100644 --- a/src/components/SaleDetailsPanel/SaleDetailsPanel.js +++ b/src/components/SaleDetailsPanel/SaleDetailsPanel.js @@ -264,14 +264,14 @@ export class SaleDetailsPanelComponent extends Component { disabled={buttonsDisabled} onClick={() => onDeclineSale(currentTransaction.id)} > - + onAcceptSale(currentTransaction.id)} > - + diff --git a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap index b89b5c5c..bc3882e1 100644 --- a/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap +++ b/src/components/SaleDetailsPanel/__snapshots__/SaleDetailsPanel.test.js.snap @@ -3441,7 +3441,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` onClick={[Function]} > @@ -3451,7 +3451,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` onClick={[Function]} > @@ -3626,7 +3626,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` onClick={[Function]} > @@ -3636,7 +3636,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = ` onClick={[Function]} > diff --git a/src/containers/CheckoutPage/CheckoutPage.js b/src/containers/CheckoutPage/CheckoutPage.js index dfcff6fe..87a6971c 100644 --- a/src/containers/CheckoutPage/CheckoutPage.js +++ b/src/containers/CheckoutPage/CheckoutPage.js @@ -123,7 +123,7 @@ export class CheckoutPageComponent extends Component { // sending failed, we tell it to the OrderDetailsPage. dispatch( OrderPage.setInitialValues({ - messageSendingFailedToTransaction: initialMessageSuccess ? null : orderId, + initialMessageFailedToTransaction: initialMessageSuccess ? null : orderId, }) ); const orderDetailsPath = pathByRouteName('OrderDetailsPage', routes, { diff --git a/src/containers/TransactionPage/TransactionPage.css b/src/containers/TransactionPage/TransactionPage.css new file mode 100644 index 00000000..b9a2e238 --- /dev/null +++ b/src/containers/TransactionPage/TransactionPage.css @@ -0,0 +1,34 @@ +@import '../../marketplace.css'; + +.root { + display: flex; + flex-direction: column; + flex: 1; +} + +.loading { + margin-left: 24px; + margin-right: 24px; +} + +.error { + margin-left: 24px; + margin-right: 24px; + color: var(--failColor); +} + +.tabContent { + display: none; +} + +.tabContentVisible { + display: block; +} + +.footer { + display: none; + + @media (--viewportLarge) { + display: block; + } +} diff --git a/src/containers/TransactionPage/TransactionPage.duck.js b/src/containers/TransactionPage/TransactionPage.duck.js new file mode 100644 index 00000000..ba3beb4b --- /dev/null +++ b/src/containers/TransactionPage/TransactionPage.duck.js @@ -0,0 +1,440 @@ +import { pick } from 'lodash'; +import { types } from '../../util/sdkLoader'; +import { isTransactionsTransitionInvalidTransition, storableError } from '../../util/errors'; +import * as propTypes from '../../util/propTypes'; +import * as log from '../../util/log'; +import { updatedEntities, denormalisedEntities } from '../../util/data'; +import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; +import { fetchCurrentUserNotifications } from '../../ducks/user.duck'; + +const MESSAGES_PAGE_SIZE = 100; + +// ================ Action types ================ // + +export const SET_INITAL_VALUES = 'app/TransactionPage/SET_INITIAL_VALUES'; + +export const FETCH_TRANSACTION_REQUEST = 'app/TransactionPage/FETCH_TRANSACTION_REQUEST'; +export const FETCH_TRANSACTION_SUCCESS = 'app/TransactionPage/FETCH_TRANSACTION_SUCCESS'; +export const FETCH_TRANSACTION_ERROR = 'app/TransactionPage/FETCH_TRANSACTION_ERROR'; + +export const ACCEPT_SALE_REQUEST = 'app/TransactionPage/ACCEPT_SALE_REQUEST'; +export const ACCEPT_SALE_SUCCESS = 'app/TransactionPage/ACCEPT_SALE_SUCCESS'; +export const ACCEPT_SALE_ERROR = 'app/TransactionPage/ACCEPT_SALE_ERROR'; + +export const DECLINE_SALE_REQUEST = 'app/TransactionPage/DECLINE_SALE_REQUEST'; +export const DECLINE_SALE_SUCCESS = 'app/TransactionPage/DECLINE_SALE_SUCCESS'; +export const DECLINE_SALE_ERROR = 'app/TransactionPage/DECLINE_SALE_ERROR'; + +export const FETCH_MESSAGES_REQUEST = 'app/TransactionPage/FETCH_MESSAGES_REQUEST'; +export const FETCH_MESSAGES_SUCCESS = 'app/TransactionPage/FETCH_MESSAGES_SUCCESS'; +export const FETCH_MESSAGES_ERROR = 'app/TransactionPage/FETCH_MESSAGES_ERROR'; + +export const SEND_MESSAGE_REQUEST = 'app/TransactionPage/SEND_MESSAGE_REQUEST'; +export const SEND_MESSAGE_SUCCESS = 'app/TransactionPage/SEND_MESSAGE_SUCCESS'; +export const SEND_MESSAGE_ERROR = 'app/TransactionPage/SEND_MESSAGE_ERROR'; + +export const SEND_REVIEW_REQUEST = 'app/TransactionPage/SEND_REVIEW_REQUEST'; +export const SEND_REVIEW_SUCCESS = 'app/TransactionPage/SEND_REVIEW_SUCCESS'; +export const SEND_REVIEW_ERROR = 'app/TransactionPage/SEND_REVIEW_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + fetchTransactionInProgress: false, + fetchTransactionError: null, + transactionRef: null, + acceptInProgress: false, + acceptSaleError: null, + declineInProgress: false, + declineSaleError: null, + fetchMessagesInProgress: false, + fetchMessagesError: null, + totalMessages: 0, + totalMessagePages: 0, + oldestMessagePageFetched: 0, + messages: [], + initialMessageFailedToTransaction: null, + sendMessageInProgress: false, + sendMessageError: null, + sendReviewInProgress: false, + sendReviewError: null, +}; + +// Merge entity arrays using ids, so that conflicting items in newer array (b) overwrite old values (a). +// const a = [{ id: { uuid: 1 } }, { id: { uuid: 3 } }]; +// const b = [{ id: : { uuid: 2 } }, { id: : { uuid: 1 } }]; +// mergeEntityArrays(a, b) +// => [{ id: { uuid: 3 } }, { id: : { uuid: 2 } }, { id: : { uuid: 1 } }] +const mergeEntityArrays = (a, b) => { + return a.filter(aEntity => !b.find(bEntity => aEntity.id.uuid === bEntity.id.uuid)).concat(b); +}; + +export default function checkoutPageReducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case SET_INITAL_VALUES: + return { ...initialState, ...payload }; + + case FETCH_TRANSACTION_REQUEST: + return { ...state, fetchTransactionInProgress: true, fetchTransactionError: null }; + case FETCH_TRANSACTION_SUCCESS: { + const transactionRef = { id: payload.data.data.id, type: 'transaction' }; + return { ...state, fetchTransactionInProgress: false, transactionRef }; + } + case FETCH_TRANSACTION_ERROR: + console.error(payload); // eslint-disable-line + return { ...state, fetchTransactionInProgress: false, fetchTransactionError: payload }; + + case ACCEPT_SALE_REQUEST: + return { ...state, acceptInProgress: true, acceptSaleError: null, declineSaleError: null }; + case ACCEPT_SALE_SUCCESS: + return { ...state, acceptInProgress: false }; + case ACCEPT_SALE_ERROR: + return { ...state, acceptInProgress: false, acceptSaleError: payload }; + + case DECLINE_SALE_REQUEST: + return { ...state, declineInProgress: true, declineSaleError: null, acceptSaleError: null }; + case DECLINE_SALE_SUCCESS: + return { ...state, declineInProgress: false }; + case DECLINE_SALE_ERROR: + return { ...state, declineInProgress: false, declineSaleError: payload }; + + case FETCH_MESSAGES_REQUEST: + return { ...state, fetchMessagesInProgress: true, fetchMessagesError: null }; + case FETCH_MESSAGES_SUCCESS: { + const oldestMessagePageFetched = + state.oldestMessagePageFetched > payload.page + ? state.oldestMessagePageFetched + : payload.page; + return { + ...state, + fetchMessagesInProgress: false, + messages: mergeEntityArrays(state.messages, payload.messages), + totalMessages: payload.totalItems, + totalMessagePages: payload.totalPages, + oldestMessagePageFetched, + }; + } + case FETCH_MESSAGES_ERROR: + return { ...state, fetchMessagesInProgress: false, fetchMessagesError: payload }; + + case SEND_MESSAGE_REQUEST: + return { ...state, sendMessageInProgress: true, sendMessageError: null }; + case SEND_MESSAGE_SUCCESS: + return { ...state, sendMessageInProgress: false }; + case SEND_MESSAGE_ERROR: + return { ...state, sendMessageInProgress: false, sendMessageError: payload }; + + case SEND_REVIEW_REQUEST: + return { ...state, sendReviewInProgress: true, sendReviewError: null }; + case SEND_REVIEW_SUCCESS: + return { ...state, sendReviewInProgress: false }; + case SEND_REVIEW_ERROR: + return { ...state, sendReviewInProgress: false, sendReviewError: payload }; + + default: + return state; + } +} + +// ================ Selectors ================ // + +export const acceptOrDeclineInProgress = state => { + return state.TransactionPage.acceptInProgress || state.TransactionPage.declineInProgress; +}; + +// ================ Action creators ================ // +export const setInitialValues = initialValues => ({ + type: SET_INITAL_VALUES, + payload: pick(initialValues, Object.keys(initialState)), +}); + +const fetchTransactionRequest = () => ({ type: FETCH_TRANSACTION_REQUEST }); +const fetchTransactionSuccess = response => ({ + type: FETCH_TRANSACTION_SUCCESS, + payload: response, +}); +const fetchTransactionError = e => ({ type: FETCH_TRANSACTION_ERROR, error: true, payload: e }); + +const acceptSaleRequest = () => ({ type: ACCEPT_SALE_REQUEST }); +const acceptSaleSuccess = () => ({ type: ACCEPT_SALE_SUCCESS }); +const acceptSaleError = e => ({ type: ACCEPT_SALE_ERROR, error: true, payload: e }); + +const declineSaleRequest = () => ({ type: DECLINE_SALE_REQUEST }); +const declineSaleSuccess = () => ({ type: DECLINE_SALE_SUCCESS }); +const declineSaleError = e => ({ type: DECLINE_SALE_ERROR, error: true, payload: e }); + +const fetchMessagesRequest = () => ({ type: FETCH_MESSAGES_REQUEST }); +const fetchMessagesSuccess = (messages, pagination) => ({ + type: FETCH_MESSAGES_SUCCESS, + payload: { messages, ...pagination }, +}); +const fetchMessagesError = e => ({ type: FETCH_MESSAGES_ERROR, error: true, payload: e }); + +const sendMessageRequest = () => ({ type: SEND_MESSAGE_REQUEST }); +const sendMessageSuccess = () => ({ type: SEND_MESSAGE_SUCCESS }); +const sendMessageError = e => ({ type: SEND_MESSAGE_ERROR, error: true, payload: e }); + +const sendReviewRequest = () => ({ type: SEND_REVIEW_REQUEST }); +const sendReviewSuccess = () => ({ type: SEND_REVIEW_SUCCESS }); +const sendReviewError = e => ({ type: SEND_REVIEW_ERROR, error: true, payload: e }); + +// ================ Thunks ================ // + +const listingRelationship = txResponse => { + return txResponse.data.data.relationships.listing.data; +}; + +export const fetchTransaction = id => (dispatch, getState, sdk) => { + dispatch(fetchTransactionRequest()); + let txResponse = null; + + return sdk.transactions + .show( + { + id, + include: [ + 'customer', + 'customer.profileImage', + 'provider', + 'provider.profileImage', + 'listing', + 'booking', + 'reviews', + 'reviews.author', + 'reviews.subject', + ], + }, + { expand: true } + ) + .then(response => { + txResponse = response; + const listingId = listingRelationship(response).id; + const entities = updatedEntities({}, response.data); + const denormalised = denormalisedEntities(entities, 'listing', [listingId]); + const listing = denormalised[0]; + + const canFetchListing = listing && listing.attributes && !listing.attributes.deleted; + + if (canFetchListing) { + return sdk.listings.show({ + id: listingId, + include: ['author', 'author.profileImage', 'images'], + }); + } else { + return response; + } + }) + .then(response => { + dispatch(addMarketplaceEntities(txResponse)); + dispatch(addMarketplaceEntities(response)); + dispatch(fetchTransactionSuccess(txResponse)); + return response; + }) + .catch(e => { + dispatch(fetchTransactionError(storableError(e))); + throw e; + }); +}; + +export const acceptSale = id => (dispatch, getState, sdk) => { + if (acceptOrDeclineInProgress(getState())) { + return Promise.reject(new Error('Accept or decline already in progress')); + } + dispatch(acceptSaleRequest()); + + return sdk.transactions + .transition({ id, transition: propTypes.TX_TRANSITION_ACCEPT, params: {} }, { expand: true }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(acceptSaleSuccess()); + dispatch(fetchCurrentUserNotifications()); + return response; + }) + .catch(e => { + dispatch(acceptSaleError(storableError(e))); + log.error(e, 'accept-sale-failed', { + txId: id, + transition: propTypes.TX_TRANSITION_ACCEPT, + }); + throw e; + }); +}; + +export const declineSale = id => (dispatch, getState, sdk) => { + if (acceptOrDeclineInProgress(getState())) { + return Promise.reject(new Error('Accept or decline already in progress')); + } + dispatch(declineSaleRequest()); + + return sdk.transactions + .transition({ id, transition: propTypes.TX_TRANSITION_DECLINE, params: {} }, { expand: true }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(declineSaleSuccess()); + dispatch(fetchCurrentUserNotifications()); + return response; + }) + .catch(e => { + dispatch(declineSaleError(storableError(e))); + log.error(e, 'reject-sale-failed', { + txId: id, + transition: propTypes.TX_TRANSITION_DECLINE, + }); + throw e; + }); +}; + +const fetchMessages = (txId, page) => (dispatch, getState, sdk) => { + const paging = { page, per_page: MESSAGES_PAGE_SIZE }; + dispatch(fetchMessagesRequest()); + + return sdk.messages + .query({ transaction_id: txId, include: ['sender', 'sender.profileImage'], ...paging }) + .then(response => { + const entities = updatedEntities({}, response.data); + const messageIds = response.data.data.map(d => d.id); + const denormalizedMessages = denormalisedEntities(entities, 'message', messageIds); + const { totalItems, totalPages, page: fetchedPage } = response.data.meta; + const pagination = { totalItems, totalPages, page: fetchedPage }; + const totalMessages = getState().TransactionPage.totalMessages; + + // Original fetchMessages call succeeded + dispatch(fetchMessagesSuccess(denormalizedMessages, pagination)); + + // Check if totalItems has changed between fetched pagination pages + // if totalItems has changed, fetch first page again to include new incoming messages. + // TODO if there're more than 100 incoming messages, + // this should loop through most recent pages instead of fetching just the first one. + if (totalItems > totalMessages && page > 1) { + dispatch(fetchMessages(txId, 1)) + .then(() => { + // Original fetch was enough as a response for user action, + // this just includes new incoming messages + }) + .catch(() => { + // Background update, no need to to do anything atm. + }); + } + }) + .catch(e => { + dispatch(fetchMessagesError(storableError(e))); + throw e; + }); +}; + +export const fetchMoreMessages = txId => (dispatch, getState, sdk) => { + const state = getState(); + const { oldestMessagePageFetched, totalMessagePages } = state.TransactionPage; + const hasMoreOldMessages = totalMessagePages > oldestMessagePageFetched; + + // In case there're no more old pages left we default to fetching the current cursor position + const nextPage = hasMoreOldMessages ? oldestMessagePageFetched + 1 : oldestMessagePageFetched; + + return dispatch(fetchMessages(txId, nextPage)); +}; + +export const sendMessage = (txId, message) => (dispatch, getState, sdk) => { + dispatch(sendMessageRequest()); + + return sdk.messages + .send({ transactionId: txId, content: message }) + .then(response => { + const messageId = response.data.data.id; + + // We fetch the first page again to add sent message to the page data + // and update possible incoming messages too. + // TODO if there're more than 100 incoming messages, + // this should loop through most recent pages instead of fetching just the first one. + return dispatch(fetchMessages(txId, 1)) + .then(() => { + dispatch(sendMessageSuccess()); + return messageId; + }) + .catch(() => dispatch(sendMessageSuccess())); + }) + .catch(e => { + dispatch(sendMessageError(storableError(e))); + // Rethrow so the page can track whether the sending failed, and + // keep the message in the form for a retry. + throw e; + }); +}; + +const REVIEW_TX_INCLUDES = ['reviews', 'reviews.author', 'reviews.subject']; + +// If other party (customer) has already sent a review, we need to make transition to +// TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND +const sendReviewAsSecond = (id, params, dispatch, sdk) => { + const transition = propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_SECOND; + const include = REVIEW_TX_INCLUDES; + + return sdk.transactions + .transition({ id, transition, params }, { expand: true, include }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(sendReviewSuccess()); + return response; + }) + .catch(e => { + dispatch(sendReviewError(storableError(e))); + + // Rethrow so the page can track whether the sending failed, and + // keep the message in the form for a retry. + throw e; + }); +}; + +// If other party (customer) has not yet sent a review, we need to make transition to +// TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST +// However, the other party might have made the review after previous data synch point. +// So, error is likely to happen and then we must try another state transition +// by calling sendReviewAsSecond(). +const sendReviewAsFirst = (id, params, dispatch, sdk) => { + const transition = propTypes.TX_TRANSITION_REVIEW_BY_PROVIDER_FIRST; + const include = REVIEW_TX_INCLUDES; + + return sdk.transactions + .transition({ id, transition, params }, { expand: true, include }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(sendReviewSuccess()); + return response; + }) + .catch(e => { + // If transaction transition is invalid, lets try another endpoint. + if (isTransactionsTransitionInvalidTransition(e)) { + return sendReviewAsSecond(id, params, dispatch, sdk); + } else { + dispatch(sendReviewError(storableError(e))); + + // Rethrow so the page can track whether the sending failed, and + // keep the message in the form for a retry. + throw e; + } + }); +}; + +export const sendReview = (tx, reviewRating, reviewContent) => (dispatch, getState, sdk) => { + const params = { reviewRating, reviewContent }; + const txStateProviderFirst = + tx.attributes.lastTransition === propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_FIRST; + + dispatch(sendReviewRequest()); + + return txStateProviderFirst + ? sendReviewAsSecond(tx.id, params, dispatch, sdk) + : sendReviewAsFirst(tx.id, params, dispatch, sdk); +}; + +// loadData is a collection of async calls that need to be made +// before page has all the info it needs to render itself +export const loadData = params => dispatch => { + const txId = new types.UUID(params.id); + + // Clear the send error since the message form is emptied as well. + dispatch(setInitialValues({ sendMessageError: null, sendReviewError: null })); + + // Sale / order (i.e. transaction entity in API) + return Promise.all([dispatch(fetchTransaction(txId)), dispatch(fetchMessages(txId, 1))]); +}; diff --git a/src/containers/TransactionPage/TransactionPage.js b/src/containers/TransactionPage/TransactionPage.js new file mode 100644 index 00000000..b1e9db68 --- /dev/null +++ b/src/containers/TransactionPage/TransactionPage.js @@ -0,0 +1,313 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import classNames from 'classnames'; +import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; +import { reset as resetForm } from 'redux-form'; +import * as propTypes from '../../util/propTypes'; +import { ensureListing, ensureTransaction } from '../../util/data'; +import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck'; +import { isScrollingDisabled, manageDisableScrolling } from '../../ducks/UI.duck'; +import { + NamedRedirect, + OrderDetailsPanel, + SaleDetailsPanel, + Page, + LayoutSingleColumn, + LayoutWrapperTopbar, + LayoutWrapperMain, + LayoutWrapperFooter, + Footer, +} from '../../components'; +import { TopbarContainer } from '../../containers'; + +import { + acceptSale, + declineSale, + loadData, + setInitialValues, + sendMessage, + sendReview, + fetchMoreMessages, +} from './TransactionPage.duck'; +import css from './TransactionPage.css'; + +const PROVIDER = 'provider'; +const CUSTOMER = 'customer'; + +// TransactionPage handles data loading for Sale and Order views to transaction pages in Inbox. +export const TransactionPageComponent = props => { + const { + currentUser, + initialMessageFailedToTransaction, + fetchMessagesError, + fetchMessagesInProgress, + totalMessagePages, + oldestMessagePageFetched, + fetchTransactionError, + intl, + messages, + onManageDisableScrolling, + onResetForm, + onSendMessage, + onSendReview, + onShowMoreMessages, + params, + scrollingDisabled, + sendMessageError, + sendMessageInProgress, + sendReviewError, + sendReviewInProgress, + transaction, + transactionRole, + acceptInProgress, + acceptSaleError, + declineInProgress, + declineSaleError, + onAcceptSale, + onDeclineSale, + } = props; + + const currentTransaction = ensureTransaction(transaction); + const currentListing = ensureListing(currentTransaction.listing); + const listingTitle = currentListing.attributes.title; + + // Redirect users with someone else's direct link to their own inbox/sales or inbox/orders page. + const isDataAvailable = + currentUser && + currentTransaction.id && + currentTransaction.id.uuid === params.id && + currentTransaction.customer && + currentTransaction.provider && + !fetchTransactionError; + + const isProviderRole = transactionRole === PROVIDER; + const isCustomerRole = transactionRole === CUSTOMER; + const isOwnSale = + isDataAvailable && + isProviderRole && + currentUser.id.uuid === currentTransaction.provider.id.uuid; + const isOwnOrder = + isDataAvailable && + isCustomerRole && + currentUser.id.uuid === currentTransaction.customer.id.uuid; + + if (isDataAvailable && isProviderRole && !isOwnSale) { + // eslint-disable-next-line no-console + console.error('Tried to access a sale that was not owned by the current user'); + return ; + } else if (isDataAvailable && isCustomerRole && !isOwnOrder) { + // eslint-disable-next-line no-console + console.error('Tried to access an order that was not owned by the current user'); + return ; + } + + const detailsClassName = classNames(css.tabContent, css.tabContentVisible); + + const fetchErrorMessage = isCustomerRole + ? 'TransactionPage.fetchOrderFailed' + : 'TransactionPage.fetchSaleFailed'; + const loadingMessage = isCustomerRole + ? 'TransactionPage.loadingOrderData' + : 'TransactionPage.loadingSaleData'; + + const loadingOrFailedFetching = fetchTransactionError ? ( +

+ +

+ ) : ( +

+ +

+ ); + + const salePanel = isDataAvailable ? ( + + ) : null; + + const initialMessageFailed = !!( + initialMessageFailedToTransaction && + currentTransaction.id && + initialMessageFailedToTransaction.uuid === currentTransaction.id.uuid + ); + + const orderPanel = isDataAvailable ? ( + + ) : null; + + const saleOrOrderPanel = isDataAvailable && isOwnSale ? salePanel : orderPanel; + const panel = isDataAvailable ? saleOrOrderPanel : loadingOrFailedFetching; + + return ( + + + + + + +
{panel}
+
+ +