mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
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:
commit
1e4c26ac77
44 changed files with 1989 additions and 642 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
.select {
|
||||
color: var(--matterColorAnti);
|
||||
border-bottom-color: var(--attentionColor);
|
||||
padding-right: 20px;
|
||||
|
||||
& > option {
|
||||
color: var(--matterColor);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ exports[`PasswordChangePage matches snapshot 1`] = `
|
|||
"firstName": "user1 first name",
|
||||
"lastName": "user1 last name",
|
||||
},
|
||||
"stripeConnected": true,
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "user1",
|
||||
|
|
|
|||
|
|
@ -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 ================ //
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
461
src/ducks/stripe.duck.js
Normal 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));
|
||||
}
|
||||
};
|
||||
|
|
@ -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'));
|
||||
|
|
|
|||
98
src/forms/PayoutDetailsForm/PayoutDetailsAccountOpener.js
Normal file
98
src/forms/PayoutDetailsForm/PayoutDetailsAccountOpener.js
Normal 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;
|
||||
144
src/forms/PayoutDetailsForm/PayoutDetailsAdditionalPersons.js
Normal file
144
src/forms/PayoutDetailsForm/PayoutDetailsAdditionalPersons.js
Normal 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;
|
||||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
88
src/forms/PayoutDetailsForm/PayoutDetailsBusinessProfile.js
Normal file
88
src/forms/PayoutDetailsForm/PayoutDetailsBusinessProfile.js
Normal 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;
|
||||
137
src/forms/PayoutDetailsForm/PayoutDetailsCompany.js
Normal file
137
src/forms/PayoutDetailsForm/PayoutDetailsCompany.js
Normal 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;
|
||||
112
src/forms/PayoutDetailsForm/PayoutDetailsCompanyAccount.js
Normal file
112
src/forms/PayoutDetailsForm/PayoutDetailsCompanyAccount.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
115
src/forms/PayoutDetailsForm/PayoutDetailsIndividualAccount.js
Normal file
115
src/forms/PayoutDetailsForm/PayoutDetailsIndividualAccount.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
292
src/forms/PayoutDetailsForm/merchantCategoryCodesUS.js
Normal file
292
src/forms/PayoutDetailsForm/merchantCategoryCodesUS.js
Normal 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;
|
||||
32
src/forms/PayoutDetailsForm/normalizePhoneNumberUS.js
Normal file
32
src/forms/PayoutDetailsForm/normalizePhoneNumberUS.js
Normal 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)}`;
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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 business’s 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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue