Merge pull request #1049 from sharetribe/fix-stripe-api-breaking-changes

There was breaking changes in Stripe API 2019-02-19
This commit is contained in:
Vesa Luusua 2019-03-27 16:53:03 +02:00 committed by GitHub
commit 1e4c26ac77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1989 additions and 642 deletions

View file

@ -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)

View file

@ -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/).

View file

@ -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",

View file

@ -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}
/>

View file

@ -145,6 +145,7 @@ class BirthdayInputComponent extends Component {
<div className={css.selectWrapper}>
{dateLabel}
<select
autoComplete="bday-day"
disabled={disabled}
id={dateId}
value={selectedValue(this.state.selected.day)}
@ -168,6 +169,7 @@ class BirthdayInputComponent extends Component {
<div className={css.selectWrapper}>
{monthLabel}
<select
autoComplete="bday-month"
disabled={disabled}
id={monthId}
value={selectedValue(this.state.selected.month)}
@ -191,6 +193,7 @@ class BirthdayInputComponent extends Component {
<div className={css.selectWrapper}>
{yearLabel}
<select
autoComplete="bday-year"
disabled={disabled}
id={yearId}
value={selectedValue(this.state.selected.year)}

View file

@ -11,10 +11,10 @@ import { format, parse } from './fiFormatter';
const FieldPhoneNumberInput = props => {
const inputProps = {
...props,
type: 'text',
format: format,
parse: parse,
...props,
};
return <FieldTextInput {...inputProps} />;

View file

@ -6,6 +6,7 @@
.select {
color: var(--matterColorAnti);
border-bottom-color: var(--attentionColor);
padding-right: 20px;
& > option {
color: var(--matterColor);

View file

@ -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

View file

@ -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",

View file

@ -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: {

View file

@ -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;

View file

@ -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",

View file

@ -87,7 +87,6 @@ exports[`PasswordChangePage matches snapshot 1`] = `
"firstName": "user1 first name",
"lastName": "user1 last name",
},
"stripeConnected": true,
},
"id": UUID {
"uuid": "user1",

View file

@ -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 ================ //

View file

@ -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 = <FormattedMessage id="PayoutPreferencesPage.stripeNotConnected" />;
}
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,

View file

@ -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(
<PayoutPreferencesPageComponent
currentUser={currentUser}
@ -25,8 +23,14 @@ describe('PayoutPreferencesPage', () => {
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(
<PayoutPreferencesPageComponent
currentUser={currentUser}
@ -41,8 +45,14 @@ describe('PayoutPreferencesPage', () => {
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(
<PayoutPreferencesPageComponent
currentUser={currentUser}

View file

@ -170,6 +170,11 @@ exports[`PayoutPreferencesPage matches snapshot with Stripe not connected 1`] =
</p>
<InjectIntl(PayoutDetailsFormComponent)
createStripeAccountError={null}
currentUserId={
UUID {
"uuid": "stripe-not-connected",
}
}
disabled={false}
inProgress={false}
onChange={[Function]}
@ -269,6 +274,11 @@ exports[`PayoutPreferencesPage matches snapshot with details submitted 1`] = `
</p>
<InjectIntl(PayoutDetailsFormComponent)
createStripeAccountError={null}
currentUserId={
UUID {
"uuid": "stripe-connected",
}
}
disabled={true}
inProgress={false}
onChange={[Function]}

View file

@ -37,7 +37,6 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
"firstName": "customer1 first name",
"lastName": "customer1 last name",
},
"stripeConnected": true,
},
"id": UUID {
"uuid": "customer1",
@ -242,7 +241,6 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
"firstName": "provider1 first name",
"lastName": "provider1 last name",
},
"stripeConnected": true,
},
"id": UUID {
"uuid": "provider1",

View file

@ -11,6 +11,7 @@ import LocationFilter from './LocationFilter.duck';
import Routing from './Routing.duck';
import UI from './UI.duck';
import marketplaceData from './marketplaceData.duck';
import stripe from './stripe.duck';
import user from './user.duck';
export {
@ -21,5 +22,6 @@ export {
Routing,
UI,
marketplaceData,
stripe,
user,
};

461
src/ducks/stripe.duck.js Normal file
View file

@ -0,0 +1,461 @@
import config from '../config';
import { storableError } from '../util/errors';
import * as log from '../util/log';
// ================ Action types ================ //
export const STRIPE_ACCOUNT_CREATE_REQUEST = 'app/stripe/STRIPE_ACCOUNT_CREATE_REQUEST';
export const STRIPE_ACCOUNT_CREATE_SUCCESS = 'app/stripe/STRIPE_ACCOUNT_CREATE_SUCCESS';
export const STRIPE_ACCOUNT_CREATE_ERROR = 'app/stripe/STRIPE_ACCOUNT_CREATE_ERROR';
export const STRIPE_ACCOUNT_CLEAR_ERROR = 'app/stripe/STRIPE_ACCOUNT_CLEAR_ERROR';
export const ACCOUNT_OPENER_CREATE_REQUEST = 'app/stripe/ACCOUNT_OPENER_CREATE_REQUEST';
export const ACCOUNT_OPENER_CREATE_SUCCESS = 'app/stripe/ACCOUNT_OPENER_CREATE_SUCCESS';
export const ACCOUNT_OPENER_CREATE_ERROR = 'app/stripe/ACCOUNT_OPENER_CREATE_ERROR';
export const PERSON_CREATE_REQUEST = 'app/stripe/PERSON_CREATE_REQUEST';
export const PERSON_CREATE_SUCCESS = 'app/stripe/PERSON_CREATE_SUCCESS';
export const PERSON_CREATE_ERROR = 'app/stripe/PERSON_CREATE_ERROR';
// ================ Reducer ================ //
const initialState = {
createStripeAccountInProgress: false,
createStripeAccountError: null,
createAccountOpenerInProgress: false,
createAccountOpenerError: false,
personAccountOpener: null,
persons: [],
stripeAccount: null,
stripeAccountFetched: false,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case STRIPE_ACCOUNT_CREATE_REQUEST:
return { ...state, createStripeAccountError: null, createStripeAccountInProgress: true };
case STRIPE_ACCOUNT_CREATE_SUCCESS:
return {
...state,
createStripeAccountInProgress: false,
stripeAccount: payload,
stripeAccountFetched: true,
};
case STRIPE_ACCOUNT_CREATE_ERROR:
console.error(payload);
return { ...state, createStripeAccountError: payload, createStripeAccountInProgress: false };
case STRIPE_ACCOUNT_CLEAR_ERROR:
return { ...initialState };
case ACCOUNT_OPENER_CREATE_REQUEST:
return {
...state,
createAccountOpenerError: null,
createAccountOpenerInProgress: true,
};
case ACCOUNT_OPENER_CREATE_SUCCESS:
return { ...state, createAccountOpenerInProgress: false, personAccountOpener: payload };
case ACCOUNT_OPENER_CREATE_ERROR:
console.error(payload);
return { ...state, createAccountOpenerError: payload, createAccountOpenerInProgress: false };
case PERSON_CREATE_REQUEST:
return {
...state,
persons: [
...state.persons,
{
...payload,
createStripePersonError: null,
createStripePersonInProgress: true,
},
],
};
case PERSON_CREATE_SUCCESS:
return {
...state,
persons: state.persons.map(p => {
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));
}
};

View file

@ -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'));

View file

@ -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 (
<div className={css.accountOpenerWrapper}>
<div className={css.accountOpenerInputsWrapper}>
<PayoutDetailsPersonalDetails
accountType="company"
country={country}
disabled={disabled}
fieldId={fieldId}
intl={intl}
showEmailField={showEmailField}
showOrganizationTitleField={showOrganizationTitleField}
showOwnerField={showOwnerField}
showOwnershipPercentageField={!!showOwnershipPercentageField}
showPersonalIdNumberField={showPersonalIdNumberField}
showPhoneNumberField={showPhoneNumberField}
sectionTitle={intl.formatMessage({ id: 'PayoutDetailsForm.accountOpenerTitle' })}
values={values}
/>
{showPersonalAddressField ? (
<PayoutDetailsAddress
className={css.personalAddressContainer}
country={country}
disabled={disabled}
fieldId={`${fieldId}.address`}
form={form}
intl={intl}
/>
) : null}
</div>
<p className={css.accountOpenerInfo}>
<FormattedMessage id="PayoutDetailsForm.accountOpenerInfoText" />
</p>
</div>
);
};
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;

View file

@ -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 = (
<ExternalLink
href="https://support.stripe.com/questions/owners-and-directors"
className={css.termsLink}
>
<FormattedMessage id="PayoutDetailsForm.additionalPersonInfoLink" />
</ExternalLink>
);
const showOwnershipPercentageField = index =>
showOwnerField &&
values &&
values[fieldId] &&
values[fieldId][index] &&
values[fieldId][index].role &&
values[fieldId][index].role.find(r => r === 'owner');
return (
<div className={css.additionalPersonsWrapper}>
<FieldArray id={`${fieldId}`} name={`${fieldId}`}>
{({ fields }) =>
fields.map((name, index) => (
<div className={css.additionalPersonWrapper} key={name}>
<div
className={css.fieldArrayRemove}
onClick={() => fields.remove(index)}
style={{ cursor: 'pointer' }}
>
<span className={css.additionalPersonLabel}>
<IconClose rootClassName={css.closeIcon} size="small" />
<FormattedMessage id="PayoutDetailsForm.additionalPersonRemove" />
</span>
</div>
<PayoutDetailsPersonalDetails
intl={intl}
disabled={disabled}
values={values}
country={country}
fieldId={`${fieldId}.${index}`}
accountType="company"
sectionTitle={intl.formatMessage({ id: 'PayoutDetailsForm.additionalPersonTitle' })}
showEmailField={showEmailField}
showOrganizationTitleField={showOrganizationTitleField}
showOwnerField={showOwnerField}
showOwnershipPercentageField={!!showOwnershipPercentageField(index)}
showPersonalIdNumberField={showPersonalIdNumberField}
showPhoneNumberField={showPhoneNumberField}
showRoleField
/>
{showPersonalAddressField ? (
<PayoutDetailsAddress
className={css.personalAddressContainer}
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId={`${fieldId}.${index}.address`}
/>
) : null}
</div>
))
}
</FieldArray>
<React.Fragment>
<InlineTextButton
type="button"
rootClassName={css.fieldArrayAdd}
onClick={() => push(fieldId, undefined)}
>
<span className={css.additionalPersonLabel}>
<IconAdd rootClassName={css.addIcon} />
<FormattedMessage id="PayoutDetailsForm.additionalPersonLink" />
</span>
</InlineTextButton>
<p className={css.additionalPersonInfo}>
<FormattedMessage
id="PayoutDetailsForm.additionalPersonInfoText"
values={{ additionalPersonInfoLink }}
/>
</p>
</React.Fragment>
</div>
);
};
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;

View file

@ -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}
>

View file

@ -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';

View file

@ -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 ? (
<React.Fragment>
{showMCCForUSField ? (
<FieldSelect
id={`${fieldId}.mcc`}
name={`${fieldId}.mcc`}
className={css.selectMCC}
autoComplete="mcc"
disabled={disabled}
label={mccLabel}
validate={mccRequired}
>
<option disabled value="">
{mccPlaceholder}
</option>
{merchantCategoryCodesUS.map(merchantCategory => (
<option key={merchantCategory.category} value={merchantCategory.mcc}>
{merchantCategory.label}
</option>
))}
</FieldSelect>
) : null}
{showBusinessURLField ? (
<FieldTextInput
id={`${fieldId}.url`}
name={`${fieldId}.url`}
className={css.textInputRow}
autoComplete="url"
disabled={disabled}
label={businessUrlLabel}
placeholder={businessUrlPlaceholder}
type="text"
validate={businessUrlRequired}
/>
) : null}
</React.Fragment>
) : 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;

View file

@ -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 (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.companyDetailsTitle" />
</h3>
<FieldTextInput
id={`${fieldId}.name`}
name={`${fieldId}.name`}
className={css.textInputRow}
autoComplete="organization"
disabled={disabled}
label={companyNameLabel}
placeholder={companyNamePlaceholder}
type="text"
validate={companyNameRequired}
/>
<FieldTextInput
id={`${fieldId}.taxId`}
name={`${fieldId}.taxId`}
className={css.textInputRow}
autoComplete="company-tax-id"
disabled={disabled}
label={companyTaxIdLabel}
placeholder={companyTaxIdPlaceholder}
type="text"
validate={companyTaxIdRequired}
/>
<PayoutDetailsBusinessProfile
disabled={disabled}
fieldId={`${fieldId}.businessProfile`}
showBusinessURLField={showBusinessURLField}
showMCCForUSField={showMCCForUSField}
intl={intl}
/>
{showPhoneNumberField ? (
<FieldPhoneNumberInput
id={`${fieldId}.phone`}
name={`${fieldId}.phone`}
className={css.textInputRow}
autoComplete="tel-national"
label={phoneLabel}
format={normalizePhoneNumberUS.format}
parse={normalizePhoneNumberUS.parse}
placeholder={phonePlaceholder}
type="text"
validate={phoneNumberForUSRequired}
/>
) : null}
</div>
);
};
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;

View file

@ -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 (
<React.Fragment>
{country ? (
<React.Fragment>
<PayoutDetailsCompany
country={country}
disabled={disabled}
fieldId="company"
intl={intl}
showPhoneNumberField={showCompanyPhoneNumberField}
showMCCForUSField={showMCCForUSField}
showBusinessURLField={showBusinessURLField}
/>
<PayoutDetailsAddress
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId="company.address"
/>
<PayoutDetailsBankDetails country={country} disabled={disabled} fieldId="company" />
<PayoutDetailsAccountOpener
country={country}
disabled={disabled}
fieldId="accountOpener"
form={form}
intl={intl}
showEmailField={showPersonalEmailField}
showOrganizationTitleField
showOwnerField={showOwnerFields}
showPersonalAddressField={showPersonalAddressField}
showPersonalIdNumberField={showPersonalIdNumberField}
showPhoneNumberField={showPersonalPhoneNumberField}
values={values}
/>
{showOwnerFields ? (
<PayoutDetailsAdditionalPersons
country={country}
disabled={disabled}
fieldId="persons"
form={form}
intl={intl}
push={push}
showEmailField={showPersonalEmailField}
showOrganizationTitleField
showOwnerField={showOwnerFields}
showPersonalAddressField={showPersonalAddressField}
showPersonalIdNumberField={showPersonalIdNumberField}
showPhoneNumberField={showPersonalPhoneNumberField}
values={values}
/>
) : null}
</React.Fragment>
) : null}
</React.Fragment>
);
};
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;

View file

@ -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;
}
}

View file

@ -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 => (
<FormattedMessage id="PayoutDetailsForm.createStripeAccountFailedInvalidPostalCode" />
</div>
);
} else if (isStripeError(createStripeAccountError)) {
const stripeMessage = createStripeAccountError.apiErrors[0].meta.stripeMessage;
error = (
<div className={css.error}>
<FormattedMessage
id="PayoutDetailsForm.createStripeAccountFailedWithStripeError"
values={{ stripeMessage }}
/>
</div>
);
} else if (createStripeAccountError) {
error = (
<div className={css.error}>
@ -103,29 +113,27 @@ const PayoutDetailsFormComponent = props => (
return config.stripe.publishableKey ? (
<Form className={classes} onSubmit={handleSubmit}>
{usesOldAPI ? (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.accountTypeTitle" />
</h3>
<div className={css.radioButtonRow}>
<FieldRadioButton
id="individual"
name="accountType"
label={individualAccountLabel}
value="individual"
showAsRequired={showAsRequired}
/>
<FieldRadioButton
id="company"
name="accountType"
label={companyAccountLabel}
value="company"
showAsRequired={showAsRequired}
/>
</div>
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.accountTypeTitle" />
</h3>
<div className={css.radioButtonRow}>
<FieldRadioButton
id="individual"
name="accountType"
label={individualAccountLabel}
value="individual"
showAsRequired={showAsRequired}
/>
<FieldRadioButton
id="company"
name="accountType"
label={companyAccountLabel}
value="company"
showAsRequired={showAsRequired}
/>
</div>
) : null}
</div>
{accountType ? (
<React.Fragment>
@ -152,12 +160,16 @@ const PayoutDetailsFormComponent = props => (
</div>
{showIndividual ? (
<PayoutDetailsFormIndividual
<PayoutDetailsIndividualAccount
fieldRenderProps={fieldRenderProps}
country={country}
currentUserId={currentUserId}
/>
) : showCompany ? (
<PayoutDetailsCompanyAccount
fieldRenderProps={fieldRenderProps}
country={country}
/>
) : showCompany ? (
<PayoutDetailsFormCompany fieldRenderProps={fieldRenderProps} country={country} />
) : null}
{error}
@ -195,12 +207,13 @@ const PayoutDetailsFormComponent = props => (
PayoutDetailsFormComponent.defaultProps = {
className: null,
country: null,
createStripeAccountError: null,
disabled: false,
inProgress: false,
ready: false,
submitButtonText: null,
currentUserId: null,
fieldRenderProps: null,
};
PayoutDetailsFormComponent.propTypes = {
@ -210,6 +223,13 @@ PayoutDetailsFormComponent.propTypes = {
inProgress: bool,
ready: bool,
submitButtonText: string,
currentUserId: propTypes.uuid,
fieldRenderProps: shape({
handleSubmit: func,
invalid: bool,
pristine: bool,
values: object,
}),
// from injectIntl
intl: intlShape.isRequired,

View file

@ -1,235 +0,0 @@
import React from 'react';
import { bool, string } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { FieldArray } from 'react-final-form-arrays';
import {
ExternalLink,
FieldTextInput,
IconAdd,
IconClose,
InlineTextButton,
} from '../../components';
import * as validators from '../../util/validators';
import PayoutDetailsAddress from './PayoutDetailsAddress';
import PayoutDetailsBankDetails from './PayoutDetailsBankDetails';
import PayoutDetailsPersonalDetails from './PayoutDetailsPersonalDetails';
import { stripeCountryConfigs } from './PayoutDetailsForm';
import css from './PayoutDetailsForm.css';
// In EU, there can be a maximum of 4 additional owners
const MAX_NUMBER_OF_ADDITIONAL_OWNERS = 4;
const PayoutDetailsFormCompanyComponent = ({ fieldRenderProps }) => {
const {
id,
disabled,
form,
intl,
values,
form: {
mutators: { push },
},
} = fieldRenderProps;
const { country } = values;
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 showPersonalAddressField =
country &&
stripeCountryConfigs(country).companyConfig &&
stripeCountryConfigs(country).companyConfig.personalAddress;
const showAdditionalOwnersField =
country &&
stripeCountryConfigs(country).companyConfig &&
stripeCountryConfigs(country).companyConfig.additionalOwners;
const hasAdditionalOwners = values.company && values.company.additionalOwners;
const hasMaxNumberOfAdditionalOwners =
hasAdditionalOwners &&
values.company.additionalOwners.length >= MAX_NUMBER_OF_ADDITIONAL_OWNERS;
const additionalOwnersInfoLink = (
<ExternalLink
href="https://support.stripe.com/questions/owners-and-directors"
className={css.termsLink}
>
<FormattedMessage id="PayoutDetailsForm.additionalOwnersInfoLink" />
</ExternalLink>
);
return (
<React.Fragment>
{country ? (
<React.Fragment>
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.companyDetailsTitle" />
</h3>
<FieldTextInput
id="company.companyName"
name="company.companyName"
disabled={disabled}
type="text"
autoComplete="company-name"
label={companyNameLabel}
placeholder={companyNamePlaceholder}
validate={companyNameRequired}
/>
<FieldTextInput
id="company.companyTaxId"
name="company.companyTaxId"
className={css.taxId}
disabled={disabled}
type="text"
autoComplete="company-tax-id"
label={companyTaxIdLabel}
placeholder={companyTaxIdPlaceholder}
validate={companyTaxIdRequired}
/>
</div>
<PayoutDetailsAddress
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId="company.address"
/>
<PayoutDetailsBankDetails country={country} disabled={disabled} fieldId="company" />
<PayoutDetailsPersonalDetails
intl={intl}
disabled={disabled}
values={values}
country={country}
fieldId="company"
/>
{showPersonalAddressField ? (
<PayoutDetailsAddress
className={css.personalAddressContainer}
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId="company.personalAddress"
/>
) : null}
{showAdditionalOwnersField ? (
<div className={css.additionalOwnerWrapper}>
<FieldArray id={id} name={'company.additionalOwners'}>
{({ fields }) =>
fields.map((name, index) => (
<div className={css.additionalOwnerWrapper} key={name}>
<div
className={css.fieldArrayRemove}
onClick={() => fields.remove(index)}
style={{ cursor: 'pointer' }}
>
<span className={css.additionalOwnerLabel}>
<IconClose rootClassName={css.closeIcon} size="small" />
<FormattedMessage id="PayoutDetailsForm.additionalOwnerRemove" />
</span>
</div>
<PayoutDetailsPersonalDetails
intl={intl}
disabled={disabled}
values={values}
country={country}
fieldId={`company.additionalOwners.${index}`}
/>
{showPersonalAddressField ? (
<PayoutDetailsAddress
className={css.personalAddressContainer}
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId={`company.additionalOwners.${index}`}
/>
) : null}
</div>
))
}
</FieldArray>
{!hasAdditionalOwners || !hasMaxNumberOfAdditionalOwners ? (
<React.Fragment>
<InlineTextButton
type="button"
rootClassName={css.fieldArrayAdd}
onClick={() => push('company.additionalOwners', undefined)}
>
<span className={css.additionalOwnerLabel}>
<IconAdd rootClassName={css.addIcon} />
<FormattedMessage id="PayoutDetailsForm.additionalOwnerLabel" />
</span>
</InlineTextButton>
<p className={css.additionalOwnerInfo}>
<FormattedMessage
id="PayoutDetailsForm.additionalOwnerInfoText"
values={{ additionalOwnersInfoLink }}
/>
</p>
</React.Fragment>
) : null}
</div>
) : null}
</React.Fragment>
) : null}
</React.Fragment>
);
};
PayoutDetailsFormCompanyComponent.defaultProps = {
id: null,
disabled: false,
};
PayoutDetailsFormCompanyComponent.propTypes = {
id: string,
disabled: bool,
// from injectIntl
intl: intlShape.isRequired,
};
const PayoutDetailsFormCompany = compose(injectIntl)(PayoutDetailsFormCompanyComponent);
export default PayoutDetailsFormCompany;

View file

@ -1,48 +0,0 @@
import React from 'react';
import { bool } from 'prop-types';
import { compose } from 'redux';
import { injectIntl, intlShape } from 'react-intl';
import PayoutDetailsAddress from './PayoutDetailsAddress';
import PayoutDetailsBankDetails from './PayoutDetailsBankDetails';
import PayoutDetailsPersonalDetails from './PayoutDetailsPersonalDetails';
const PayoutDetailsFormIndividualComponent = ({ fieldRenderProps }) => {
const { disabled, form, intl, values } = fieldRenderProps;
const { country } = values;
return (
<React.Fragment>
<PayoutDetailsPersonalDetails
intl={intl}
disabled={disabled}
values={values}
country={country}
fieldId="individual"
/>
<PayoutDetailsAddress
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId="individual.address"
/>
<PayoutDetailsBankDetails country={country} disabled={disabled} fieldId="individual" />
</React.Fragment>
);
};
PayoutDetailsFormIndividualComponent.defaultProps = {
disabled: false,
};
PayoutDetailsFormIndividualComponent.propTypes = {
disabled: bool,
// from injectIntl
intl: intlShape.isRequired,
};
const PayoutDetailsFormIndividual = compose(injectIntl)(PayoutDetailsFormIndividualComponent);
export default PayoutDetailsFormIndividual;

View file

@ -0,0 +1,115 @@
import React from 'react';
import { bool, object, shape } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import { propTypes } from '../../util/types';
import { createResourceLocatorString } from '../../util/routes';
import { stripeCountryConfigs } from './PayoutDetailsForm';
import PayoutDetailsAddress from './PayoutDetailsAddress';
import PayoutDetailsBankDetails from './PayoutDetailsBankDetails';
import PayoutDetailsPersonalDetails from './PayoutDetailsPersonalDetails';
import PayoutDetailsBusinessProfile from './PayoutDetailsBusinessProfile';
import css from './PayoutDetailsForm.css';
const PayoutDetailsIndividualAccountComponent = props => {
const { fieldRenderProps, currentUserId, intl, appConfig } = props;
const { disabled, form, values } = fieldRenderProps;
const { country } = values;
const individualConfig =
country && stripeCountryConfigs(country).individualConfig
? stripeCountryConfigs(country).individualConfig
: {};
const showEmailField = !!individualConfig.personalEmail;
const showPhoneNumberField = !!individualConfig.personalPhone;
const showPersonalIdNumberField =
!!individualConfig.personalIdNumberRequired || !!individualConfig.ssnLast4Required;
const showBusinessURLField = !!individualConfig.businessURL;
const showMCCForUSField = !!individualConfig.mccForUS;
const showBusinssProfileSection = showBusinessURLField || showMCCForUSField;
const hasBusinessURL = values && values.businessProfile && values.businessProfile.url;
// Use user profile page as business_url on this marketplace
// or just fake it if it's dev environment using Stripe test endpoints.
// NOTE: All US accounts need to provide business URL or product description
if (showBusinssProfileSection && !hasBusinessURL && currentUserId) {
const pathToProfilePage = uuid =>
createResourceLocatorString('ProfilePage', routeConfiguration(), { id: uuid }, {});
const defaultBusinessURL =
appConfig && appConfig.canonicalRootURL && !appConfig.dev
? `${config.canonicalRootURL}${pathToProfilePage(currentUserId.uuid)}`
: `https://test-marketplace.com${pathToProfilePage(currentUserId.uuid)}`;
form.change('individual.businessProfile.url', defaultBusinessURL);
}
return (
<React.Fragment>
<PayoutDetailsPersonalDetails
intl={intl}
disabled={disabled}
values={values}
country={country}
fieldId="individual"
accountType="individual"
showEmailField={showEmailField}
showPersonalIdNumberField={showPersonalIdNumberField}
showPhoneNumberField={showPhoneNumberField}
/>
<PayoutDetailsAddress
country={country}
intl={intl}
disabled={disabled}
form={form}
fieldId="individual.address"
/>
{showBusinssProfileSection ? (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.businessProfile" />
</h3>
<PayoutDetailsBusinessProfile
disabled={disabled}
fieldId="individual.businessProfile"
form={form}
showBusinessURLField={showBusinessURLField}
showMCCForUSField={showMCCForUSField}
intl={intl}
/>
</div>
) : null}
<PayoutDetailsBankDetails country={country} disabled={disabled} fieldId="individual" />
</React.Fragment>
);
};
PayoutDetailsIndividualAccountComponent.defaultProps = {
currentUserId: null,
appConfig: config,
};
PayoutDetailsIndividualAccountComponent.propTypes = {
fieldRenderProps: shape({
disabled: bool,
form: object.isRequired,
values: object,
}).isRequired,
currentUserId: propTypes.uuid,
appConfig: object,
// from injectIntl
intl: intlShape.isRequired,
};
const PayoutDetailsIndividualAccount = compose(injectIntl)(PayoutDetailsIndividualAccountComponent);
export default PayoutDetailsIndividualAccount;

View file

@ -1,23 +1,40 @@
import React from 'react';
import { bool, string } from 'prop-types';
import { FieldBirthdayInput, FieldTextInput } from '../../components';
import { bool, node, object, oneOf, string } from 'prop-types';
import { FormattedMessage, intlShape } from 'react-intl';
import * as validators from '../../util/validators';
import { intlShape } from 'react-intl';
import { FieldBirthdayInput, FieldCheckbox, FieldTextInput } from '../../components';
import { stripeCountryConfigs } from './PayoutDetailsForm';
import * as normalizePhoneNumberUS from './normalizePhoneNumberUS';
import css from './PayoutDetailsForm.css';
const MIN_STRIPE_ACCOUNT_AGE = 18;
const PayoutDetailsPersonalDetails = props => {
const { intl, disabled, values, country, fieldId } = props;
const {
intl,
disabled,
values,
country,
fieldId,
sectionTitle,
showEmailField,
showOrganizationTitleField,
showOwnerField,
showOwnershipPercentageField,
showPersonalIdNumberField,
showPhoneNumberField,
} = props;
const personalDetailsTitle = intl.formatMessage({
id:
fieldId === 'company' || fieldId === 'individual'
? 'PayoutDetailsForm.personalDetailsTitle'
: 'PayoutDetailsForm.personalDetailsAdditionalOwnerTitle',
const organizationTitleLabel = intl.formatMessage({
id: 'PayoutDetailsForm.organizationTitleLabel',
});
const organizationTitlePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.organizationTitlePlaceholder',
});
const personalDetailsTitle = sectionTitle
? sectionTitle
: intl.formatMessage({ id: 'PayoutDetailsForm.personalDetailsTitle' });
const firstNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.firstNameLabel' });
const firstNamePlaceholder = intl.formatMessage({
@ -61,10 +78,6 @@ const PayoutDetailsPersonalDetails = props => {
MIN_STRIPE_ACCOUNT_AGE
);
const showPersonalIdNumber =
(country && stripeCountryConfigs(country).personalIdNumberRequired) ||
(country && stripeCountryConfigs(country).ssnLast4Required);
const personalIdNumberRequired = validators.required(
intl.formatMessage({
id: `PayoutDetailsForm.personalIdNumberRequired`,
@ -104,13 +117,45 @@ const PayoutDetailsPersonalDetails = props => {
personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validHKID);
}
const phoneLabel = intl.formatMessage({ id: 'PayoutDetailsForm.personalPhoneLabel' });
const phonePlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.personalPhonePlaceholder' });
const phoneNumberForUSRequired = validators.required(
intl.formatMessage({ id: 'PayoutDetailsForm.personalPhoneRequired' })
);
const emailLabel = intl.formatMessage({ id: 'PayoutDetailsForm.personalEmailLabel' });
const emailPlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.personalEmailPlaceholder' });
const emailRequired = validators.required(
intl.formatMessage({ id: 'PayoutDetailsForm.personalEmailRequired' })
);
const parseOwnershipPercentage = value => {
if (!value) {
return value;
}
const pattern = /^\d{0,3}(?:\.\d{1,2})?$/;
const hasCorrectFormat = value.match(pattern);
const floatValue = Number.parseFloat(value);
const isInRange = 0 <= floatValue && floatValue <= 100;
return hasCorrectFormat && isInRange
? value
: hasCorrectFormat && floatValue < 0
? 0
: hasCorrectFormat && floatValue > 100
? 100
: value.substring(0, value.length - 1);
};
// Note: fname and lname are input names for browser autofill functionality.
return (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>{personalDetailsTitle}</h3>
<div className={css.formRow}>
<FieldTextInput
id={`${fieldId}.firstName`}
name={`${fieldId}.firstName`}
name={`${fieldId}.fname`}
disabled={disabled}
className={css.firstName}
type="text"
@ -121,7 +166,7 @@ const PayoutDetailsPersonalDetails = props => {
/>
<FieldTextInput
id={`${fieldId}.lastName`}
name={`${fieldId}.lastName`}
name={`${fieldId}.lname`}
disabled={disabled}
className={css.lastName}
type="text"
@ -131,6 +176,53 @@ const PayoutDetailsPersonalDetails = props => {
validate={lastNameRequired}
/>
</div>
{showOwnerField ? (
<fieldset className={css.roleField}>
<legend>
<FormattedMessage id="PayoutDetailsForm.role" />
</legend>
<FieldCheckbox
id={`${fieldId}.owner`}
className={css.textInputRow}
name={`${fieldId}.role`}
label={intl.formatMessage({ id: 'PayoutDetailsForm.owner' })}
value="owner"
/>
</fieldset>
) : null}
{showOwnershipPercentageField ? (
<FieldTextInput
id={`${fieldId}.ownershipPercentage`}
name={`${fieldId}.ownershipPercentage`}
className={css.ownershipPercentage}
disabled={disabled}
label={intl.formatMessage({ id: 'PayoutDetailsForm.ownershipPercentageLabel' })}
placeholder={intl.formatMessage({
id: 'PayoutDetailsForm.ownershipPercentagePlaceholder',
})}
type="number"
min={0}
max={100}
step="0.01"
parse={parseOwnershipPercentage}
/>
) : null}
{showOrganizationTitleField ? (
<FieldTextInput
id={`${fieldId}.title`}
name={`${fieldId}.title`}
className={css.textInputRow}
autoComplete="organization-title"
disabled={disabled}
label={organizationTitleLabel}
placeholder={organizationTitlePlaceholder}
type="text"
/>
) : null}
<div className={css.formRow}>
<FieldBirthdayInput
id={`${fieldId}.birthDate`}
@ -146,18 +238,47 @@ const PayoutDetailsPersonalDetails = props => {
/>
</div>
{showPersonalIdNumber ? (
{showPersonalIdNumberField ? (
<FieldTextInput
id={`${fieldId}.personalIdNumber`}
name={`${fieldId}.personalIdNumber`}
disabled={disabled}
className={css.personalIdNumber}
className={css.textInputRow}
type="text"
label={personalIdNumberLabel}
placeholder={personalIdNumberPlaceholder}
validate={personalIdNumberValid}
/>
) : null}
{showPhoneNumberField ? (
<FieldTextInput
id={`${fieldId}.phone`}
name={`${fieldId}.phone`}
className={css.textInputRow}
autoComplete="tel-national"
disabled={disabled}
format={normalizePhoneNumberUS.format}
label={phoneLabel}
parse={normalizePhoneNumberUS.parse}
placeholder={phonePlaceholder}
type="text"
validate={phoneNumberForUSRequired}
/>
) : null}
{showEmailField ? (
<FieldTextInput
id={`${fieldId}.email`}
name={`${fieldId}.email`}
className={css.textInputRow}
autoComplete="email"
disabled={disabled}
label={emailLabel}
placeholder={emailPlaceholder}
type="text"
validate={emailRequired}
/>
) : null}
</div>
);
};
@ -165,13 +286,30 @@ PayoutDetailsPersonalDetails.defaultProps = {
country: null,
disabled: false,
fieldId: null,
sectionTitle: null,
showEmailField: false,
showOrganizationTitleField: false,
showOwnerField: false,
showOwnershipPercentageField: false,
showPersonalIdNumberField: false,
showPhoneNumberField: false,
values: null,
};
PayoutDetailsPersonalDetails.propTypes = {
accountType: oneOf(['company', 'individual']).isRequired,
country: string,
disabled: bool,
fieldId: string,
intl: intlShape.isRequired,
sectionTitle: node,
showEmailField: bool,
showOrganizationTitleField: bool,
showOwnerField: bool,
showOwnershipPercentageField: bool,
showPersonalIdNumberField: bool,
showPhoneNumberField: bool,
values: object,
};
export default PayoutDetailsPersonalDetails;

View file

@ -0,0 +1,292 @@
// Merchant Category Codes (MCC) for US
// Stripe requires this info to comply with US legistlation
// This data is extracted from https://stripe.com/docs/issuing/authorizations/categories
// prettier-ignore
const merchantCategoryCodes = [
{ "mcc": "7623", "label": "A/C, Refrigeration Repair", "category": "ac_refrigeration_repair" },
{ "mcc": "8931", "label": "Accounting/Bookkeeping Services", "category": "accounting_bookkeeping_services" },
{ "mcc": "7311", "label": "Advertising Services", "category": "advertising_services" },
{ "mcc": "0763", "label": "Agricultural Cooperative", "category": "agricultural_cooperative" },
{ "mcc": "4511", "label": "Airlines, Air Carriers", "category": "airlines_air_carriers" },
{ "mcc": "4582", "label": "Airports, Flying Fields", "category": "airports_flying_fields" },
{ "mcc": "4119", "label": "Ambulance Services", "category": "ambulance_services" },
{ "mcc": "7996", "label": "Amusement Parks/Carnivals", "category": "amusement_parks_carnivals" },
{ "mcc": "5937", "label": "Antique Reproductions", "category": "antique_reproductions" },
{ "mcc": "5932", "label": "Antique Shops", "category": "antique_shops" },
{ "mcc": "7998", "label": "Aquariums", "category": "aquariums" },
{ "mcc": "8911", "label": "Architectural/Surveying Services", "category": "architectural_surveying_services" },
{ "mcc": "5971", "label": "Art Dealers and Galleries", "category": "art_dealers_and_galleries" },
{ "mcc": "5970", "label": "Artists Supply and Craft Shops", "category": "artists_supply_and_craft_shops" },
{ "mcc": "7531", "label": "Auto Body Repair Shops", "category": "auto_body_repair_shops" },
{ "mcc": "7535", "label": "Auto Paint Shops", "category": "auto_paint_shops" },
{ "mcc": "7538", "label": "Auto Service Shops", "category": "auto_service_shops" },
{ "mcc": "5531", "label": "Auto and Home Supply Stores", "category": "auto_and_home_supply_stores" },
{ "mcc": "6011", "label": "Automated Cash Disburse", "category": "automated_cash_disburse" },
{ "mcc": "5542", "label": "Automated Fuel Dispensers", "category": "automated_fuel_dispensers" },
{ "mcc": "8675", "label": "Automobile Associations", "category": "automobile_associations" },
{ "mcc": "5533", "label": "Automotive Parts and Accessories Stores", "category": "automotive_parts_and_accessories_stores" },
{ "mcc": "5532", "label": "Automotive Tire Stores", "category": "automotive_tire_stores" },
{ "mcc": "9223", "label": "Bail and Bond Payments (payment to the surety for the bond, not the actual bond paid to the government agency)", "category": "bail_and_bond_payments" },
{ "mcc": "5462", "label": "Bakeries", "category": "bakeries" },
{ "mcc": "7929", "label": "Bands, Orchestras", "category": "bands_orchestras" },
{ "mcc": "7230", "label": "Barber and Beauty Shops", "category": "barber_and_beauty_shops" },
{ "mcc": "7995", "label": "Betting/Casino Gambling", "category": "betting_casino_gambling" },
{ "mcc": "5940", "label": "Bicycle Shops", "category": "bicycle_shops" },
{ "mcc": "7932", "label": "Billiard/Pool Establishments", "category": "billiard_pool_establishments" },
{ "mcc": "5551", "label": "Boat Dealers", "category": "boat_dealers" },
{ "mcc": "4457", "label": "Boat Rentals and Leases", "category": "boat_rentals_and_leases" },
{ "mcc": "5942", "label": "Book Stores", "category": "book_stores" },
{ "mcc": "5192", "label": "Books, Periodicals, and Newspapers", "category": "books_periodicals_and_newspapers" },
{ "mcc": "7933", "label": "Bowling Alleys", "category": "bowling_alleys" },
{ "mcc": "4131", "label": "Bus Lines", "category": "bus_lines" },
{ "mcc": "8244", "label": "Business/Secretarial Schools", "category": "business_secretarial_schools" },
{ "mcc": "7278", "label": "Buying/Shopping Services", "category": "buying_shopping_services" },
{ "mcc": "4899", "label": "Cable, Satellite, and Other Pay Television and Radio", "category": "cable_satellite_and_other_pay_television_and_radio" },
{ "mcc": "5946", "label": "Camera and Photographic Supply Stores", "category": "camera_and_photographic_supply_stores" },
{ "mcc": "5441", "label": "Candy, Nut, and Confectionery Stores", "category": "candy_nut_and_confectionery_stores" },
{ "mcc": "7512", "label": "Car Rental Agencies", "category": "car_rental_agencies" },
{ "mcc": "7542", "label": "Car Washes", "category": "car_washes" },
{ "mcc": "5511", "label": "Car and Truck Dealers (New & Used) Sales, Service, Repairs Parts and Leasing", "category": "car_and_truck_dealers_new_used" },
{ "mcc": "5521", "label": "Car and Truck Dealers (Used Only) Sales, Service, Repairs Parts and Leasing", "category": "car_and_truck_dealers_used_only" },
{ "mcc": "1750", "label": "Carpentry Services", "category": "carpentry_services" },
{ "mcc": "7217", "label": "Carpet/Upholstery Cleaning", "category": "carpet_upholstery_cleaning" },
{ "mcc": "5811", "label": "Caterers", "category": "caterers" },
{ "mcc": "8398", "label": "Charitable and Social Service Organizations - Fundraising", "category": "charitable_and_social_service_organizations_fundraising" },
{ "mcc": "5169", "label": "Chemicals and Allied Products (Not Elsewhere Classified)", "category": "chemicals_and_allied_products" },
{ "mcc": "5641", "label": "Chidrens and Infants Wear Stores", "category": "chidrens_and_infants_wear_stores" },
{ "mcc": "8351", "label": "Child Care Services", "category": "child_care_services" },
{ "mcc": "8049", "label": "Chiropodists, Podiatrists", "category": "chiropodists_podiatrists" },
{ "mcc": "8041", "label": "Chiropractors", "category": "chiropractors" },
{ "mcc": "5993", "label": "Cigar Stores and Stands", "category": "cigar_stores_and_stands" },
{ "mcc": "8641", "label": "Civic, Social, Fraternal Associations", "category": "civic_social_fraternal_associations" },
{ "mcc": "7349", "label": "Cleaning and Maintenance", "category": "cleaning_and_maintenance" },
{ "mcc": "7296", "label": "Clothing Rental", "category": "clothing_rental" },
{ "mcc": "8220", "label": "Colleges, Universities", "category": "colleges_universities" },
{ "mcc": "5046", "label": "Commercial Equipment (Not Elsewhere Classified)", "category": "commercial_equipment" },
{ "mcc": "5139", "label": "Commercial Footwear", "category": "commercial_footwear" },
{ "mcc": "7333", "label": "Commercial Photography, Art and Graphics", "category": "commercial_photography_art_and_graphics" },
{ "mcc": "4111", "label": "Commuter Transport, Ferries", "category": "commuter_transport_and_ferries" },
{ "mcc": "4816", "label": "Computer Network Services", "category": "computer_network_services" },
{ "mcc": "7372", "label": "Computer Programming", "category": "computer_programming" },
{ "mcc": "7379", "label": "Computer Repair", "category": "computer_repair" },
{ "mcc": "5734", "label": "Computer Software Stores", "category": "computer_software_stores" },
{ "mcc": "5045", "label": "Computers, Peripherals, and Software", "category": "computers_peripherals_and_software" },
{ "mcc": "1771", "label": "Concrete Work Services", "category": "concrete_work_services" },
{ "mcc": "5039", "label": "Construction Materials (Not Elsewhere Classified)", "category": "construction_materials" },
{ "mcc": "7392", "label": "Consulting, Public Relations", "category": "consulting_public_relations" },
{ "mcc": "8241", "label": "Correspondence Schools", "category": "correspondence_schools" },
{ "mcc": "5977", "label": "Cosmetic Stores", "category": "cosmetic_stores" },
{ "mcc": "7277", "label": "Counseling Services", "category": "counseling_services" },
{ "mcc": "7997", "label": "Country Clubs", "category": "country_clubs" },
{ "mcc": "4215", "label": "Courier Services", "category": "courier_services" },
{ "mcc": "9211", "label": "Court Costs, Including Alimony and Child Support - Courts of Law", "category": "court_costs" },
{ "mcc": "7321", "label": "Credit Reporting Agencies", "category": "credit_reporting_agencies" },
{ "mcc": "4411", "label": "Cruise Lines", "category": "cruise_lines" },
{ "mcc": "5451", "label": "Dairy Products Stores", "category": "dairy_products_stores" },
{ "mcc": "7911", "label": "Dance Hall, Studios, Schools", "category": "dance_hall_studios_schools" },
{ "mcc": "7273", "label": "Dating/Escort Services", "category": "dating_escort_services" },
{ "mcc": "8021", "label": "Dentists, Orthodontists", "category": "dentists_orthodontists" },
{ "mcc": "5311", "label": "Department Stores", "category": "department_stores" },
{ "mcc": "7393", "label": "Detective Agencies", "category": "detective_agencies" },
{ "mcc": "5964", "label": "Direct Marketing - Catalog Merchant", "category": "direct_marketing_catalog_merchant" },
{ "mcc": "5965", "label": "Direct Marketing - Combination Catalog and Retail Merchant", "category": "direct_marketing_combination_catalog_and_retail_merchant" },
{ "mcc": "5967", "label": "Direct Marketing - Inbound Telemarketing", "category": "direct_marketing_inbound_telemarketing" },
{ "mcc": "5960", "label": "Direct Marketing - Insurance Services", "category": "direct_marketing_insurance_services" },
{ "mcc": "5969", "label": "Direct Marketing - Other", "category": "direct_marketing_other" },
{ "mcc": "5966", "label": "Direct Marketing - Outbound Telemarketing", "category": "direct_marketing_outbound_telemarketing" },
{ "mcc": "5968", "label": "Direct Marketing - Subscription", "category": "direct_marketing_subscription" },
{ "mcc": "5962", "label": "Direct Marketing - Travel", "category": "direct_marketing_travel" },
{ "mcc": "5310", "label": "Discount Stores", "category": "discount_stores" },
{ "mcc": "8011", "label": "Doctors", "category": "doctors" },
{ "mcc": "5963", "label": "Door-To-Door Sales", "category": "door_to_door_sales" },
{ "mcc": "5714", "label": "Drapery, Window Covering, and Upholstery Stores", "category": "drapery_window_covering_and_upholstery_stores" },
{ "mcc": "5813", "label": "Drinking Places", "category": "drinking_places" },
{ "mcc": "5912", "label": "Drug Stores and Pharmacies", "category": "drug_stores_and_pharmacies" },
{ "mcc": "5122", "label": "Drugs, Drug Proprietaries, and Druggist Sundries", "category": "drugs_drug_proprietaries_and_druggist_sundries" },
{ "mcc": "7216", "label": "Dry Cleaners", "category": "dry_cleaners" },
{ "mcc": "5099", "label": "Durable Goods (Not Elsewhere Classified)", "category": "durable_goods" },
{ "mcc": "5309", "label": "Duty Free Stores", "category": "duty_free_stores" },
{ "mcc": "5812", "label": "Eating Places, Restaurants", "category": "eating_places_restaurants" },
{ "mcc": "8299", "label": "Educational Services", "category": "educational_services" },
{ "mcc": "5997", "label": "Electric Razor Stores", "category": "electric_razor_stores" },
{ "mcc": "5065", "label": "Electrical Parts and Equipment", "category": "electrical_parts_and_equipment" },
{ "mcc": "1731", "label": "Electrical Services", "category": "electrical_services" },
{ "mcc": "7622", "label": "Electronics Repair Shops", "category": "electronics_repair_shops" },
{ "mcc": "5732", "label": "Electronics Stores", "category": "electronics_stores" },
{ "mcc": "8211", "label": "Elementary, Secondary Schools", "category": "elementary_secondary_schools" },
{ "mcc": "7361", "label": "Employment/Temp Agencies", "category": "employment_temp_agencies" },
{ "mcc": "7394", "label": "Equipment Rental", "category": "equipment_rental" },
{ "mcc": "7342", "label": "Exterminating Services", "category": "exterminating_services" },
{ "mcc": "5651", "label": "Family Clothing Stores", "category": "family_clothing_stores" },
{ "mcc": "5814", "label": "Fast Food Restaurants", "category": "fast_food_restaurants" },
{ "mcc": "6012", "label": "Financial Institutions", "category": "financial_institutions" },
{ "mcc": "9222", "label": "Fines - Government Administrative Entities", "category": "fines_government_administrative_entities" },
{ "mcc": "5718", "label": "Fireplace, Fireplace Screens, and Accessories Stores", "category": "fireplace_fireplace_screens_and_accessories_stores" },
{ "mcc": "5713", "label": "Floor Covering Stores", "category": "floor_covering_stores" },
{ "mcc": "5992", "label": "Florists", "category": "florists" },
{ "mcc": "5193", "label": "Florists Supplies, Nursery Stock, and Flowers", "category": "florists_supplies_nursery_stock_and_flowers" },
{ "mcc": "5422", "label": "Freezer and Locker Meat Provisioners", "category": "freezer_and_locker_meat_provisioners" },
{ "mcc": "5983", "label": "Fuel Dealers (Non Automotive)", "category": "fuel_dealers_non_automotive" },
{ "mcc": "7261", "label": "Funeral Services, Crematories", "category": "funeral_services_crematories" },
{ "mcc": "7641", "label": "Furniture Repair, Refinishing", "category": "furniture_repair_refinishing" },
{ "mcc": "5712", "label": "Furniture, Home Furnishings, and Equipment Stores, Except Appliances", "category": "furniture_home_furnishings_and_equipment_stores_except_appliances" },
{ "mcc": "5681", "label": "Furriers and Fur Shops", "category": "furriers_and_fur_shops" },
{ "mcc": "1520", "label": "General Services", "category": "general_services" },
{ "mcc": "5947", "label": "Gift, Card, Novelty, and Souvenir Shops", "category": "gift_card_novelty_and_souvenir_shops" },
{ "mcc": "5231", "label": "Glass, Paint, and Wallpaper Stores", "category": "glass_paint_and_wallpaper_stores" },
{ "mcc": "5950", "label": "Glassware, Crystal Stores", "category": "glassware_crystal_stores" },
{ "mcc": "7992", "label": "Golf Courses - Public", "category": "golf_courses_public" },
{ "mcc": "9399", "label": "Government Services (Not Elsewhere Classified)", "category": "government_services" },
{ "mcc": "5411", "label": "Grocery Stores, Supermarkets", "category": "grocery_stores_supermarkets" },
{ "mcc": "5251", "label": "Hardware Stores", "category": "hardware_stores" },
{ "mcc": "5072", "label": "Hardware, Equipment, and Supplies", "category": "hardware_equipment_and_supplies" },
{ "mcc": "7298", "label": "Health and Beauty Spas", "category": "health_and_beauty_spas" },
{ "mcc": "5975", "label": "Hearing Aids Sales and Supplies", "category": "hearing_aids_sales_and_supplies" },
{ "mcc": "1711", "label": "Heating, Plumbing, A/C", "category": "heating_plumbing_a_c" },
{ "mcc": "5945", "label": "Hobby, Toy, and Game Shops", "category": "hobby_toy_and_game_shops" },
{ "mcc": "5200", "label": "Home Supply Warehouse Stores", "category": "home_supply_warehouse_stores" },
{ "mcc": "8062", "label": "Hospitals", "category": "hospitals" },
{ "mcc": "7011", "label": "Hotels, Motels, and Resorts", "category": "hotels_motels_and_resorts" },
{ "mcc": "5722", "label": "Household Appliance Stores", "category": "household_appliance_stores" },
{ "mcc": "5085", "label": "Industrial Supplies (Not Elsewhere Classified)", "category": "industrial_supplies" },
{ "mcc": "7375", "label": "Information Retrieval Services", "category": "information_retrieval_services" },
{ "mcc": "6399", "label": "Insurance - Default", "category": "insurance_default" },
{ "mcc": "6300", "label": "Insurance Underwriting, Premiums", "category": "insurance_underwriting_premiums" },
{ "mcc": "9950", "label": "Intra-Company Purchases", "category": "intra_company_purchases" },
{ "mcc": "5944", "label": "Jewelry Stores, Watches, Clocks, and Silverware Stores", "category": "jewelry_stores_watches_clocks_and_silverware_stores" },
{ "mcc": "0780", "label": "Landscaping Services", "category": "landscaping_services" },
{ "mcc": "7211", "label": "Laundries", "category": "laundries" },
{ "mcc": "7210", "label": "Laundry, Cleaning Services", "category": "laundry_cleaning_services" },
{ "mcc": "8111", "label": "Legal Services, Attorneys", "category": "legal_services_attorneys" },
{ "mcc": "5948", "label": "Luggage and Leather Goods Stores", "category": "luggage_and_leather_goods_stores" },
{ "mcc": "5211", "label": "Lumber, Building Materials Stores", "category": "lumber_building_materials_stores" },
{ "mcc": "6010", "label": "Manual Cash Disburse", "category": "manual_cash_disburse" },
{ "mcc": "4468", "label": "Marinas, Service and Supplies", "category": "marinas_service_and_supplies" },
{ "mcc": "1740", "label": "Masonry, Stonework, and Plaster", "category": "masonry_stonework_and_plaster" },
{ "mcc": "7297", "label": "Massage Parlors", "category": "massage_parlors" },
{ "mcc": "8099", "label": "Medical Services", "category": "medical_services" },
{ "mcc": "8071", "label": "Medical and Dental Labs", "category": "medical_and_dental_labs" },
{ "mcc": "5047", "label": "Medical, Dental, Ophthalmic, and Hospital Equipment and Supplies", "category": "medical_dental_ophthalmic_and_hospital_equipment_and_supplies" },
{ "mcc": "8699", "label": "Membership Organizations", "category": "membership_organizations" },
{ "mcc": "5611", "label": "Mens and Boys Clothing and Accessories Stores", "category": "mens_and_boys_clothing_and_accessories_stores" },
{ "mcc": "5691", "label": "Mens, Womens Clothing Stores", "category": "mens_womens_clothing_stores" },
{ "mcc": "5051", "label": "Metal Service Centers", "category": "metal_service_centers" },
{ "mcc": "5699", "label": "Miscellaneous Apparel and Accessory Shops", "category": "miscellaneous_apparel_and_accessory_shops" },
{ "mcc": "5599", "label": "Miscellaneous Auto Dealers", "category": "miscellaneous_auto_dealers" },
{ "mcc": "7399", "label": "Miscellaneous Business Services", "category": "miscellaneous_business_services" },
{ "mcc": "5499", "label": "Miscellaneous Food Stores - Convenience Stores and Specialty Markets", "category": "miscellaneous_food_stores" },
{ "mcc": "5399", "label": "Miscellaneous General Merchandise", "category": "miscellaneous_general_merchandise" },
{ "mcc": "7299", "label": "Miscellaneous General Services", "category": "miscellaneous_general_services" },
{ "mcc": "5719", "label": "Miscellaneous Home Furnishing Specialty Stores", "category": "miscellaneous_home_furnishing_specialty_stores" },
{ "mcc": "2741", "label": "Miscellaneous Publishing and Printing", "category": "miscellaneous_publishing_and_printing" },
{ "mcc": "7999", "label": "Miscellaneous Recreation Services", "category": "miscellaneous_recreation_services" },
{ "mcc": "7699", "label": "Miscellaneous Repair Shops", "category": "miscellaneous_repair_shops" },
{ "mcc": "5999", "label": "Miscellaneous Specialty Retail", "category": "miscellaneous_specialty_retail" },
{ "mcc": "5271", "label": "Mobile Home Dealers", "category": "mobile_home_dealers" },
{ "mcc": "7832", "label": "Motion Picture Theaters", "category": "motion_picture_theaters" },
{ "mcc": "4214", "label": "Motor Freight Carriers and Trucking - Local and Long Distance, Moving and Storage Companies, and Local Delivery Services", "category": "motor_freight_carriers_and_trucking" },
{ "mcc": "5592", "label": "Motor Homes Dealers", "category": "motor_homes_dealers" },
{ "mcc": "5013", "label": "Motor Vehicle Supplies and New Parts", "category": "motor_vehicle_supplies_and_new_parts" },
{ "mcc": "5571", "label": "Motorcycle Shops and Dealers", "category": "motorcycle_shops_and_dealers" },
{ "mcc": "5561", "label": "Motorcycle Shops, Dealers", "category": "motorcycle_shops_dealers" },
{ "mcc": "5733", "label": "Music Stores-Musical Instruments, Pianos, and Sheet Music", "category": "music_stores_musical_instruments_pianos_and_sheet_music" },
{ "mcc": "5994", "label": "News Dealers and Newsstands", "category": "news_dealers_and_newsstands" },
{ "mcc": "6051", "label": "Non-FI, Money Orders", "category": "non_fi_money_orders" },
{ "mcc": "6540", "label": "Non-FI, Stored Value Card Purchase/Load", "category": "non_fi_stored_value_card_purchase_load" },
{ "mcc": "5199", "label": "Nondurable Goods (Not Elsewhere Classified)", "category": "nondurable_goods" },
{ "mcc": "5261", "label": "Nurseries, Lawn and Garden Supply Stores", "category": "nurseries_lawn_and_garden_supply_stores" },
{ "mcc": "8050", "label": "Nursing/Personal Care", "category": "nursing_personal_care" },
{ "mcc": "5021", "label": "Office and Commercial Furniture", "category": "office_and_commercial_furniture" },
{ "mcc": "8043", "label": "Opticians, Eyeglasses", "category": "opticians_eyeglasses" },
{ "mcc": "8042", "label": "Optometrists, Ophthalmologist", "category": "optometrists_ophthalmologist" },
{ "mcc": "5976", "label": "Orthopedic Goods - Prosthetic Devices", "category": "orthopedic_goods_prosthetic_devices" },
{ "mcc": "8031", "label": "Osteopaths", "category": "osteopaths" },
{ "mcc": "5921", "label": "Package Stores-Beer, Wine, and Liquor", "category": "package_stores_beer_wine_and_liquor" },
{ "mcc": "5198", "label": "Paints, Varnishes, and Supplies", "category": "paints_varnishes_and_supplies" },
{ "mcc": "7523", "label": "Parking Lots, Garages", "category": "parking_lots_garages" },
{ "mcc": "4112", "label": "Passenger Railways", "category": "passenger_railways" },
{ "mcc": "5933", "label": "Pawn Shops", "category": "pawn_shops" },
{ "mcc": "5995", "label": "Pet Shops, Pet Food, and Supplies", "category": "pet_shops_pet_food_and_supplies" },
{ "mcc": "5172", "label": "Petroleum and Petroleum Products", "category": "petroleum_and_petroleum_products" },
{ "mcc": "7395", "label": "Photo Developing", "category": "photo_developing" },
{ "mcc": "7221", "label": "Photographic Studios", "category": "photographic_studios" },
{ "mcc": "5044", "label": "Photographic, Photocopy, Microfilm Equipment, and Supplies", "category": "photographic_photocopy_microfilm_equipment_and_supplies" },
{ "mcc": "7829", "label": "Picture/Video Production", "category": "picture_video_production" },
{ "mcc": "5131", "label": "Piece Goods, Notions, and Other Dry Goods", "category": "piece_goods_notions_and_other_dry_goods" },
{ "mcc": "5074", "label": "Plumbing, Heating Equipment, and Supplies", "category": "plumbing_heating_equipment_and_supplies" },
{ "mcc": "8651", "label": "Political Organizations", "category": "political_organizations" },
{ "mcc": "9402", "label": "Postal Services - Government Only", "category": "postal_services_government_only" },
{ "mcc": "5094", "label": "Precious Stones and Metals, Watches and Jewelry", "category": "precious_stones_and_metals_watches_and_jewelry" },
{ "mcc": "8999", "label": "Professional Services", "category": "professional_services" },
{ "mcc": "4225", "label": "Public Warehousing and Storage - Farm Products, Refrigerated Goods, Household Goods, and Storage", "category": "public_warehousing_and_storage" },
{ "mcc": "7338", "label": "Quick Copy, Repro, and Blueprint", "category": "quick_copy_repro_and_blueprint" },
{ "mcc": "4011", "label": "Railroads", "category": "railroads" },
{ "mcc": "6513", "label": "Real Estate Agents and Managers - Rentals", "category": "real_estate_agents_and_managers_rentals" },
{ "mcc": "5735", "label": "Record Stores", "category": "record_stores" },
{ "mcc": "7519", "label": "Recreational Vehicle Rentals", "category": "recreational_vehicle_rentals" },
{ "mcc": "5973", "label": "Religious Goods Stores", "category": "religious_goods_stores" },
{ "mcc": "8661", "label": "Religious Organizations", "category": "religious_organizations" },
{ "mcc": "1761", "label": "Roofing/Siding, Sheet Metal", "category": "roofing_siding_sheet_metal" },
{ "mcc": "7339", "label": "Secretarial Support Services", "category": "secretarial_support_services" },
{ "mcc": "6211", "label": "Security Brokers/Dealers", "category": "security_brokers_dealers" },
{ "mcc": "5541", "label": "Service Stations", "category": "service_stations" },
{ "mcc": "5949", "label": "Sewing, Needlework, Fabric, and Piece Goods Stores", "category": "sewing_needlework_fabric_and_piece_goods_stores" },
{ "mcc": "7251", "label": "Shoe Repair/Hat Cleaning", "category": "shoe_repair_hat_cleaning" },
{ "mcc": "5661", "label": "Shoe Stores", "category": "shoe_stores" },
{ "mcc": "7629", "label": "Small Appliance Repair", "category": "small_appliance_repair" },
{ "mcc": "5598", "label": "Snowmobile Dealers", "category": "snowmobile_dealers" },
{ "mcc": "1799", "label": "Special Trade Services", "category": "special_trade_services" },
{ "mcc": "2842", "label": "Specialty Cleaning", "category": "specialty_cleaning" },
{ "mcc": "5941", "label": "Sporting Goods Stores", "category": "sporting_goods_stores" },
{ "mcc": "7032", "label": "Sporting/Recreation Camps", "category": "sporting_recreation_camps" },
{ "mcc": "7941", "label": "Sports Clubs/Fields", "category": "sports_clubs_fields" },
{ "mcc": "5655", "label": "Sports and Riding Apparel Stores", "category": "sports_and_riding_apparel_stores" },
{ "mcc": "5972", "label": "Stamp and Coin Stores", "category": "stamp_and_coin_stores" },
{ "mcc": "5111", "label": "Stationary, Office Supplies, Printing and Writing Paper", "category": "stationary_office_supplies_printing_and_writing_paper" },
{ "mcc": "5943", "label": "Stationery Stores, Office, and School Supply Stores", "category": "stationery_stores_office_and_school_supply_stores" },
{ "mcc": "5996", "label": "Swimming Pools Sales", "category": "swimming_pools_sales" },
{ "mcc": "4723", "label": "TUI Travel - Germany", "category": "t_ui_travel_germany" },
{ "mcc": "5697", "label": "Tailors, Alterations", "category": "tailors_alterations" },
{ "mcc": "9311", "label": "Tax Payments - Government Agencies", "category": "tax_payments_government_agencies" },
{ "mcc": "7276", "label": "Tax Preparation Services", "category": "tax_preparation_services" },
{ "mcc": "4121", "label": "Taxicabs/Limousines", "category": "taxicabs_limousines" },
{ "mcc": "4812", "label": "Telecommunication Equipment and Telephone Sales", "category": "telecommunication_equipment_and_telephone_sales" },
{ "mcc": "4814", "label": "Telecommunication Services", "category": "telecommunication_services" },
{ "mcc": "4821", "label": "Telegraph Services", "category": "telegraph_services" },
{ "mcc": "5998", "label": "Tent and Awning Shops", "category": "tent_and_awning_shops" },
{ "mcc": "8734", "label": "Testing Laboratories", "category": "testing_laboratories" },
{ "mcc": "7922", "label": "Theatrical Ticket Agencies", "category": "theatrical_ticket_agencies" },
{ "mcc": "7012", "label": "Timeshares", "category": "timeshares" },
{ "mcc": "7534", "label": "Tire Retreading and Repair", "category": "tire_retreading_and_repair" },
{ "mcc": "4784", "label": "Tolls/Bridge Fees", "category": "tolls_bridge_fees" },
{ "mcc": "7991", "label": "Tourist Attractions and Exhibits", "category": "tourist_attractions_and_exhibits" },
{ "mcc": "7549", "label": "Towing Services", "category": "towing_services" },
{ "mcc": "7033", "label": "Trailer Parks, Campgrounds", "category": "trailer_parks_campgrounds" },
{ "mcc": "4789", "label": "Transportation Services (Not Elsewhere Classified)", "category": "transportation_services" },
{ "mcc": "4722", "label": "Travel Agencies, Tour Operators", "category": "travel_agencies_tour_operators" },
{ "mcc": "7511", "label": "Truck StopIteration", "category": "truck_stop_iteration" },
{ "mcc": "7513", "label": "Truck/Utility Trailer Rentals", "category": "truck_utility_trailer_rentals" },
{ "mcc": "2791", "label": "Typesetting, Plate Making, and Related Services", "category": "typesetting_plate_making_and_related_services" },
{ "mcc": "5978", "label": "Typewriter Stores", "category": "typewriter_stores" },
{ "mcc": "9405", "label": "U.S. Federal Government Agencies or Departments", "category": "u_s_federal_government_agencies_or_departments" },
{ "mcc": "5137", "label": "Uniforms, Commercial Clothing", "category": "uniforms_commercial_clothing" },
{ "mcc": "5931", "label": "Used Merchandise and Secondhand Stores", "category": "used_merchandise_and_secondhand_stores" },
{ "mcc": "4900", "label": "Utilities", "category": "utilities" },
{ "mcc": "5331", "label": "Variety Stores", "category": "variety_stores" },
{ "mcc": "0742", "label": "Veterinary Services", "category": "veterinary_services" },
{ "mcc": "7993", "label": "Video Amusement Game Supplies", "category": "video_amusement_game_supplies" },
{ "mcc": "7994", "label": "Video Game Arcades", "category": "video_game_arcades" },
{ "mcc": "7841", "label": "Video Tape Rental Stores", "category": "video_tape_rental_stores" },
{ "mcc": "8249", "label": "Vocational/Trade Schools", "category": "vocational_trade_schools" },
{ "mcc": "7631", "label": "Watch/Jewelry Repair", "category": "watch_jewelry_repair" },
{ "mcc": "7692", "label": "Welding Repair", "category": "welding_repair" },
{ "mcc": "5300", "label": "Wholesale Clubs", "category": "wholesale_clubs" },
{ "mcc": "5698", "label": "Wig and Toupee Stores", "category": "wig_and_toupee_stores" },
{ "mcc": "4829", "label": "Wires, Money Orders", "category": "wires_money_orders" },
{ "mcc": "5631", "label": "Womens Accessory and Specialty Shops", "category": "womens_accessory_and_specialty_shops" },
{ "mcc": "5621", "label": "Womens Ready-To-Wear Stores", "category": "womens_ready_to_wear_stores" },
{ "mcc": "5935", "label": "Wrecking and Salvage Yards", "category": "wrecking_and_salvage_yards" },
];
export default merchantCategoryCodes;

View file

@ -0,0 +1,32 @@
// Normalize US phone number
// Stripe requires phone numbers for US companies and their owners
/**
* Format a phone number: this is just an override for default formatting.
*/
export const format = value => (!value ? '' : value);
/**
* Parser that strips non-digit characters away from a phone number
* string unless they are dashes in correct places.
*
* Returns the given US phone number in format: 202-555-0102
*/
export const parse = value => {
if (!value) {
return value;
}
const phoneNumber =
typeof value === 'string' && value.indexOf('+1') >= 0 ? value.substring(2) : value;
const onlyNums = phoneNumber.replace(/[^\d]/g, '');
if (onlyNums.length <= 3) {
return onlyNums;
} else if (onlyNums.length <= 7) {
return `${onlyNums.slice(0, 3)}-${onlyNums.slice(3, 7)}`;
} else {
return `${onlyNums.slice(0, 3)}-${onlyNums.slice(3, 6)}-${onlyNums.slice(6, 10)}`;
}
};

View file

@ -1,5 +1,9 @@
/*
* Stripe related configuration.
*
* Note: this setup is for API version '2019-02-19' and later.
* If you have an older API version in use, you need to update your Stripe API.
* You can check your Stripe API version from Stripe Dashboard -> Developers.
*/
// NOTE: REACT_APP_STRIPE_PUBLISHABLE_KEY is mandatory environment variable.
@ -7,13 +11,6 @@
// To make Stripe connection work, you also need to set Stripe's private key in the Flex Console.
export const stripePublishableKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY;
// You can check your Stripe API version from Stripe Dashboard -> Developers.
// If you are using the API version that is earlier than '2019-02-19' change this value to 'true'
// See Stripe API changelog: https://stripe.com/docs/upgrades#api-changelog
// NOTE: we are not supporting company accounts with new Stripe API yet!
// The option for company accounts will be hidden if this value is set to 'false'
export const useDeprecatedLegalEntityWithStripe = false;
// Stripe only supports payments in certain countries, see full list
// at https://stripe.com/global
//
@ -47,7 +44,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -64,7 +61,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -97,7 +94,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -114,7 +111,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -131,7 +128,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -148,7 +145,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -164,9 +161,12 @@ export const stripeSupportedCountries = [
branchCode: true,
accountNumber: true,
},
personalIdNumberRequired: true,
companyConfig: {
personalAddress: true,
personalIdNumberRequired: true,
},
individualConfig: {
personalIdNumberRequired: true,
},
},
{
@ -183,7 +183,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -200,7 +200,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -217,7 +217,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -234,7 +234,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -264,7 +264,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -281,7 +281,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -298,7 +298,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -315,7 +315,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -332,7 +332,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -350,7 +350,7 @@ export const stripeSupportedCountries = [
},
companyConfig: {
personalAddress: true,
additionalOwners: true,
owners: true,
},
},
{
@ -367,6 +367,22 @@ export const stripeSupportedCountries = [
routingNumber: true,
accountNumber: true,
},
ssnLast4Required: true,
companyConfig: {
businessURL: true,
companyPhone: true,
mccForUS: true,
owners: true,
personalAddress: true,
personalEmail: true,
personalPhone: true,
ssnLast4Required: true,
},
individualConfig: {
businessURL: true,
mccForUS: true,
ssnLast4Required: true,
personalEmail: true,
personalPhone: true,
},
},
];

View file

@ -455,11 +455,14 @@
"PasswordResetPage.recoveryLinkText": "password recovery page",
"PasswordResetPage.resetFailed": "Reset failed. Please try again.",
"PasswordResetPage.title": "Reset password",
"PayoutDetailsForm.accountOpenerInfoText": "Account opener needs to be an individual with authorization to sign on behalf of the organization.",
"PayoutDetailsForm.accountOpenerTitle": "Account opener",
"PayoutDetailsForm.accountTypeTitle": "Account type",
"PayoutDetailsForm.additionalOwnersInfoLink": "Stripe support.",
"PayoutDetailsForm.additionalOwnerInfoText": "For Hong Kong, Singapore, and Single Euro Payments Area member countries, Stripe requires information of every person that owns at least 25% of the company. For more information, see {additionalOwnersInfoLink}",
"PayoutDetailsForm.additionalOwnerLabel": "Add additional owner",
"PayoutDetailsForm.additionalOwnerRemove": "Remove additional owner",
"PayoutDetailsForm.additionalPersonInfoLink": "Stripe support.",
"PayoutDetailsForm.additionalPersonInfoText": "Stripe requires information of every person that owns at least 25% of the company or exercise significant control over your company. For more information, see {additionalPersonInfoLink}",
"PayoutDetailsForm.additionalPersonLink": "Add owners and directors",
"PayoutDetailsForm.additionalPersonRemove": "Remove person",
"PayoutDetailsForm.additionalPersonTitle": "Owner or director",
"PayoutDetailsForm.addressTitle": "Address",
"PayoutDetailsForm.bankDetails": "Bank details",
"PayoutDetailsForm.birthdayDatePlaceholder": "dd",
@ -470,6 +473,13 @@
"PayoutDetailsForm.birthdayMonthPlaceholder": "mm",
"PayoutDetailsForm.birthdayRequired": "Birthday is required and must be a valid date.",
"PayoutDetailsForm.birthdayYearPlaceholder": "yyyy",
"PayoutDetailsForm.businessMCCForUSLabel": "Merchant Category Code (MCC)",
"PayoutDetailsForm.businessMCCForUSPlaceholder": "Select MCC",
"PayoutDetailsForm.businessMCCForUSRequired": "This field is required",
"PayoutDetailsForm.businessProfile": "Business profile",
"PayoutDetailsForm.businessURLLabel": "Business URL",
"PayoutDetailsForm.businessURLPlaceholder": "The businesss publicly available website.",
"PayoutDetailsForm.businessURLRequired": "Valid business URL is required.",
"PayoutDetailsForm.canadianProvinceLabel": "Province",
"PayoutDetailsForm.canadianProvinceNames.AB": "Alberta",
"PayoutDetailsForm.canadianProvinceNames.BC": "British Columbia",
@ -495,6 +505,9 @@
"PayoutDetailsForm.companyNameLabel": "Company name",
"PayoutDetailsForm.companyNamePlaceholder": "Enter company name…",
"PayoutDetailsForm.companyNameRequired": "Company name is required",
"PayoutDetailsForm.companyPhoneLabel": "Phone number",
"PayoutDetailsForm.companyPhonePlaceholder": "202-555-0102",
"PayoutDetailsForm.companyPhoneRequired": "This field is required",
"PayoutDetailsForm.companyTaxIdLabel.AT": "Firmenbuchnummer (FN)",
"PayoutDetailsForm.companyTaxIdLabel.AU": "Company ACN/ABN - TFN",
"PayoutDetailsForm.companyTaxIdLabel.BE": "TVA/BTW/CBE",
@ -544,6 +557,7 @@
"PayoutDetailsForm.countryRequired": "This field is required",
"PayoutDetailsForm.createStripeAccountFailed": "Whoops, something went wrong. Please try again.",
"PayoutDetailsForm.createStripeAccountFailedInvalidPostalCode": "Whoops, check that the selected country and postal code are correct and try again.",
"PayoutDetailsForm.createStripeAccountFailedWithStripeError": "Whoops, something went wrong. Stripe returned an error message: \"{stripeMessage}\"",
"PayoutDetailsForm.firstNameLabel": "First name",
"PayoutDetailsForm.firstNamePlaceholder": "John",
"PayoutDetailsForm.firstNameRequired": "This field is required",
@ -553,18 +567,30 @@
"PayoutDetailsForm.lastNamePlaceholder": "Doe",
"PayoutDetailsForm.lastNameRequired": "This field is required",
"PayoutDetailsForm.missingStripeKey": "Stripe publishable key has not been configured to this marketplace. Unfortunately, you can't save your payout preferences yet.",
"PayoutDetailsForm.organizationTitleLabel": "Job title",
"PayoutDetailsForm.organizationTitlePlaceholder": "CEO, Manager, Partner",
"PayoutDetailsForm.owner": "Owner (25% or more)",
"PayoutDetailsForm.ownershipPercentageLabel": "Ownership percentage",
"PayoutDetailsForm.ownershipPercentagePlaceholder":"100",
"PayoutDetailsForm.personalDetailsAdditionalOwnerTitle": "Additional owner details",
"PayoutDetailsForm.personalDetailsTitle": "Personal details",
"PayoutDetailsForm.personalIdNumberTitle": "Personal id number",
"PayoutDetailsForm.personalEmailLabel": "Email",
"PayoutDetailsForm.personalEmailPlaceholder": "Email address",
"PayoutDetailsForm.personalEmailRequired": "This field is required",
"PayoutDetailsForm.personalIdNumberLabel.HK": "Hong Kong Identity Card Number (HKID)",
"PayoutDetailsForm.personalIdNumberLabel.US": "Last 4 digits of social security number (SSN)",
"PayoutDetailsForm.personalIdNumberPlaceholder.HK": "XY123456",
"PayoutDetailsForm.personalIdNumberPlaceholder.US": "1234",
"PayoutDetailsForm.personalIdNumberRequired": "This field is required",
"PayoutDetailsForm.personalIdNumberTitle": "Personal id number",
"PayoutDetailsForm.personalIdNumberValid": "Invalid value",
"PayoutDetailsForm.personalPhoneLabel": "Phone number",
"PayoutDetailsForm.personalPhonePlaceholder": "202-555-0102",
"PayoutDetailsForm.personalPhoneRequired": "This field is required",
"PayoutDetailsForm.postalCodeLabel": "Postal code",
"PayoutDetailsForm.postalCodePlaceholder": "00100",
"PayoutDetailsForm.postalCodeRequired": "This field is required",
"PayoutDetailsForm.role": "Role",
"PayoutDetailsForm.stateLabel": "State",
"PayoutDetailsForm.statePlaceholder": "Enter your state",
"PayoutDetailsForm.stateRequired": "This field is required",

View file

@ -191,7 +191,7 @@ export const isChangePasswordWrongPassword = error => error && error.status ===
/**
* Check if the given API error (from
* 'sdk.currentUser.createStripeAccount(payoutDetails)') is due to
* 'sdk.stripeAccount.create(payoutDetails)') is due to
* invalid postal code in the given country.
*/
export const isStripeInvalidPostalCode = error => {
@ -204,6 +204,14 @@ export const isStripeInvalidPostalCode = error => {
});
};
export const isStripeError = error => {
return errorAPIErrors(error).some(apiError => {
// Stripe doesn't seem to give an error code for this specific
// case, so we have to recognize it from the message.
return !!(apiError.meta && apiError.meta.stripeMessage);
});
};
export const storableError = err => {
const error = err || {};
const { name, message, status, statusText } = error;

View file

@ -25,6 +25,16 @@ export const createBooking = (id, attributes = {}) => ({
},
});
// Create a stripeAccount that conforms to the util/types stripeAccount schema
export const createStripeAccount = (id, attributes = {}) => ({
id: new UUID(id),
type: 'stripeAccount',
attributes: {
stripeAccountId: 'acc_testiaccountid',
...attributes,
},
});
// Create a user that conforms to the util/types user schema
export const createUser = (id, attributes = {}) => ({
id: new UUID(id),
@ -41,7 +51,7 @@ export const createUser = (id, attributes = {}) => ({
});
// Create a user that conforms to the util/types currentUser schema
export const createCurrentUser = (id, attributes = {}) => ({
export const createCurrentUser = (id, attributes = {}, includes = {}) => ({
id: new UUID(id),
type: 'currentUser',
attributes: {
@ -55,9 +65,9 @@ export const createCurrentUser = (id, attributes = {}) => ({
displayName: `${id} display name`,
abbreviatedName: `${id} abbreviated name`,
},
stripeConnected: true,
...attributes,
},
...includes,
});
// Create a user that conforms to the util/types user schema

View file

@ -104,7 +104,6 @@ propTypes.currentUser = shape({
abbreviatedName: string.isRequired,
bio: string,
}).isRequired,
stripeConnected: bool.isRequired,
}),
profileImage: propTypes.image,
});
@ -283,6 +282,15 @@ propTypes.review = shape({
subject: propTypes.user,
});
// A Stripe account entity
propTypes.stripeAccount = shape({
id: propTypes.uuid.isRequired,
type: propTypes.value('stripeAccount').isRequired,
attributes: shape({
stripeAccountId: string.isRequired,
}),
});
export const LINE_ITEM_NIGHT = 'line-item/night';
export const LINE_ITEM_DAY = 'line-item/day';
export const LINE_ITEM_UNITS = 'line-item/units';

View file

@ -136,6 +136,24 @@ export const ageAtLeast = (message, minYears) => value => {
return message;
};
export const validBusinessURL = message => value => {
if (typeof value === 'undefined' || value === null) {
return message;
}
const disallowedChars = /[^-A-Za-z0-9+&@#/%?=~_|!:,.;()]/;
const protocolTokens = value.split(':');
const includesProtocol = protocolTokens.length > 1;
const usesHttpProtocol = includesProtocol && !!protocolTokens[0].match(/^(https?)/);
const invalidCharacters = !!value.match(disallowedChars);
const invalidProtocol = !(usesHttpProtocol || !includesProtocol);
// Stripe checks against example.com
const isExampleDotCom = !!value.match(/^(https?:\/\/example\.com|example\.com)/);
const isLocalhost = !!value.match(/^(https?:\/\/localhost($|:|\/)|localhost($|:|\/))/);
return invalidCharacters || invalidProtocol || isExampleDotCom || isLocalhost ? message : VALID;
};
export const validSsnLast4 = message => value => {
return value.length === 4 ? VALID : message;
};

View file

@ -6,6 +6,7 @@ import {
maxLength,
moneySubUnitAmountAtLeast,
composeValidators,
validBusinessURL,
validHKID,
} from './validators';
@ -150,6 +151,35 @@ describe('validators', () => {
expect(moneySubUnitAmountAtLeast('fail', 50)(new Money(100, 'USD'))).toBeUndefined();
});
});
describe('validBusinessURL()', () => {
it('should fail on example.com', () => {
expect(validBusinessURL('fail')('example.com')).toEqual('fail');
});
it('should fail on http://example.com', () => {
expect(validBusinessURL('fail')('http://example.com')).toEqual('fail');
});
it('should fail on localhost', () => {
expect(validBusinessURL('fail')('localhost/')).toEqual('fail');
});
it('should fail on http://localhost', () => {
expect(validBusinessURL('fail')('http://localhost/')).toEqual('fail');
});
it('should fail on localhost:3000', () => {
expect(validBusinessURL('fail')('localhost:3000')).toEqual('fail');
});
it('should fail on <localhosttunnel.com>', () => {
expect(validBusinessURL('fail')('<localhosttunnel.com>')).toEqual('fail');
});
it('should allow on localhosttunnel.com', () => {
expect(validBusinessURL('fail')('localhosttunnel.com')).toBeUndefined();
});
it('should allow on http://localhosttunnel.com', () => {
expect(validBusinessURL('fail')('http://localhosttunnel.com')).toBeUndefined();
});
it('should allow on https://localhosttunnel.com', () => {
expect(validBusinessURL('fail')('https://localhosttunnel.com')).toBeUndefined();
});
});
describe('composeValidators()', () => {
const validateLength = composeValidators(minLength('minLength', 4), maxLength('maxLength', 6));

View file

@ -7773,10 +7773,10 @@ shallowequal@^0.2.2:
dependencies:
lodash.keys "^3.1.2"
sharetribe-flex-sdk@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.2.0.tgz#8600d57a23bf693dddec49080e526cca664cb4a5"
integrity sha512-1fywSn+FI/0sqnEmT5O6PHd/DK2Y1uKPNV6MzKgQ1hpz3RTVkxL96g6Fj6MF6QP3xs/T81lQxoekZ3Bp2HyKeQ==
sharetribe-flex-sdk@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.3.0.tgz#0b6cc5a956cb314c2faf0c3d384e0f5ebbc19b8b"
integrity sha512-OWsKN1tGrfIpu96kF+Oj17DSzaOKBSaR3BVXK0FRdhQNXZWwevCyB9eRK0YShN1er0+Li4JZ05r09IILMux3AQ==
dependencies:
axios "^0.18.0"
js-cookie "^2.1.3"