Merge pull request #101 from sharetribe/new-listing-connect-to-stripe

Connect provider bank account in new listing form
This commit is contained in:
Kimmo Puputti 2017-04-05 09:53:18 +03:00 committed by GitHub
commit e6664f8d45
10 changed files with 191 additions and 32 deletions

View file

@ -9,7 +9,7 @@ import css from './StripeBankAccountToken.css';
const DEBOUNCE_WAIT_TIME = 200;
const ErrorMessage = props => <p style={{ color: 'red' }}>{props.children}</p>;
const ErrorMessage = props => <span style={{ color: 'red' }}>{props.children}</span>;
ErrorMessage.propTypes = { children: PropTypes.any.isRequired };
/**

View file

@ -40,3 +40,7 @@
composes: error;
}
.submitButton {
margin-top: 1rem;
}

View file

@ -1,9 +1,12 @@
import React, { Component, PropTypes } from 'react';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { isEqual } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
import config from '../../config';
import * as propTypes from '../../util/propTypes';
import {
noEmptyArray,
maxLength,
@ -17,6 +20,7 @@ import {
LocationAutocompleteInput,
Button,
Input,
StripeBankAccountToken,
} from '../../components';
import css from './EditListingForm.css';
@ -111,7 +115,7 @@ RenderAddImage.propTypes = {
type: string.isRequired,
};
class EditListingForm extends Component {
export class EditListingFormComponent extends Component {
constructor(props) {
super(props);
this.handleInitialize = this.handleInitialize.bind(this);
@ -155,6 +159,7 @@ class EditListingForm extends Component {
pristine,
saveActionMsg = 'Save listing',
submitting,
selectedPlace,
} = this.props;
const titleRequiredMessage = intl.formatMessage({ id: 'EditListingForm.titleRequired' });
const maxLengthMessage = intl.formatMessage(
@ -172,11 +177,17 @@ class EditListingForm extends Component {
const locationNotRecognizedMessage = intl.formatMessage({
id: 'EditListingForm.locationNotRecognized',
});
const bankAccountNumberRequiredMessage = intl.formatMessage({
id: 'EditListingForm.bankAccountNumberRequired',
});
const descriptionRequiredMessage = intl.formatMessage({
id: 'EditListingForm.descriptionRequired',
});
const pricePlaceholderMessage = intl.formatMessage({ id: 'EditListingForm.pricePlaceholder' });
const country = selectedPlace ? selectedPlace.country : null;
const currency = config.currencyConfig.currency;
return (
<form onSubmit={handleSubmit}>
<Field
@ -241,21 +252,55 @@ class EditListingForm extends Component {
component={EnhancedTextArea}
validate={[required(descriptionRequiredMessage)]}
/>
<Button type="submit" disabled={pristine || submitting || disabled}>{saveActionMsg}</Button>
{country
? <Field
name="bankAccountToken"
component={StripeBankAccountToken}
props={{ country, currency }}
validate={required(bankAccountNumberRequiredMessage)}
/>
: null}
<Button
className={css.submitButton}
type="submit"
disabled={pristine || submitting || disabled}
>
{saveActionMsg}
</Button>
</form>
);
}
}
EditListingForm.defaultProps = { initData: {} };
EditListingFormComponent.defaultProps = { initData: {}, selectedPlace: null };
EditListingForm.propTypes = {
EditListingFormComponent.propTypes = {
...formPropTypes,
initData: shape({ title: string, description: string }),
selectedPlace: propTypes.place,
intl: intlShape.isRequired,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
onSubmit: func.isRequired,
};
export default reduxForm({ form: 'EditListingForm' })(injectIntl(EditListingForm));
const formName = 'EditListingForm';
// When a field depends on the value of another field, we must connect
// to the store and select the required values to inject to the
// component.
//
// See: http://redux-form.com/6.6.1/examples/selectingFormValues/
const selector = formValueSelector(formName);
const mapStateToProps = state => {
const location = selector(state, 'location');
return {
selectedPlace: location && location.selectedPlace ? location.selectedPlace : null,
};
};
export default compose(connect(mapStateToProps), reduxForm({ form: formName }), injectIntl)(
EditListingFormComponent
);

View file

@ -4,13 +4,18 @@
// (react-sortable-hoc uses them)
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import EditListingForm from './EditListingForm';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { EditListingFormComponent } from './EditListingForm';
const noop = () => null;
describe('EditListingForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<EditListingForm
images={[]}
<EditListingFormComponent
{...fakeFormProps}
intl={fakeIntl}
dispatch={noop}
onImageUpload={v => v}
onSubmit={v => v}
onUpdateImageOrder={v => v}

View file

@ -1,19 +1,86 @@
exports[`EditListingForm matches snapshot 1`] = `
<Connect(Form(InjectIntl(EditListingForm)))
destroyOnUnmount={true}
enableReinitialize={false}
forceUnregisterOnUnmount={false}
form="EditListingForm"
getFormState={[Function]}
images={Array []}
keepDirtyOnReinitialize={false}
onImageUpload={[Function]}
onSubmit={[Function]}
onUpdateImageOrder={[Function]}
persistentSubmitErrors={false}
pure={true}
shouldAsyncValidate={[Function]}
shouldValidate={[Function]}
touchOnBlur={true}
touchOnChange={false} />
<form
onSubmit={[Function]}>
<Field
component={[Function]}
label="Title"
name="title"
type="text"
validate={
Array [
[Function],
[Function],
]
} />
<Field
component={[Function]}
currencyConfig={
Object {
"currency": "USD",
"currencyDisplay": "symbol",
"maximumFractionDigits": 2,
"minimumFractionDigits": 2,
"style": "currency",
"subUnitDivisor": 100,
"useGrouping": true,
}
}
label="Price"
name="price"
placeholder="EditListingForm.pricePlaceholder"
validate={
Array [
[Function],
]
} />
<h3>
Images
</h3>
<AddImages
images={Array []}
onSortEnd={[Function]}>
<Field
accept="image/*"
component={[Function]}
label="+ Add image"
name="addImage"
onChange={[Function]}
type="file" />
<Field
component={[Function]}
name="images"
type="hidden"
validate={
Array [
[Function],
]
} />
</AddImages>
<Field
component={[Function]}
format={null}
label="Location"
name="location"
validate={
Array [
[Function],
[Function],
]
} />
<Field
component={[Function]}
label="Description"
name="description"
validate={
Array [
[Function],
]
} />
<Button
className={null}
disabled={true}
type="submit">
Save listing
</Button>
</form>
`;

View file

@ -1,4 +1,3 @@
/* eslint-disable no-console */
import { showListingsSuccess as globalShowListingsSuccess } from '../../ducks/sdk.duck';
const requestAction = actionType => params => ({ type: actionType, payload: { params } });
@ -48,6 +47,7 @@ export default function reducer(state = initialState, action = {}) {
case CREATE_LISTING_SUCCESS:
return { ...state, submittedListingId: payload.data.id, redirectToListing: true };
case CREATE_LISTING_ERROR:
// eslint-disable-next-line no-console
console.error(payload);
return { ...state, createListingsError: payload, redirectToListing: false };
@ -56,6 +56,7 @@ export default function reducer(state = initialState, action = {}) {
case SHOW_LISTINGS_SUCCESS:
return initialState;
case SHOW_LISTINGS_ERROR:
// eslint-disable-next-line no-console
console.error(payload);
return { ...state, showListingsError: payload, redirectToListing: false };
@ -75,6 +76,7 @@ export default function reducer(state = initialState, action = {}) {
return { ...state, images };
}
case UPLOAD_IMAGE_ERROR: {
// eslint-disable-next-line no-console
console.error(payload);
const { id, error } = payload;
const { file } = state.images[id];
@ -138,8 +140,11 @@ export function requestShowListing(actionPayload) {
};
}
export function requestCreateListing(actionPayload) {
export function requestCreateListing(data) {
return (dispatch, getState, sdk) => {
const { bankAccountToken, country, ...actionPayload } = data;
// eslint-disable-next-line no-console
console.log('TODO: call API with Stripe token:', bankAccountToken, 'and country:', country);
dispatch(createListing(actionPayload));
return sdk.listings
.create(actionPayload)

View file

@ -14,8 +14,8 @@ import {
} from './EditListingPage.duck';
const formatRequestData = values => {
const { description, images, location, price, title } = values;
const { selectedPlace: { address, origin } } = location;
const { description, images, location, price, title, bankAccountToken } = values;
const { selectedPlace: { address, origin, country } } = location;
return {
address,
@ -24,6 +24,8 @@ const formatRequestData = values => {
images: images.map(i => i.imageId),
price,
title,
country,
bankAccountToken,
};
};

View file

@ -1,7 +1,7 @@
exports[`EditListingPageComponent matches snapshot 1`] = `
<Connect(withRouter(PageLayout))
title="EditListingPage.titleCreateListing">
<ReduxForm
<Connect(ReduxForm)
images={Array []}
initData={Object {}}
onImageUpload={[Function]}

View file

@ -1,6 +1,7 @@
{
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
"AuthenticationPage.loginRequiredFor": "You must log in to view the page at",
"EditListingForm.bankAccountNumberRequired": "You need to add a bank account number.",
"EditListingForm.descriptionRequired": "You need to add a description.",
"EditListingForm.imageRequired": "You need to add at least one image.",
"EditListingForm.locationNotRecognized": "We didn't recognize this location. Please try another location.",

View file

@ -47,3 +47,33 @@ export const fakeIntl = {
formatTime: d => d,
now: d => d,
};
const noop = () => null;
export const fakeFormProps = {
anyTouched: false,
asyncValidating: false,
dirty: false,
form: 'fakeTestForm',
invalid: false,
pristine: true,
clearSubmit: noop,
touch: noop,
untouch: noop,
submit: noop,
reset: noop,
initialize: noop,
handleSubmit: noop,
destroy: noop,
clearAsyncError: noop,
change: noop,
blur: noop,
autofill: noop,
asyncValidate: noop,
valid: true,
submitSucceeded: false,
submitFailed: false,
submitting: false,
pure: true,
initialized: true,
};