mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 19:42:11 +10:00
TransactionPage
This commit is contained in:
parent
a861954b9f
commit
cbc1fcc05c
12 changed files with 1284 additions and 14 deletions
|
|
@ -264,14 +264,14 @@ export class SaleDetailsPanelComponent extends Component {
|
|||
disabled={buttonsDisabled}
|
||||
onClick={() => onDeclineSale(currentTransaction.id)}
|
||||
>
|
||||
<FormattedMessage id="SalePage.declineButton" />
|
||||
<FormattedMessage id="SaleDetailsPanel.declineButton" />
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
inProgress={acceptInProgress}
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() => onAcceptSale(currentTransaction.id)}
|
||||
>
|
||||
<FormattedMessage id="SalePage.acceptButton" />
|
||||
<FormattedMessage id="SaleDetailsPanel.acceptButton" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3441,7 +3441,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
|
|||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="SalePage.declineButton"
|
||||
id="SaleDetailsPanel.declineButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</SecondaryButton>
|
||||
|
|
@ -3451,7 +3451,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
|
|||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="SalePage.acceptButton"
|
||||
id="SaleDetailsPanel.acceptButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</PrimaryButton>
|
||||
|
|
@ -3626,7 +3626,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
|
|||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="SalePage.declineButton"
|
||||
id="SaleDetailsPanel.declineButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</SecondaryButton>
|
||||
|
|
@ -3636,7 +3636,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
|
|||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="SalePage.acceptButton"
|
||||
id="SaleDetailsPanel.acceptButton"
|
||||
values={Object {}}
|
||||
/>
|
||||
</PrimaryButton>
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
34
src/containers/TransactionPage/TransactionPage.css
Normal file
34
src/containers/TransactionPage/TransactionPage.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
440
src/containers/TransactionPage/TransactionPage.duck.js
Normal file
440
src/containers/TransactionPage/TransactionPage.duck.js
Normal file
|
|
@ -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))]);
|
||||
};
|
||||
313
src/containers/TransactionPage/TransactionPage.js
Normal file
313
src/containers/TransactionPage/TransactionPage.js
Normal file
|
|
@ -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 <NamedRedirect name="InboxPage" params={{ tab: 'sales' }} />;
|
||||
} 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 <NamedRedirect name="InboxPage" params={{ tab: 'orders' }} />;
|
||||
}
|
||||
|
||||
const detailsClassName = classNames(css.tabContent, css.tabContentVisible);
|
||||
|
||||
const fetchErrorMessage = isCustomerRole
|
||||
? 'TransactionPage.fetchOrderFailed'
|
||||
: 'TransactionPage.fetchSaleFailed';
|
||||
const loadingMessage = isCustomerRole
|
||||
? 'TransactionPage.loadingOrderData'
|
||||
: 'TransactionPage.loadingSaleData';
|
||||
|
||||
const loadingOrFailedFetching = fetchTransactionError ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id={`${fetchErrorMessage}`} />
|
||||
</p>
|
||||
) : (
|
||||
<p className={css.loading}>
|
||||
<FormattedMessage id={`${loadingMessage}`} />
|
||||
</p>
|
||||
);
|
||||
|
||||
const salePanel = isDataAvailable ? (
|
||||
<SaleDetailsPanel
|
||||
className={detailsClassName}
|
||||
currentUser={currentUser}
|
||||
transaction={currentTransaction}
|
||||
onAcceptSale={onAcceptSale}
|
||||
onDeclineSale={onDeclineSale}
|
||||
acceptInProgress={acceptInProgress}
|
||||
declineInProgress={declineInProgress}
|
||||
acceptSaleError={acceptSaleError}
|
||||
declineSaleError={declineSaleError}
|
||||
totalMessagePages={totalMessagePages}
|
||||
oldestMessagePageFetched={oldestMessagePageFetched}
|
||||
messages={messages}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
fetchMessagesError={fetchMessagesError}
|
||||
sendMessageInProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
sendReviewInProgress={sendReviewInProgress}
|
||||
sendReviewError={sendReviewError}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
onShowMoreMessages={onShowMoreMessages}
|
||||
onSendMessage={onSendMessage}
|
||||
onSendReview={onSendReview}
|
||||
onResetForm={onResetForm}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const initialMessageFailed = !!(
|
||||
initialMessageFailedToTransaction &&
|
||||
currentTransaction.id &&
|
||||
initialMessageFailedToTransaction.uuid === currentTransaction.id.uuid
|
||||
);
|
||||
|
||||
const orderPanel = isDataAvailable ? (
|
||||
<OrderDetailsPanel
|
||||
className={detailsClassName}
|
||||
currentUser={currentUser}
|
||||
transaction={currentTransaction}
|
||||
fetchMessagesInProgress={fetchMessagesInProgress}
|
||||
totalMessagePages={totalMessagePages}
|
||||
oldestMessagePageFetched={oldestMessagePageFetched}
|
||||
messages={messages}
|
||||
initialMessageFailed={initialMessageFailed}
|
||||
fetchMessagesError={fetchMessagesError}
|
||||
sendMessageInProgress={sendMessageInProgress}
|
||||
sendMessageError={sendMessageError}
|
||||
sendReviewInProgress={sendReviewInProgress}
|
||||
sendReviewError={sendReviewError}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
onShowMoreMessages={onShowMoreMessages}
|
||||
onSendMessage={onSendMessage}
|
||||
onSendReview={onSendReview}
|
||||
onResetForm={onResetForm}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const saleOrOrderPanel = isDataAvailable && isOwnSale ? salePanel : orderPanel;
|
||||
const panel = isDataAvailable ? saleOrOrderPanel : loadingOrFailedFetching;
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={intl.formatMessage({ id: 'TransactionPage.title' }, { title: listingTitle })}
|
||||
scrollingDisabled={scrollingDisabled}
|
||||
>
|
||||
<LayoutSingleColumn>
|
||||
<LayoutWrapperTopbar>
|
||||
<TopbarContainer />
|
||||
</LayoutWrapperTopbar>
|
||||
<LayoutWrapperMain>
|
||||
<div className={css.root}>{panel}</div>
|
||||
</LayoutWrapperMain>
|
||||
<LayoutWrapperFooter className={css.footer}>
|
||||
<Footer />
|
||||
</LayoutWrapperFooter>
|
||||
</LayoutSingleColumn>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
TransactionPageComponent.defaultProps = {
|
||||
currentUser: null,
|
||||
fetchTransactionError: null,
|
||||
acceptSaleError: null,
|
||||
declineSaleError: null,
|
||||
transaction: null,
|
||||
fetchMessagesError: null,
|
||||
initialMessageFailedToTransaction: null,
|
||||
sendMessageError: null,
|
||||
};
|
||||
|
||||
const { bool, func, oneOf, shape, string, arrayOf, number } = PropTypes;
|
||||
|
||||
TransactionPageComponent.propTypes = {
|
||||
params: shape({ id: string }).isRequired,
|
||||
transactionRole: oneOf([PROVIDER, CUSTOMER]).isRequired,
|
||||
currentUser: propTypes.currentUser,
|
||||
fetchTransactionError: propTypes.error,
|
||||
acceptSaleError: propTypes.error,
|
||||
declineSaleError: propTypes.error,
|
||||
acceptInProgress: bool.isRequired,
|
||||
declineInProgress: bool.isRequired,
|
||||
onAcceptSale: func.isRequired,
|
||||
onDeclineSale: func.isRequired,
|
||||
scrollingDisabled: bool.isRequired,
|
||||
transaction: propTypes.transaction,
|
||||
fetchMessagesError: propTypes.error,
|
||||
totalMessagePages: number.isRequired,
|
||||
oldestMessagePageFetched: number.isRequired,
|
||||
messages: arrayOf(propTypes.message).isRequired,
|
||||
initialMessageFailedToTransaction: propTypes.uuid,
|
||||
sendMessageInProgress: bool.isRequired,
|
||||
sendMessageError: propTypes.error,
|
||||
onShowMoreMessages: func.isRequired,
|
||||
onSendMessage: func.isRequired,
|
||||
onResetForm: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
fetchTransactionError,
|
||||
acceptSaleError,
|
||||
declineSaleError,
|
||||
acceptInProgress,
|
||||
declineInProgress,
|
||||
transactionRef,
|
||||
fetchMessagesInProgress,
|
||||
fetchMessagesError,
|
||||
totalMessagePages,
|
||||
oldestMessagePageFetched,
|
||||
messages,
|
||||
initialMessageFailedToTransaction,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
sendReviewInProgress,
|
||||
sendReviewError,
|
||||
} = state.TransactionPage;
|
||||
const { currentUser } = state.user;
|
||||
|
||||
const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []);
|
||||
const transaction = transactions.length > 0 ? transactions[0] : null;
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
fetchTransactionError,
|
||||
acceptSaleError,
|
||||
declineSaleError,
|
||||
acceptInProgress,
|
||||
declineInProgress,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
transaction,
|
||||
fetchMessagesInProgress,
|
||||
fetchMessagesError,
|
||||
totalMessagePages,
|
||||
oldestMessagePageFetched,
|
||||
messages,
|
||||
initialMessageFailedToTransaction,
|
||||
sendMessageInProgress,
|
||||
sendMessageError,
|
||||
sendReviewInProgress,
|
||||
sendReviewError,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onAcceptSale: transactionId => dispatch(acceptSale(transactionId)),
|
||||
onDeclineSale: transactionId => dispatch(declineSale(transactionId)),
|
||||
onShowMoreMessages: txId => dispatch(fetchMoreMessages(txId)),
|
||||
onSendMessage: (txId, message) => dispatch(sendMessage(txId, message)),
|
||||
onResetForm: formName => dispatch(resetForm(formName)),
|
||||
onManageDisableScrolling: (componentId, disableScrolling) =>
|
||||
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
||||
onSendReview: (tx, reviewRating, reviewContent) =>
|
||||
dispatch(sendReview(tx, reviewRating, reviewContent)),
|
||||
};
|
||||
};
|
||||
|
||||
const TransactionPage = compose(connect(mapStateToProps, mapDispatchToProps), injectIntl)(
|
||||
TransactionPageComponent
|
||||
);
|
||||
|
||||
TransactionPage.loadData = loadData;
|
||||
TransactionPage.setInitialValues = setInitialValues;
|
||||
|
||||
export default TransactionPage;
|
||||
101
src/containers/TransactionPage/TransactionPage.test.js
Normal file
101
src/containers/TransactionPage/TransactionPage.test.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
createBooking,
|
||||
createCurrentUser,
|
||||
createListing,
|
||||
createTransaction,
|
||||
createUser,
|
||||
fakeIntl,
|
||||
} from '../../util/test-data';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { TX_TRANSITION_PREAUTHORIZE } from '../../util/propTypes';
|
||||
import { TransactionPageComponent } from './TransactionPage';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('TransactionPage - Sale', () => {
|
||||
it('matches snapshot', () => {
|
||||
const txId = 'tx-sale-1';
|
||||
const transaction = createTransaction({
|
||||
id: txId,
|
||||
lastTransition: TX_TRANSITION_PREAUTHORIZE,
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
customer: createUser('customer1'),
|
||||
provider: createUser('provider1'),
|
||||
});
|
||||
|
||||
const props = {
|
||||
params: { id: txId },
|
||||
transactionRole: 'provider',
|
||||
|
||||
currentUser: createCurrentUser('provider1'),
|
||||
acceptInProgress: false,
|
||||
declineInProgress: false,
|
||||
onAcceptSale: noop,
|
||||
onDeclineSale: noop,
|
||||
scrollingDisabled: false,
|
||||
transaction,
|
||||
totalMessages: 0,
|
||||
totalMessagePages: 0,
|
||||
oldestMessagePageFetched: 0,
|
||||
messages: [],
|
||||
sendMessageInProgress: false,
|
||||
onShowMoreMessages: noop,
|
||||
onSendMessage: noop,
|
||||
onResetForm: noop,
|
||||
intl: fakeIntl,
|
||||
};
|
||||
|
||||
const tree = renderShallow(<TransactionPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('TransactionPage - Order', () => {
|
||||
it('matches snapshot', () => {
|
||||
const txId = 'tx-order-1';
|
||||
const transaction = createTransaction({
|
||||
id: txId,
|
||||
lastTransition: TX_TRANSITION_PREAUTHORIZE,
|
||||
booking: createBooking('booking1', {
|
||||
start: new Date(Date.UTC(2017, 5, 10)),
|
||||
end: new Date(Date.UTC(2017, 5, 13)),
|
||||
}),
|
||||
listing: createListing('listing1'),
|
||||
customer: createUser('customer1'),
|
||||
provider: createUser('provider1'),
|
||||
});
|
||||
|
||||
const props = {
|
||||
params: { id: txId },
|
||||
transactionRole: 'customer',
|
||||
|
||||
currentUser: createCurrentUser('customer1'),
|
||||
totalMessages: 0,
|
||||
totalMessagePages: 0,
|
||||
oldestMessagePageFetched: 0,
|
||||
messages: [],
|
||||
fetchMessagesInProgress: false,
|
||||
sendMessageInProgress: false,
|
||||
scrollingDisabled: false,
|
||||
transaction,
|
||||
onShowMoreMessages: noop,
|
||||
onSendMessage: noop,
|
||||
onResetForm: noop,
|
||||
intl: fakeIntl,
|
||||
|
||||
acceptInProgress: false,
|
||||
declineInProgress: false,
|
||||
onAcceptSale: noop,
|
||||
onDeclineSale: noop,
|
||||
};
|
||||
|
||||
const tree = renderShallow(<TransactionPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TransactionPage - Order matches snapshot 1`] = `
|
||||
<Page
|
||||
scrollingDisabled={false}
|
||||
title="TransactionPage.title"
|
||||
>
|
||||
<LayoutSingleColumn
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<LayoutWrapperTopbar
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<withRouter(Connect(TopbarContainerComponent)) />
|
||||
</LayoutWrapperTopbar>
|
||||
<LayoutWrapperMain
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<div>
|
||||
<InjectIntl(OrderDetailsPanelComponent)
|
||||
className=""
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "customer1@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "customer1 abbreviated name",
|
||||
"displayName": "customer1 display name",
|
||||
"firstName": "customer1 first name",
|
||||
"lastName": "customer1 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "customer1",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
fetchMessagesError={null}
|
||||
fetchMessagesInProgress={false}
|
||||
initialMessageFailed={false}
|
||||
messages={Array []}
|
||||
oldestMessagePageFetched={0}
|
||||
onResetForm={[Function]}
|
||||
onSendMessage={[Function]}
|
||||
onShowMoreMessages={[Function]}
|
||||
sendMessageError={null}
|
||||
sendMessageInProgress={false}
|
||||
totalMessagePages={0}
|
||||
transaction={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"createdAt": 2017-05-01T00:00:00.000Z,
|
||||
"lastTransition": "transition/preauthorize",
|
||||
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
|
||||
"lineItems": Array [
|
||||
Object {
|
||||
"code": "line-item/night",
|
||||
"lineTotal": Money {
|
||||
"amount": 1000,
|
||||
"currency": "USD",
|
||||
},
|
||||
"quantity": "3",
|
||||
"reversal": false,
|
||||
"unitPrice": Money {
|
||||
"amount": 333.3333333333333,
|
||||
"currency": "USD",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"code": "line-item/provider-commission",
|
||||
"lineTotal": Money {
|
||||
"amount": -100,
|
||||
"currency": "USD",
|
||||
},
|
||||
"reversal": false,
|
||||
"unitPrice": Money {
|
||||
"amount": -100,
|
||||
"currency": "USD",
|
||||
},
|
||||
},
|
||||
],
|
||||
"payinTotal": Money {
|
||||
"amount": 1000,
|
||||
"currency": "USD",
|
||||
},
|
||||
"payoutTotal": Money {
|
||||
"amount": 900,
|
||||
"currency": "USD",
|
||||
},
|
||||
"transitions": Array [
|
||||
Object {
|
||||
"at": 2017-05-01T00:00:00.000Z,
|
||||
"by": "customer",
|
||||
"transition": "transition/preauthorize",
|
||||
},
|
||||
Object {
|
||||
"at": 2017-06-01T00:00:00.000Z,
|
||||
"by": "provider",
|
||||
"transition": "transition/accept",
|
||||
},
|
||||
],
|
||||
},
|
||||
"booking": Object {
|
||||
"attributes": Object {
|
||||
"end": 2017-06-13T00:00:00.000Z,
|
||||
"start": 2017-06-10T00:00:00.000Z,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "booking1",
|
||||
},
|
||||
"type": "booking",
|
||||
},
|
||||
"customer": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "customer1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "customer1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "tx-order-1",
|
||||
},
|
||||
"listing": Object {
|
||||
"attributes": Object {
|
||||
"address": "listing1 address",
|
||||
"closed": false,
|
||||
"deleted": false,
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"type": "listing",
|
||||
},
|
||||
"provider": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "provider1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "provider1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"reviews": Array [],
|
||||
"type": "transaction",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</LayoutWrapperMain>
|
||||
<LayoutWrapperFooter
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<InjectIntl(Footer) />
|
||||
</LayoutWrapperFooter>
|
||||
</LayoutSingleColumn>
|
||||
</Page>
|
||||
`;
|
||||
|
||||
exports[`TransactionPage - Sale matches snapshot 1`] = `
|
||||
<Page
|
||||
scrollingDisabled={false}
|
||||
title="TransactionPage.title"
|
||||
>
|
||||
<LayoutSingleColumn
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<LayoutWrapperTopbar
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<withRouter(Connect(TopbarContainerComponent)) />
|
||||
</LayoutWrapperTopbar>
|
||||
<LayoutWrapperMain
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<div>
|
||||
<InjectIntl(SaleDetailsPanelComponent)
|
||||
acceptInProgress={false}
|
||||
acceptSaleError={null}
|
||||
className=""
|
||||
currentUser={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"email": "provider1@example.com",
|
||||
"emailVerified": true,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "provider1 abbreviated name",
|
||||
"displayName": "provider1 display name",
|
||||
"firstName": "provider1 first name",
|
||||
"lastName": "provider1 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "provider1",
|
||||
},
|
||||
"type": "currentUser",
|
||||
}
|
||||
}
|
||||
declineInProgress={false}
|
||||
declineSaleError={null}
|
||||
fetchMessagesError={null}
|
||||
messages={Array []}
|
||||
oldestMessagePageFetched={0}
|
||||
onAcceptSale={[Function]}
|
||||
onDeclineSale={[Function]}
|
||||
onResetForm={[Function]}
|
||||
onSendMessage={[Function]}
|
||||
onShowMoreMessages={[Function]}
|
||||
sendMessageError={null}
|
||||
sendMessageInProgress={false}
|
||||
totalMessagePages={0}
|
||||
transaction={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"createdAt": 2017-05-01T00:00:00.000Z,
|
||||
"lastTransition": "transition/preauthorize",
|
||||
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
|
||||
"lineItems": Array [
|
||||
Object {
|
||||
"code": "line-item/night",
|
||||
"lineTotal": Money {
|
||||
"amount": 1000,
|
||||
"currency": "USD",
|
||||
},
|
||||
"quantity": "3",
|
||||
"reversal": false,
|
||||
"unitPrice": Money {
|
||||
"amount": 333.3333333333333,
|
||||
"currency": "USD",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"code": "line-item/provider-commission",
|
||||
"lineTotal": Money {
|
||||
"amount": -100,
|
||||
"currency": "USD",
|
||||
},
|
||||
"reversal": false,
|
||||
"unitPrice": Money {
|
||||
"amount": -100,
|
||||
"currency": "USD",
|
||||
},
|
||||
},
|
||||
],
|
||||
"payinTotal": Money {
|
||||
"amount": 1000,
|
||||
"currency": "USD",
|
||||
},
|
||||
"payoutTotal": Money {
|
||||
"amount": 900,
|
||||
"currency": "USD",
|
||||
},
|
||||
"transitions": Array [
|
||||
Object {
|
||||
"at": 2017-05-01T00:00:00.000Z,
|
||||
"by": "customer",
|
||||
"transition": "transition/preauthorize",
|
||||
},
|
||||
Object {
|
||||
"at": 2017-06-01T00:00:00.000Z,
|
||||
"by": "provider",
|
||||
"transition": "transition/accept",
|
||||
},
|
||||
],
|
||||
},
|
||||
"booking": Object {
|
||||
"attributes": Object {
|
||||
"end": 2017-06-13T00:00:00.000Z,
|
||||
"start": 2017-06-10T00:00:00.000Z,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "booking1",
|
||||
},
|
||||
"type": "booking",
|
||||
},
|
||||
"customer": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "customer1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "customer1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "tx-sale-1",
|
||||
},
|
||||
"listing": Object {
|
||||
"attributes": Object {
|
||||
"address": "listing1 address",
|
||||
"closed": false,
|
||||
"deleted": false,
|
||||
"description": "listing1 description",
|
||||
"geolocation": LatLng {
|
||||
"lat": 40,
|
||||
"lng": 60,
|
||||
},
|
||||
"price": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
"title": "listing1 title",
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "listing1",
|
||||
},
|
||||
"type": "listing",
|
||||
},
|
||||
"provider": Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "TT",
|
||||
"displayName": "provider1 display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "provider1",
|
||||
},
|
||||
"type": "user",
|
||||
},
|
||||
"reviews": Array [],
|
||||
"type": "transaction",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</LayoutWrapperMain>
|
||||
<LayoutWrapperFooter
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<InjectIntl(Footer) />
|
||||
</LayoutWrapperFooter>
|
||||
</LayoutSingleColumn>
|
||||
</Page>
|
||||
`;
|
||||
|
|
@ -46,3 +46,4 @@ export { default as StyleguidePage } from './StyleguidePage/StyleguidePage';
|
|||
export { default as TermsOfServicePage } from './TermsOfServicePage/TermsOfServicePage';
|
||||
export { default as TopbarContainer } from './TopbarContainer/TopbarContainer';
|
||||
export { default as TopbarSearchForm } from './TopbarSearchForm/TopbarSearchForm';
|
||||
export { default as TransactionPage } from './TransactionPage/TransactionPage';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import ProfilePage from './ProfilePage/ProfilePage.duck';
|
|||
import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck';
|
||||
import SalePage from './SalePage/SalePage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
import TransactionPage from './TransactionPage/TransactionPage.duck';
|
||||
|
||||
export {
|
||||
CheckoutPage,
|
||||
|
|
@ -33,4 +34,5 @@ export {
|
|||
ProfileSettingsPage,
|
||||
SalePage,
|
||||
SearchPage,
|
||||
TransactionPage,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
ListingPage,
|
||||
ManageListingsPage,
|
||||
NotFoundPage,
|
||||
OrderPage,
|
||||
PasswordChangePage,
|
||||
PasswordRecoveryPage,
|
||||
PasswordResetPage,
|
||||
|
|
@ -19,10 +18,10 @@ import {
|
|||
PrivacyPolicyPage,
|
||||
ProfilePage,
|
||||
ProfileSettingsPage,
|
||||
SalePage,
|
||||
SearchPage,
|
||||
StyleguidePage,
|
||||
TermsOfServicePage,
|
||||
TransactionPage,
|
||||
} from './containers';
|
||||
|
||||
// routeConfiguration needs to initialize containers first
|
||||
|
|
@ -187,9 +186,9 @@ const routeConfiguration = () => {
|
|||
name: 'OrderDetailsPage',
|
||||
auth: true,
|
||||
authPage: 'LoginPage',
|
||||
component: props => <OrderPage {...props} tab="details" />,
|
||||
loadData: OrderPage.loadData,
|
||||
setInitialValues: OrderPage.setInitialValues,
|
||||
component: props => <TransactionPage {...props} transactionRole="customer" />,
|
||||
loadData: TransactionPage.loadData,
|
||||
setInitialValues: TransactionPage.setInitialValues,
|
||||
},
|
||||
{
|
||||
path: '/sale/:id',
|
||||
|
|
@ -203,8 +202,8 @@ const routeConfiguration = () => {
|
|||
name: 'SaleDetailsPage',
|
||||
auth: true,
|
||||
authPage: 'LoginPage',
|
||||
component: props => <SalePage {...props} tab="details" />,
|
||||
loadData: SalePage.loadData,
|
||||
component: props => <TransactionPage {...props} transactionRole="provider" />,
|
||||
loadData: TransactionPage.loadData,
|
||||
},
|
||||
{
|
||||
path: '/listings',
|
||||
|
|
|
|||
|
|
@ -455,11 +455,13 @@
|
|||
"ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.",
|
||||
"ReviewModal.later": "Later",
|
||||
"ReviewModal.title": "Leave a review for {revieweeName}",
|
||||
"SaleDetailsPanel.acceptButton": "Accept",
|
||||
"SaleDetailsPanel.acceptSaleFailed": "Oops, accepting failed. Please try again.",
|
||||
"SaleDetailsPanel.activityHeading": "Activity",
|
||||
"SaleDetailsPanel.bannedUserDisplayName": "Banned user",
|
||||
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
"SaleDetailsPanel.customerBannedStatus": "The user made the request, but was later banned.",
|
||||
"SaleDetailsPanel.declineButton": "Decline",
|
||||
"SaleDetailsPanel.declineSaleFailed": "Oops, declining failed. Please try again.",
|
||||
"SaleDetailsPanel.messageLoadingFailed": "Something went wrong when loading messages. Please refresh the page and try again.",
|
||||
"SaleDetailsPanel.saleAcceptedTitle": "You accepted a request from {customerName} to book {listingLink}.",
|
||||
|
|
@ -622,6 +624,11 @@
|
|||
"TopbarMobileMenu.yourListingsLink": "Your listings",
|
||||
"TopbarSearchForm.placeholder": "Search saunas…",
|
||||
"TopbarSearchForm.searchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district \"Sörnäinen\".",
|
||||
"TransactionPage.fetchOrderFailed": "Fetching order data failed.",
|
||||
"TransactionPage.fetchSaleFailed": "Fetching sale data failed.",
|
||||
"TransactionPage.loadingOrderData": "Loading order data.",
|
||||
"TransactionPage.loadingSaleData": "Loading sale data.",
|
||||
"TransactionPage.title": "Sale details: {title}",
|
||||
"UserCard.contactUser": "Contact",
|
||||
"UserCard.heading": "Hello, I'm {name}.",
|
||||
"UserCard.showFullBioLink": "more",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue