mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
Merge pull request #122 from sharetribe/prevent-booking-own-listing
Prevent booking own listing
This commit is contained in:
commit
f899a51472
11 changed files with 95 additions and 26 deletions
|
|
@ -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());
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -90,3 +90,8 @@
|
|||
.bookingModalTitle {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.editListing {
|
||||
float: right;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue