diff --git a/CHANGELOG.md b/CHANGELOG.md
index e408305d..7d808fcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,11 @@ way to update this template, but currently, we follow a pattern:
## Upcoming version 2019-XX-XX
+- [add] Complete rewrite to `PayoutDetailsForm` due to breaking changes in Stripe API.
+ [#1049](https://github.com/sharetribe/flex-template-web/pull/1049)
+ - You should track all your customizations to `PayoutDetailsForm` and related changes in
+ `user.duck.js` and elsewhere before merging this upstream-update.
+ - You should update Stripe API to "2019-02-19" or later
- [add] Booking: use attributes `displayStart` and `displayEnd`, instead of reading booking period
directly from `start` and `end` attributes.
[#1050](https://github.com/sharetribe/flex-template-web/pull/1050)
diff --git a/README.md b/README.md
index 00e26b9c..ea3d8f0a 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,6 @@ yarn run config # add the mandato
yarn run dev # start the dev server, this will open a browser in localhost:3000
```
-### If you are using Stripe account created after 19th of February 2019 creating compan accounts is temporalily unavailable.
-
-This is due the changes in Stripe API (https://stripe.com/docs/upgrades#api-changelog). Issues with
-individual accounts were fixed in release
-[2.12.0](https://github.com/sharetribe/flex-template-web/releases/tag/v2.12.0).
-
You can also follow along the
[Getting started with FTW](https://www.sharetribe.com/docs/tutorials/getting-started-with-ftw/)
tutorial in the [Flex Docs website](https://www.sharetribe.com/docs/).
diff --git a/package.json b/package.json
index b22a0478..ef514d92 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
"redux-thunk": "^2.3.0",
"sanitize.css": "^5.0.0",
"seedrandom": "^2.4.4",
- "sharetribe-flex-sdk": "^1.2.0",
+ "sharetribe-flex-sdk": "^1.3.0",
"sharetribe-scripts": "1.1.5",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.9",
diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js
index 4dc5b903..1ba249cc 100644
--- a/src/components/EditListingWizard/EditListingWizard.js
+++ b/src/components/EditListingWizard/EditListingWizard.js
@@ -156,7 +156,7 @@ class EditListingWizard extends Component {
handlePublishListing(id) {
const { onPublishListingDraft, currentUser } = this.props;
const stripeConnected =
- currentUser && currentUser.attributes && currentUser.attributes.stripeConnected;
+ currentUser && currentUser.stripeAccount && !!currentUser.stripeAccount.id;
if (stripeConnected) {
onPublishListingDraft(id);
} else {
@@ -172,9 +172,8 @@ class EditListingWizard extends Component {
}
handlePayoutSubmit(values) {
- const { fname: firstName, lname: lastName, ...rest } = values;
this.props
- .onPayoutDetailsSubmit({ firstName, lastName, ...rest })
+ .onPayoutDetailsSubmit(values)
.then(() => {
this.setState({ showPayoutDetails: false });
this.props.onManageDisableScrolling('EditListingWizard.payoutModal', false);
@@ -289,6 +288,7 @@ class EditListingWizard extends Component {
className={css.payoutDetails}
inProgress={fetchInProgress}
createStripeAccountError={errors ? errors.createStripeAccountError : null}
+ currentUserId={this.props.currentUser.id}
onChange={onPayoutDetailsFormChange}
onSubmit={this.handlePayoutSubmit}
/>
diff --git a/src/components/FieldBirthdayInput/FieldBirthdayInput.js b/src/components/FieldBirthdayInput/FieldBirthdayInput.js
index 6152592e..96896483 100644
--- a/src/components/FieldBirthdayInput/FieldBirthdayInput.js
+++ b/src/components/FieldBirthdayInput/FieldBirthdayInput.js
@@ -145,6 +145,7 @@ class BirthdayInputComponent extends Component {
{dateLabel}
{monthLabel}
{yearLabel}
{
const inputProps = {
- ...props,
type: 'text',
format: format,
parse: parse,
+ ...props,
};
return ;
diff --git a/src/components/FieldSelect/FieldSelect.css b/src/components/FieldSelect/FieldSelect.css
index 1cba1eba..69ab9261 100644
--- a/src/components/FieldSelect/FieldSelect.css
+++ b/src/components/FieldSelect/FieldSelect.css
@@ -6,6 +6,7 @@
.select {
color: var(--matterColorAnti);
border-bottom-color: var(--attentionColor);
+ padding-right: 20px;
& > option {
color: var(--matterColor);
diff --git a/src/components/ModalMissingInformation/ModalMissingInformation.js b/src/components/ModalMissingInformation/ModalMissingInformation.js
index 40457033..51fa1553 100644
--- a/src/components/ModalMissingInformation/ModalMissingInformation.js
+++ b/src/components/ModalMissingInformation/ModalMissingInformation.js
@@ -78,7 +78,7 @@ class ModalMissingInformation extends Component {
const emailUnverified = !!currentUser.id && !currentUser.attributes.emailVerified;
const emailVerificationNeeded = hasListingsOrOrders && emailUnverified;
- const stripeAccountMissing = !!currentUser.id && !currentUser.attributes.stripeConnected;
+ const stripeAccountMissing = !!currentUser.id && !currentUser.stripeAccount;
const stripeAccountNeeded = currentUserHasListings && stripeAccountMissing;
// Show reminder
diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
index 9ba55f5d..584f98ce 100644
--- a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
+++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
@@ -392,7 +392,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -1084,7 +1083,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -1776,7 +1774,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -2468,7 +2465,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -3170,7 +3166,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -3867,7 +3862,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -4557,7 +4551,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
"firstName": "customer first name",
"lastName": "customer last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "customer",
@@ -5249,7 +5242,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
@@ -5941,7 +5933,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
@@ -6633,7 +6624,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
@@ -7325,7 +7315,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
@@ -8027,7 +8016,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
@@ -8724,7 +8712,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
@@ -9414,7 +9401,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
"firstName": "provider first name",
"lastName": "provider last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "provider",
diff --git a/src/config.js b/src/config.js
index cc12b4d5..57d0ad06 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,10 +1,6 @@
import * as custom from './marketplace-custom-config.js';
import defaultLocationSearches from './default-location-searches';
-import {
- stripePublishableKey,
- useDeprecatedLegalEntityWithStripe,
- stripeSupportedCountries,
-} from './stripe-config';
+import { stripePublishableKey, stripeSupportedCountries } from './stripe-config';
const env = process.env.REACT_APP_ENV;
const dev = process.env.REACT_APP_ENV === 'development';
@@ -212,7 +208,6 @@ const config = {
stripe: {
publishableKey: stripePublishableKey,
supportedCountries: stripeSupportedCountries,
- useDeprecatedLegalEntityWithStripe,
},
canonicalRootURL,
address: {
diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js
index d3890c3c..04dede19 100644
--- a/src/containers/EditListingPage/EditListingPage.js
+++ b/src/containers/EditListingPage/EditListingPage.js
@@ -1,5 +1,5 @@
import React from 'react';
-import PropTypes from 'prop-types';
+import { bool, func, object, shape, string, oneOf } from 'prop-types';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { intlShape, injectIntl } from 'react-intl';
@@ -16,7 +16,7 @@ import { LISTING_STATE_DRAFT, LISTING_STATE_PENDING_APPROVAL, propTypes } from '
import { ensureOwnListing } from '../../util/data';
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
-import { stripeAccountClearError, createStripeAccount } from '../../ducks/user.duck';
+import { stripeAccountClearError, createStripeAccount } from '../../ducks/stripe.duck';
import { EditListingWizard, NamedRedirect, Page } from '../../components';
import { TopbarContainer } from '../../containers';
@@ -210,8 +210,6 @@ EditListingPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
-const { bool, func, object, shape, string, oneOf } = PropTypes;
-
EditListingPageComponent.propTypes = {
createStripeAccountError: propTypes.error,
currentUser: propTypes.currentUser,
@@ -249,7 +247,8 @@ EditListingPageComponent.propTypes = {
const mapStateToProps = state => {
const page = state.EditListingPage;
- const { createStripeAccountInProgress, createStripeAccountError, currentUser } = state.user;
+ const { createStripeAccountInProgress, createStripeAccountError } = state.stripe;
+ const { currentUser } = state.user;
const fetchInProgress = createStripeAccountInProgress;
diff --git a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap
index 634ef15e..6cf9aa0c 100644
--- a/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap
+++ b/src/containers/ListingPage/__snapshots__/ListingPage.test.js.snap
@@ -210,7 +210,6 @@ exports[`ListingPage matches snapshot 1`] = `
"firstName": "user-2 first name",
"lastName": "user-2 last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "user-2",
diff --git a/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap b/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
index 4429f8c2..a6048b43 100644
--- a/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
+++ b/src/containers/PasswordChangePage/__snapshots__/PasswordChangePage.test.js.snap
@@ -87,7 +87,6 @@ exports[`PasswordChangePage matches snapshot 1`] = `
"firstName": "user1 first name",
"lastName": "user1 last name",
},
- "stripeConnected": true,
},
"id": UUID {
"uuid": "user1",
diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js
index a00cc246..9d0972f4 100644
--- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js
+++ b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js
@@ -1,4 +1,5 @@
-import { fetchCurrentUser, createStripeAccount } from '../../ducks/user.duck';
+import { createStripeAccount } from '../../ducks/stripe.duck';
+import { fetchCurrentUser } from '../../ducks/user.duck';
// ================ Action types ================ //
diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js
index 320bbbf9..88f75e3d 100644
--- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js
+++ b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js
@@ -6,7 +6,7 @@ import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { ensureCurrentUser } from '../../util/data';
import { propTypes } from '../../util/types';
import { isScrollingDisabled } from '../../ducks/UI.duck';
-import { stripeAccountClearError } from '../../ducks/user.duck';
+import { stripeAccountClearError } from '../../ducks/stripe.duck';
import {
LayoutSideNavigation,
LayoutWrapperMain,
@@ -37,7 +37,10 @@ export const PayoutPreferencesPageComponent = props => {
const ensuredCurrentUser = ensureCurrentUser(currentUser);
const currentUserLoaded = !!ensuredCurrentUser.id;
- const { stripeConnected } = ensuredCurrentUser.attributes;
+ const stripeConnected =
+ currentUserLoaded &&
+ !!ensuredCurrentUser.stripeAccount &&
+ !!ensuredCurrentUser.stripeAccount.id;
const tabs = [
{
@@ -76,11 +79,6 @@ export const PayoutPreferencesPageComponent = props => {
message = ;
}
- const handlePayoutDetailsSubmit = values => {
- const { fname: firstName, lname: lastName, ...rest } = values;
- onPayoutDetailsFormSubmit({ firstName, lastName, ...rest });
- };
-
const showForm =
currentUserLoaded && (payoutDetailsSaveInProgress || payoutDetailsSaved || !stripeConnected);
const form = showForm ? (
@@ -91,7 +89,8 @@ export const PayoutPreferencesPageComponent = props => {
submitButtonText={intl.formatMessage({ id: 'PayoutPreferencesPage.submitButtonText' })}
createStripeAccountError={createStripeAccountError}
onChange={onPayoutDetailsFormChange}
- onSubmit={handlePayoutDetailsSubmit}
+ onSubmit={onPayoutDetailsFormSubmit}
+ currentUserId={ensuredCurrentUser.id}
/>
) : null;
@@ -144,7 +143,8 @@ PayoutPreferencesPageComponent.propTypes = {
};
const mapStateToProps = state => {
- const { createStripeAccountError, currentUser } = state.user;
+ const { createStripeAccountError } = state.stripe;
+ const { currentUser } = state.user;
const { payoutDetailsSaveInProgress, payoutDetailsSaved } = state.PayoutPreferencesPage;
return {
currentUser,
diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js
index b9a1f5c8..5211503f 100644
--- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js
+++ b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js
@@ -1,16 +1,14 @@
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
-import { fakeIntl, createCurrentUser } from '../../util/test-data';
+import { fakeIntl, createCurrentUser, createStripeAccount } from '../../util/test-data';
import { PayoutPreferencesPageComponent } from './PayoutPreferencesPage';
const noop = () => null;
describe('PayoutPreferencesPage', () => {
it('matches snapshot with Stripe not connected', () => {
- const currentUser = createCurrentUser('stripe-not-connected', {
- stripeConnected: false,
- });
- expect(currentUser.attributes.stripeConnected).toEqual(false);
+ const currentUser = createCurrentUser('stripe-not-connected');
+ expect(currentUser.stripeAccount).toBeUndefined();
const tree = renderShallow(
{
expect(tree).toMatchSnapshot();
});
it('matches snapshot with Stripe connected', () => {
- const currentUser = createCurrentUser('stripe-connected');
- expect(currentUser.attributes.stripeConnected).toEqual(true);
+ const currentUser = createCurrentUser(
+ 'stripe-connected',
+ {},
+ {
+ stripeAccount: createStripeAccount(),
+ }
+ );
+ expect(currentUser.stripeAccount).toBeDefined();
const tree = renderShallow(
{
expect(tree).toMatchSnapshot();
});
it('matches snapshot with details submitted', () => {
- const currentUser = createCurrentUser('stripe-connected');
- expect(currentUser.attributes.stripeConnected).toEqual(true);
+ const currentUser = createCurrentUser(
+ 'stripe-connected',
+ {},
+ {
+ stripeAccount: createStripeAccount(),
+ }
+ );
+ expect(currentUser.stripeAccount).toBeDefined();
const tree = renderShallow(
{
+ return p.personToken === payload.personToken
+ ? { ...payload, createStripePersonInProgress: false }
+ : p;
+ }),
+ };
+ case PERSON_CREATE_ERROR:
+ console.error(payload);
+ return {
+ ...state,
+ persons: state.persons.map(p => {
+ return p.personToken === payload.personToken
+ ? { ...p, createStripePersonInProgress: false, createStripePersonError: payload.error }
+ : p;
+ }),
+ };
+
+ default:
+ return state;
+ }
+}
+
+// ================ Action creators ================ //
+
+export const stripeAccountCreateRequest = () => ({ type: STRIPE_ACCOUNT_CREATE_REQUEST });
+
+export const stripeAccountCreateSuccess = stripeAccount => ({
+ type: STRIPE_ACCOUNT_CREATE_SUCCESS,
+ payload: stripeAccount,
+});
+
+export const stripeAccountCreateError = e => ({
+ type: STRIPE_ACCOUNT_CREATE_ERROR,
+ payload: e,
+ error: true,
+});
+
+export const stripeAccountClearError = () => ({
+ type: STRIPE_ACCOUNT_CLEAR_ERROR,
+});
+
+export const accountOpenerCreateRequest = personToken => ({
+ type: ACCOUNT_OPENER_CREATE_REQUEST,
+ payload: personToken,
+});
+
+export const accountOpenerCreateSuccess = payload => ({
+ type: ACCOUNT_OPENER_CREATE_SUCCESS,
+ payload,
+});
+
+export const accountOpenerCreateError = payload => ({
+ type: ACCOUNT_OPENER_CREATE_ERROR,
+ payload,
+ error: true,
+});
+
+export const personCreateRequest = personToken => ({
+ type: PERSON_CREATE_REQUEST,
+ payload: personToken,
+});
+
+export const personCreateSuccess = payload => ({
+ type: PERSON_CREATE_SUCCESS,
+ payload,
+});
+
+export const personCreateError = payload => ({
+ type: PERSON_CREATE_ERROR,
+ payload,
+ error: true,
+});
+
+// ================ Thunks ================ //
+
+// Util: rename address fields to match Stripe API specifications
+const formatAddress = address => {
+ const { city, streetAddress, postalCode, state, province } = address;
+ const cityMaybe = city ? { city } : {};
+ const streetAddressMaybe = streetAddress ? { line1: streetAddress } : {};
+ const postalCodeMaybe = postalCode ? { postal_code: postalCode } : {};
+ const stateMaybe = state ? { state } : province ? { state: province } : {};
+
+ return {
+ ...cityMaybe,
+ ...streetAddressMaybe,
+ ...postalCodeMaybe,
+ ...stateMaybe,
+ };
+};
+
+// Util: rename personToken params to match Stripe API specifications
+const personTokenParams = (personData, country) => {
+ const {
+ isAccountOpener,
+ fname: firstName,
+ lname: lastName,
+ birthDate,
+ address,
+ personalIdNumber,
+ email,
+ phone,
+ role,
+ ownershipPercentage,
+ title,
+ } = personData;
+
+ const addressMaybe = address ? { address: formatAddress(address) } : {};
+ const emailMaybe = email ? { email } : {};
+ const phoneMaybe = phone ? { phone } : {};
+ const idNumberMaybe =
+ country === 'US'
+ ? { ssn_last_4: personalIdNumber }
+ : personalIdNumber
+ ? { personal_id_number: personalIdNumber }
+ : {};
+
+ const accountOpenerMaybe = isAccountOpener ? { account_opener: true } : {};
+ const jobTitleMaybe = title ? { title } : {};
+ const ownerMaybe = role && role.find(r => r === 'owner') ? { owner: true } : {};
+ const ownershipPercentageMaybe = ownershipPercentage
+ ? { percent_ownership: Number.parseFloat(ownershipPercentage) }
+ : {};
+
+ const relationshipMaybe =
+ isAccountOpener || title || role
+ ? {
+ relationship: {
+ ...accountOpenerMaybe,
+ ...jobTitleMaybe,
+ ...ownerMaybe,
+ ...ownershipPercentageMaybe,
+ },
+ }
+ : {};
+
+ return {
+ person: {
+ first_name: firstName,
+ last_name: lastName,
+ dob: birthDate,
+ ...addressMaybe,
+ ...idNumberMaybe,
+ ...emailMaybe,
+ ...phoneMaybe,
+ ...relationshipMaybe,
+ },
+ };
+};
+
+const createStripePerson = (personParams, country, stripe) => (dispatch, getState, sdk) => {
+ const { isAccountOpener } = personParams;
+ let personToken = 'no-token';
+ return stripe
+ .createToken('person', personTokenParams(personParams, country))
+ .then(response => {
+ personToken = response.token.id;
+
+ // Request to create person in progress
+ // Account opener is mandatory for all - so it's handled separately
+ const createPersonRequest = isAccountOpener
+ ? accountOpenerCreateRequest
+ : personCreateRequest;
+ dispatch(createPersonRequest({ personToken }));
+
+ return sdk.stripePersons.create({ personToken }, { expand: true });
+ })
+ .then(response => {
+ // Stripe person created successfully
+ const createPersonSuccess = isAccountOpener
+ ? accountOpenerCreateSuccess
+ : personCreateSuccess;
+ dispatch(createPersonSuccess({ personToken, stripePerson: response.data.data }));
+
+ return response;
+ })
+ .catch(err => {
+ const e = storableError(err);
+
+ // Stripe person creation failed
+ const createPersonError = isAccountOpener ? accountOpenerCreateError : personCreateError;
+ dispatch(createPersonError({ personToken, error: e }));
+
+ const stripeMessage =
+ e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta
+ ? e.apiErrors[0].meta.stripeMessage
+ : null;
+ log.error(err, 'create-stripe-person-failed', { stripeMessage });
+ throw e;
+ });
+};
+
+// accountData should be either individual or company
+const bankAccountTokenParams = accountData => accountData.bankAccountToken;
+const businessProfileParams = accountData => {
+ const { mcc, url } =
+ accountData && accountData.businessProfile ? accountData.businessProfile : {};
+ return mcc && url
+ ? {
+ businessProfileMCC: mcc,
+ businessProfileURL: url,
+ }
+ : {};
+};
+
+// Util: rename accountToken params to match Stripe API specifications
+const accountTokenParamsForCompany = company => {
+ const { address, name, phone, taxId } = company;
+ const addressMaybe = address ? { address: formatAddress(address) } : {};
+ const phoneMaybe = phone ? { phone } : {};
+ return {
+ business_type: 'company',
+ company: {
+ name,
+ tax_id: taxId,
+ ...addressMaybe,
+ ...phoneMaybe,
+ },
+ tos_shown_and_accepted: true,
+ };
+};
+
+export const createStripeCompanyAccount = (payoutDetails, stripe) => (dispatch, getState, sdk) => {
+ const { company, country, accountOpener, persons = [] } = payoutDetails;
+ const state = getState();
+ let stripeAccount =
+ state.stripe && state.stripe.stripeAccount ? state.stripe.stripeAccount : null;
+
+ dispatch(stripeAccountCreateRequest());
+
+ const createPersons = () => {
+ return Promise.all([
+ dispatch(createStripePerson({ ...accountOpener, isAccountOpener: true }, country, stripe)),
+ ...persons.map(p => dispatch(createStripePerson(p, country, stripe))),
+ ]);
+ };
+
+ // If stripeAccount exists, stripePersons call must have failed.
+ // Retry person creation
+ if (stripeAccount) {
+ return createPersons()
+ .then(response => {
+ // Return created stripe account from this thunk function
+ return stripeAccount;
+ })
+ .catch(err => {
+ const e = storableError(err);
+ dispatch(stripeAccountCreateError(e));
+ const stripeMessage =
+ e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta
+ ? e.apiErrors[0].meta.stripeMessage
+ : null;
+ log.error(err, 'create-stripe-company-persons-failed', { stripeMessage });
+ throw e;
+ });
+ }
+
+ return stripe
+ .createToken('account', accountTokenParamsForCompany(company))
+ .then(response => {
+ const accountToken = response.token.id;
+ const bankAccountToken = bankAccountTokenParams(company);
+ const stripeAccountParams = {
+ accountToken,
+ bankAccountToken,
+ country,
+ ...businessProfileParams(company),
+ };
+ return sdk.stripeAccount.create(stripeAccountParams, { expand: true });
+ })
+ .then(response => {
+ stripeAccount = response;
+ dispatch(stripeAccountCreateSuccess(response.data.data));
+ return createPersons();
+ })
+ .then(response => {
+ // Return created stripe account from this thunk function
+ return stripeAccount;
+ })
+ .catch(err => {
+ const e = storableError(err);
+ dispatch(stripeAccountCreateError(e));
+ const stripeMessage =
+ e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta
+ ? e.apiErrors[0].meta.stripeMessage
+ : null;
+ const errorFlag = !stripeAccount
+ ? 'create-stripe-company-account-failed'
+ : 'create-stripe-company-persons-failed';
+ log.error(err, errorFlag, { stripeMessage });
+ throw e;
+ });
+};
+
+const accountTokenParamsForIndividual = (individual, country) => {
+ const {
+ fname: firstName,
+ lname: lastName,
+ birthDate,
+ address,
+ phone,
+ email,
+ personalIdNumber,
+ } = individual;
+ const addressMaybe = address ? { address: formatAddress(address) } : {};
+ const dobMaybe = birthDate ? { dob: birthDate } : {};
+ const emailMaybe = email ? { email } : {};
+ const phoneMaybe = phone ? { phone } : {};
+ const idNumberMaybe =
+ country === 'US'
+ ? { ssn_last_4: personalIdNumber }
+ : personalIdNumber
+ ? { personal_id_number: personalIdNumber }
+ : {};
+
+ return {
+ business_type: 'individual',
+ individual: {
+ first_name: firstName,
+ last_name: lastName,
+ ...dobMaybe,
+ ...addressMaybe,
+ ...emailMaybe,
+ ...phoneMaybe,
+ ...idNumberMaybe,
+ },
+ tos_shown_and_accepted: true,
+ };
+};
+
+export const createStripeIndividualAccount = (payoutDetails, stripe) => (
+ dispatch,
+ getState,
+ sdk
+) => {
+ const { country, individual } = payoutDetails;
+ let stripeAccount;
+ dispatch(stripeAccountCreateRequest());
+
+ return stripe
+ .createToken('account', accountTokenParamsForIndividual(individual, country))
+ .then(response => {
+ const accountToken = response.token.id;
+ const bankAccountToken = bankAccountTokenParams(individual);
+ const stripeAccountParams = {
+ accountToken,
+ bankAccountToken,
+ country,
+ ...businessProfileParams(individual),
+ };
+ return sdk.stripeAccount.create(stripeAccountParams, { expand: true });
+ })
+ .then(response => {
+ stripeAccount = response;
+ dispatch(stripeAccountCreateSuccess(response.data.data));
+ return stripeAccount;
+ })
+ .catch(err => {
+ const e = storableError(err);
+ dispatch(stripeAccountCreateError(e));
+ const stripeMessage =
+ e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta
+ ? e.apiErrors[0].meta.stripeMessage
+ : null;
+ log.error(err, 'create-stripe-individual-account-failed', { stripeMessage });
+ throw e;
+ });
+};
+
+export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) => {
+ if (typeof window === 'undefined' || !window.Stripe) {
+ throw new Error('Stripe must be loaded for submitting PayoutPreferences');
+ }
+
+ const stripe = window.Stripe(config.stripe.publishableKey);
+
+ if (payoutDetails.accountType === 'individual') {
+ return dispatch(createStripeIndividualAccount(payoutDetails, stripe));
+ } else {
+ return dispatch(createStripeCompanyAccount(payoutDetails, stripe));
+ }
+};
diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js
index d6a9a179..a2b96945 100644
--- a/src/ducks/user.duck.js
+++ b/src/ducks/user.duck.js
@@ -1,12 +1,10 @@
-import omitBy from 'lodash/omitBy';
-import isUndefined from 'lodash/isUndefined';
-import config from '../config';
import { denormalisedResponseEntities, ensureOwnListing } from '../util/data';
import { storableError } from '../util/errors';
import { transitionsToRequested } from '../util/transaction';
import { LISTING_STATE_DRAFT } from '../util/types';
import * as log from '../util/log';
import { authInfo } from './Auth.duck';
+import { stripeAccountCreateSuccess } from './stripe.duck.js';
// ================ Action types ================ //
@@ -16,12 +14,6 @@ export const CURRENT_USER_SHOW_ERROR = 'app/user/CURRENT_USER_SHOW_ERROR';
export const CLEAR_CURRENT_USER = 'app/user/CLEAR_CURRENT_USER';
-export const STRIPE_ACCOUNT_CREATE_REQUEST = 'app/user/STRIPE_ACCOUNT_CREATE_REQUEST';
-export const STRIPE_ACCOUNT_CREATE_SUCCESS = 'app/user/STRIPE_ACCOUNT_CREATE_SUCCESS';
-export const STRIPE_ACCOUNT_CREATE_ERROR = 'app/user/STRIPE_ACCOUNT_CREATE_ERROR';
-
-export const STRIPE_ACCOUNT_CLEAR_ERROR = 'app/user/STRIPE_ACCOUNT_CLEAR_ERROR';
-
export const FETCH_CURRENT_USER_HAS_LISTINGS_REQUEST =
'app/user/FETCH_CURRENT_USER_HAS_LISTINGS_REQUEST';
export const FETCH_CURRENT_USER_HAS_LISTINGS_SUCCESS =
@@ -51,8 +43,6 @@ export const SEND_VERIFICATION_EMAIL_ERROR = 'app/user/SEND_VERIFICATION_EMAIL_E
const initialState = {
currentUser: null,
currentUserShowError: null,
- createStripeAccountInProgress: false,
- createStripeAccountError: null,
currentUserHasListings: false,
currentUserHasListingsError: null,
currentUserNotificationCount: 0,
@@ -110,18 +100,6 @@ export default function reducer(state = initialState, action = {}) {
console.error(payload); // eslint-disable-line
return { ...state, currentUserHasOrdersError: payload };
- case STRIPE_ACCOUNT_CREATE_REQUEST:
- return { ...state, createStripeAccountError: null, createStripeAccountInProgress: true };
- case STRIPE_ACCOUNT_CREATE_SUCCESS:
- return { ...state, createStripeAccountInProgress: false };
- case STRIPE_ACCOUNT_CREATE_ERROR:
- // eslint-disable-next-line no-console
- console.error(payload);
- return { ...state, createStripeAccountError: payload, createStripeAccountInProgress: false };
-
- case STRIPE_ACCOUNT_CLEAR_ERROR:
- return { ...state, createStripeAccountError: null, createStripeAccountInProgress: false };
-
case SEND_VERIFICATION_EMAIL_REQUEST:
return {
...state,
@@ -178,23 +156,6 @@ export const currentUserShowError = e => ({
export const clearCurrentUser = () => ({ type: CLEAR_CURRENT_USER });
-export const stripeAccountCreateRequest = () => ({ type: STRIPE_ACCOUNT_CREATE_REQUEST });
-
-export const stripeAccountCreateSuccess = response => ({
- type: STRIPE_ACCOUNT_CREATE_SUCCESS,
- payload: response,
-});
-
-export const stripeAccountCreateError = e => ({
- type: STRIPE_ACCOUNT_CREATE_ERROR,
- payload: e,
- error: true,
-});
-
-export const stripeAccountClearError = () => ({
- type: STRIPE_ACCOUNT_CLEAR_ERROR,
-});
-
const fetchCurrentUserHasListingsRequest = () => ({
type: FETCH_CURRENT_USER_HAS_LISTINGS_REQUEST,
});
@@ -341,7 +302,7 @@ export const fetchCurrentUser = () => (dispatch, getState, sdk) => {
}
const params = {
- include: ['profileImage'],
+ include: ['profileImage', 'stripeAccount'],
'fields.image': ['variants.square-small', 'variants.square-small2x'],
};
@@ -354,6 +315,11 @@ export const fetchCurrentUser = () => (dispatch, getState, sdk) => {
}
const currentUser = entities[0];
+ // Save stripeAccount to store.stripe.stripeAccount if it exists
+ if (currentUser.stripeAccount) {
+ dispatch(stripeAccountCreateSuccess(currentUser.stripeAccount));
+ }
+
// set current user id to the logger
log.setUserId(currentUser.id.uuid);
dispatch(currentUserShowSuccess(currentUser));
@@ -377,144 +343,6 @@ export const fetchCurrentUser = () => (dispatch, getState, sdk) => {
});
};
-export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) => {
- if (typeof window === 'undefined' || !window.Stripe) {
- throw new Error('Stripe must be loaded for submitting PayoutPreferences');
- }
-
- const stripe = window.Stripe(config.stripe.publishableKey);
-
- dispatch(stripeAccountCreateRequest());
-
- const { accountType, country } = payoutDetails;
-
- let payoutDetailValues;
- if (accountType === 'company') {
- payoutDetailValues = payoutDetails['company'];
- } else {
- payoutDetailValues = payoutDetails['individual'];
- }
-
- const {
- firstName,
- lastName,
- birthDate,
- address,
- bankAccountToken,
- personalIdNumber,
- companyName,
- companyTaxId,
- personalAddress,
- additionalOwners,
- } = payoutDetailValues;
-
- const hasProvince = address.province && !address.state;
-
- const addressValue = {
- city: address.city,
- line1: address.streetAddress,
- postal_code: address.postalCode,
- state: hasProvince ? address.province : address.state ? address.state : '',
- };
-
- let personalAddressValue;
- if (personalAddress) {
- personalAddressValue = {
- city: personalAddress.city,
- line1: personalAddress.streetAddress,
- postal_code: personalAddress.postalCode,
- state: hasProvince
- ? personalAddress.province
- : personalAddress.state
- ? personalAddress.state
- : '',
- };
- }
-
- const additionalOwnersValue = additionalOwners
- ? additionalOwners.map(owner => {
- return {
- first_name: owner.firstName,
- last_name: owner.lastName,
- dob: owner.birthDate,
- address: {
- city: owner.city,
- line1: owner.streetAddress,
- postal_code: owner.postalCode,
- state: hasProvince ? owner.province : owner.state ? owner.state : '',
- },
- };
- })
- : [];
-
- const idNumber =
- country === 'US' ? { ssn_last_4: personalIdNumber } : { personal_id_number: personalIdNumber };
-
- let params;
-
- // You can check which API version you are using from Stripe Dasboard -> Developers.
- // If you are using older version than '2019-02-19'
- // edit 'useDeprecatedLegalEntityWithStripe' config in the stripe-config.js
-
- const isNewAPI = !config.stripe.useDeprecatedLegalEntityWithStripe;
-
- if (isNewAPI) {
- params = {
- business_type: 'individual',
- individual: {
- first_name: firstName,
- last_name: lastName,
- address: omitBy(addressValue, isUndefined),
- dob: birthDate,
- ...idNumber,
- },
- tos_shown_and_accepted: true,
- };
- } else {
- params = {
- legal_entity: {
- first_name: firstName,
- last_name: lastName,
- address: omitBy(addressValue, isUndefined),
- dob: birthDate,
- type: accountType,
- business_name: companyName,
- business_tax_id: companyTaxId,
- personal_address: personalAddressValue,
- additional_owners: additionalOwnersValue,
- ...idNumber,
- },
- tos_shown_and_accepted: true,
- };
- }
-
- let accountResponse;
-
- return stripe
- .createToken('account', params)
- .then(response => {
- const accountToken = response.token.id;
- return sdk.currentUser.createStripeAccount({ accountToken, bankAccountToken, country });
- })
- .then(response => {
- accountResponse = response;
- return dispatch(fetchCurrentUser());
- })
- .then(() => {
- dispatch(stripeAccountCreateSuccess(accountResponse));
- })
- .catch(err => {
- const e = storableError(err);
- dispatch(stripeAccountCreateError(e));
- const stripeMessage =
- e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta
- ? e.apiErrors[0].meta.stripeMessage
- : null;
- log.error(err, 'create-stripe-account-failed', { stripeMessage });
- throw e;
- });
-};
-
export const sendVerificationEmail = () => (dispatch, getState, sdk) => {
if (verificationSendingInProgress(getState())) {
return Promise.reject(new Error('Verification email sending already in progress'));
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsAccountOpener.js b/src/forms/PayoutDetailsForm/PayoutDetailsAccountOpener.js
new file mode 100644
index 00000000..98817d09
--- /dev/null
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsAccountOpener.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import { bool, object, string } from 'prop-types';
+import { FormattedMessage, intlShape } from 'react-intl';
+
+import PayoutDetailsAddress from './PayoutDetailsAddress';
+import PayoutDetailsPersonalDetails from './PayoutDetailsPersonalDetails';
+
+import css from './PayoutDetailsForm.css';
+
+const PayoutDetailsAccountOpener = props => {
+ const {
+ fieldId,
+ country,
+ disabled,
+ form,
+ intl,
+ showEmailField,
+ showOrganizationTitleField,
+ showOwnerField,
+ showPersonalAddressField,
+ showPersonalIdNumberField,
+ showPhoneNumberField,
+ values,
+ } = props;
+
+ const showOwnershipPercentageField =
+ showOwnerField &&
+ values &&
+ values[fieldId] &&
+ values[fieldId].role &&
+ values[fieldId].role.find(r => r === 'owner');
+
+ return (
+
+
+
+ {showPersonalAddressField ? (
+
+ ) : null}
+
+
+
+
+
+
+ );
+};
+
+PayoutDetailsAccountOpener.defaultProps = {
+ disabled: false,
+ showEmailField: false,
+ showOrganizationTitleField: false,
+ showOwnerField: false,
+ showPersonalAddressField: false,
+ showPersonalIdNumberField: false,
+ showPhoneNumberField: false,
+ values: null,
+};
+
+PayoutDetailsAccountOpener.propTypes = {
+ country: string.isRequired,
+ fieldId: string.isRequired,
+ form: object.isRequired,
+ disabled: bool,
+ showEmailField: bool,
+ showOrganizationTitleField: bool,
+ showOwnerField: bool,
+ showPersonalAddressField: bool,
+ showPersonalIdNumberField: bool,
+ showPhoneNumberField: bool,
+ values: object,
+
+ // from parent
+ intl: intlShape.isRequired,
+};
+
+export default PayoutDetailsAccountOpener;
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsAdditionalPersons.js b/src/forms/PayoutDetailsForm/PayoutDetailsAdditionalPersons.js
new file mode 100644
index 00000000..37661cb4
--- /dev/null
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsAdditionalPersons.js
@@ -0,0 +1,144 @@
+import React from 'react';
+import { bool, func, object, string } from 'prop-types';
+import { FormattedMessage, intlShape } from 'react-intl';
+import { FieldArray } from 'react-final-form-arrays';
+import { ExternalLink, IconAdd, IconClose, InlineTextButton } from '../../components';
+
+import PayoutDetailsAddress from './PayoutDetailsAddress';
+import PayoutDetailsPersonalDetails from './PayoutDetailsPersonalDetails';
+
+import css from './PayoutDetailsForm.css';
+
+const PayoutDetailsAdditionalPersons = props => {
+ const {
+ fieldId,
+ country,
+ disabled,
+ form,
+ intl,
+ push,
+ showEmailField,
+ showOrganizationTitleField,
+ showOwnerField,
+ showPersonalAddressField,
+ showPersonalIdNumberField,
+ showPhoneNumberField,
+ values,
+ } = props;
+
+ const additionalPersonInfoLink = (
+
+
+
+ );
+
+ const showOwnershipPercentageField = index =>
+ showOwnerField &&
+ values &&
+ values[fieldId] &&
+ values[fieldId][index] &&
+ values[fieldId][index].role &&
+ values[fieldId][index].role.find(r => r === 'owner');
+
+ return (
+
+
+ {({ fields }) =>
+ fields.map((name, index) => (
+
+
fields.remove(index)}
+ style={{ cursor: 'pointer' }}
+ >
+
+
+
+
+
+
+ {showPersonalAddressField ? (
+
+ ) : null}
+
+ ))
+ }
+
+
+
+ push(fieldId, undefined)}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+PayoutDetailsAdditionalPersons.defaultProps = {
+ disabled: false,
+ showEmailField: false,
+ showOrganizationTitleField: false,
+ showOwnerField: false,
+ showPersonalAddressField: false,
+ showPersonalIdNumberField: false,
+ showPhoneNumberField: false,
+ values: null,
+};
+
+PayoutDetailsAdditionalPersons.propTypes = {
+ country: string.isRequired,
+ fieldId: string.isRequired,
+ form: object.isRequired,
+ push: func.isRequired,
+ disabled: bool,
+ showEmailField: bool,
+ showOrganizationTitleField: bool,
+ showOwnerField: bool,
+ showPersonalAddressField: bool,
+ showPersonalIdNumberField: bool,
+ showPhoneNumberField: bool,
+ values: object,
+
+ // from parent
+ intl: intlShape.isRequired,
+};
+
+export default PayoutDetailsAdditionalPersons;
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js b/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js
index bda6342f..51a63184 100644
--- a/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js
@@ -1,8 +1,8 @@
import React from 'react';
-import { bool, object, string } from 'prop-types';
-import { FieldSelect, FieldTextInput } from '../../components';
-import * as validators from '../../util/validators';
import { intlShape } from 'react-intl';
+import { bool, object, string } from 'prop-types';
+import * as validators from '../../util/validators';
+import { FieldSelect, FieldTextInput } from '../../components';
import { stripeCountryConfigs } from './PayoutDetailsForm';
import css from './PayoutDetailsForm.css';
@@ -155,7 +155,7 @@ const PayoutDetailsAddress = props => {
disabled={disabled}
className={css.state}
type="text"
- autoComplete="state"
+ autoComplete="address-level1"
label={stateLabel}
placeholder={statePlaceholder}
validate={stateRequired}
@@ -169,7 +169,7 @@ const PayoutDetailsAddress = props => {
name={`${fieldId}.province`}
disabled={disabled}
className={css.selectCountry}
- autoComplete="province"
+ autoComplete="address-level1"
label={provinceLabel}
validate={provinceRequired}
>
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js b/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js
index ee5381f6..0243a827 100644
--- a/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js
@@ -1,8 +1,8 @@
import React from 'react';
import { bool, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
-import { StripeBankAccountTokenInputField } from '../../components';
import * as validators from '../../util/validators';
+import { StripeBankAccountTokenInputField } from '../../components';
import { stripeCountryConfigs } from './PayoutDetailsForm';
import css from './PayoutDetailsForm.css';
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsBusinessProfile.js b/src/forms/PayoutDetailsForm/PayoutDetailsBusinessProfile.js
new file mode 100644
index 00000000..a16d08a6
--- /dev/null
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsBusinessProfile.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import { bool, string } from 'prop-types';
+import { intlShape } from 'react-intl';
+import * as validators from '../../util/validators';
+import { FieldSelect, FieldTextInput } from '../../components';
+
+import merchantCategoryCodesUS from './merchantCategoryCodesUS';
+import css from './PayoutDetailsForm.css';
+
+const PayoutDetailsBusinessProfile = props => {
+ const { fieldId, disabled, intl, showBusinessURLField, showMCCForUSField } = props;
+
+ const isBusinessProfileNeeded = showBusinessURLField || showMCCForUSField;
+ const mccLabel = intl.formatMessage({ id: 'PayoutDetailsForm.businessMCCForUSLabel' });
+ const mccPlaceholder = intl.formatMessage({
+ id: 'PayoutDetailsForm.businessMCCForUSPlaceholder',
+ });
+ const mccRequired = validators.required(
+ intl.formatMessage({ id: 'PayoutDetailsForm.businessMCCForUSRequired' })
+ );
+
+ const businessUrlLabel = intl.formatMessage({ id: 'PayoutDetailsForm.businessURLLabel' });
+ const businessUrlPlaceholder = intl.formatMessage({
+ id: 'PayoutDetailsForm.businessURLPlaceholder',
+ });
+
+ const businessUrlRequired = validators.validBusinessURL(
+ intl.formatMessage({ id: 'PayoutDetailsForm.businessURLRequired' })
+ );
+
+ return isBusinessProfileNeeded ? (
+
+ {showMCCForUSField ? (
+
+
+ {mccPlaceholder}
+
+ {merchantCategoryCodesUS.map(merchantCategory => (
+
+ {merchantCategory.label}
+
+ ))}
+
+ ) : null}
+
+ {showBusinessURLField ? (
+
+ ) : null}
+
+ ) : null;
+};
+
+PayoutDetailsBusinessProfile.defaultProps = {
+ fieldId: null,
+ disabled: false,
+ showBusinessURLField: false,
+ showMCCForUSField: false,
+};
+
+PayoutDetailsBusinessProfile.propTypes = {
+ fieldId: string,
+ disabled: bool,
+ showBusinessURLField: bool,
+ showMCCForUSField: bool,
+
+ // from injectIntl
+ intl: intlShape.isRequired,
+};
+
+export default PayoutDetailsBusinessProfile;
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsCompany.js b/src/forms/PayoutDetailsForm/PayoutDetailsCompany.js
new file mode 100644
index 00000000..e0f5385d
--- /dev/null
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsCompany.js
@@ -0,0 +1,137 @@
+import React from 'react';
+import { bool, string } from 'prop-types';
+import { FormattedMessage, intlShape } from 'react-intl';
+import * as validators from '../../util/validators';
+import { FieldPhoneNumberInput, FieldTextInput } from '../../components';
+
+import * as normalizePhoneNumberUS from './normalizePhoneNumberUS';
+import PayoutDetailsBusinessProfile from './PayoutDetailsBusinessProfile';
+import css from './PayoutDetailsForm.css';
+
+const PayoutDetailsCompany = props => {
+ const {
+ fieldId,
+ disabled,
+ intl,
+ country,
+ showBusinessURLField,
+ showMCCForUSField,
+ showPhoneNumberField,
+ } = props;
+
+ const companyNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.companyNameLabel' });
+ const companyNamePlaceholder = intl.formatMessage({
+ id: 'PayoutDetailsForm.companyNamePlaceholder',
+ });
+ const companyNameRequired = validators.required(
+ intl.formatMessage({
+ id: 'PayoutDetailsForm.companyNameRequired',
+ })
+ );
+
+ const companyTaxIdLabel = intl.formatMessage({
+ id: `PayoutDetailsForm.companyTaxIdLabel.${country}`,
+ });
+ const companyTaxIdPlaceholder = intl.formatMessage(
+ {
+ id: 'PayoutDetailsForm.companyTaxIdPlaceholder',
+ },
+ {
+ idName: companyTaxIdLabel,
+ }
+ );
+ const companyTaxIdRequired = validators.required(
+ intl.formatMessage(
+ {
+ id: 'PayoutDetailsForm.companyTaxIdRequired',
+ },
+ {
+ idName: companyTaxIdLabel,
+ }
+ )
+ );
+
+ const phoneLabel = intl.formatMessage({ id: 'PayoutDetailsForm.companyPhoneLabel' });
+ const phonePlaceholder = intl.formatMessage({
+ id: 'PayoutDetailsForm.companyPhonePlaceholder',
+ });
+ const phoneNumberForUSRequired = validators.required(
+ intl.formatMessage({ id: 'PayoutDetailsForm.companyPhoneRequired' })
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {showPhoneNumberField ? (
+
+ ) : null}
+
+ );
+};
+
+PayoutDetailsCompany.defaultProps = {
+ fieldId: null,
+ disabled: false,
+ showBusinessURLField: false,
+ showMCCForUSField: false,
+ showPhoneNumberField: false,
+};
+
+PayoutDetailsCompany.propTypes = {
+ fieldId: string,
+ disabled: bool,
+ country: string.isRequired,
+ showBusinessURLField: bool,
+ showMCCForUSField: bool,
+ showPhoneNumberField: bool,
+
+ // from injectIntl
+ intl: intlShape.isRequired,
+};
+
+export default PayoutDetailsCompany;
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsCompanyAccount.js b/src/forms/PayoutDetailsForm/PayoutDetailsCompanyAccount.js
new file mode 100644
index 00000000..5a82addf
--- /dev/null
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsCompanyAccount.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import { bool, object, shape } from 'prop-types';
+import { compose } from 'redux';
+import { injectIntl, intlShape } from 'react-intl';
+
+import PayoutDetailsAddress from './PayoutDetailsAddress';
+import PayoutDetailsCompany from './PayoutDetailsCompany';
+import PayoutDetailsBankDetails from './PayoutDetailsBankDetails';
+import PayoutDetailsAccountOpener from './PayoutDetailsAccountOpener';
+import PayoutDetailsAdditionalPersons from './PayoutDetailsAdditionalPersons';
+import { stripeCountryConfigs } from './PayoutDetailsForm';
+
+const CompanyAccountComponent = props => {
+ const { fieldRenderProps, intl } = props;
+ const { disabled, form, values } = fieldRenderProps;
+ const { country } = values;
+ const { push } = form && form.mutators ? form.mutators : {};
+
+ const companyConfig =
+ country && stripeCountryConfigs(country).companyConfig
+ ? stripeCountryConfigs(country).companyConfig
+ : {};
+
+ const showBusinessURLField = !!companyConfig.businessURL;
+ const showCompanyPhoneNumberField = !!companyConfig.companyPhone;
+ const showMCCForUSField = !!companyConfig.mccForUS;
+ const showPersonalEmailField = !!companyConfig.personalEmail;
+ const showPersonalAddressField = !!companyConfig.personalAddress;
+ const showPersonalIdNumberField =
+ !!companyConfig.personalIdNumberRequired || !!companyConfig.ssnLast4Required;
+ const showPersonalPhoneNumberField = !!companyConfig.personalPhone;
+ const showOwnerFields = !!companyConfig.owners;
+
+ return (
+
+ {country ? (
+
+
+
+
+
+
+
+ {showOwnerFields ? (
+
+ ) : null}
+
+ ) : null}
+
+ );
+};
+
+CompanyAccountComponent.defaultProps = {
+ id: null,
+ disabled: false,
+};
+
+CompanyAccountComponent.propTypes = {
+ fieldRenderProps: shape({
+ disabled: bool,
+ form: object.isRequired,
+ values: object,
+ }).isRequired,
+
+ // from injectIntl
+ intl: intlShape.isRequired,
+};
+
+const PayoutDetailsCompanyAccount = compose(injectIntl)(CompanyAccountComponent);
+
+export default PayoutDetailsCompanyAccount;
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsForm.css b/src/forms/PayoutDetailsForm/PayoutDetailsForm.css
index 67687201..57c8a10b 100644
--- a/src/forms/PayoutDetailsForm/PayoutDetailsForm.css
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsForm.css
@@ -79,8 +79,9 @@
width: calc(60% - 9px);
}
-.taxId {
- margin-top: 24px;
+.selectMCC,
+.textInputRow {
+ margin-bottom: 24px;
}
.error {
@@ -110,6 +111,10 @@
@apply --marketplaceH4FontStyles;
}
+.missingStripeKey {
+ color: var(--failColor);
+}
+
.personalAddressContainer {
margin-bottom: 28px;
}
@@ -134,25 +139,6 @@
.closeIcon {
@apply --marketplaceModalCloseIcon;
-}
-
-.additionalOwnerWrapper {
- margin-bottom: 35px;
-
- @media (--viewportMedium) {
- margin-bottom: 56px;
- }
-}
-
-.additionalOwnerWrapper .sectionContainer {
- margin-bottom: 24px;
-}
-
-.additionalOwnerLabel {
- display: inline-block;
-}
-
-.closeIcon {
margin-right: 5px;
}
@@ -163,7 +149,40 @@
padding-top: 1px;
}
-.additionalOwnerInfo {
+.roleField {
+ border: 0;
+ padding: 0;
+ margin-bottom: 24px;
+}
+
+.ownershipPercentage {
+ position: relative;
+ max-width: 90px;
+ padding-right: 24px;
+ margin-bottom: 24px;
+ white-space: nowrap;
+
+ &:after {
+ content: '%';
+ position: absolute;
+ top: 36px;
+ right: 6px;
+ }
+}
+
+/* Company account opener */
+.accountOpenerWrapper {
+ margin-bottom: 35px;
+
+ @media (--viewportMedium) {
+ margin-bottom: 56px;
+ }
+}
+.accountOpenerInputsWrapper .sectionContainer {
+ margin-bottom: 24px;
+}
+
+.accountOpenerInfo {
@apply --marketplaceH5FontStyles;
color: var(--matterColorAnti);
margin-top: 0;
@@ -179,6 +198,34 @@
}
}
-.missingStripeKey {
- color: var(--failColor);
+/* Company owners and directors */
+.additionalPersonsWrapper {
+ margin-bottom: 35px;
+
+ @media (--viewportMedium) {
+ margin-bottom: 56px;
+ }
+}
+.additionalPersonWrapper .sectionContainer {
+ margin-bottom: 24px;
+}
+
+.additionalPersonLabel {
+ display: inline-block;
+}
+
+.additionalPersonInfo {
+ @apply --marketplaceH5FontStyles;
+ color: var(--matterColorAnti);
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 5px;
+ padding-bottom: 1px;
+
+ @media (--viewportMedium) {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 6px;
+ padding-bottom: 2px;
+ }
}
diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsForm.js b/src/forms/PayoutDetailsForm/PayoutDetailsForm.js
index 94a127ef..acbd2e32 100644
--- a/src/forms/PayoutDetailsForm/PayoutDetailsForm.js
+++ b/src/forms/PayoutDetailsForm/PayoutDetailsForm.js
@@ -1,17 +1,18 @@
import React from 'react';
-import { bool, object, string } from 'prop-types';
+import { bool, func, object, shape, string } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { Form as FinalForm } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
import classNames from 'classnames';
import config from '../../config';
-import { Button, ExternalLink, FieldRadioButton, FieldSelect, Form } from '../../components';
-import { isStripeInvalidPostalCode } from '../../util/errors';
+import { propTypes } from '../../util/types';
+import { isStripeInvalidPostalCode, isStripeError } from '../../util/errors';
import * as validators from '../../util/validators';
+import { Button, ExternalLink, FieldRadioButton, FieldSelect, Form } from '../../components';
-import PayoutDetailsFormCompany from './PayoutDetailsFormCompany';
-import PayoutDetailsFormIndividual from './PayoutDetailsFormIndividual';
+import PayoutDetailsCompanyAccount from './PayoutDetailsCompanyAccount';
+import PayoutDetailsIndividualAccount from './PayoutDetailsIndividualAccount';
import css from './PayoutDetailsForm.css';
const supportedCountries = config.stripe.supportedCountries.map(c => c.code);
@@ -43,14 +44,13 @@ const PayoutDetailsFormComponent = props => (
pristine,
ready,
submitButtonText,
+ currentUserId,
values,
} = fieldRenderProps;
const { country } = values;
- const usesOldAPI = config.stripe.useDeprecatedLegalEntityWithStripe;
-
- const accountType = usesOldAPI ? values.accountType : 'individual';
+ const accountType = values.accountType;
const individualAccountLabel = intl.formatMessage({
id: 'PayoutDetailsForm.individualAccount',
@@ -87,6 +87,16 @@ const PayoutDetailsFormComponent = props => (
);
+ } else if (isStripeError(createStripeAccountError)) {
+ const stripeMessage = createStripeAccountError.apiErrors[0].meta.stripeMessage;
+ error = (
+
+
+
+ );
} else if (createStripeAccountError) {
error = (
@@ -103,29 +113,27 @@ const PayoutDetailsFormComponent = props => (
return config.stripe.publishableKey ? (