mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 21:21:19 +10:00
CheckoutPage mobile
This commit is contained in:
parent
f0a2d3b52a
commit
c82fe44b61
7 changed files with 227 additions and 107 deletions
|
|
@ -1,21 +1,75 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.heading {
|
||||
margin-top: 29px;
|
||||
margin-bottom: 35px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 1rem;
|
||||
/* Font */
|
||||
@apply --marketplaceH1FontStyles;
|
||||
color: var(--matterColor);
|
||||
|
||||
/* Layout */
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: 7px;
|
||||
@media (--desktopViewport) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.authorContainer {
|
||||
padding: 1rem;
|
||||
.author {
|
||||
width: 100%;
|
||||
@apply --marketplaceH4FontStyles;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.receipt {
|
||||
margin: 1rem 1rem 2rem 1rem;
|
||||
.priceBreakdownContainer {
|
||||
padding: 0 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.payment {
|
||||
padding: 0 1rem;
|
||||
.priceBreakdownTitle {
|
||||
/* Font */
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
@media (--desktopViewport) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.paymentContainer {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 24px;
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.paymentTitle {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin: 1rem 0 0 0;
|
||||
/* Font */
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
@media (--desktopViewport) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.orderError {
|
||||
/* Font */
|
||||
color: var(--failColor);
|
||||
}
|
||||
|
||||
.paymentForm {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ 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 { withFlattenedRoutes } from '../../util/contextHelpers';
|
||||
import { nightsBetween } from '../../util/dates';
|
||||
import { convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency';
|
||||
import { AuthorInfo, BookingBreakdown, NamedRedirect, PageLayout } from '../../components';
|
||||
import { BookingBreakdown, NamedRedirect, PageLayout } from '../../components';
|
||||
import { StripePaymentForm } from '../../containers';
|
||||
import { initiateOrder, setInitialValues } from './CheckoutPage.duck';
|
||||
|
||||
|
|
@ -51,12 +52,6 @@ const breakdown = (bookingStart, bookingEnd, unitPrice) => {
|
|||
);
|
||||
};
|
||||
|
||||
const ensureListingProperties = listing => {
|
||||
const empty = { id: null, type: 'listing', attributes: {}, author: {}, images: [] };
|
||||
// assume own properties: id, type, attributes etc.
|
||||
return { ...empty, ...listing };
|
||||
};
|
||||
|
||||
// Validate that given 'obj' has all the keys of defined by validPropTypes parameter
|
||||
// and values must pass related test-value-format function.
|
||||
const validateProperties = (obj, validPropTypes) => {
|
||||
|
|
@ -188,16 +183,26 @@ export class CheckoutPageComponent extends Component {
|
|||
// Get page data from passed-in props or from storage
|
||||
const pageData = bookingDates && listing ? { bookingDates, listing } : storedData();
|
||||
const { bookingStart, bookingEnd } = pageData.bookingDates || {};
|
||||
const currentListing = ensureListingProperties(pageData.listing);
|
||||
const currentListing = ensureListing(pageData.listing);
|
||||
const currentAuthor = ensureUser(currentListing.author);
|
||||
|
||||
const isOwnListing = currentListing.id &&
|
||||
currentUser &&
|
||||
currentListing.author &&
|
||||
currentAuthor.id &&
|
||||
currentListing.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;
|
||||
const hasBookingInfo = bookingStart && bookingEnd;
|
||||
|
||||
// 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) {
|
||||
// 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 }
|
||||
);
|
||||
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.
|
||||
|
|
@ -213,51 +218,54 @@ export class CheckoutPageComponent extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
const hasBookingInfo = bookingStart && bookingEnd;
|
||||
// Allow showing page when currentUser is still being downloaded,
|
||||
// but show payment form only when user info is loaded.
|
||||
const showPaymentForm = currentUser && !isOwnListing;
|
||||
|
||||
if (!currentListing.id || isOwnListing || !hasBookingInfo) {
|
||||
// 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 }
|
||||
);
|
||||
return <NamedRedirect name="ListingPage" params={params} />;
|
||||
}
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
const title = intl.formatMessage({ id: 'CheckoutPage.title' }, { listingTitle });
|
||||
const authorFirstName = currentAuthor.attributes.profile.firstName;
|
||||
|
||||
const title = intl.formatMessage(
|
||||
{
|
||||
id: 'CheckoutPage.title',
|
||||
},
|
||||
{
|
||||
listingTitle: currentListing.attributes.title,
|
||||
}
|
||||
const priceBreakdown = (
|
||||
<div className={css.priceBreakdownContainer}>
|
||||
<h3 className={css.priceBreakdownTitle}>
|
||||
<FormattedMessage id="CheckoutPage.priceBreakdownTitle" />
|
||||
</h3>
|
||||
{breakdown(bookingStart, bookingEnd, unitPrice)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const errorMessage = initiateOrderError
|
||||
? <p style={{ color: 'red' }}>
|
||||
? <p className={css.orderError}>
|
||||
<FormattedMessage id="CheckoutPage.initiateOrderError" />
|
||||
</p>
|
||||
: null;
|
||||
|
||||
const bookingInfo = breakdown(bookingStart, bookingEnd, unitPrice);
|
||||
|
||||
return (
|
||||
<PageLayout title={title}>
|
||||
<h1 className={css.title}>{title}</h1>
|
||||
<AuthorInfo author={currentListing.author} className={css.authorContainer} />
|
||||
{bookingInfo}
|
||||
<section className={css.payment}>
|
||||
{errorMessage}
|
||||
<h2 className={css.paymentTitle}>
|
||||
<div className={css.heading}>
|
||||
<h1 className={css.title}>{title}</h1>
|
||||
<div className={css.author}>
|
||||
<span className={css.authorName}>
|
||||
<FormattedMessage id="ListingPage.hostedBy" values={{ name: authorFirstName }} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{priceBreakdown}
|
||||
|
||||
<section className={css.paymentContainer}>
|
||||
<h3 className={css.paymentTitle}>
|
||||
<FormattedMessage id="CheckoutPage.paymentTitle" />
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage id="CheckoutPage.paymentInfo" />
|
||||
</p>
|
||||
</h3>
|
||||
{errorMessage}
|
||||
{showPaymentForm
|
||||
? <StripePaymentForm
|
||||
className={css.paymentForm}
|
||||
onSubmit={this.handleSubmit}
|
||||
disableSubmit={this.state.submitting}
|
||||
formId="CheckoutPagePaymentForm"
|
||||
paymentInfo={intl.formatMessage({ id: 'CheckoutPage.paymentInfo'})}
|
||||
/>
|
||||
: null}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,54 +1,55 @@
|
|||
exports[`CheckoutPage matches snapshot 1`] = `
|
||||
<Connect(withRouter(PageLayout))
|
||||
title="CheckoutPage.title">
|
||||
<h1>
|
||||
CheckoutPage.title
|
||||
</h1>
|
||||
<AuthorInfo
|
||||
author={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"profile": Object {
|
||||
"firstName": "author first name",
|
||||
"lastName": "author last name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "author",
|
||||
},
|
||||
"type": "user",
|
||||
<div>
|
||||
<h1>
|
||||
CheckoutPage.title
|
||||
</h1>
|
||||
<div>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="ListingPage.hostedBy"
|
||||
values={
|
||||
Object {
|
||||
"name": "author first 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}
|
||||
totalPrice={
|
||||
Money {
|
||||
"amount": 11000,
|
||||
"currency": "USD",
|
||||
}
|
||||
}
|
||||
}
|
||||
className={null} />
|
||||
<BookingBreakdown
|
||||
bookingEnd={2017-04-16T00:00:00.000Z}
|
||||
bookingStart={2017-04-14T00:00:00.000Z}
|
||||
totalPrice={
|
||||
Money {
|
||||
"amount": 11000,
|
||||
"currency": "USD",
|
||||
}
|
||||
}
|
||||
unitPrice={
|
||||
Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
}
|
||||
} />
|
||||
unitPrice={
|
||||
Money {
|
||||
"amount": 5500,
|
||||
"currency": "USD",
|
||||
}
|
||||
} />
|
||||
</div>
|
||||
<section>
|
||||
<h2>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.paymentTitle"
|
||||
values={Object {}} />
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.paymentInfo"
|
||||
values={Object {}} />
|
||||
</p>
|
||||
</h3>
|
||||
<InjectIntl(StripePaymentForm)
|
||||
disableSubmit={false}
|
||||
onSubmit={[Function]} />
|
||||
formId="CheckoutPagePaymentForm"
|
||||
onSubmit={[Function]}
|
||||
paymentInfo="CheckoutPage.paymentInfo" />
|
||||
</section>
|
||||
</Connect(withRouter(PageLayout))>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,46 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 0 0px 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #979797;
|
||||
padding: 15px 18px 0 18px;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
@apply --marketplaceInputStyles;
|
||||
|
||||
/* Layout */
|
||||
padding: 9px 0 6px 0;
|
||||
height: 36px;
|
||||
|
||||
/* Border */
|
||||
border-bottom-color: var(--attentionColor);
|
||||
}
|
||||
|
||||
.cardSuccess {
|
||||
border-bottom-color: var(--successColor);
|
||||
}
|
||||
|
||||
.cardError {
|
||||
border-bottom-color: var(--failColor);
|
||||
}
|
||||
|
||||
|
||||
.submitContainer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.paymentInfo {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
color: var(--matterColorAnti);
|
||||
text-align: center;
|
||||
padding: 0 42px;
|
||||
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
margin-top: 1rem;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ class StripePaymentFormExample extends Component {
|
|||
|
||||
export const Empty = {
|
||||
component: StripePaymentFormExample,
|
||||
props: { intl: fakeIntl },
|
||||
props: { intl: fakeIntl, formId: 'StripePaymentFormExample' },
|
||||
group: 'forms',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '../../components';
|
||||
import config from '../../config';
|
||||
|
||||
|
|
@ -125,33 +126,50 @@ class StripePaymentForm extends Component {
|
|||
});
|
||||
}
|
||||
render() {
|
||||
const { disableSubmit } = this.props;
|
||||
const { className, rootClassName, disableSubmit, formId, paymentInfo } = this.props;
|
||||
const submitDisabled = disableSubmit || this.state.submitting || !this.state.cardValueValid;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const cardClasses = classNames(css.card, {
|
||||
[css.cardSuccess]: this.state.cardValueValid,
|
||||
[css.cardError]: this.state.error,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<form className={classes} onSubmit={this.handleSubmit}>
|
||||
<label className={css.label} htmlFor={`${formId}-card`}>
|
||||
<FormattedMessage id="StripePaymentForm.creditCardDetails" />
|
||||
</label>
|
||||
<div
|
||||
className={css.card}
|
||||
className={cardClasses}
|
||||
id={`${formId}-card`}
|
||||
ref={el => {
|
||||
this.cardContainer = el;
|
||||
}}
|
||||
/>
|
||||
{this.state.error ? <span style={{ color: 'red' }}>{this.state.error}</span> : null}
|
||||
<Button className={css.submitButton} type="submit" disabled={submitDisabled}>
|
||||
<FormattedMessage id="StripePaymentForm.submitPaymentInfo" />
|
||||
</Button>
|
||||
<div className={css.submitContainer}>
|
||||
{paymentInfo ? <p className={css.paymentInfo}>{paymentInfo}</p> : null}
|
||||
<Button className={css.submitButton} type="submit" disabled={submitDisabled}>
|
||||
<FormattedMessage id="StripePaymentForm.submitPaymentInfo" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StripePaymentForm.defaultProps = { disableSubmit: false };
|
||||
StripePaymentForm.defaultProps = { className: null, rootClassName: null, disableSubmit: false, paymentInfo: null };
|
||||
|
||||
const { func, bool } = PropTypes;
|
||||
const { bool, func, string } = PropTypes;
|
||||
|
||||
StripePaymentForm.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
disableSubmit: bool,
|
||||
formId: string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
disableSubmit: bool,
|
||||
paymentInfo: string,
|
||||
};
|
||||
|
||||
export default injectIntl(StripePaymentForm);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@
|
|||
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
|
||||
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
|
||||
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
|
||||
"CheckoutPage.hostedBy": "Hosted by {name}",
|
||||
"CheckoutPage.initiateOrderError": "Payment request failed. Please try again.",
|
||||
"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}",
|
||||
"DateInput.clearDate": "Clear Date",
|
||||
"DateInput.closeDatePicker": "Close",
|
||||
|
|
@ -190,6 +192,7 @@
|
|||
"StripeBankAccountTokenInputField.routingNumberPlaceholder": "Type in routing number...",
|
||||
"StripeBankAccountTokenInputField.routingNumberRequired": "Routing number is required",
|
||||
"StripeBankAccountTokenInputField.unsupportedCountry": "Country not supported: {country}",
|
||||
"StripePaymentForm.creditCardDetails": "Credit card details",
|
||||
"StripePaymentForm.genericError": "Could not handle payment data. Please try again.",
|
||||
"StripePaymentForm.stripe.api_connection_error": "Could not connect to Stripe API.",
|
||||
"StripePaymentForm.stripe.api_error": "Error in Stripe API.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue