text: import React, { Component } from 'react';
  import { bool, func, instanceOf, object, oneOfType, shape, string } from 'prop-types';
  import { compose } from 'redux';
  import { connect } from 'react-redux';
  import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl';
  import { withRouter } from 'react-router-dom';
  import classNames from 'classnames';
  import config from '../../config';
  import routeConfiguration from '../../routeConfiguration';
  import { pathByRouteName, findRouteByRouteName } from '../../util/routes';
  import { propTypes, LINE_ITEM_NIGHT, LINE_ITEM_DAY, DATE_TYPE_DATETIME } from '../../util/types';
  import {
    ensureListing,
    ensureCurrentUser,
    ensureUser,
    ensureTransaction,
    ensureBooking,
    ensureStripeCustomer,
    ensurePaymentMethodCard,
  } from '../../util/data';
  import { minutesBetween } from '../../util/dates';
  import { createSlug } from '../../util/urlHelpers';
  import {
    isTransactionInitiateAmountTooLowError,
    isTransactionInitiateListingNotFoundError,
    isTransactionInitiateMissingStripeAccountError,
    isTransactionInitiateBookingTimeNotAvailableError,
    isTransactionChargeDisabledError,
    isTransactionZeroPaymentError,
    transactionInitiateOrderStripeErrors,
  } from '../../util/errors';
  import { formatMoney } from '../../util/currency';
  import { TRANSITION_ENQUIRE, txIsPaymentPending, txIsPaymentExpired } from '../../util/transaction';
  import {
    AvatarMedium,
    BookingBreakdown,
    Logo,
    NamedLink,
    NamedRedirect,
    Page,
    ResponsiveImage,
  } from '../../components';
  import { StripePaymentForm } from '../../forms';
  import { isScrollingDisabled } from '../../ducks/UI.duck';
  import { handleCardPayment, retrievePaymentIntent } from '../../ducks/stripe.duck';
  import { savePaymentMethod } from '../../ducks/paymentMethods.duck';
  
  import {
    initiateOrder,
    setInitialValues,
    speculateTransaction,
    stripeCustomer,
    confirmPayment,
    sendMessage,
  } from './CheckoutPage.duck';
  import { storeData, storedData, clearData } from './CheckoutPageSessionHelpers';
  import css from './CheckoutPage.css';
  
  const STORAGE_KEY = 'CheckoutPage';
  
  // Stripe PaymentIntent statuses, where user actions are already completed
  // https://stripe.com/docs/payments/payment-intents/status
  const STRIPE_PI_USER_ACTIONS_DONE_STATUSES = ['processing', 'requires_capture', 'succeeded'];
  
  // Payment charge options
  const ONETIME_PAYMENT = 'ONETIME_PAYMENT';
  const PAY_AND_SAVE_FOR_LATER_USE = 'PAY_AND_SAVE_FOR_LATER_USE';
  const USE_SAVED_CARD = 'USE_SAVED_CARD';
  
  const paymentFlow = (selectedPaymentMethod, saveAfterOnetimePayment) => {
    // Payment mode could be 'replaceCard', but without explicit saveAfterOnetimePayment flag,
    // we'll handle it as one-time payment
    return selectedPaymentMethod === 'defaultCard'
      ? USE_SAVED_CARD
      : saveAfterOnetimePayment
      ? PAY_AND_SAVE_FOR_LATER_USE
      : ONETIME_PAYMENT;
  };
  
  const initializeOrderPage = (initialValues, routes, dispatch) => {
    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(initialValues));
  };
  
  const checkIsPaymentExpired = existingTransaction => {
    return txIsPaymentExpired(existingTransaction)
      ? true
      : txIsPaymentPending(existingTransaction)
      ? minutesBetween(existingTransaction.attributes.lastTransitionedAt, new Date()) >= 15
      : false;
  };
  
  export class CheckoutPageComponent extends Component {
    constructor(props) {
      super(props);
  
      this.state = {
        pageData: {},
        dataLoaded: false,
        submitting: false,
      };
      this.stripe = null;
  
      this.onStripeInitialized = this.onStripeInitialized.bind(this);
      this.loadInitialData = this.loadInitialData.bind(this);
      this.handlePaymentIntent = this.handlePaymentIntent.bind(this);
      this.handleSubmit = this.handleSubmit.bind(this);
    }
  
    componentDidMount() {
      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,
        transaction,
        fetchSpeculatedTransaction,
        fetchStripeCustomer,
        history,
      } = this.props;
  
      // Fetch currentUser with stripeCustomer entity
      // Note: since there's need for data loading in "componentWillMount" function,
      //       this is added here instead of loadData static function.
      fetchStripeCustomer();
  
      // 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, transaction, STORAGE_KEY);
      }
  
      // NOTE: stored data can be empty if user has already successfully completed transaction.
      const pageData = hasDataInProps
        ? { bookingData, bookingDates, listing, transaction }
        : storedData(STORAGE_KEY);
  
      // Check if a booking is already created according to stored data.
      const tx = pageData ? pageData.transaction : null;
      const isBookingCreated = tx && tx.booking && tx.booking.id;
  
      const shouldFetchSpeculatedTransaction =
        pageData &&
        pageData.listing &&
        pageData.listing.id &&
        pageData.bookingData &&
        pageData.bookingDates &&
        pageData.bookingDates.bookingStart &&
        pageData.bookingDates.bookingEnd &&
        pageData.bookingData.quantity &&
        !isBookingCreated;
  
      if (shouldFetchSpeculatedTransaction) {
        const listingId = pageData.listing.id;
        const { bookingStart, bookingEnd } = pageData.bookingDates;
        const { quantity } = pageData.bookingData;
  
        // 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,
          bookingEnd,
          quantity,
        });
      }
  
      this.setState({ pageData: pageData || {}, dataLoaded: true });
    }
  
    handlePaymentIntent(handlePaymentParams) {
      const {
        currentUser,
        stripeCustomerFetched,
        onInitiateOrder,
        onHandleCardPayment,
        onConfirmPayment,
        onSendMessage,
        onSavePaymentMethod,
      } = this.props;
      const {
        pageData,
        speculatedTransaction,
        message,
        paymentIntent,
        selectedPaymentMethod,
        saveAfterOnetimePayment,
      } = handlePaymentParams;
      const storedTx = ensureTransaction(pageData.transaction);
  
      const ensuredCurrentUser = ensureCurrentUser(currentUser);
      const ensuredStripeCustomer = ensureStripeCustomer(ensuredCurrentUser.stripeCustomer);
      const ensuredDefaultPaymentMethod = ensurePaymentMethodCard(
        ensuredStripeCustomer.defaultPaymentMethod
      );
  
      let createdPaymentIntent = null;
  
      const hasDefaultPaymentMethod = !!(
        stripeCustomerFetched &&
        ensuredStripeCustomer.attributes.stripeCustomerId &&
        ensuredDefaultPaymentMethod.id
      );
      const stripePaymentMethodId = hasDefaultPaymentMethod
        ? ensuredDefaultPaymentMethod.attributes.stripePaymentMethodId
        : null;
  
      const selectedPaymentFlow = paymentFlow(selectedPaymentMethod, saveAfterOnetimePayment);
  
      // Step 1: initiate order by requesting payment from Marketplace API
      const fnRequestPayment = fnParams => {
        // fnParams should be { listingId, bookingStart, bookingEnd }
        const hasPaymentIntents =
          storedTx.attributes.protectedData && storedTx.attributes.protectedData.stripePaymentIntents;
  
        // If paymentIntent exists, order has been initiated previously.
        return hasPaymentIntents ? Promise.resolve(storedTx) : onInitiateOrder(fnParams, storedTx.id);
      };
  
      // Step 2: pay using Stripe SDK
      const fnHandleCardPayment = fnParams => {
        // fnParams should be returned transaction entity
  
        const order = ensureTransaction(fnParams);
        if (order.id) {
          // Store order.
          const { bookingData, bookingDates, listing } = pageData;
          storeData(bookingData, bookingDates, listing, order, STORAGE_KEY);
          this.setState({ pageData: { ...pageData, transaction: order } });
        }
  
        const hasPaymentIntents =
          order.attributes.protectedData && order.attributes.protectedData.stripePaymentIntents;
  
        if (!hasPaymentIntents) {
          throw new Error(
            `Missing StripePaymentIntents key in transaction's protectedData. Check that your transaction process is configured to use payment intents.`
          );
        }
  
        const { stripePaymentIntentClientSecret } = hasPaymentIntents
          ? order.attributes.protectedData.stripePaymentIntents.default
          : null;
  
        const { stripe, card, billingDetails, paymentIntent } = handlePaymentParams;
        const stripeElementMaybe = selectedPaymentFlow !== USE_SAVED_CARD ? { card } : {};
  
        // Note: payment_method could be set here for USE_SAVED_CARD flow.
        // { payment_method: stripePaymentMethodId }
        // However, we have set it already on API side, when PaymentIntent was created.
        const paymentParams =
          selectedPaymentFlow !== USE_SAVED_CARD
            ? {
                payment_method_data: {
                  billing_details: billingDetails,
                },
              }
            : {};
  
        const params = {
          stripePaymentIntentClientSecret,
          orderId: order.id,
          stripe,
          ...stripeElementMaybe,
          paymentParams,
        };
  
        // If paymentIntent status is not waiting user action,
        // handleCardPayment has been called previously.
        const hasPaymentIntentUserActionsDone =
          paymentIntent && STRIPE_PI_USER_ACTIONS_DONE_STATUSES.includes(paymentIntent.status);
        return hasPaymentIntentUserActionsDone
          ? Promise.resolve({ transactionId: order.id, paymentIntent })
          : onHandleCardPayment(params);
      };
  
      // Step 3: complete order by confirming payment to Marketplace API
      // Parameter should contain { paymentIntent, transactionId } returned in step 2
      const fnConfirmPayment = fnParams => {
        createdPaymentIntent = fnParams.paymentIntent;
        return onConfirmPayment(fnParams);
      };
  
      // Step 4: send initial message
      const fnSendMessage = fnParams => {
        return onSendMessage({ ...fnParams, message });
      };
  
      // Step 5: optionally save card as defaultPaymentMethod
      const fnSavePaymentMethod = fnParams => {
        const pi = createdPaymentIntent || paymentIntent;
  
        if (selectedPaymentFlow === PAY_AND_SAVE_FOR_LATER_USE) {
          return onSavePaymentMethod(ensuredStripeCustomer, pi.payment_method)
            .then(response => {
              if (response.errors) {
                return { ...fnParams, paymentMethodSaved: false };
              }
              return { ...fnParams, paymentMethodSaved: true };
            })
            .catch(e => {
              // Real error cases are catched already in paymentMethods page.
              return { ...fnParams, paymentMethodSaved: false };
            });
        } else {
          return Promise.resolve({ ...fnParams, paymentMethodSaved: true });
        }
      };
  
      // Here we create promise calls in sequence
      // This is pretty much the same as:
      // fnRequestPayment({...initialParams})
      //   .then(result => fnHandleCardPayment({...result}))
      //   .then(result => fnConfirmPayment({...result}))
      const applyAsync = (acc, val) => acc.then(val);
      const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
      const handlePaymentIntentCreation = composeAsync(
        fnRequestPayment,
        fnHandleCardPayment,
        fnConfirmPayment,
        fnSendMessage,
        fnSavePaymentMethod
      );
  
      // 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 tx = speculatedTransaction ? speculatedTransaction : storedTx;
  
      // Note: optionalPaymentParams contains Stripe paymentMethod,
      // but that can also be passed on Step 2
      // stripe.handleCardPayment(stripe, { payment_method: stripePaymentMethodId })
      const optionalPaymentParams =
        selectedPaymentFlow === USE_SAVED_CARD && hasDefaultPaymentMethod
          ? { paymentMethod: stripePaymentMethodId }
          : selectedPaymentFlow === PAY_AND_SAVE_FOR_LATER_USE
          ? { setupPaymentMethodForSaving: true }
          : {};
  
      const orderParams = {
        listingId: pageData.listing.id,
        bookingStart: tx.booking.attributes.start,
        bookingEnd: tx.booking.attributes.end,
        quantity: pageData.bookingData ? pageData.bookingData.quantity : null,
        ...optionalPaymentParams,
      };
  
      return handlePaymentIntentCreation(orderParams);
    }
  
    handleSubmit(values) {
      if (this.state.submitting) {
        return;
      }
      this.setState({ submitting: true });
  
      const { history, speculatedTransaction, currentUser, paymentIntent, dispatch } = this.props;
      const { card, message, paymentMethod, formValues } = values;
      const {
        name,
        addressLine1,
        addressLine2,
        postal,
        city,
        state,
        country,
        saveAfterOnetimePayment,
      } = formValues;
  
      // Billing address is recommended.
      // However, let's not assume that <StripePaymentAddress> data is among formValues.
      // Read more about this from Stripe's docs
      // https://stripe.com/docs/stripe-js/reference#stripe-handle-card-payment-no-element
      const addressMaybe =
        addressLine1 && postal
          ? {
              address: {
                city: city,
                country: country,
                line1: addressLine1,
                line2: addressLine2,
                postal_code: postal,
                state: state,
              },
            }
          : {};
      const billingDetails = {
        name,
        email: ensureCurrentUser(currentUser).attributes.email,
        ...addressMaybe,
      };
  
      const requestPaymentParams = {
        pageData: this.state.pageData,
        speculatedTransaction,
        stripe: this.stripe,
        card,
        billingDetails,
        message,
        paymentIntent,
        selectedPaymentMethod: paymentMethod,
        saveAfterOnetimePayment: !!saveAfterOnetimePayment,
      };
  
      this.handlePaymentIntent(requestPaymentParams)
        .then(res => {
          const { orderId, messageSuccess, paymentMethodSaved } = res;
          this.setState({ submitting: false });
  
          const routes = routeConfiguration();
          const initialMessageFailedToTransaction = messageSuccess ? null : orderId;
          const orderDetailsPath = pathByRouteName('OrderDetailsPage', routes, { id: orderId.uuid });
          const initialValues = {
            initialMessageFailedToTransaction,
            savePaymentMethodFailed: !paymentMethodSaved,
          };
  
          initializeOrderPage(initialValues, routes, dispatch);
          clearData(STORAGE_KEY);
          history.push(orderDetailsPath);
        })
        .catch(err => {
          console.error(err);
          this.setState({ submitting: false });
        });
    }
  
    onStripeInitialized(stripe) {
      this.stripe = stripe;
  
      const { paymentIntent, onRetrievePaymentIntent } = this.props;
      const tx = this.state.pageData ? this.state.pageData.transaction : null;
  
      // We need to get up to date PI, if booking is created but payment is not expired.
      const shouldFetchPaymentIntent =
        this.stripe &&
        !paymentIntent &&
        tx &&
        tx.id &&
        tx.booking &&
        tx.booking.id &&
        txIsPaymentPending(tx) &&
        !checkIsPaymentExpired(tx);
  
      if (shouldFetchPaymentIntent) {
        const { stripePaymentIntentClientSecret } =
          tx.attributes.protectedData && tx.attributes.protectedData.stripePaymentIntents
            ? tx.attributes.protectedData.stripePaymentIntents.default
            : {};
  
        // Fetch up to date PaymentIntent from Stripe
        onRetrievePaymentIntent({ stripe, stripePaymentIntentClientSecret });
      }
    }
  
    render() {
      const {
        scrollingDisabled,
        speculateTransactionInProgress,
        speculateTransactionError,
        speculatedTransaction: speculatedTransactionMaybe,
        initiateOrderError,
        confirmPaymentError,
        intl,
        params,
        currentUser,
        handleCardPaymentError,
        paymentIntent,
        retrievePaymentIntentError,
        stripeCustomerFetched,
      } = 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, transaction } = this.state.pageData;
      const existingTransaction = ensureTransaction(transaction);
      const speculatedTransaction = ensureTransaction(speculatedTransactionMaybe, {}, null);
      const currentListing = ensureListing(listing);
      const currentAuthor = ensureUser(currentListing.author);
  
      const listingTitle = currentListing.attributes.title;
      const title = intl.formatMessage({ id: 'CheckoutPage.title' }, { listingTitle });
  
      const pageProps = { title, scrollingDisabled };
      const topbar = (
        <div className={css.topbar}>
          <NamedLink className={css.home} name="LandingPage">
            <Logo
              className={css.logoMobile}
              title={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
              format="mobile"
            />
            <Logo
              className={css.logoDesktop}
              alt={intl.formatMessage({ id: 'CheckoutPage.goToLandingPage' })}
              format="desktop"
            />
          </NamedLink>
        </div>
      );
  
      if (isLoading) {
        return <Page {...pageProps}>{topbar}</Page>;
      }
  
      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: speculatedTransaction,
          bookingDates,
          listing,
        });
        return <NamedRedirect name="ListingPage" params={params} />;
      }
  
      // Show breakdown only when speculated transaction and booking are loaded
      // (i.e. have an id)
      const tx = existingTransaction.booking ? existingTransaction : speculatedTransaction;
      const txBooking = ensureBooking(tx.booking);
      const timeZone = currentListing.attributes.availabilityPlan
        ? currentListing.attributes.availabilityPlan.timezone
        : 'Etc/UTC';
      const breakdown =
        tx.id && txBooking.id ? (
          <BookingBreakdown
            className={css.bookingBreakdown}
            userRole="customer"
            unitType={config.bookingUnitType}
            transaction={tx}
            booking={txBooking}
            dateType={DATE_TYPE_DATETIME}
            timeZone={timeZone}
          />
        ) : null;
  
      const isPaymentExpired = checkIsPaymentExpired(existingTransaction);
      const hasDefaultPaymentMethod = !!(
        stripeCustomerFetched &&
        ensureStripeCustomer(currentUser.stripeCustomer).attributes.stripeCustomerId &&
        ensurePaymentMethodCard(currentUser.stripeCustomer.defaultPaymentMethod).id
      );
  
      // Allow showing page when currentUser is still being downloaded,
      // but show payment form only when user info is loaded.
      const showPaymentForm = !!(
        currentUser &&
        hasRequiredData &&
        !listingNotFound &&
        !initiateOrderError &&
        !speculateTransactionError &&
        !retrievePaymentIntentError &&
        !isPaymentExpired
      );
  
      const firstImage =
        currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
  
      const listingLink = (
        <NamedLink
          name="ListingPage"
          params={{ id: currentListing.id.uuid, slug: createSlug(listingTitle) }}
        >
          <FormattedMessage id="CheckoutPage.errorlistingLinkText" />
        </NamedLink>
      );
  
      const isAmountTooLowError = isTransactionInitiateAmountTooLowError(initiateOrderError);
      const isChargeDisabledError = isTransactionChargeDisabledError(initiateOrderError);
      const isBookingTimeNotAvailableError = isTransactionInitiateBookingTimeNotAvailableError(
        initiateOrderError
      );
      const stripeErrors = transactionInitiateOrderStripeErrors(initiateOrderError);
  
      let initiateOrderErrorMessage = null;
      let listingNotFoundErrorMessage = null;
  
      if (listingNotFound) {
        listingNotFoundErrorMessage = (
          <p className={css.notFoundError}>
            <FormattedMessage id="CheckoutPage.listingNotFoundError" />
          </p>
        );
      } else if (isAmountTooLowError) {
        initiateOrderErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.initiateOrderAmountTooLow" />
          </p>
        );
      } else if (isBookingTimeNotAvailableError) {
        initiateOrderErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.bookingTimeNotAvailableMessage" />
          </p>
        );
      } else if (isChargeDisabledError) {
        initiateOrderErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.chargeDisabledMessage" />
          </p>
        );
      } else if (stripeErrors && stripeErrors.length > 0) {
        // NOTE: Error messages from Stripes are not part of translations.
        // By default they are in English.
        const stripeErrorsAsString = stripeErrors.join(', ');
        initiateOrderErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage
              id="CheckoutPage.initiateOrderStripeError"
              values={{ stripeErrors: stripeErrorsAsString }}
            />
          </p>
        );
      } else if (initiateOrderError) {
        // Generic initiate order error
        initiateOrderErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.initiateOrderError" values={{ listingLink }} />
          </p>
        );
      }
  
      const speculateTransactionErrorMessage = speculateTransactionError ? (
        <p className={css.speculateError}>
          <FormattedMessage id="CheckoutPage.speculateTransactionError" />
        </p>
      ) : null;
      let speculateErrorMessage = null;
  
      if (isTransactionInitiateMissingStripeAccountError(speculateTransactionError)) {
        speculateErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.providerStripeAccountMissingError" />
          </p>
        );
      } else if (isTransactionInitiateBookingTimeNotAvailableError(speculateTransactionError)) {
        speculateErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.bookingTimeNotAvailableMessage" />
          </p>
        );
      } else if (isTransactionZeroPaymentError(speculateTransactionError)) {
        speculateErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.initiateOrderAmountTooLow" />
          </p>
        );
      } else if (speculateTransactionError) {
        speculateErrorMessage = (
          <p className={css.orderError}>
            <FormattedMessage id="CheckoutPage.speculateFailedMessage" />
          </p>
        );
      }
  
      const unitType = config.bookingUnitType;
      const isNightly = unitType === LINE_ITEM_NIGHT;
      const isDaily = unitType === LINE_ITEM_DAY;
  
      const unitTranslationKey = isNightly
        ? 'CheckoutPage.perNight'
        : isDaily
        ? 'CheckoutPage.perDay'
        : 'CheckoutPage.perUnit';
  
      const price = currentListing.attributes.price;
      const formattedPrice = formatMoney(intl, price);
      const detailsSubTitle = `${formattedPrice} ${intl.formatMessage({ id: unitTranslationKey })}`;
  
      const showInitialMessageInput = !(
        existingTransaction && existingTransaction.attributes.lastTransition === TRANSITION_ENQUIRE
      );
  
      // Get first and last name of the current user and use it in the StripePaymentForm to autofill the name field
      const userName =
        currentUser && currentUser.attributes
          ? `${currentUser.attributes.profile.firstName} ${currentUser.attributes.profile.lastName}`
          : null;
  
      // If paymentIntent status is not waiting user action,
      // handleCardPayment has been called previously.
      const hasPaymentIntentUserActionsDone =
        paymentIntent && STRIPE_PI_USER_ACTIONS_DONE_STATUSES.includes(paymentIntent.status);
  
      // If your marketplace works mostly in one country you can use initial values to select country automatically
      // e.g. {country: 'FI'}
  
      const initalValuesForStripePayment = { name: userName };
  
      return (
        <Page {...pageProps}>
          {topbar}
          <div className={css.contentContainer}>
            <div className={css.aspectWrapper}>
              <ResponsiveImage
                rootClassName={css.rootForImage}
                alt={listingTitle}
                image={firstImage}
                variants={['landscape-crop', 'landscape-crop2x']}
              />
            </div>
            <div className={classNames(css.avatarWrapper, css.avatarMobile)}>
              <AvatarMedium user={currentAuthor} disableProfileLink />
            </div>
            <div className={css.bookListingContainer}>
              <div className={css.heading}>
                <h1 className={css.title}>{title}</h1>
              </div>
  
              <div className={css.priceBreakdownContainer}>
                {speculateTransactionErrorMessage}
                {breakdown}
              </div>
  
              <section className={css.paymentContainer}>
                {initiateOrderErrorMessage}
                {listingNotFoundErrorMessage}
                {speculateErrorMessage}
                {retrievePaymentIntentError ? (
                  <p className={css.orderError}>
                    <FormattedMessage
                      id="CheckoutPage.retrievingStripePaymentIntentFailed"
                      values={{ listingLink }}
                    />
                  </p>
                ) : null}
                {showPaymentForm ? (
                  <StripePaymentForm
                    className={css.paymentForm}
                    onSubmit={this.handleSubmit}
                    inProgress={this.state.submitting}
                    formId="CheckoutPagePaymentForm"
                    paymentInfo={intl.formatMessage({ id: 'CheckoutPage.paymentInfo' })}
                    authorDisplayName={currentAuthor.attributes.profile.displayName}
                    showInitialMessageInput={showInitialMessageInput}
                    initialValues={initalValuesForStripePayment}
                    initiateOrderError={initiateOrderError}
                    handleCardPaymentError={handleCardPaymentError}
                    confirmPaymentError={confirmPaymentError}
                    hasHandledCardPayment={hasPaymentIntentUserActionsDone}
                    loadingData={!stripeCustomerFetched}
                    defaultPaymentMethod={
                      hasDefaultPaymentMethod ? currentUser.stripeCustomer.defaultPaymentMethod : null
                    }
                    paymentIntent={paymentIntent}
                    onStripeInitialized={this.onStripeInitialized}
                  />
                ) : null}
                {isPaymentExpired ? (
                  <p className={css.orderError}>
                    <FormattedMessage
                      id="CheckoutPage.paymentExpiredMessage"
                      values={{ listingLink }}
                    />
                  </p>
                ) : null}
              </section>
            </div>
  
            <div className={css.detailsContainerDesktop}>
              <div className={css.detailsAspectWrapper}>
                <ResponsiveImage
                  rootClassName={css.rootForImage}
                  alt={listingTitle}
                  image={firstImage}
                  variants={['landscape-crop', 'landscape-crop2x']}
                />
              </div>
              <div className={css.avatarWrapper}>
                <AvatarMedium user={currentAuthor} disableProfileLink />
              </div>
              <div className={css.detailsHeadings}>
                <h2 className={css.detailsTitle}>{listingTitle}</h2>
                <p className={css.detailsSubtitle}>{detailsSubTitle}</p>
              </div>
              {speculateTransactionErrorMessage}
              {breakdown}
            </div>
          </div>
        </Page>
      );
    }
  }
  
  CheckoutPageComponent.defaultProps = {
    initiateOrderError: null,
    confirmPaymentError: null,
    listing: null,
    bookingData: {},
    bookingDates: null,
    speculateTransactionError: null,
    speculatedTransaction: null,
    transaction: null,
    currentUser: null,
    paymentIntent: null,
  };
  
  CheckoutPageComponent.propTypes = {
    scrollingDisabled: bool.isRequired,
    listing: propTypes.listing,
    bookingData: object,
    bookingDates: shape({
      bookingStart: instanceOf(Date).isRequired,
      bookingEnd: instanceOf(Date).isRequired,
    }),
    fetchStripeCustomer: func.isRequired,
    stripeCustomerFetched: bool.isRequired,
    fetchSpeculatedTransaction: func.isRequired,
    speculateTransactionInProgress: bool.isRequired,
    speculateTransactionError: propTypes.error,
    speculatedTransaction: propTypes.transaction,
    transaction: propTypes.transaction,
    currentUser: propTypes.currentUser,
    params: shape({
      id: string,
      slug: string,
    }).isRequired,
    onConfirmPayment: func.isRequired,
    onInitiateOrder: func.isRequired,
    onHandleCardPayment: func.isRequired,
    onRetrievePaymentIntent: func.isRequired,
    onSavePaymentMethod: func.isRequired,
    onSendMessage: func.isRequired,
    initiateOrderError: propTypes.error,
    confirmPaymentError: propTypes.error,
    // handleCardPaymentError comes from Stripe so that's why we can't expect it to be in a specific form
    handleCardPaymentError: oneOfType([propTypes.error, object]),
    paymentIntent: object,
  
    // from connect
    dispatch: func.isRequired,
  
    // from injectIntl
    intl: intlShape.isRequired,
  
    // from withRouter
    history: shape({
      push: func.isRequired,
    }).isRequired,
  };
  
  const mapStateToProps = state => {
    const {
      listing,
      bookingData,
      bookingDates,
      stripeCustomerFetched,
      speculateTransactionInProgress,
      speculateTransactionError,
      speculatedTransaction,
      transaction,
      initiateOrderError,
      confirmPaymentError,
    } = state.CheckoutPage;
    const { currentUser } = state.user;
    const { handleCardPaymentError, paymentIntent, retrievePaymentIntentError } = state.stripe;
    return {
      scrollingDisabled: isScrollingDisabled(state),
      currentUser,
      stripeCustomerFetched,
      bookingData,
      bookingDates,
      speculateTransactionInProgress,
      speculateTransactionError,
      speculatedTransaction,
      transaction,
      listing,
      initiateOrderError,
      handleCardPaymentError,
      confirmPaymentError,
      paymentIntent,
      retrievePaymentIntentError,
    };
  };
  
  const mapDispatchToProps = dispatch => ({
    dispatch,
    fetchSpeculatedTransaction: params => dispatch(speculateTransaction(params)),
    fetchStripeCustomer: () => dispatch(stripeCustomer()),
    onInitiateOrder: (params, transactionId) => dispatch(initiateOrder(params, transactionId)),
    onRetrievePaymentIntent: params => dispatch(retrievePaymentIntent(params)),
    onHandleCardPayment: params => dispatch(handleCardPayment(params)),
    onConfirmPayment: params => dispatch(confirmPayment(params)),
    onSendMessage: params => dispatch(sendMessage(params)),
    onSavePaymentMethod: (stripeCustomer, stripePaymentMethodId) =>
      dispatch(savePaymentMethod(stripeCustomer, stripePaymentMethodId)),
  });
  
  const CheckoutPage = compose(
    withRouter,
    connect(
      mapStateToProps,
      mapDispatchToProps
    ),
    injectIntl
  )(CheckoutPageComponent);
  
  CheckoutPage.setInitialValues = initialValues => setInitialValues(initialValues);
  
  CheckoutPage.displayName = 'CheckoutPage';
  
  export default CheckoutPage;
  