CheckoutPage mobile

This commit is contained in:
Vesa Luusua 2017-06-29 14:54:35 +03:00
parent f0a2d3b52a
commit c82fe44b61
7 changed files with 227 additions and 107 deletions

View file

@ -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;
}

View file

@ -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>

View file

@ -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))>
`;

View file

@ -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;
}

View file

@ -22,6 +22,6 @@ class StripePaymentFormExample extends Component {
export const Empty = {
component: StripePaymentFormExample,
props: { intl: fakeIntl },
props: { intl: fakeIntl, formId: 'StripePaymentFormExample' },
group: 'forms',
};

View file

@ -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);

View file

@ -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.",