mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
Merge pull request #543 from sharetribe/checkout-initial-message
Initial message in Checkout
This commit is contained in:
commit
13d9626152
13 changed files with 256 additions and 88 deletions
|
|
@ -19,6 +19,12 @@
|
|||
|
||||
@media (--viewportMedium) {
|
||||
margin: -1px 0 8px 0;
|
||||
|
||||
&:last-of-type {
|
||||
/* Last item should not have bottom margin that goes to the bottom of
|
||||
the component */
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
@media (--viewportLarge) {
|
||||
flex-grow: 0;
|
||||
flex-basis: 519px;
|
||||
margin-top: 177px;
|
||||
margin-top: 121px;
|
||||
margin-right: 132px;
|
||||
}
|
||||
}
|
||||
|
|
@ -228,23 +228,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.paymentTitle {
|
||||
/* Font */
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.orderError,
|
||||
.notFoundError {
|
||||
margin: 11px 0 12px 0;
|
||||
|
|
@ -340,7 +323,7 @@
|
|||
margin: 5px 24px 25px 24px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 38px 48px 25px 48px;
|
||||
margin: 38px 48px 26px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,11 +104,11 @@ export const speculateTransactionError = e => ({
|
|||
|
||||
/* ================ Thunks ================ */
|
||||
|
||||
export const initiateOrder = params => (dispatch, getState, sdk) => {
|
||||
export const initiateOrder = (orderParams, initialMessage) => (dispatch, getState, sdk) => {
|
||||
dispatch(initiateOrderRequest());
|
||||
const bodyParams = {
|
||||
transition: 'transition/preauthorize',
|
||||
params,
|
||||
params: orderParams,
|
||||
};
|
||||
return sdk.transactions
|
||||
.initiate(bodyParams)
|
||||
|
|
@ -116,14 +116,27 @@ export const initiateOrder = params => (dispatch, getState, sdk) => {
|
|||
const orderId = response.data.data.id;
|
||||
dispatch(initiateOrderSuccess(orderId));
|
||||
dispatch(fetchCurrentUserHasOrdersSuccess(true));
|
||||
return orderId;
|
||||
|
||||
if (initialMessage) {
|
||||
return sdk.messages
|
||||
.send({ transactionId: orderId, content: initialMessage })
|
||||
.then(() => {
|
||||
return { orderId, initialMessageSuccess: true };
|
||||
})
|
||||
.catch(e => {
|
||||
log.error(e, 'initial-message-send-failed', { txId: orderId });
|
||||
return { orderId, initialMessageSuccess: false };
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve({ orderId, initialMessageSuccess: true });
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(initiateOrderError(storableError(e)));
|
||||
log.error(e, 'initiate-order-failed', {
|
||||
listingId: params.listingId.uuid,
|
||||
bookingStart: params.bookingStart,
|
||||
bookingEnd: params.bookingEnd,
|
||||
listingId: orderParams.listingId.uuid,
|
||||
bookingStart: orderParams.bookingStart,
|
||||
bookingEnd: orderParams.bookingEnd,
|
||||
});
|
||||
throw e;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
|||
import { withRouter } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import routeConfiguration from '../../routeConfiguration';
|
||||
import { pathByRouteName } from '../../util/routes';
|
||||
import { pathByRouteName, findRouteByRouteName } from '../../util/routes';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { ensureListing, ensureUser, ensureTransaction, ensureBooking } from '../../util/data';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
|
|
@ -96,13 +96,15 @@ export class CheckoutPageComponent extends Component {
|
|||
this.setState({ pageData: pageData || {}, dataLoaded: true });
|
||||
}
|
||||
|
||||
handleSubmit(cardToken) {
|
||||
handleSubmit(values) {
|
||||
if (this.state.submitting) {
|
||||
return;
|
||||
}
|
||||
this.setState({ submitting: true });
|
||||
|
||||
const { history, sendOrderRequest, speculatedTransaction } = this.props;
|
||||
const cardToken = values.token;
|
||||
const initialMessage = values.message;
|
||||
const { history, sendOrderRequest, speculatedTransaction, dispatch } = this.props;
|
||||
const requestParams = {
|
||||
listingId: this.state.pageData.listing.id,
|
||||
cardToken,
|
||||
|
|
@ -110,10 +112,21 @@ export class CheckoutPageComponent extends Component {
|
|||
bookingEnd: speculatedTransaction.booking.attributes.end,
|
||||
};
|
||||
|
||||
sendOrderRequest(requestParams)
|
||||
.then(orderId => {
|
||||
sendOrderRequest(requestParams, initialMessage)
|
||||
.then(values => {
|
||||
const { orderId, initialMessageSuccess } = values;
|
||||
this.setState({ submitting: false });
|
||||
const orderDetailsPath = pathByRouteName('OrderDetailsPage', routeConfiguration(), {
|
||||
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({
|
||||
messageSendingFailedToTransaction: initialMessageSuccess ? null : orderId,
|
||||
})
|
||||
);
|
||||
const orderDetailsPath = pathByRouteName('OrderDetailsPage', routes, {
|
||||
id: orderId.uuid,
|
||||
});
|
||||
clearData(STORAGE_KEY);
|
||||
|
|
@ -317,9 +330,6 @@ export class CheckoutPageComponent extends Component {
|
|||
</div>
|
||||
|
||||
<section className={css.paymentContainer}>
|
||||
<h3 className={css.paymentTitle}>
|
||||
<FormattedMessage id="CheckoutPage.paymentTitle" />
|
||||
</h3>
|
||||
{initiateOrderErrorMessage}
|
||||
{listingNotFoundErrorMessage}
|
||||
{showPaymentForm ? (
|
||||
|
|
@ -329,6 +339,7 @@ export class CheckoutPageComponent extends Component {
|
|||
inProgress={this.state.submitting}
|
||||
formId="CheckoutPagePaymentForm"
|
||||
paymentInfo={intl.formatMessage({ id: 'CheckoutPage.paymentInfo' })}
|
||||
authorDisplayName={currentAuthor.attributes.profile.displayName}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
|
@ -401,6 +412,9 @@ CheckoutPageComponent.propTypes = {
|
|||
}).isRequired,
|
||||
sendOrderRequest: func.isRequired,
|
||||
|
||||
// from connect
|
||||
dispatch: func.isRequired,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
|
|
@ -433,7 +447,8 @@ const mapStateToProps = state => {
|
|||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
sendOrderRequest: params => dispatch(initiateOrder(params)),
|
||||
dispatch,
|
||||
sendOrderRequest: (params, initialMessage) => dispatch(initiateOrder(params, initialMessage)),
|
||||
fetchSpeculatedTransaction: (listingId, bookingStart, bookingEnd) =>
|
||||
dispatch(speculateTransaction(listingId, bookingStart, bookingEnd)),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ describe('CheckoutPage', () => {
|
|||
bookingStart: new Date(Date.UTC(2017, 3, 14)),
|
||||
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
|
||||
},
|
||||
dispatch: noop,
|
||||
history: { push: noop },
|
||||
intl: fakeIntl,
|
||||
listing,
|
||||
|
|
|
|||
|
|
@ -94,13 +94,8 @@ exports[`CheckoutPage matches snapshot 1`] = `
|
|||
</h3>
|
||||
</div>
|
||||
<section>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="CheckoutPage.paymentTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</h3>
|
||||
<InjectIntl(StripePaymentForm)
|
||||
authorDisplayName="author display name"
|
||||
formId="CheckoutPagePaymentForm"
|
||||
inProgress={false}
|
||||
onSubmit={[Function]}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { pick } from 'lodash';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import { storableError } from '../../util/errors';
|
||||
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
|
||||
|
|
@ -5,6 +6,8 @@ import { updatedEntities, denormalisedEntities } from '../../util/data';
|
|||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const SET_INITAL_VALUES = 'app/OrderPage/SET_INITIAL_VALUES';
|
||||
|
||||
export const FETCH_ORDER_REQUEST = 'app/OrderPage/FETCH_ORDER_REQUEST';
|
||||
export const FETCH_ORDER_SUCCESS = 'app/OrderPage/FETCH_ORDER_SUCCESS';
|
||||
export const FETCH_ORDER_ERROR = 'app/OrderPage/FETCH_ORDER_ERROR';
|
||||
|
|
@ -22,11 +25,15 @@ const initialState = {
|
|||
fetchMessagesInProgress: false,
|
||||
fetchMessagesError: null,
|
||||
messages: [],
|
||||
messageSendingFailedToTransaction: null,
|
||||
};
|
||||
|
||||
export default function checkoutPageReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SET_INITAL_VALUES:
|
||||
return { ...initialState, ...payload };
|
||||
|
||||
case FETCH_ORDER_REQUEST:
|
||||
return { ...state, fetchOrderInProgress: true, fetchOrderError: null };
|
||||
case FETCH_ORDER_SUCCESS: {
|
||||
|
|
@ -50,6 +57,11 @@ export default function checkoutPageReducer(state = initialState, action = {}) {
|
|||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const setInitialValues = initialValues => ({
|
||||
type: SET_INITAL_VALUES,
|
||||
payload: pick(initialValues, Object.keys(initialState)),
|
||||
});
|
||||
|
||||
const fetchOrderRequest = () => ({ type: FETCH_ORDER_REQUEST });
|
||||
const fetchOrderSuccess = response => ({ type: FETCH_ORDER_SUCCESS, payload: response });
|
||||
const fetchOrderError = e => ({ type: FETCH_ORDER_ERROR, error: true, payload: e });
|
||||
|
|
|
|||
|
|
@ -20,17 +20,33 @@ import {
|
|||
} from '../../components';
|
||||
import { TopbarContainer } from '../../containers';
|
||||
|
||||
import { loadData } from './OrderPage.duck';
|
||||
import { loadData, setInitialValues } from './OrderPage.duck';
|
||||
import css from './OrderPage.css';
|
||||
|
||||
// OrderPage handles data loading
|
||||
// It show loading data text or OrderDetailsPanel (and later also another panel for messages).
|
||||
export const OrderPageComponent = props => {
|
||||
const { currentUser, fetchOrderError, intl, params, scrollingDisabled, transaction } = props;
|
||||
const {
|
||||
currentUser,
|
||||
fetchOrderError,
|
||||
messageSendingFailedToTransaction,
|
||||
intl,
|
||||
params,
|
||||
scrollingDisabled,
|
||||
transaction,
|
||||
} = props;
|
||||
const currentTransaction = ensureTransaction(transaction);
|
||||
const currentListing = ensureListing(currentTransaction.listing);
|
||||
const listingTitle = currentListing.attributes.title;
|
||||
|
||||
if (messageSendingFailedToTransaction) {
|
||||
// TODO: render error message with other messages
|
||||
console.error(
|
||||
'failed to send initial message to transaction:',
|
||||
messageSendingFailedToTransaction
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect users with someone else's direct link to their own inbox/orders page.
|
||||
const isDataAvailable =
|
||||
currentUser &&
|
||||
|
|
@ -95,6 +111,7 @@ const { bool, oneOf, shape, string } = PropTypes;
|
|||
OrderPageComponent.propTypes = {
|
||||
currentUser: propTypes.currentUser,
|
||||
fetchOrderError: propTypes.error,
|
||||
messageSendingFailedToTransaction: propTypes.uuid,
|
||||
intl: intlShape.isRequired,
|
||||
params: shape({ id: string }).isRequired,
|
||||
scrollingDisabled: bool.isRequired,
|
||||
|
|
@ -103,7 +120,7 @@ OrderPageComponent.propTypes = {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { fetchOrderError, transactionRef } = state.OrderPage;
|
||||
const { fetchOrderError, transactionRef, messageSendingFailedToTransaction } = state.OrderPage;
|
||||
const { currentUser } = state.user;
|
||||
const transactions = getMarketplaceEntities(state, transactionRef ? [transactionRef] : []);
|
||||
const transaction = transactions.length > 0 ? transactions[0] : null;
|
||||
|
|
@ -111,6 +128,7 @@ const mapStateToProps = state => {
|
|||
return {
|
||||
currentUser,
|
||||
fetchOrderError,
|
||||
messageSendingFailedToTransaction,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
transaction,
|
||||
};
|
||||
|
|
@ -118,6 +136,7 @@ const mapStateToProps = state => {
|
|||
|
||||
const OrderPage = compose(connect(mapStateToProps), injectIntl)(OrderPageComponent);
|
||||
|
||||
OrderPage.setInitialValues = setInitialValues;
|
||||
OrderPage.loadData = loadData;
|
||||
|
||||
export default OrderPage;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 0 0px 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply --marketplaceInputStyles;
|
||||
|
||||
|
|
@ -21,12 +17,10 @@
|
|||
|
||||
@media (--viewportMedium) {
|
||||
height: 35px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
height: 42px;
|
||||
padding: 6px 0 14px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,11 +32,59 @@
|
|||
border-bottom-color: var(--failColor);
|
||||
}
|
||||
|
||||
.paymentHeading {
|
||||
margin: 0 0 14px 0;
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0 0 26px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.paymentLabel {
|
||||
margin: 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageHeading {
|
||||
color: var(--matterColorAnti);
|
||||
margin: 40px 0 14px 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 41px 0 26px 0;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 40px 0 26px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.messageLabel {
|
||||
margin: 0 0 5px 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.messageOptional {
|
||||
color: var(--matterColorAnti);
|
||||
}
|
||||
|
||||
.message {
|
||||
border-bottom-color: var(--matterColorAnti);
|
||||
}
|
||||
|
||||
.submitContainer {
|
||||
margin-top: auto;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 133px;
|
||||
margin-top: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +93,7 @@
|
|||
color: var(--matterColorAnti);
|
||||
text-align: center;
|
||||
padding: 0 42px;
|
||||
margin: 28px 0 0 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
/* TODO this is ugly overwrite to fix unconsistent font styles */
|
||||
|
|
@ -60,9 +103,12 @@
|
|||
}
|
||||
|
||||
.submitButton {
|
||||
margin-top: 21px;
|
||||
margin-top: 22px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 26px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 18px;
|
||||
margin-top: 17px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,19 @@
|
|||
import React, { Component } from 'react';
|
||||
import { fakeIntl } from '../../util/test-data';
|
||||
import StripePaymentForm from './StripePaymentForm';
|
||||
|
||||
class StripePaymentFormExample extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { token: null };
|
||||
}
|
||||
render() {
|
||||
const handleSubmit = token => {
|
||||
this.setState({ token });
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<StripePaymentForm {...this.props} onSubmit={handleSubmit} />
|
||||
{this.state.token ? <p>Token: {this.state.token}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Empty = {
|
||||
component: StripePaymentFormExample,
|
||||
props: { intl: fakeIntl, formId: 'StripePaymentFormExample' },
|
||||
component: StripePaymentForm,
|
||||
props: {
|
||||
formId: 'StripePaymentFormExample',
|
||||
authorDisplayName: 'Janne K',
|
||||
paymentInfo: 'You might or might not be charged yet',
|
||||
onChange: values => {
|
||||
console.log('form onChange:', values);
|
||||
},
|
||||
onSubmit: values => {
|
||||
console.log('form onSubmit:', values);
|
||||
},
|
||||
intl: fakeIntl,
|
||||
},
|
||||
group: 'forms',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { Form, PrimaryButton } from '../../components';
|
||||
import { Form, PrimaryButton, ExpandingTextarea } from '../../components';
|
||||
import * as log from '../../util/log';
|
||||
import config from '../../config';
|
||||
|
||||
|
|
@ -70,6 +70,14 @@ const cardStyles = {
|
|||
},
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
error: null,
|
||||
submitting: false,
|
||||
cardValueValid: false,
|
||||
token: null,
|
||||
message: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Payment form that asks for credit card info using Stripe Elements.
|
||||
*
|
||||
|
|
@ -82,7 +90,7 @@ const cardStyles = {
|
|||
class StripePaymentForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, submitting: false, cardValueValid: false };
|
||||
this.state = initialState;
|
||||
this.handleCardValueChange = this.handleCardValueChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
|
@ -105,6 +113,9 @@ class StripePaymentForm extends Component {
|
|||
}
|
||||
});
|
||||
}
|
||||
componentWillReceiveProps() {
|
||||
this.setState(initialState);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
if (this.card) {
|
||||
this.card.removeEventListener('change', this.handleCardValueChange);
|
||||
|
|
@ -112,11 +123,22 @@ class StripePaymentForm extends Component {
|
|||
}
|
||||
}
|
||||
handleCardValueChange(event) {
|
||||
const { intl } = this.props;
|
||||
const { intl, onChange } = this.props;
|
||||
const { error, complete } = event;
|
||||
this.setState({
|
||||
error: error ? stripeErrorTranslation(intl, error) : null,
|
||||
cardValueValid: complete,
|
||||
|
||||
// A change in the card should clear the token and trigger a call
|
||||
// to the onChange prop with the cleared token and the current
|
||||
// message.
|
||||
|
||||
this.setState(prevState => {
|
||||
const { message } = prevState;
|
||||
const token = null;
|
||||
onChange({ token, message });
|
||||
return {
|
||||
error: error ? stripeErrorTranslation(intl, error) : null,
|
||||
cardValueValid: complete,
|
||||
token,
|
||||
};
|
||||
});
|
||||
}
|
||||
handleSubmit(event) {
|
||||
|
|
@ -128,6 +150,13 @@ class StripePaymentForm extends Component {
|
|||
}
|
||||
|
||||
const { intl, onSubmit } = this.props;
|
||||
|
||||
if (this.state.token) {
|
||||
// Token already fetched for the current card value
|
||||
onSubmit({ token: this.state.token, message: this.state.message.trim() });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ submitting: true });
|
||||
|
||||
this.stripe
|
||||
|
|
@ -138,10 +167,11 @@ class StripePaymentForm extends Component {
|
|||
this.setState({
|
||||
submitting: false,
|
||||
error: stripeErrorTranslation(intl, error),
|
||||
token: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({ submitting: false });
|
||||
onSubmit(token.id);
|
||||
this.setState({ submitting: false, token: token.id });
|
||||
onSubmit({ token: token.id, message: this.state.message.trim() });
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
@ -157,7 +187,16 @@ class StripePaymentForm extends Component {
|
|||
});
|
||||
}
|
||||
render() {
|
||||
const { className, rootClassName, inProgress, formId, paymentInfo } = this.props;
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
inProgress,
|
||||
formId,
|
||||
paymentInfo,
|
||||
onChange,
|
||||
authorDisplayName,
|
||||
intl,
|
||||
} = this.props;
|
||||
const submitInProgress = this.state.submitting || inProgress;
|
||||
const submitDisabled = !this.state.cardValueValid || submitInProgress;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
|
@ -166,9 +205,35 @@ class StripePaymentForm extends Component {
|
|||
[css.cardError]: this.state.error,
|
||||
});
|
||||
|
||||
const messagePlaceholder = intl.formatMessage(
|
||||
{ id: 'StripePaymentForm.messagePlaceholder' },
|
||||
{ name: authorDisplayName }
|
||||
);
|
||||
|
||||
const handleMessageChange = e => {
|
||||
// A change in the message should call the onChange prop with
|
||||
// the current token and the new message.
|
||||
const message = e.target.value;
|
||||
this.setState(prevState => {
|
||||
const { token } = prevState;
|
||||
const newState = { token, message };
|
||||
onChange(newState);
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const messageOptionalText = (
|
||||
<span className={css.messageOptional}>
|
||||
<FormattedMessage id="StripePaymentForm.messageOptionalText" />
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form className={classes} onSubmit={this.handleSubmit}>
|
||||
<label className={css.label} htmlFor={`${formId}-card`}>
|
||||
<h3 className={css.paymentHeading}>
|
||||
<FormattedMessage id="StripePaymentForm.paymentHeading" />
|
||||
</h3>
|
||||
<label className={css.paymentLabel} htmlFor={`${formId}-card`}>
|
||||
<FormattedMessage id="StripePaymentForm.creditCardDetails" />
|
||||
</label>
|
||||
<div
|
||||
|
|
@ -179,8 +244,21 @@ class StripePaymentForm extends Component {
|
|||
}}
|
||||
/>
|
||||
{this.state.error ? <span style={{ color: 'red' }}>{this.state.error}</span> : null}
|
||||
<h3 className={css.messageHeading}>
|
||||
<FormattedMessage id="StripePaymentForm.messageHeading" />
|
||||
</h3>
|
||||
<label className={css.messageLabel} htmlFor={`${formId}-message`}>
|
||||
<FormattedMessage id="StripePaymentForm.messageLabel" values={{ messageOptionalText }} />
|
||||
</label>
|
||||
<ExpandingTextarea
|
||||
id={`${formId}-message`}
|
||||
className={css.message}
|
||||
placeholder={messagePlaceholder}
|
||||
value={this.state.message}
|
||||
onChange={handleMessageChange}
|
||||
/>
|
||||
<div className={css.submitContainer}>
|
||||
{paymentInfo ? <p className={css.paymentInfo}>{paymentInfo}</p> : null}
|
||||
<p className={css.paymentInfo}>{paymentInfo}</p>
|
||||
<PrimaryButton
|
||||
className={css.submitButton}
|
||||
type="submit"
|
||||
|
|
@ -199,7 +277,7 @@ StripePaymentForm.defaultProps = {
|
|||
className: null,
|
||||
rootClassName: null,
|
||||
inProgress: false,
|
||||
paymentInfo: null,
|
||||
onChange: () => null,
|
||||
};
|
||||
|
||||
const { bool, func, string } = PropTypes;
|
||||
|
|
@ -211,7 +289,9 @@ StripePaymentForm.propTypes = {
|
|||
formId: string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
paymentInfo: string,
|
||||
onChange: func,
|
||||
paymentInfo: string.isRequired,
|
||||
authorDisplayName: string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StripePaymentForm);
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ const routeConfiguration = () => {
|
|||
name: 'OrderDetailsPage',
|
||||
component: props => <OrderPage {...props} tab="details" />,
|
||||
loadData: OrderPage.loadData,
|
||||
setInitialValues: OrderPage.setInitialValues,
|
||||
},
|
||||
{
|
||||
path: '/order/:id/discussion',
|
||||
|
|
@ -191,6 +192,7 @@ const routeConfiguration = () => {
|
|||
name: 'OrderDiscussionPage',
|
||||
component: props => <OrderPage {...props} tab="discussion" />,
|
||||
loadData: OrderPage.loadData,
|
||||
setInitialValues: OrderPage.setInitialValues,
|
||||
},
|
||||
{
|
||||
path: '/sale/:id',
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@
|
|||
"CheckoutPage.listingNotFoundError": "Unfortunately the listing is not available anymore.",
|
||||
"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.speculateTransactionError": "Failed to fetch breakdown information.",
|
||||
"CheckoutPage.title": "Book {listingTitle}",
|
||||
|
|
@ -521,6 +520,11 @@
|
|||
"StripeBankAccountTokenInputField.unsupportedCountry": "Country not supported: {country}",
|
||||
"StripePaymentForm.creditCardDetails": "Credit card details",
|
||||
"StripePaymentForm.genericError": "Could not handle payment data. Please try again.",
|
||||
"StripePaymentForm.messageHeading": "Message",
|
||||
"StripePaymentForm.messageLabel": "Say hello to your host {messageOptionalText}",
|
||||
"StripePaymentForm.messageOptionalText": "• optional",
|
||||
"StripePaymentForm.messagePlaceholder": "Hello {name}! I'm looking forward to…",
|
||||
"StripePaymentForm.paymentHeading": "Payment",
|
||||
"StripePaymentForm.stripe.api_connection_error": "Could not connect to Stripe API.",
|
||||
"StripePaymentForm.stripe.api_error": "Error in Stripe API.",
|
||||
"StripePaymentForm.stripe.authentication_error": "Could not authenticate with Stripe.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue