Remove OrderPage and SalePage

This commit is contained in:
Vesa Luusua 2017-12-12 14:35:31 +02:00
parent cbc1fcc05c
commit 9f54bbb67d
14 changed files with 0 additions and 1822 deletions

View file

@ -1,35 +0,0 @@
@import '../../marketplace.css';
.mainContent {
}
.loading {
margin-left: 24px;
margin-right: 24px;
}
.error {
margin-left: 24px;
margin-right: 24px;
color: var(--failColor);
}
.tabContent {
display: none;
}
.tabContentVisible {
display: block;
}
.activeTab {
font-weight: bold;
}
.footer {
display: none;
@media (--viewportLarge) {
display: block;
}
}

View file

@ -1,348 +0,0 @@
import { pick } from 'lodash';
import { types } from '../../util/sdkLoader';
import { isTransactionsTransitionInvalidTransition, storableError } from '../../util/errors';
import * as propTypes from '../../util/propTypes';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { updatedEntities, denormalisedEntities } from '../../util/data';
const MESSAGES_PAGE_SIZE = 100;
// ================ Action types ================ //
export const SET_INITAL_VALUES = 'app/OrderPage/SET_INITIAL_VALUES';
export const FETCH_ORDER_REQUEST = 'app/OrderPage/FETCH_ORDER_REQUEST';
export const FETCH_ORDER_SUCCESS = 'app/OrderPage/FETCH_ORDER_SUCCESS';
export const FETCH_ORDER_ERROR = 'app/OrderPage/FETCH_ORDER_ERROR';
export const FETCH_MESSAGES_REQUEST = 'app/OrderPage/FETCH_MESSAGES_REQUEST';
export const FETCH_MESSAGES_SUCCESS = 'app/OrderPage/FETCH_MESSAGES_SUCCESS';
export const FETCH_MESSAGES_ERROR = 'app/OrderPage/FETCH_MESSAGES_ERROR';
export const SEND_MESSAGE_REQUEST = 'app/OrderPage/SEND_MESSAGE_REQUEST';
export const SEND_MESSAGE_SUCCESS = 'app/OrderPage/SEND_MESSAGE_SUCCESS';
export const SEND_MESSAGE_ERROR = 'app/OrderPage/SEND_MESSAGE_ERROR';
export const SEND_REVIEW_REQUEST = 'app/OrderPage/SEND_REVIEW_REQUEST';
export const SEND_REVIEW_SUCCESS = 'app/OrderPage/SEND_REVIEW_SUCCESS';
export const SEND_REVIEW_ERROR = 'app/OrderPage/SEND_REVIEW_ERROR';
// ================ Reducer ================ //
const initialState = {
fetchOrderInProgress: false,
fetchOrderError: null,
transactionRef: null,
fetchMessagesInProgress: false,
fetchMessagesError: null,
totalMessages: 0,
totalMessagePages: 0,
oldestMessagePageFetched: 0,
messages: [],
messageSendingFailedToTransaction: 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_ORDER_REQUEST:
return { ...state, fetchOrderInProgress: true, fetchOrderError: null };
case FETCH_ORDER_SUCCESS: {
const transactionRef = { id: payload.data.data.id, type: 'transaction' };
return { ...state, fetchOrderInProgress: false, transactionRef };
}
case FETCH_ORDER_ERROR:
console.error(payload); // eslint-disable-line
return { ...state, fetchOrderInProgress: false, fetchOrderError: 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;
}
}
// ================ Action creators ================ //
export const setInitialValues = initialValues => ({
type: SET_INITAL_VALUES,
payload: pick(initialValues, Object.keys(initialState)),
});
const fetchOrderRequest = () => ({ type: FETCH_ORDER_REQUEST });
const fetchOrderSuccess = response => ({ type: FETCH_ORDER_SUCCESS, payload: response });
const fetchOrderError = e => ({ type: FETCH_ORDER_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 fetchOrder = id => (dispatch, getState, sdk) => {
dispatch(fetchOrderRequest());
let txResponse = null;
return sdk.transactions
.show(
{
id,
include: [
'customer',
'provider',
'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(fetchOrderSuccess(txResponse));
return response;
})
.catch(e => {
dispatch(fetchOrderError(storableError(e)));
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().OrderPage.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.OrderPage;
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 = (orderId, message) => (dispatch, getState, sdk) => {
dispatch(sendMessageRequest());
return sdk.messages
.send({ transactionId: orderId, 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(orderId, 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 (provider) has already sent a review, we need to make transition to
// TX_TRANSITION_REVIEW_BY_CUSTOMER_SECOND
const sendReviewAsSecond = (id, params, dispatch, sdk) => {
const transition = propTypes.TX_TRANSITION_REVIEW_BY_CUSTOMER_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 (provider) has not yet sent a review, we need to make transition to
// TX_TRANSITION_REVIEW_BY_CUSTOMER_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_CUSTOMER_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_PROVIDER_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 orderId = new types.UUID(params.id);
// Clear the send error since the message form is emptied as well.
dispatch(setInitialValues({ sendMessageError: null, sendReviewError: null }));
// Order (i.e. transaction entity in API, but from buyers perspective) contains order details
return Promise.all([dispatch(fetchOrder(orderId)), dispatch(fetchMessages(orderId, 1))]);
};

View file

@ -1,230 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reset as resetForm } from 'redux-form';
import classNames from 'classnames';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
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,
Page,
LayoutSingleColumn,
LayoutWrapperTopbar,
LayoutWrapperMain,
LayoutWrapperFooter,
Footer,
} from '../../components';
import { TopbarContainer } from '../../containers';
import {
loadData,
setInitialValues,
sendMessage,
sendReview,
fetchMoreMessages,
} from './OrderPage.duck';
import css from './OrderPage.css';
// OrderPage handles data loading
// It show loading data text or OrderDetailsPanel (and later also another panel for messages).
export const OrderPageComponent = props => {
const {
currentUser,
fetchOrderError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessagePages,
oldestMessagePageFetched,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
onManageDisableScrolling,
onShowMoreMessages,
onSendMessage,
onSendReview,
onResetForm,
intl,
params,
scrollingDisabled,
transaction,
} = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
const listingTitle = currentListing.attributes.title;
const initialMessageFailed = !!(
messageSendingFailedToTransaction &&
currentTransaction.id &&
messageSendingFailedToTransaction.uuid === currentTransaction.id.uuid
);
// Redirect users with someone else's direct link to their own inbox/orders page.
const isDataAvailable =
currentUser &&
currentTransaction.id &&
currentTransaction.id.uuid === params.id &&
currentTransaction.customer &&
!fetchOrderError;
const isOwnSale = isDataAvailable && currentUser.id.uuid === currentTransaction.customer.id.uuid;
if (isDataAvailable && !isOwnSale) {
// 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]: props.tab === 'details',
});
const loadingOrFaildFetching = fetchOrderError ? (
<p className={css.error}>
<FormattedMessage id="OrderPage.fetchOrderFailed" />
</p>
) : (
<p className={css.loading}>
<FormattedMessage id="OrderPage.loadingData" />
</p>
);
const panel =
isDataAvailable && currentTransaction.id ? (
<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}
/>
) : (
loadingOrFaildFetching
);
return (
<Page
title={intl.formatMessage({ id: 'OrderPage.title' }, { listingTitle })}
scrollingDisabled={scrollingDisabled}
>
<LayoutSingleColumn>
<LayoutWrapperTopbar>
<TopbarContainer />
</LayoutWrapperTopbar>
<LayoutWrapperMain className={css.mainContent}>{panel}</LayoutWrapperMain>
<LayoutWrapperFooter>
<Footer className={css.footer} />
</LayoutWrapperFooter>
</LayoutSingleColumn>
</Page>
);
};
OrderPageComponent.defaultProps = {
currentUser: null,
fetchOrderError: null,
fetchMessagesError: null,
messageSendingFailedToTransaction: null,
sendMessageError: null,
transaction: null,
};
const { bool, oneOf, shape, string, array, func, number } = PropTypes;
OrderPageComponent.propTypes = {
params: shape({ id: string }).isRequired,
tab: oneOf(['details', 'discussion']).isRequired,
currentUser: propTypes.currentUser,
fetchOrderError: propTypes.error,
fetchMessagesInProgress: bool.isRequired,
fetchMessagesError: propTypes.error,
totalMessagePages: number.isRequired,
oldestMessagePageFetched: number.isRequired,
messages: array.isRequired,
messageSendingFailedToTransaction: propTypes.uuid,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,
scrollingDisabled: bool.isRequired,
transaction: propTypes.transaction,
onShowMoreMessages: func.isRequired,
onSendMessage: func.isRequired,
onResetForm: func.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
const { currentUser } = state.user;
const {
fetchOrderError,
transactionRef,
fetchMessagesInProgress,
fetchMessagesError,
totalMessagePages,
oldestMessagePageFetched,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
} = state.OrderPage;
const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []);
const transaction = transactions.length > 0 ? transactions[0] : null;
return {
currentUser,
fetchOrderError,
fetchMessagesInProgress,
fetchMessagesError,
totalMessagePages,
oldestMessagePageFetched,
messages,
messageSendingFailedToTransaction,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
scrollingDisabled: isScrollingDisabled(state),
transaction,
};
};
const mapDispatchToProps = dispatch => ({
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
onShowMoreMessages: orderId => dispatch(fetchMoreMessages(orderId)),
onSendMessage: (orderId, message) => dispatch(sendMessage(orderId, message)),
onSendReview: (tx, reviewRating, reviewContent) =>
dispatch(sendReview(tx, reviewRating, reviewContent)),
onResetForm: formName => dispatch(resetForm(formName)),
});
const OrderPage = compose(connect(mapStateToProps, mapDispatchToProps), injectIntl)(
OrderPageComponent
);
OrderPage.setInitialValues = setInitialValues;
OrderPage.loadData = loadData;
export default OrderPage;

View file

@ -1,54 +0,0 @@
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 { OrderPageComponent } from './OrderPage';
const noop = () => null;
describe('OrderPage', () => {
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 },
tab: 'details',
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,
};
const tree = renderShallow(<OrderPageComponent {...props} />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,183 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OrderPage matches snapshot 1`] = `
<Page
scrollingDisabled={false}
title="OrderPage.title"
>
<LayoutSingleColumn
className={null}
rootClassName={null}
>
<LayoutWrapperTopbar
className={null}
rootClassName={null}
>
<withRouter(Connect(TopbarContainerComponent)) />
</LayoutWrapperTopbar>
<LayoutWrapperMain
className={null}
rootClassName={null}
>
<InjectIntl(OrderDetailsPanelComponent)
className="undefined"
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",
}
}
/>
</LayoutWrapperMain>
<LayoutWrapperFooter
className={null}
rootClassName={null}
>
<InjectIntl(Footer) />
</LayoutWrapperFooter>
</LayoutSingleColumn>
</Page>
`;

View file

@ -1,34 +0,0 @@
@import '../../marketplace.css';
.root {
display: flex;
flex-direction: column;
flex: 1;
}
.footer {
display: none;
@media (--viewportLarge) {
display: block;
}
}
.loading {
margin-left: 24px;
margin-right: 24px;
}
.error {
margin-left: 24px;
margin-right: 24px;
color: var(--failColor);
}
.tabContent {
display: none;
}
.tabContentVisible {
display: block;
}

View file

@ -1,426 +0,0 @@
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/OrderPage/SET_INITIAL_VALUES';
export const FETCH_SALE_REQUEST = 'app/SalePagePage/FETCH_SALE_REQUEST';
export const FETCH_SALE_SUCCESS = 'app/SalePage/FETCH_SALE_SUCCESS';
export const FETCH_SALE_ERROR = 'app/SalePage/FETCH_SALE_ERROR';
export const ACCEPT_SALE_REQUEST = 'app/SalePage/ACCEPT_SALE_REQUEST';
export const ACCEPT_SALE_SUCCESS = 'app/SalePage/ACCEPT_SALE_SUCCESS';
export const ACCEPT_SALE_ERROR = 'app/SalePage/ACCEPT_SALE_ERROR';
export const DECLINE_SALE_REQUEST = 'app/SalePage/DECLINE_SALE_REQUEST';
export const DECLINE_SALE_SUCCESS = 'app/SalePage/DECLINE_SALE_SUCCESS';
export const DECLINE_SALE_ERROR = 'app/SalePage/DECLINE_SALE_ERROR';
export const FETCH_MESSAGES_REQUEST = 'app/SalePage/FETCH_MESSAGES_REQUEST';
export const FETCH_MESSAGES_SUCCESS = 'app/SalePage/FETCH_MESSAGES_SUCCESS';
export const FETCH_MESSAGES_ERROR = 'app/SalePage/FETCH_MESSAGES_ERROR';
export const SEND_MESSAGE_REQUEST = 'app/SalePage/SEND_MESSAGE_REQUEST';
export const SEND_MESSAGE_SUCCESS = 'app/SalePage/SEND_MESSAGE_SUCCESS';
export const SEND_MESSAGE_ERROR = 'app/SalePage/SEND_MESSAGE_ERROR';
export const SEND_REVIEW_REQUEST = 'app/SalePage/SEND_REVIEW_REQUEST';
export const SEND_REVIEW_SUCCESS = 'app/SalePage/SEND_REVIEW_SUCCESS';
export const SEND_REVIEW_ERROR = 'app/SalePage/SEND_REVIEW_ERROR';
// ================ Reducer ================ //
const initialState = {
fetchSaleInProgress: false,
fetchSaleError: null,
transactionRef: null,
acceptInProgress: false,
declineInProgress: false,
acceptSaleError: null,
declineSaleError: null,
fetchMessagesInProgress: false,
fetchMessagesError: null,
totalMessages: 0,
totalMessagePages: 0,
oldestMessagePageFetched: 0,
messages: [],
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_SALE_REQUEST:
return { ...state, fetchSaleInProgress: true, fetchSaleError: null };
case FETCH_SALE_SUCCESS: {
const transactionRef = { id: payload.data.data.id, type: 'transaction' };
return { ...state, fetchSaleInProgress: false, transactionRef };
}
case FETCH_SALE_ERROR:
console.error(payload); // eslint-disable-line
return { ...state, fetchSaleInProgress: false, fetchSaleError: 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.SalePage.acceptInProgress || state.SalePage.declineInProgress;
};
// ================ Action creators ================ //
export const setInitialValues = initialValues => ({
type: SET_INITAL_VALUES,
payload: pick(initialValues, Object.keys(initialState)),
});
const fetchSaleRequest = () => ({ type: FETCH_SALE_REQUEST });
const fetchSaleSuccess = response => ({ type: FETCH_SALE_SUCCESS, payload: response });
const fetchSaleError = e => ({ type: FETCH_SALE_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 fetchSale = id => (dispatch, getState, sdk) => {
dispatch(fetchSaleRequest());
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;
return sdk.listings.show({
id: listingId,
include: ['author', 'author.profileImage', 'images'],
});
})
.then(response => {
dispatch(addMarketplaceEntities(txResponse));
dispatch(addMarketplaceEntities(response));
dispatch(fetchSaleSuccess(txResponse));
return response;
})
.catch(e => {
dispatch(fetchSaleError(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().OrderPage.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.SalePage;
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 = (saleId, message) => (dispatch, getState, sdk) => {
dispatch(sendMessageRequest());
return sdk.messages
.send({ transactionId: saleId, 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(saleId, 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 saleId = new types.UUID(params.id);
// Clear the send error since the message form is emptied as well.
dispatch(setInitialValues({ sendMessageError: null, sendReviewError: null }));
// Sale (i.e. transaction entity in API, but from buyers perspective) contains sale details
return Promise.all([dispatch(fetchSale(saleId)), dispatch(fetchMessages(saleId, 1))]);
};

View file

@ -1,252 +0,0 @@
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,
SaleDetailsPanel,
Page,
LayoutSingleColumn,
LayoutWrapperTopbar,
LayoutWrapperMain,
LayoutWrapperFooter,
Footer,
} from '../../components';
import { TopbarContainer } from '../../containers';
import {
acceptSale,
declineSale,
loadData,
sendMessage,
sendReview,
fetchMoreMessages,
} from './SalePage.duck';
import css from './SalePage.css';
// SalePage handles data loading
// It show loading data text or SaleDetailsPanel (and later also another panel for messages).
export const SalePageComponent = props => {
const {
currentUser,
fetchSaleError,
acceptSaleError,
declineSaleError,
acceptInProgress,
declineInProgress,
intl,
onAcceptSale,
onDeclineSale,
params,
scrollingDisabled,
transaction,
fetchMessagesInProgress,
fetchMessagesError,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
onManageDisableScrolling,
onShowMoreMessages,
onSendMessage,
onSendReview,
onResetForm,
} = 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 page.
const isDataAvailable =
currentUser &&
currentTransaction.id &&
currentTransaction.id.uuid === params.id &&
currentTransaction.provider &&
!fetchSaleError;
const isOwnSale = isDataAvailable && currentUser.id.uuid === currentTransaction.provider.id.uuid;
if (isDataAvailable && !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' }} />;
}
const detailsClassName = classNames(css.tabContent, {
[css.tabContentVisible]: props.tab === 'details',
});
const loadingOrFailedFetching = fetchSaleError ? (
<p className={css.error}>
<FormattedMessage id="SalePage.fetchSaleFailed" />
</p>
) : (
<p className={css.loading}>
<FormattedMessage id="SalePage.loadingData" />
</p>
);
const panel =
isDataAvailable && currentTransaction.id ? (
<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}
/>
) : (
loadingOrFailedFetching
);
return (
<Page
title={intl.formatMessage({ id: 'SalePage.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>
);
};
SalePageComponent.defaultProps = {
currentUser: null,
fetchSaleError: null,
acceptSaleError: null,
declineSaleError: null,
transaction: null,
fetchMessagesError: null,
sendMessageError: null,
};
const { bool, func, oneOf, shape, string, arrayOf, number } = PropTypes;
SalePageComponent.propTypes = {
params: shape({ id: string }).isRequired,
tab: oneOf(['details', 'discussion']).isRequired,
currentUser: propTypes.currentUser,
fetchSaleError: 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,
sendMessageInProgress: bool.isRequired,
sendMessageError: propTypes.error,
onShowMoreMessages: func.isRequired,
onSendMessage: func.isRequired,
onResetForm: func.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const mapStateToProps = state => {
const {
fetchSaleError,
acceptSaleError,
declineSaleError,
acceptInProgress,
declineInProgress,
transactionRef,
fetchMessagesInProgress,
fetchMessagesError,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
} = state.SalePage;
const { currentUser } = state.user;
const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []);
const transaction = transactions.length > 0 ? transactions[0] : null;
return {
currentUser,
fetchSaleError,
acceptSaleError,
declineSaleError,
acceptInProgress,
declineInProgress,
scrollingDisabled: isScrollingDisabled(state),
transaction,
fetchMessagesInProgress,
fetchMessagesError,
totalMessagePages,
oldestMessagePageFetched,
messages,
sendMessageInProgress,
sendMessageError,
sendReviewInProgress,
sendReviewError,
};
};
const mapDispatchToProps = dispatch => {
return {
onAcceptSale: transactionId => dispatch(acceptSale(transactionId)),
onDeclineSale: transactionId => dispatch(declineSale(transactionId)),
onShowMoreMessages: saleId => dispatch(fetchMoreMessages(saleId)),
onSendMessage: (saleId, message) => dispatch(sendMessage(saleId, message)),
onResetForm: formName => dispatch(resetForm(formName)),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
onSendReview: (tx, reviewRating, reviewContent) =>
dispatch(sendReview(tx, reviewRating, reviewContent)),
};
};
const SalePage = compose(connect(mapStateToProps, mapDispatchToProps), injectIntl)(
SalePageComponent
);
SalePage.loadData = loadData;
export default SalePage;

View file

@ -1,55 +0,0 @@
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 { SalePageComponent } from './SalePage';
const noop = () => null;
describe('SalePage', () => {
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 },
tab: 'details',
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(<SalePageComponent {...props} />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,189 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SalePage matches snapshot 1`] = `
<Page
scrollingDisabled={false}
title="SalePage.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="undefined"
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>
`;

View file

@ -55,7 +55,6 @@ describe('TransactionPage - Sale', () => {
});
});
describe('TransactionPage - Order', () => {
it('matches snapshot', () => {
const txId = 'tx-order-1';

View file

@ -22,7 +22,6 @@ export { default as LocationSearchForm } from './LocationSearchForm/LocationSear
export { default as LoginForm } from './LoginForm/LoginForm';
export { default as ManageListingsPage } from './ManageListingsPage/ManageListingsPage';
export { default as NotFoundPage } from './NotFoundPage/NotFoundPage';
export { default as OrderPage } from './OrderPage/OrderPage';
export { default as PasswordChangeForm } from './PasswordChangeForm/PasswordChangeForm';
export { default as PasswordChangePage } from './PasswordChangePage/PasswordChangePage';
export { default as PasswordRecoveryForm } from './PasswordRecoveryForm/PasswordRecoveryForm';
@ -36,7 +35,6 @@ export { default as ProfilePage } from './ProfilePage/ProfilePage';
export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSettingsForm';
export { default as ProfileSettingsPage } from './ProfileSettingsPage/ProfileSettingsPage';
export { default as ReviewForm } from './ReviewForm/ReviewForm';
export { default as SalePage } from './SalePage/SalePage';
export { default as SearchPage } from './SearchPage/SearchPage';
export { default as SendMessageForm } from './SendMessageForm/SendMessageForm';
export { default as SignupForm } from './SignupForm/SignupForm';

View file

@ -9,13 +9,11 @@ import EditListingPage from './EditListingPage/EditListingPage.duck';
import InboxPage from './InboxPage/InboxPage.duck';
import ListingPage from './ListingPage/ListingPage.duck';
import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck';
import OrderPage from './OrderPage/OrderPage.duck';
import PasswordChangePage from './PasswordChangePage/PasswordChangePage.duck';
import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.duck';
import PasswordResetPage from './PasswordResetPage/PasswordResetPage.duck';
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';
@ -26,13 +24,11 @@ export {
InboxPage,
ListingPage,
ManageListingsPage,
OrderPage,
PasswordChangePage,
PasswordRecoveryPage,
PasswordResetPage,
ProfilePage,
ProfileSettingsPage,
SalePage,
SearchPage,
TransactionPage,
};

View file

@ -296,9 +296,6 @@
"OrderDetailsPanel.orderPreauthorizedSubtitle": "You have requested to book {listingLink}.",
"OrderDetailsPanel.orderPreauthorizedTitle": "Great success, {customerName}!",
"OrderDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
"OrderPage.fetchOrderFailed": "Fetching order data failed.",
"OrderPage.loadingData": "Loading order data.",
"OrderPage.title": "Order details: {listingTitle}",
"Page.schemaDescription": "Book a sauna using Saunatime or earn some income by sharing your sauna",
"Page.schemaTitle": "Book saunas everywhere | {siteTitle}",
"PaginationLinks.next": "Next page",
@ -471,12 +468,6 @@
"SaleDetailsPanel.saleRequestedInfo": "{customerName} is waiting for your response.",
"SaleDetailsPanel.saleRequestedTitle": "{customerName} has requested to book {listingLink}.",
"SaleDetailsPanel.sendMessagePlaceholder": "Send a message to {name}…",
"SalePage.acceptButton": "Accept",
"SalePage.declineButton": "Decline",
"SalePage.declineSaleFailed": "Declining the sale failed.",
"SalePage.fetchSaleFailed": "Fetching sale data failed.",
"SalePage.loadingData": "Loading sale data.",
"SalePage.title": "Sale details: {title}",
"SearchMapInfoCard.noImage": "No image",
"SearchPage.foundResults": "{count, number} {count, plural, one {sauna} other {saunas}} found",
"SearchPage.foundResultsMobile": "{count, number} {count, plural, one {result} other {results}}",