import React, { Component } from 'react';
import { bool, func, instanceOf, object, shape, string } from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { withRouter } from 'react-router-dom';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import { pathByRouteName, findRouteByRouteName } from '../../util/routes';
import { propTypes } from '../../util/types';
import { ensureListing, ensureUser, ensureTransaction, ensureBooking } from '../../util/data';
import { dateFromLocalToAPI } from '../../util/dates';
import { createSlug } from '../../util/urlHelpers';
import {
isTransactionInitiateAmountTooLowError,
isTransactionInitiateListingNotFoundError,
isTransactionInitiateMissingStripeAccountError,
isTransactionInitiateBookingTimeNotAvailableError,
isTransactionZeroPaymentError,
transactionInitiateOrderStripeErrors,
} from '../../util/errors';
import {
AvatarMedium,
BookingBreakdown,
Logo,
NamedLink,
NamedRedirect,
Page,
ResponsiveImage,
} from '../../components';
import { StripePaymentForm } from '../../forms';
import { isScrollingDisabled } from '../../ducks/UI.duck';
import { initiateOrder, setInitialValues, speculateTransaction } from './CheckoutPage.duck';
import config from '../../config';
import { storeData, storedData, clearData } from './CheckoutPageSessionHelpers';
import css from './CheckoutPage.css';
const STORAGE_KEY = 'CheckoutPage';
export class CheckoutPageComponent extends Component {
constructor(props) {
super(props);
this.state = {
pageData: {},
dataLoaded: false,
submitting: false,
};
this.loadInitialData = this.loadInitialData.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentWillMount() {
if (window) {
this.loadInitialData();
}
}
/**
* Load initial data for the page
*
* Since the data for the checkout is not passed in the URL (there
* might be lots of options in the future), we must pass in the data
* some other way. Currently the ListingPage sets the initial data
* for the CheckoutPage's Redux store.
*
* For some cases (e.g. a refresh in the CheckoutPage), the Redux
* store is empty. To handle that case, we store the received data
* to window.sessionStorage and read it from there if no props from
* the store exist.
*
* This function also sets of fetching the speculative transaction
* based on this initial data.
*/
loadInitialData() {
const { bookingData, bookingDates, listing, fetchSpeculatedTransaction, history } = this.props;
// Browser's back navigation should not rewrite data in session store.
// Action is 'POP' on both history.back() and page refresh cases.
// Action is 'PUSH' when user has directed through a link
// Action is 'REPLACE' when user has directed through login/signup process
const hasNavigatedThroughLink = history.action === 'PUSH' || history.action === 'REPLACE';
const hasDataInProps = !!(bookingData && bookingDates && listing) && hasNavigatedThroughLink;
if (hasDataInProps) {
// Store data only if data is passed through props and user has navigated through a link.
storeData(bookingData, bookingDates, listing, STORAGE_KEY);
}
// NOTE: stored data can be empty if user has already successfully completed transaction.
const pageData = hasDataInProps
? { bookingData, bookingDates, listing }
: storedData(STORAGE_KEY);
const hasData =
pageData &&
pageData.listing &&
pageData.listing.id &&
pageData.bookingData &&
pageData.bookingDates &&
pageData.bookingDates.bookingStart &&
pageData.bookingDates.bookingEnd;
if (hasData) {
const listingId = pageData.listing.id;
const { bookingStart, bookingEnd } = pageData.bookingDates;
// Convert picked date to date that will be converted on the API as
// a noon of correct year-month-date combo in UTC
const bookingStartForAPI = dateFromLocalToAPI(bookingStart);
const bookingEndForAPI = dateFromLocalToAPI(bookingEnd);
// Fetch speculated transaction for showing price in booking breakdown
// NOTE: if unit type is line-item/units, quantity needs to be added.
// The way to pass it to checkout page is through pageData.bookingData
fetchSpeculatedTransaction({
listingId,
bookingStart: bookingStartForAPI,
bookingEnd: bookingEndForAPI,
});
}
this.setState({ pageData: pageData || {}, dataLoaded: true });
}
handleSubmit(values) {
if (this.state.submitting) {
return;
}
this.setState({ submitting: true });
const cardToken = values.token;
const initialMessage = values.message;
const { history, sendOrderRequest, speculatedTransaction, dispatch } = this.props;
// Create order aka transaction
// NOTE: if unit type is line-item/units, quantity needs to be added.
// The way to pass it to checkout page is through pageData.bookingData
const requestParams = {
listingId: this.state.pageData.listing.id,
cardToken,
bookingStart: speculatedTransaction.booking.attributes.start,
bookingEnd: speculatedTransaction.booking.attributes.end,
};
sendOrderRequest(requestParams, initialMessage)
.then(values => {
const { orderId, initialMessageSuccess } = values;
this.setState({ submitting: false });
const routes = routeConfiguration();
const OrderPage = findRouteByRouteName('OrderDetailsPage', routes);
// Transaction is already created, but if the initial message
// sending failed, we tell it to the OrderDetailsPage.
dispatch(
OrderPage.setInitialValues({
initialMessageFailedToTransaction: initialMessageSuccess ? null : orderId,
})
);
const orderDetailsPath = pathByRouteName('OrderDetailsPage', routes, {
id: orderId.uuid,
});
clearData(STORAGE_KEY);
history.push(orderDetailsPath);
})
.catch(() => {
this.setState({ submitting: false });
});
}
render() {
const {
scrollingDisabled,
speculateTransactionInProgress,
speculateTransactionError,
speculatedTransaction,
initiateOrderError,
intl,
params,
currentUser,
} = this.props;
// Since the listing data is already given from the ListingPage
// and stored to handle refreshes, it might not have the possible
// deleted or closed information in it. If the transaction
// initiate or the speculative initiate fail due to the listing
// being deleted or closec, we should dig the information from the
// errors and not the listing data.
const listingNotFound =
isTransactionInitiateListingNotFoundError(speculateTransactionError) ||
isTransactionInitiateListingNotFoundError(initiateOrderError);
const isLoading = !this.state.dataLoaded || speculateTransactionInProgress;
const { listing, bookingDates } = this.state.pageData;
const currentTransaction = ensureTransaction(speculatedTransaction, {}, null);
const currentBooking = ensureBooking(currentTransaction.booking);
const currentListing = ensureListing(listing);
const currentAuthor = ensureUser(currentListing.author);
const isOwnListing =
currentUser &&
currentUser.id &&
currentAuthor &&
currentAuthor.id &&
currentAuthor.id.uuid === currentUser.id.uuid;
const hasListingAndAuthor = !!(currentListing.id && currentAuthor.id);
const hasBookingDates = !!(
bookingDates &&
bookingDates.bookingStart &&
bookingDates.bookingEnd
);
const hasRequiredData = hasListingAndAuthor && hasBookingDates;
const canShowPage = hasRequiredData && !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 (shouldRedirect) {
// eslint-disable-next-line no-console
console.error('Missing or invalid data for checkout, redirecting back to listing page.', {
transaction: currentTransaction,
bookingDates,
listing,
});
return