Merge pull request #122 from sharetribe/prevent-booking-own-listing

Prevent booking own listing
This commit is contained in:
Kimmo Puputti 2017-04-20 09:50:45 +03:00 committed by GitHub
commit f899a51472
11 changed files with 95 additions and 26 deletions

View file

@ -1,4 +1,5 @@
import { pick } from 'lodash';
import { fetchCurrentUser } from '../../ducks/user.duck';
// ================ Action types ================ //
@ -76,3 +77,8 @@ export const initiateOrder = params =>
};
// ================ Thunk ================ //
export const loadData = () =>
dispatch => {
return dispatch(fetchCurrentUser());
};

View file

@ -8,7 +8,7 @@ import * as propTypes from '../../util/propTypes';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { AuthorInfo, BookingInfo, NamedRedirect, PageLayout } from '../../components';
import { StripePaymentForm } from '../../containers';
import { initiateOrder, setInitialValues } from './CheckoutPage.duck';
import { initiateOrder, setInitialValues, loadData } from './CheckoutPage.duck';
import css from './CheckoutPage.css';
@ -47,15 +47,24 @@ export class CheckoutPageComponent extends Component {
}
render() {
const { bookingDates, initiateOrderError, intl, listing, params } = this.props;
const { bookingDates, initiateOrderError, intl, listing, params, currentUser } = this.props;
const { bookingStart, bookingEnd } = bookingDates || {};
const currentListing = ensureListingProperties(listing);
const price = currentListing.attributes.price;
if (!listing || !price) {
const isOwnListing = listing &&
currentUser &&
listing.author &&
listing.author.id.uuid === currentUser.id.uuid;
// Allow showing page when currentUser is still being downloaded,
// but show payment form only when user info is loaded.
const showPaymentForm = currentUser && !isOwnListing;
if (!listing || !price || isOwnListing) {
// eslint-disable-next-line no-console
console.error(
`Listing price is undefined for listing (${currentListing.id}). Redirecting to SearchPage.`
'Listing, price, or user invalid for checkout, redirecting back to listing page.'
);
return <NamedRedirect name="ListingPage" params={params} />;
}
@ -93,7 +102,12 @@ export class CheckoutPageComponent extends Component {
<p>
<FormattedMessage id="CheckoutPage.paymentInfo" />
</p>
<StripePaymentForm onSubmit={this.handleSubmit} disableSubmit={this.state.submitting} />
{showPaymentForm
? <StripePaymentForm
onSubmit={this.handleSubmit}
disableSubmit={this.state.submitting}
/>
: null}
</section>
</PageLayout>
);
@ -104,6 +118,7 @@ CheckoutPageComponent.defaultProps = {
bookingDates: null,
initiateOrderError: null,
listing: null,
currentUser: null,
};
const { arrayOf, func, instanceOf, shape, string } = PropTypes;
@ -115,6 +130,7 @@ CheckoutPageComponent.propTypes = {
}),
initiateOrderError: instanceOf(Error),
listing: propTypes.listing,
currentUser: propTypes.currentUser,
params: shape({
id: string,
slug: string,
@ -135,7 +151,8 @@ CheckoutPageComponent.propTypes = {
const mapStateToProps = state => {
const { initiateOrderError, listing, bookingDates } = state.CheckoutPage;
return { initiateOrderError, listing, bookingDates };
const { currentUser } = state.user;
return { initiateOrderError, listing, bookingDates, currentUser };
};
const mapDispatchToProps = dispatch => ({
@ -153,4 +170,6 @@ CheckoutPage.setInitialValues = initialValues => setInitialValues(initialValues)
CheckoutPage.displayName = 'CheckoutPage';
CheckoutPage.loadData = loadData;
export default CheckoutPage;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { createUser, createListing, fakeIntl } from '../../util/test-data';
import { createUser, createCurrentUser, createListing, fakeIntl } from '../../util/test-data';
import { CheckoutPageComponent } from './CheckoutPage';
import checkoutPageReducer, { SET_INITAL_VALUES, setInitialValues } from './CheckoutPage.duck';
@ -8,6 +8,7 @@ const noop = () => null;
describe('CheckoutPage', () => {
it('matches snapshot', () => {
const listing = createListing('listing1', createUser('author'));
const props = {
bookingDates: {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
@ -16,7 +17,8 @@ describe('CheckoutPage', () => {
flattenedRoutes: [],
history: { push: noop },
intl: fakeIntl,
listing: { ...createListing('listing1'), author: createUser('author') },
listing,
currentUser: createCurrentUser('current-user'),
params: { id: 'listing1', slug: 'listing1' },
sendOrderRequest: noop,
};

View file

@ -8,7 +8,6 @@ exports[`CheckoutPage matches snapshot 1`] = `
author={
Object {
"attributes": Object {
"email": "author@example.com",
"profile": Object {
"firstName": "author first name",
"lastName": "author last name",

View file

@ -90,3 +90,8 @@
.bookingModalTitle {
margin: 1rem 0;
}
.editListing {
float: right;
margin: 1rem;
}

View file

@ -1,4 +1,5 @@
import { showListingsSuccess } from '../../ducks/sdk.duck';
import { fetchCurrentUser } from '../../ducks/user.duck';
// ================ Action types ================ //
@ -42,6 +43,7 @@ export const showListingError = e => ({
export const showListing = listingId =>
(dispatch, getState, sdk) => {
dispatch(showListingRequest(listingId));
dispatch(fetchCurrentUser());
return sdk.listings
.show({ id: listingId, include: ['author', 'images'] })
.then(data => {

View file

@ -1,5 +1,5 @@
import React, { Component, PropTypes } from 'react';
import { intlShape, injectIntl } from 'react-intl';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { union, without } from 'lodash';
@ -9,7 +9,7 @@ import { types } from '../../util/sdkLoader';
import { createSlug } from '../../util/urlHelpers';
import { convertMoneyToNumber } from '../../util/currency';
import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes';
import { Button, Map, ModalInMobile, PageLayout } from '../../components';
import { Button, Map, ModalInMobile, PageLayout, NamedLink } from '../../components';
import { BookingDatesForm } from '../../containers';
import { getListingsById } from '../../ducks/sdk.duck';
import { showListing } from './ListingPage.duck';
@ -88,7 +88,7 @@ export class ListingPageComponent extends Component {
}
render() {
const { params, marketplaceData, showListingError, intl } = this.props;
const { params, marketplaceData, showListingError, intl, currentUser } = this.props;
const currencyConfig = config.currencyConfig;
const currentListing = denormaliseListing(marketplaceData, params);
@ -130,6 +130,11 @@ export class ListingPageComponent extends Component {
</div>
: null;
const isOwnListing = currentUser &&
currentListing &&
currentListing.author &&
currentListing.author.id.uuid === currentUser.id.uuid;
const pageContent = (
<PageLayout title={`${title} ${formattedPrice}`} className={this.state.pageClassNames}>
<div className={css.listing}>
@ -151,11 +156,15 @@ export class ListingPageComponent extends Component {
<BookingDatesForm className={css.bookingForm} onSubmit={this.onSubmit} price={price} />
</ModalInMobile>
{map ? <div className={css.map}>{map}</div> : null}
<div className={css.openBookingForm}>
<Button onClick={() => this.setState({ isBookingModalOpenOnMobile: true })}>
{bookBtnMessage}
</Button>
</div>
{isOwnListing
? <NamedLink className={css.editListing} name="EditListingPage" params={params}>
<FormattedMessage id="ListingPage.editListing" />
</NamedLink>
: <div className={css.openBookingForm}>
<Button onClick={() => this.setState({ isBookingModalOpenOnMobile: true })}>
{bookBtnMessage}
</Button>
</div>}
</div>
</PageLayout>
);
@ -174,6 +183,7 @@ export class ListingPageComponent extends Component {
ListingPageComponent.defaultProps = {
showListingError: null,
tab: 'listing',
currentUser: null,
};
const { arrayOf, func, instanceOf, object, oneOf, shape, string } = PropTypes;
@ -194,13 +204,17 @@ ListingPageComponent.propTypes = {
slug: string.isRequired,
}).isRequired,
showListingError: instanceOf(Error),
currentUser: propTypes.currentUser,
tab: oneOf(['book', 'listing']),
};
const mapStateToProps = state => ({
marketplaceData: state.data,
showListingError: state.ListingPage.showListingError,
});
const mapStateToProps = state => {
return {
marketplaceData: state.data,
showListingError: state.ListingPage.showListingError,
currentUser: state.user.currentUser,
};
};
const ListingPage = connect(mapStateToProps)(withRouter(injectIntl(ListingPageComponent)));

View file

@ -1,6 +1,6 @@
import React from 'react';
import { types } from '../../util/sdkLoader';
import { createListing, fakeIntl } from '../../util/test-data';
import { createUser, createCurrentUser, createListing, fakeIntl } from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import { ListingPageComponent } from './ListingPage';
import { showListingsSuccess } from '../../ducks/sdk.duck';
@ -10,7 +10,9 @@ const { UUID } = types;
describe('ListingPage', () => {
it('matches snapshot', () => {
const marketplaceData = { entities: { listing: { listing1: createListing('listing1') } } };
const currentUser = createCurrentUser('user-2');
const listing1 = createListing('listing1', createUser('user-1'));
const marketplaceData = { entities: { listing: { listing1 } } };
const props = {
dispatch: () => console.log('Dispatch called'),
flattenedRoutes: [],
@ -20,6 +22,7 @@ describe('ListingPage', () => {
},
params: { slug: 'listing1-title', id: 'listing1' },
marketplaceData,
currentUser,
intl: fakeIntl,
onLoadListing: () => {},
};
@ -34,13 +37,15 @@ describe('ListingPage', () => {
const dispatch = jest.fn(action => action);
const response = { status: 200 };
const show = jest.fn(() => Promise.resolve(response));
const sdk = { listings: { show } };
const me = jest.fn(() => Promise.resolve(response));
const sdk = { listings: { show }, users: { me } };
return showListing(id)(dispatch, null, sdk).then(data => {
expect(data).toEqual(response);
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'images'] }]]);
expect(dispatch.mock.calls).toEqual([
[showListingRequest(id)],
[expect.anything()], // fetchCurrentUser() call
[showListingsSuccess(data)],
]);
});
@ -62,7 +67,11 @@ describe('ListingPage', () => {
e => {
expect(e).toEqual(error);
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'images'] }]]);
expect(dispatch.mock.calls).toEqual([[showListingRequest(id)], [showListingError(e)]]);
expect(dispatch.mock.calls).toEqual([
[showListingRequest(id)],
[expect.anything()], // fetchCurrentUser() call
[showListingError(e)],
]);
}
);
});

View file

@ -83,6 +83,7 @@ const routesConfiguration = [
name: 'CheckoutPage',
setInitialValues: initialValues => CheckoutPage.setInitialValues(initialValues),
component: props => <CheckoutPage {...props} />,
loadData: (params, search) => CheckoutPage.loadData(params, search),
},
{
auth: true,

View file

@ -50,6 +50,7 @@
"InboxPage.stateRejected": "Rejected",
"InboxPage.title": "Inbox",
"ListingPage.ctaButtonMessage": "Book {title}",
"ListingPage.editListing": "Edit listing",
"ListingPage.loadingListingData": "Loading listing data",
"ListingPage.noListingData": "Could not find listing data",
"ModalInMobile.closeModal": "Close modal",

View file

@ -4,11 +4,21 @@ const { UUID, LatLng, Money } = types;
// Create a user that conforms to the util/propTypes user schema
export const createUser = id => ({
id: new UUID(id),
type: 'user',
attributes: {
profile: { firstName: `${id} first name`, lastName: `${id} last name`, slug: `${id}-slug` },
},
});
// Create a user that conforms to the util/propTypes user schema
export const createCurrentUser = id => ({
id: new UUID(id),
type: 'user',
attributes: {
email: `${id}@example.com`,
profile: { firstName: `${id} first name`, lastName: `${id} last name`, slug: `${id}-slug` },
stripeConnected: true,
},
});
@ -35,7 +45,7 @@ export const createImage = id => ({
});
// Create a user that conforms to the util/propTypes listing schema
export const createListing = id => ({
export const createListing = (id, author = null) => ({
id: new UUID(id),
type: 'listing',
attributes: {
@ -45,6 +55,7 @@ export const createListing = id => ({
address: `${id} address`,
geolocation: new LatLng(40, 60),
},
author,
});
export const createTransaction = options => {