mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 20:53:24 +10:00
Use the speculative API to get the transaction for the breakdown
This commit is contained in:
parent
5384c8530a
commit
8fe4aa2bce
6 changed files with 287 additions and 308 deletions
|
|
@ -5,6 +5,10 @@
|
|||
--logoHeightDesktop: 27px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
/* Dummy Topbar */
|
||||
|
||||
.topbar {
|
||||
|
|
@ -250,6 +254,14 @@
|
|||
color: var(--failColor);
|
||||
}
|
||||
|
||||
.speculateError {
|
||||
color: var(--failColor);
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 0 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.paymentForm {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { pick } from 'lodash';
|
||||
import { updatedEntities, denormalisedEntities } from '../../util/data';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
|
|
@ -8,25 +10,54 @@ export const INITIATE_ORDER_REQUEST = 'app/CheckoutPage/INITIATE_ORDER_REQUEST';
|
|||
export const INITIATE_ORDER_SUCCESS = 'app/CheckoutPage/INITIATE_ORDER_SUCCESS';
|
||||
export const INITIATE_ORDER_ERROR = 'app/CheckoutPage/INITIATE_ORDER_ERROR';
|
||||
|
||||
export const SPECULATE_TRANSACTION_REQUEST = 'app/ListingPage/SPECULATE_TRANSACTION_REQUEST';
|
||||
export const SPECULATE_TRANSACTION_SUCCESS = 'app/ListingPage/SPECULATE_TRANSACTION_SUCCESS';
|
||||
export const SPECULATE_TRANSACTION_ERROR = 'app/ListingPage/SPECULATE_TRANSACTION_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
bookingDates: null,
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
bookingDates: null,
|
||||
speculateTransactionInProgress: false,
|
||||
speculateTransactionError: null,
|
||||
speculatedTransaction: null,
|
||||
initiateOrderError: null,
|
||||
};
|
||||
|
||||
export default function checkoutPageReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SET_INITAL_VALUES:
|
||||
return { ...state, ...payload };
|
||||
return { ...initialState, ...payload };
|
||||
|
||||
case SPECULATE_TRANSACTION_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
speculateTransactionInProgress: true,
|
||||
speculateTransactionError: null,
|
||||
speculatedTransaction: null,
|
||||
};
|
||||
case SPECULATE_TRANSACTION_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
speculateTransactionInProgress: false,
|
||||
speculatedTransaction: payload.transaction,
|
||||
};
|
||||
case SPECULATE_TRANSACTION_ERROR:
|
||||
console.error(payload); // eslint-disable-line no-console
|
||||
return {
|
||||
...state,
|
||||
speculateTransactionInProgress: false,
|
||||
speculateTransactionError: payload,
|
||||
};
|
||||
|
||||
case INITIATE_ORDER_REQUEST:
|
||||
return { ...state, initiateOrderError: null };
|
||||
case INITIATE_ORDER_SUCCESS:
|
||||
return state;
|
||||
case INITIATE_ORDER_ERROR:
|
||||
console.error(payload); // eslint-disable-line
|
||||
console.error(payload); // eslint-disable-line no-console
|
||||
return { ...state, initiateOrderError: payload };
|
||||
default:
|
||||
return state;
|
||||
|
|
@ -55,6 +86,21 @@ const initiateOrderError = e => ({
|
|||
payload: e,
|
||||
});
|
||||
|
||||
export const speculateTransactionRequest = () => ({ type: SPECULATE_TRANSACTION_REQUEST });
|
||||
|
||||
export const speculateTransactionSuccess = transaction => ({
|
||||
type: SPECULATE_TRANSACTION_SUCCESS,
|
||||
payload: { transaction },
|
||||
});
|
||||
|
||||
export const speculateTransactionError = e => ({
|
||||
type: SPECULATE_TRANSACTION_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
/* ================ Thunks ================ */
|
||||
|
||||
export const initiateOrder = params =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(initiateOrderRequest());
|
||||
|
|
@ -74,3 +120,43 @@ export const initiateOrder = params =>
|
|||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiate the speculative transaction with the given booking details
|
||||
*
|
||||
* The API allows us to do speculative transaction initiation and
|
||||
* transitions. This way we can make create a transaction and get the
|
||||
* actual pricing information as if the transaction had been started,
|
||||
* without affecting the actual data.
|
||||
*
|
||||
* We store this speculative transaction in the page store and use the
|
||||
* pricing info for the booking breakdown to get a proper estimate for
|
||||
* the price with the chosen information.
|
||||
*/
|
||||
export const speculateTransaction = (listingId, bookingStart, bookingEnd) =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(speculateTransactionRequest());
|
||||
const bodyParams = {
|
||||
transition: propTypes.TX_TRANSITION_PREAUTHORIZE,
|
||||
params: {
|
||||
listingId,
|
||||
bookingStart,
|
||||
bookingEnd,
|
||||
cardToken: 'CheckoutPage_speculative_card_token',
|
||||
},
|
||||
};
|
||||
const queryParams = {
|
||||
include: ['booking', 'provider'],
|
||||
expand: true,
|
||||
};
|
||||
return sdk.transactions
|
||||
.initiateSpeculative(bodyParams, queryParams)
|
||||
.then(response => {
|
||||
const transactionId = response.data.data.id;
|
||||
const entities = updatedEntities({}, response.data);
|
||||
const denormalised = denormalisedEntities(entities, 'transaction', [transactionId]);
|
||||
const tx = denormalised[0];
|
||||
dispatch(speculateTransactionSuccess(tx));
|
||||
})
|
||||
.catch(e => dispatch(speculateTransactionError(e)));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,16 +3,11 @@ import { compose } from 'redux';
|
|||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import Decimal from 'decimal.js';
|
||||
import classNames from 'classnames';
|
||||
import config from '../../config';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { pathByRouteName } from '../../util/routes';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { ensureListing, ensureUser } from '../../util/data';
|
||||
import { ensureListing, ensureUser, ensureTransaction, ensureBooking } from '../../util/data';
|
||||
import { withFlattenedRoutes } from '../../util/contextHelpers';
|
||||
import { nightsBetween } from '../../util/dates';
|
||||
import { unitDivisor, convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency';
|
||||
import {
|
||||
AvatarMedium,
|
||||
BookingBreakdown,
|
||||
|
|
@ -22,75 +17,75 @@ import {
|
|||
ResponsiveImage,
|
||||
} from '../../components';
|
||||
import { StripePaymentForm } from '../../containers';
|
||||
import { initiateOrder, setInitialValues } from './CheckoutPage.duck';
|
||||
import { initiateOrder, setInitialValues, speculateTransaction } from './CheckoutPage.duck';
|
||||
|
||||
import { storeData, storedData } from './CheckoutPageSessionHelpers';
|
||||
import { storeData, storedData, clearData } from './CheckoutPageSessionHelpers';
|
||||
import LogoIcon from './LogoIcon';
|
||||
import css from './CheckoutPage.css';
|
||||
|
||||
const STORAGE_KEY = 'CheckoutPage';
|
||||
|
||||
// TODO: This is a temporary function to calculate the booking
|
||||
// price. This should be removed when the API supports dry-runs and we
|
||||
// can take the total price from the transaction itself.
|
||||
const estimatedTotalPrice = (startDate, endDate, unitPrice) => {
|
||||
const numericPrice = convertMoneyToNumber(unitPrice);
|
||||
const nightCount = nightsBetween(startDate, endDate);
|
||||
const numericTotalPrice = new Decimal(numericPrice).times(nightCount).toNumber();
|
||||
return new types.Money(
|
||||
convertUnitToSubUnit(numericTotalPrice, unitDivisor(unitPrice.currency)),
|
||||
unitPrice.currency
|
||||
);
|
||||
};
|
||||
|
||||
const breakdown = (bookingStart, bookingEnd, unitPrice) => {
|
||||
if (!bookingStart || !bookingEnd || !unitPrice) {
|
||||
return null;
|
||||
}
|
||||
const totalPrice = estimatedTotalPrice(bookingStart, bookingEnd, unitPrice);
|
||||
const nightCount = nightsBetween(bookingStart, bookingEnd);
|
||||
|
||||
return (
|
||||
<BookingBreakdown
|
||||
className={css.bookingBreakdown}
|
||||
bookingStart={bookingStart}
|
||||
bookingEnd={bookingEnd}
|
||||
userRole="customer"
|
||||
lineItems={[
|
||||
{
|
||||
code: 'line-item/night',
|
||||
quantity: new Decimal(nightCount),
|
||||
unitPrice: unitPrice,
|
||||
lineTotal: totalPrice,
|
||||
},
|
||||
]}
|
||||
payinTotal={totalPrice}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export class CheckoutPageComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { bookingDates, listing } = props;
|
||||
|
||||
// Store received data
|
||||
storeData(bookingDates, listing, STORAGE_KEY);
|
||||
// storeData(props.bookingDates, props.listing, STORAGE_KEY);
|
||||
|
||||
this.state = { submitting: false };
|
||||
this.state = {
|
||||
pageData: {},
|
||||
dataLoaded: false,
|
||||
submitting: false,
|
||||
};
|
||||
|
||||
this.loadInitialData = this.loadInitialData.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadInitialData();
|
||||
}
|
||||
|
||||
loadInitialData() {
|
||||
const { bookingDates, listing } = this.props;
|
||||
const hasDataInProps = !!(bookingDates && listing);
|
||||
const pageData = hasDataInProps ? { bookingDates, listing } : storedData(STORAGE_KEY);
|
||||
|
||||
if (hasDataInProps) {
|
||||
storeData(bookingDates, listing, STORAGE_KEY);
|
||||
}
|
||||
|
||||
const hasData = pageData &&
|
||||
pageData.listing &&
|
||||
pageData.listing.id &&
|
||||
pageData.bookingDates &&
|
||||
pageData.bookingDates.bookingStart &&
|
||||
pageData.bookingDates.bookingEnd;
|
||||
|
||||
if (hasData) {
|
||||
this.props.fetchSpeculatedTransaction(
|
||||
pageData.listing.id,
|
||||
pageData.bookingDates.bookingStart,
|
||||
pageData.bookingDates.bookingEnd
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({ pageData: pageData || {}, dataLoaded: true });
|
||||
}
|
||||
|
||||
handleSubmit(cardToken) {
|
||||
if (this.state.submitting) {
|
||||
return;
|
||||
}
|
||||
this.setState({ submitting: true });
|
||||
const { bookingDates, flattenedRoutes, history, sendOrderRequest, listing } = this.props;
|
||||
// Get page data from passed-in props or from storage
|
||||
const pageData = bookingDates && listing ? { bookingDates, listing } : storedData(STORAGE_KEY);
|
||||
const { bookingStart, bookingEnd } = pageData.bookingDates || {};
|
||||
const requestParams = { listingId: pageData.listing.id, cardToken, bookingStart, bookingEnd };
|
||||
|
||||
const { flattenedRoutes, history, sendOrderRequest, speculatedTransaction } = this.props;
|
||||
const requestParams = {
|
||||
listingId: this.state.pageData.listing.id,
|
||||
cardToken,
|
||||
bookingStart: speculatedTransaction.booking.attributes.start,
|
||||
bookingEnd: speculatedTransaction.booking.attributes.end,
|
||||
};
|
||||
|
||||
sendOrderRequest(requestParams)
|
||||
.then(orderId => {
|
||||
|
|
@ -98,7 +93,7 @@ export class CheckoutPageComponent extends Component {
|
|||
const orderDetailsPath = pathByRouteName('OrderDetailsPage', flattenedRoutes, {
|
||||
id: orderId.uuid,
|
||||
});
|
||||
window.sessionStorage.removeItem(STORAGE_KEY);
|
||||
clearData(STORAGE_KEY);
|
||||
history.push(orderDetailsPath);
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -110,55 +105,65 @@ export class CheckoutPageComponent extends Component {
|
|||
const {
|
||||
authInfoError,
|
||||
logoutError,
|
||||
bookingDates,
|
||||
speculateTransactionInProgress,
|
||||
speculateTransactionError,
|
||||
speculatedTransaction,
|
||||
initiateOrderError,
|
||||
intl,
|
||||
listing,
|
||||
params,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
|
||||
// Get page data from passed-in props or from storage
|
||||
const pageData = bookingDates && listing ? { bookingDates, listing } : storedData(STORAGE_KEY);
|
||||
const { bookingStart, bookingEnd } = pageData.bookingDates || {};
|
||||
const currentListing = ensureListing(pageData.listing);
|
||||
const isLoading = !this.state.dataLoaded || speculateTransactionInProgress;
|
||||
|
||||
const { listing, bookingDates } = this.state.pageData;
|
||||
const currentTransaction = ensureTransaction(speculatedTransaction);
|
||||
const currentBooking = ensureBooking(currentTransaction.booking);
|
||||
const currentListing = ensureListing(listing);
|
||||
const currentAuthor = ensureUser(currentListing.author);
|
||||
const authorDisplayName = currentAuthor.attributes.profile.displayName;
|
||||
|
||||
const isOwnListing = currentListing.id &&
|
||||
currentUser &&
|
||||
const isOwnListing = currentUser &&
|
||||
currentUser.id &&
|
||||
currentAuthor &&
|
||||
currentAuthor.id &&
|
||||
currentListing.author.id.uuid === currentUser.id.uuid;
|
||||
currentAuthor.id.uuid === currentUser.id.uuid;
|
||||
const listingIsOpen = currentListing.id && currentListing.attributes.open;
|
||||
|
||||
const hasBookingInfo = bookingStart && bookingEnd;
|
||||
const hasListingAndAuthor = !!(currentListing.id && currentAuthor.id);
|
||||
const hasBookingDates = !!(bookingDates &&
|
||||
bookingDates.bookingStart &&
|
||||
bookingDates.bookingEnd);
|
||||
const hasRequiredData = hasListingAndAuthor && hasBookingDates;
|
||||
const canShowPage = hasRequiredData && listingIsOpen && !isOwnListing;
|
||||
const shouldRedirect = !isLoading && !canShowPage;
|
||||
|
||||
// Redirect back to ListingPage if data is missing.
|
||||
// Redirection must happen before any data format error is thrown (e.g. wrong currency)
|
||||
if (!currentListing.id || isOwnListing || !hasBookingInfo) {
|
||||
if (shouldRedirect) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'Listing, user, or dates invalid for checkout, redirecting back to listing page.',
|
||||
{ currentListing, isOwnListing, hasBookingInfo, bookingStart, bookingEnd }
|
||||
);
|
||||
console.error('Missing or invalid data for checkout, redirecting back to listing page.', {
|
||||
transaction: currentTransaction,
|
||||
bookingDates,
|
||||
listing,
|
||||
});
|
||||
return <NamedRedirect name="ListingPage" params={params} />;
|
||||
}
|
||||
|
||||
// Estimate total price. NOTE: this will change when we can do a
|
||||
// dry-run to the API and get a proper breakdown of the price.
|
||||
const unitPrice = currentListing.attributes.price;
|
||||
|
||||
if (!unitPrice) {
|
||||
throw new Error('Listing has no price');
|
||||
}
|
||||
if (unitPrice.currency !== config.currency) {
|
||||
throw new Error(
|
||||
`Listing currency different from marketplace currency: ${unitPrice.currency}`
|
||||
);
|
||||
}
|
||||
const breakdown = currentTransaction.id
|
||||
? <BookingBreakdown
|
||||
className={css.bookingBreakdown}
|
||||
userRole="customer"
|
||||
transactionState={currentTransaction.attributes.state}
|
||||
bookingStart={currentBooking.attributes.start}
|
||||
bookingEnd={currentBooking.attributes.end}
|
||||
lineItems={currentTransaction.attributes.lineItems}
|
||||
payinTotal={currentTransaction.attributes.payinTotal}
|
||||
/>
|
||||
: null;
|
||||
|
||||
// Allow showing page when currentUser is still being downloaded,
|
||||
// but show payment form only when user info is loaded.
|
||||
const showPaymentForm = currentUser && !isOwnListing;
|
||||
const showPaymentForm = !!(currentUser && hasRequiredData);
|
||||
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
const title = intl.formatMessage({ id: 'CheckoutPage.title' }, { listingTitle });
|
||||
|
|
@ -167,28 +172,49 @@ export class CheckoutPageComponent extends Component {
|
|||
? currentListing.images[0]
|
||||
: null;
|
||||
|
||||
const errorMessage = initiateOrderError
|
||||
const initiateOrderErrorMessage = initiateOrderError
|
||||
? <p className={css.orderError}>
|
||||
<FormattedMessage id="CheckoutPage.initiateOrderError" />
|
||||
</p>
|
||||
: null;
|
||||
const speculateTransactionErrorMessage = speculateTransactionError
|
||||
? <p className={css.speculateError}>
|
||||
<FormattedMessage id="CheckoutPage.speculateTransactionError" />
|
||||
</p>
|
||||
: null;
|
||||
|
||||
const topbar = (
|
||||
<div className={css.topbar}>
|
||||
<NamedLink className={css.home} name="LandingPage">
|
||||
<LogoIcon
|
||||
className={css.logoMobile}
|
||||
title={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
|
||||
/>
|
||||
<LogoIcon
|
||||
className={css.logoDesktop}
|
||||
alt={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
|
||||
isMobile={false}
|
||||
/>
|
||||
</NamedLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
const pageLayoutProps = { authInfoError, logoutError, title };
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout {...pageLayoutProps}>
|
||||
{topbar}
|
||||
<div className={css.loading}>
|
||||
<FormattedMessage id="CheckoutPage.loadingData" />
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title={title}>
|
||||
<div className={css.topbar}>
|
||||
<NamedLink className={css.home} name="LandingPage">
|
||||
<LogoIcon
|
||||
className={css.logoMobile}
|
||||
title={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
|
||||
/>
|
||||
<LogoIcon
|
||||
className={css.logoDesktop}
|
||||
alt={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
|
||||
isMobile={false}
|
||||
/>
|
||||
</NamedLink>
|
||||
</div>
|
||||
|
||||
<PageLayout {...pageLayoutProps}>
|
||||
{topbar}
|
||||
<div className={css.contentContainer}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
|
|
@ -212,7 +238,7 @@ export class CheckoutPageComponent extends Component {
|
|||
<span className={css.authorName}>
|
||||
<FormattedMessage
|
||||
id="ListingPage.hostedBy"
|
||||
values={{ name: authorDisplayName }}
|
||||
values={{ name: currentAuthor.attributes.profile.displayName }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -222,14 +248,15 @@ export class CheckoutPageComponent extends Component {
|
|||
<h3 className={css.priceBreakdownTitle}>
|
||||
<FormattedMessage id="CheckoutPage.priceBreakdownTitle" />
|
||||
</h3>
|
||||
{breakdown(bookingStart, bookingEnd, unitPrice)}
|
||||
{speculateTransactionErrorMessage}
|
||||
{breakdown}
|
||||
</div>
|
||||
|
||||
<section className={css.paymentContainer}>
|
||||
<h3 className={css.paymentTitle}>
|
||||
<FormattedMessage id="CheckoutPage.paymentTitle" />
|
||||
</h3>
|
||||
{errorMessage}
|
||||
{initiateOrderErrorMessage}
|
||||
{showPaymentForm
|
||||
? <StripePaymentForm
|
||||
className={css.paymentForm}
|
||||
|
|
@ -261,13 +288,17 @@ export class CheckoutPageComponent extends Component {
|
|||
<div className={css.detailsHeadings}>
|
||||
<h2 className={css.detailsTitle}>{listingTitle}</h2>
|
||||
<p className={css.detailsSubtitle}>
|
||||
<FormattedMessage id="CheckoutPage.hostedBy" values={{ name: authorDisplayName }} />
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.hostedBy"
|
||||
values={{ name: currentAuthor.attributes.profile.displayName }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<h3 className={css.bookingBreakdownTitle}>
|
||||
<FormattedMessage id="CheckoutPage.priceBreakdownTitle" />
|
||||
</h3>
|
||||
{breakdown(bookingStart, bookingEnd, unitPrice)}
|
||||
{speculateTransactionErrorMessage}
|
||||
{breakdown}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -278,23 +309,29 @@ export class CheckoutPageComponent extends Component {
|
|||
|
||||
CheckoutPageComponent.defaultProps = {
|
||||
authInfoError: null,
|
||||
bookingDates: null,
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
bookingDates: null,
|
||||
speculateTransactionError: null,
|
||||
speculatedTransaction: null,
|
||||
logoutError: null,
|
||||
currentUser: null,
|
||||
};
|
||||
|
||||
const { arrayOf, func, instanceOf, shape, string } = PropTypes;
|
||||
const { arrayOf, func, instanceOf, shape, string, bool } = PropTypes;
|
||||
|
||||
CheckoutPageComponent.propTypes = {
|
||||
authInfoError: instanceOf(Error),
|
||||
listing: propTypes.listing,
|
||||
bookingDates: shape({
|
||||
bookingStart: instanceOf(Date).isRequired,
|
||||
bookingEnd: instanceOf(Date).isRequired,
|
||||
}),
|
||||
fetchSpeculatedTransaction: func.isRequired,
|
||||
speculateTransactionInProgress: bool.isRequired,
|
||||
speculateTransactionError: instanceOf(Error),
|
||||
speculatedTransaction: propTypes.transaction,
|
||||
initiateOrderError: instanceOf(Error),
|
||||
listing: propTypes.listing,
|
||||
logoutError: instanceOf(Error),
|
||||
currentUser: propTypes.currentUser,
|
||||
params: shape({
|
||||
|
|
@ -316,14 +353,33 @@ CheckoutPageComponent.propTypes = {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { initiateOrderError, listing, bookingDates } = state.CheckoutPage;
|
||||
const {
|
||||
listing,
|
||||
bookingDates,
|
||||
speculateTransactionInProgress,
|
||||
speculateTransactionError,
|
||||
speculatedTransaction,
|
||||
initiateOrderError,
|
||||
} = state.CheckoutPage;
|
||||
const { currentUser } = state.user;
|
||||
const { authInfoError, logoutError } = state.Auth;
|
||||
return { authInfoError, initiateOrderError, listing, logoutError, bookingDates, currentUser };
|
||||
return {
|
||||
currentUser,
|
||||
authInfoError,
|
||||
logoutError,
|
||||
bookingDates,
|
||||
speculateTransactionInProgress,
|
||||
speculateTransactionError,
|
||||
speculatedTransaction,
|
||||
listing,
|
||||
initiateOrderError,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
sendOrderRequest: params => dispatch(initiateOrder(params)),
|
||||
fetchSpeculatedTransaction: (listingId, bookingStart, bookingEnd) =>
|
||||
dispatch(speculateTransaction(listingId, bookingStart, bookingEnd)),
|
||||
});
|
||||
|
||||
const CheckoutPage = compose(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ describe('CheckoutPage', () => {
|
|||
currentUser: createCurrentUser('current-user'),
|
||||
params: { id: 'listing1', slug: 'listing1' },
|
||||
sendOrderRequest: noop,
|
||||
fetchSpeculatedTransaction: noop,
|
||||
speculateTransactionInProgress: false,
|
||||
};
|
||||
const tree = renderShallow(<CheckoutPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
@ -43,12 +45,16 @@ describe('CheckoutPage', () => {
|
|||
});
|
||||
|
||||
describe('Reducer', () => {
|
||||
const initialValues = {
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
bookingDates: null,
|
||||
speculateTransactionError: null,
|
||||
speculateTransactionInProgress: false,
|
||||
speculatedTransaction: null,
|
||||
};
|
||||
|
||||
it('should return the initial state', () => {
|
||||
const initialValues = {
|
||||
initiateOrderError: null,
|
||||
listing: null,
|
||||
bookingDates: null,
|
||||
};
|
||||
expect(checkoutPageReducer(undefined, {})).toEqual(initialValues);
|
||||
});
|
||||
|
||||
|
|
@ -60,7 +66,8 @@ describe('CheckoutPage', () => {
|
|||
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
|
||||
};
|
||||
const payload = { listing, bookingDates };
|
||||
expect(checkoutPageReducer({}, { type: SET_INITAL_VALUES, payload })).toEqual(payload);
|
||||
const expected = { ...initialValues, ...payload };
|
||||
expect(checkoutPageReducer({}, { type: SET_INITAL_VALUES, payload })).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,193 +18,9 @@ exports[`CheckoutPage matches snapshot 1`] = `
|
|||
</withFlattenedRoutes(withRouter(NamedLink))>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<ResponsiveImage
|
||||
alt="listing1 title"
|
||||
className={null}
|
||||
image={null}
|
||||
nameSet={
|
||||
Array [
|
||||
Object {
|
||||
"name": "landscape-crop",
|
||||
"size": "400w",
|
||||
},
|
||||
Object {
|
||||
"name": "landscape-crop2x",
|
||||
"size": "800w",
|
||||
},
|
||||
]
|
||||
}
|
||||
noImageMessage={null}
|
||||
rootClassName={null}
|
||||
sizes="100vw" />
|
||||
</div>
|
||||
<div
|
||||
className="">
|
||||
<AvatarMedium
|
||||
user={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"profile": Object {
|
||||
"abbreviatedName": "author abbreviated name",
|
||||
"displayName": "author display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "author",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
} />
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h1>
|
||||
CheckoutPage.title
|
||||
</h1>
|
||||
<div>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="ListingPage.hostedBy"
|
||||
values={
|
||||
Object {
|
||||
"name": "author display name",
|
||||
}
|
||||
} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.priceBreakdownTitle"
|
||||
values={Object {}} />
|
||||
</h3>
|
||||
<BookingBreakdown
|
||||
bookingEnd={2017-04-16T00:00:00.000Z}
|
||||
bookingStart={2017-04-14T00:00:00.000Z}
|
||||
lineItems={
|
||||
Array [
|
||||
Object {
|
||||
"code": "line-item/night",
|
||||
"lineTotal": Money {
|
||||
"amount": 11000,
|
||||
"currency": "USD",
|
||||
},
|
||||
"quantity": "2",
|
||||
"unitPrice": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
payinTotal={
|
||||
Money {
|
||||
"amount": 11000,
|
||||
"currency": "USD",
|
||||
}
|
||||
}
|
||||
userRole="customer" />
|
||||
</div>
|
||||
<section>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.paymentTitle"
|
||||
values={Object {}} />
|
||||
</h3>
|
||||
<InjectIntl(StripePaymentForm)
|
||||
disableSubmit={false}
|
||||
formId="CheckoutPagePaymentForm"
|
||||
onSubmit={[Function]}
|
||||
paymentInfo="CheckoutPage.paymentInfo" />
|
||||
</section>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<ResponsiveImage
|
||||
alt="listing1 title"
|
||||
className={null}
|
||||
image={null}
|
||||
nameSet={
|
||||
Array [
|
||||
Object {
|
||||
"name": "landscape-crop",
|
||||
"size": "400w",
|
||||
},
|
||||
Object {
|
||||
"name": "landscape-crop2x",
|
||||
"size": "800w",
|
||||
},
|
||||
]
|
||||
}
|
||||
noImageMessage={null}
|
||||
rootClassName={null}
|
||||
sizes="100%" />
|
||||
</div>
|
||||
<div>
|
||||
<AvatarMedium
|
||||
user={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"profile": Object {
|
||||
"abbreviatedName": "author abbreviated name",
|
||||
"displayName": "author display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "author",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>
|
||||
listing1 title
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.hostedBy"
|
||||
values={
|
||||
Object {
|
||||
"name": "author display name",
|
||||
}
|
||||
} />
|
||||
</p>
|
||||
</div>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.priceBreakdownTitle"
|
||||
values={Object {}} />
|
||||
</h3>
|
||||
<BookingBreakdown
|
||||
bookingEnd={2017-04-16T00:00:00.000Z}
|
||||
bookingStart={2017-04-14T00:00:00.000Z}
|
||||
lineItems={
|
||||
Array [
|
||||
Object {
|
||||
"code": "line-item/night",
|
||||
"lineTotal": Money {
|
||||
"amount": 11000,
|
||||
"currency": "USD",
|
||||
},
|
||||
"quantity": "2",
|
||||
"unitPrice": Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
payinTotal={
|
||||
Money {
|
||||
"amount": 11000,
|
||||
"currency": "USD",
|
||||
}
|
||||
}
|
||||
userRole="customer" />
|
||||
</div>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.loadingData"
|
||||
values={Object {}} />
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -30,10 +30,12 @@
|
|||
"CheckoutPage.goToLandingPage": "Go to homepage",
|
||||
"CheckoutPage.hostedBy": "Hosted by {name}",
|
||||
"CheckoutPage.initiateOrderError": "Payment request failed. Please try again.",
|
||||
"CheckoutPage.loadingData": "Loading checkout data...",
|
||||
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",
|
||||
"CheckoutPage.paymentTitle": "Payment",
|
||||
"CheckoutPage.priceBreakdownTitle": "Booking breakdown",
|
||||
"CheckoutPage.title": "Book {listingTitle}",
|
||||
"CheckoutPage.speculateTransactionError": "Failed to fetch breakdown information. Please try again.",
|
||||
"DateInput.clearDate": "Clear Date",
|
||||
"DateInput.closeDatePicker": "Close",
|
||||
"DateInput.defaultPlaceholder": "Date input",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue