diff --git a/CHANGELOG.md b/CHANGELOG.md index e9545d3011..0833972aeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2019-XX-XX +- [add] Support for Stripe company accounts. `PayoutDetailsForm` was separated into smaller + subcomponents. Multiple new translation keys were added and they might not be translated into + French yet. [#980](https://github.com/sharetribe/flex-template-web/pull/980) - Manage availability of listings. This works for listings that have booking unit type: 'line-item/night', or 'line-item/day'. There's also 'manage availability' link in the ManageListingCards of "your listings" page. diff --git a/src/components/EditListingWizard/EditListingWizard.css b/src/components/EditListingWizard/EditListingWizard.css index 4bdde1708e..85415243bc 100644 --- a/src/components/EditListingWizard/EditListingWizard.css +++ b/src/components/EditListingWizard/EditListingWizard.css @@ -102,3 +102,14 @@ margin-bottom: 0; } } + +.modalTitle { + @apply --marketplaceModalTitleStyles; +} + +.modalPayoutDetailsWrapper { + @media (--viewportMedium) { + width: 604px; + padding-top: 11px; + } +} diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index 262b3680c6..6871dc51b4 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -264,7 +264,7 @@ class EditListingWizard extends Component { onClose={this.handlePayoutModalClose} onManageDisableScrolling={onManageDisableScrolling} > -
+


@@ -273,14 +273,14 @@ class EditListingWizard extends Component {

+

-
); diff --git a/src/components/FieldRadioButton/FieldRadioButton.css b/src/components/FieldRadioButton/FieldRadioButton.css index f2a933be1f..e5df682ab6 100644 --- a/src/components/FieldRadioButton/FieldRadioButton.css +++ b/src/components/FieldRadioButton/FieldRadioButton.css @@ -15,6 +15,16 @@ display: inline; } + /* Highlight the borders if the checkbox is hovered, focused or checked */ + &:hover + label .notChecked, + &:hover + label .required, + &:focus + label .notChecked, + &:focus + label .required, + &:checked + label .notChecked, + &:checked + label .required { + stroke: var(--matterColorDark); + } + /* Hightlight the text on checked, hover and focus */ &:focus + label .text, &:hover + label .text, @@ -26,13 +36,13 @@ .label { display: flex; align-items: center; - padding: 0; + padding-top: 6px; } .radioButtonWrapper { /* This should follow line-height */ height: 32px; - margin-top: -1px; + margin-top: -2px; margin-right: 12px; align-self: baseline; @@ -49,10 +59,16 @@ .notChecked { stroke: var(--matterColorAnti); + &:hover { + stroke: pink; + } } .required { stroke: var(--attentionColor); + &:hover { + stroke: pink; + } } .text { diff --git a/src/components/IconAdd/IconAdd.css b/src/components/IconAdd/IconAdd.css new file mode 100644 index 0000000000..d6ab5b7e4e --- /dev/null +++ b/src/components/IconAdd/IconAdd.css @@ -0,0 +1,5 @@ +@import '../../marketplace.css'; + +.root { + fill: var(--marketplaceColor); +} diff --git a/src/components/IconAdd/IconAdd.example.js b/src/components/IconAdd/IconAdd.example.js new file mode 100644 index 0000000000..217260a4af --- /dev/null +++ b/src/components/IconAdd/IconAdd.example.js @@ -0,0 +1,7 @@ +import IconAdd from './IconAdd'; + +export const Icon = { + component: IconAdd, + props: {}, + group: 'icons', +}; diff --git a/src/components/IconAdd/IconAdd.js b/src/components/IconAdd/IconAdd.js new file mode 100644 index 0000000000..09ce32f8b7 --- /dev/null +++ b/src/components/IconAdd/IconAdd.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import css from './IconAdd.css'; + +const IconAdd = props => { + const { className, rootClassName } = props; + const classes = classNames(rootClassName || css.root, className); + + return ( + + + + ); +}; + +const { string } = PropTypes; + +IconAdd.defaultProps = { + className: null, + rootClassName: null, +}; + +IconAdd.propTypes = { + className: string, + rootClassName: string, +}; + +export default IconAdd; diff --git a/src/components/IconClose/IconClose.example.js b/src/components/IconClose/IconClose.example.js index a369dff737..0bdfc3b843 100644 --- a/src/components/IconClose/IconClose.example.js +++ b/src/components/IconClose/IconClose.example.js @@ -5,3 +5,11 @@ export const Icon = { props: {}, group: 'icons', }; + +export const IconSmall = { + component: IconClose, + props: { + size: 'small', + }, + group: 'icons', +}; diff --git a/src/components/IconClose/IconClose.js b/src/components/IconClose/IconClose.js index b8c9858acb..7ff131ca05 100644 --- a/src/components/IconClose/IconClose.js +++ b/src/components/IconClose/IconClose.js @@ -3,11 +3,23 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import css from './IconClose.css'; +const SIZE_SMALL = 'small'; const IconClose = props => { - const { className, rootClassName } = props; + const { className, rootClassName, size } = props; const classes = classNames(rootClassName || css.root, className); + if (size === SIZE_SMALL) { + return ( + + + + ); + } + return ( (dispatch, getState, sdk) => dispatch(stripeAccountCreateRequest()); + const { accountType, country } = payoutDetails; + + let payoutDetailValues; + if (accountType === 'company') { + payoutDetailValues = payoutDetails['company']; + } else if (accountType === 'individual') { + payoutDetailValues = payoutDetails['individual']; + } + const { firstName, lastName, birthDate, - country, - streetAddress, - postalCode, - city, - state, - province, + address, bankAccountToken, personalIdNumber, - } = payoutDetails; + 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 : '', + }; - const hasProvince = province && !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 address = { - city, - line1: streetAddress, - postal_code: postalCode, - state: hasProvince ? province : 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 }; @@ -420,9 +458,13 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) => legal_entity: { first_name: firstName, last_name: lastName, - address: omitBy(address, isUndefined), + address: omitBy(addressValue, isUndefined), dob: birthDate, - type: 'individual', + type: accountType, + business_name: companyName, + business_tax_id: companyTaxId, + personal_address: personalAddressValue, + additional_owners: additionalOwnersValue, ...idNumber, }, tos_shown_and_accepted: true, diff --git a/src/examples.js b/src/examples.js index aa1798837b..87ab1e3ec4 100644 --- a/src/examples.js +++ b/src/examples.js @@ -20,6 +20,7 @@ import * as FieldReviewRating from './components/FieldReviewRating/FieldReviewRa import * as FieldSelect from './components/FieldSelect/FieldSelect.example'; import * as FieldTextInput from './components/FieldTextInput/FieldTextInput.example'; import * as Footer from './components/Footer/Footer.example'; +import * as IconAdd from './components/IconAdd/IconAdd.example'; import * as IconBannedUser from './components/IconBannedUser/IconBannedUser.example'; import * as IconCheckmark from './components/IconCheckmark/IconCheckmark.example'; import * as IconClose from './components/IconClose/IconClose.example'; @@ -118,6 +119,7 @@ export { FieldSelect, FieldTextInput, Footer, + IconAdd, IconBannedUser, IconCheckmark, IconClose, diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js b/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js index 0e9ed0e186..bda6342fc9 100644 --- a/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js +++ b/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js @@ -2,6 +2,7 @@ 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 { stripeCountryConfigs } from './PayoutDetailsForm'; import css from './PayoutDetailsForm.css'; @@ -23,13 +24,24 @@ const CANADIAN_PROVINCES = [ ]; const PayoutDetailsAddress = props => { - const { country, intl, disabled, form } = props; + const { className, country, intl, disabled, form, fieldId } = props; const countryConfig = country ? stripeCountryConfigs(country).addressConfig : null; const isRequired = (countryConfig, field) => { return countryConfig[field]; }; + const showTitle = + fieldId === 'company.address' || + fieldId === 'individual' || + fieldId === 'company.personalAddress'; + const addressTitle = intl.formatMessage({ + id: + fieldId === 'company.address' + ? 'PayoutDetailsForm.companyAddressTitle' + : 'PayoutDetailsForm.streetAddressLabel', + }); + const showAddressLine = country && isRequired(countryConfig, 'addressLine'); const streetAddressLabel = intl.formatMessage({ @@ -89,11 +101,13 @@ const PayoutDetailsAddress = props => { ); return ( -
+
+ {showTitle ?

{addressTitle}

: null} + {showAddressLine ? ( { label={streetAddressLabel} placeholder={streetAddressPlaceholder} validate={streetAddressRequired} - onUnmount={() => form.change('streetAddress', undefined)} + onUnmount={() => form.change(`${fieldId}.streetAddress`, undefined)} /> ) : null}
{showPostalCode ? ( { label={postalCodeLabel} placeholder={postalCodePlaceholder} validate={postalCodeRequired} - onUnmount={() => form.change('postalCode', undefined)} + onUnmount={() => form.change(`${fieldId}.postalCode`, undefined)} /> ) : null} {showCity ? ( { label={cityLabel} placeholder={cityPlaceholder} validate={cityRequired} - onUnmount={() => form.change('city', undefined)} + onUnmount={() => form.change(`${fieldId}.city`, undefined)} /> ) : null}
{showState ? ( { label={stateLabel} placeholder={statePlaceholder} validate={stateRequired} - onUnmount={() => form.change('state', undefined)} + onUnmount={() => form.change(`${fieldId}.state`, undefined)} /> ) : null} {showProvince ? ( { PayoutDetailsAddress.defaultProps = { country: null, disabled: false, + fieldId: null, }; PayoutDetailsAddress.propTypes = { country: string, disabled: bool, form: object.isRequired, + fieldId: string, + + // from injectIntl + intl: intlShape.isRequired, }; export default PayoutDetailsAddress; diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js b/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js new file mode 100644 index 0000000000..ee5381f60d --- /dev/null +++ b/src/forms/PayoutDetailsForm/PayoutDetailsBankDetails.js @@ -0,0 +1,52 @@ +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 { stripeCountryConfigs } from './PayoutDetailsForm'; +import css from './PayoutDetailsForm.css'; + +const countryCurrency = countryCode => { + const country = stripeCountryConfigs(countryCode); + return country.currency; +}; + +const PayoutDetailsBankDetails = props => { + const { country, disabled, fieldId } = props; + + // StripeBankAccountTokenInputField handles the error messages + // internally, we just have to make sure we require a valid token + // out of the field. Therefore the empty validation message. + const bankAccountRequired = validators.required(' '); + + return ( +
+

+ +

+ +
+ ); +}; +PayoutDetailsBankDetails.defaultProps = { + country: null, + disabled: false, + fieldId: null, +}; + +PayoutDetailsBankDetails.propTypes = { + country: string, + disabled: bool, + fieldId: string, +}; + +export default PayoutDetailsBankDetails; diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsForm.css b/src/forms/PayoutDetailsForm/PayoutDetailsForm.css index 7101f65d68..3cc8235c0a 100644 --- a/src/forms/PayoutDetailsForm/PayoutDetailsForm.css +++ b/src/forms/PayoutDetailsForm/PayoutDetailsForm.css @@ -1,6 +1,7 @@ @import '../../marketplace.css'; .root { + margin-top: 48px; } .disabled { @@ -38,6 +39,20 @@ margin-bottom: 24px; } +.radioButtonRow { + display: flex; + justify-content: left; + flex-shrink: 0; + flex-wrap: wrap; + width: 100%; + margin-bottom: 24px; + white-space: nowrap; +} + +.radioButtonRow > :first-child { + margin-right: 36px; +} + .field { width: 100%; } @@ -64,6 +79,10 @@ width: calc(60% - 9px); } +.taxId { + margin-top: 24px; +} + .error { @apply --marketplaceModalErrorStyles; } @@ -86,3 +105,60 @@ cursor: pointer; } } + +.bankDetailsStripeField p { + @apply --marketplaceH4FontStyles; +} + +.personalAddressContainer { + margin-bottom: 28px; +} + +.fieldArrayAdd { + @apply --marketplaceLinkStyles; + @apply --marketplaceSearchFilterSublabelFontStyles; + font-weight: var(--fontWeightMedium); + margin-bottom: 2px; +} + +.fieldArrayRemove { + @apply --marketplaceH5FontStyles; + color: var(--matterColorAnti); + float: right; + line-height: 20px; + + &:hover { + color: var(--matterColor); + } +} + +.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; +} + +.addIcon { + margin-right: 7px; + display: inline-block; + height: 18px; + padding-top: 1px; +} diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsForm.js b/src/forms/PayoutDetailsForm/PayoutDetailsForm.js index 2be1746402..4964c8b3a9 100644 --- a/src/forms/PayoutDetailsForm/PayoutDetailsForm.js +++ b/src/forms/PayoutDetailsForm/PayoutDetailsForm.js @@ -3,25 +3,17 @@ import { bool, object, 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, - StripeBankAccountTokenInputField, - FieldSelect, - FieldBirthdayInput, - FieldTextInput, - Form, -} from '../../components'; -import * as validators from '../../util/validators'; +import { Button, ExternalLink, FieldRadioButton, FieldSelect, Form } from '../../components'; import { isStripeInvalidPostalCode } from '../../util/errors'; +import * as validators from '../../util/validators'; -import PayoutDetailsAddress from './PayoutDetailsAddress'; +import PayoutDetailsFormCompany from './PayoutDetailsFormCompany'; +import PayoutDetailsFormIndividual from './PayoutDetailsFormIndividual'; import css from './PayoutDetailsForm.css'; -const MIN_STRIPE_ACCOUNT_AGE = 18; - const supportedCountries = config.stripe.supportedCountries.map(c => c.code); export const stripeCountryConfigs = countryCode => { @@ -33,20 +25,17 @@ export const stripeCountryConfigs = countryCode => { return country; }; -const countryCurrency = countryCode => { - const country = stripeCountryConfigs(countryCode); - return country.currency; -}; - const PayoutDetailsFormComponent = props => ( { const { className, createStripeAccountError, disabled, - form, handleSubmit, inProgress, intl, @@ -56,49 +45,14 @@ const PayoutDetailsFormComponent = props => ( submitButtonText, values, } = fieldRenderProps; - const { country } = values; - const firstNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.firstNameLabel' }); - const firstNamePlaceholder = intl.formatMessage({ - id: 'PayoutDetailsForm.firstNamePlaceholder', - }); - const firstNameRequired = validators.required( - intl.formatMessage({ - id: 'PayoutDetailsForm.firstNameRequired', - }) - ); + const { country, accountType } = values; - const lastNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.lastNameLabel' }); - const lastNamePlaceholder = intl.formatMessage({ - id: 'PayoutDetailsForm.lastNamePlaceholder', + const individualAccountLabel = intl.formatMessage({ + id: 'PayoutDetailsForm.individualAccount', }); - const lastNameRequired = validators.required( - intl.formatMessage({ - id: 'PayoutDetailsForm.lastNameRequired', - }) - ); - const birthdayLabel = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabel' }); - const birthdayLabelMonth = intl.formatMessage({ - id: 'PayoutDetailsForm.birthdayLabelMonth', - }); - const birthdayLabelYear = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabelYear' }); - const birthdayRequired = validators.required( - intl.formatMessage({ - id: 'PayoutDetailsForm.birthdayRequired', - }) - ); - const birthdayMinAge = validators.ageAtLeast( - intl.formatMessage( - { - id: 'PayoutDetailsForm.birthdayMinAge', - }, - { - minAge: MIN_STRIPE_ACCOUNT_AGE, - } - ), - MIN_STRIPE_ACCOUNT_AGE - ); + const companyAccountLabel = intl.formatMessage({ id: 'PayoutDetailsForm.companyAccount' }); const countryLabel = intl.formatMessage({ id: 'PayoutDetailsForm.countryLabel' }); const countryPlaceholder = intl.formatMessage({ @@ -110,59 +64,16 @@ const PayoutDetailsFormComponent = props => ( }) ); - // StripeBankAccountTokenInputField handles the error messages - // internally, we just have to make sure we require a valid token - // out of the field. Therefore the empty validation message. - const bankAccountRequired = validators.required(' '); - - const showPersonalIdNumber = - (country && stripeCountryConfigs(country).personalIdNumberRequired) || - (country && stripeCountryConfigs(country).ssnLast4Required); - - const personalIdNumberRequired = validators.required( - intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberRequired`, - }) - ); - - let personalIdNumberLabel = null; - let personalIdNumberPlaceholder = null; - let personalIdNumberValid = personalIdNumberRequired; - - if (country === 'US') { - personalIdNumberLabel = intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberLabel.US`, - }); - personalIdNumberPlaceholder = intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberPlaceholder.US`, - }); - - const validSSN = validators.validSsnLast4( - intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberValid`, - }) - ); - personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validSSN); - } else if (country === 'HK') { - personalIdNumberLabel = intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberLabel.HK`, - }); - personalIdNumberPlaceholder = intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberPlaceholder.HK`, - }); - const validHKID = validators.validHKID( - intl.formatMessage({ - id: `PayoutDetailsForm.personalIdNumberValid`, - }) - ); - personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validHKID); - } - const classes = classNames(css.root, className, { [css.disabled]: disabled, }); + const submitInProgress = inProgress; const submitDisabled = pristine || invalid || disabled || submitInProgress; + const showAsRequired = pristine; + + const showIndividual = country && accountType && accountType === 'individual'; + const showCompany = country && accountType && accountType === 'company'; let error = null; @@ -185,130 +96,87 @@ const PayoutDetailsFormComponent = props => ( ); + return (

- +

-
- + -
-
-
-

- -

- - - {supportedCountries.map(c => ( - - ))} - - - -
- {country ? ( -
-

- -

- -
+ {accountType ? ( + +
+

Country

+ + + {supportedCountries.map(c => ( + + ))} + +
+ + {showIndividual ? ( + + ) : showCompany ? ( + + ) : null} + + {error} + +

+ +

+ +
) : null} - - {showPersonalIdNumber ? ( -
-

- -

- -
- ) : null} - - {error} - -

- -

-
); }} diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsFormCompany.js b/src/forms/PayoutDetailsForm/PayoutDetailsFormCompany.js new file mode 100644 index 0000000000..12597d589b --- /dev/null +++ b/src/forms/PayoutDetailsForm/PayoutDetailsFormCompany.js @@ -0,0 +1,211 @@ +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 { 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; + return ( + + {country ? ( + +
+

+ +

+ + + +
+ + + + + + + + {showPersonalAddressField ? ( + + ) : null} + + {showAdditionalOwnersField ? ( +
+ + {({ fields }) => + fields.map((name, index) => ( +
+
fields.remove(index)} + style={{ cursor: 'pointer' }} + > + + + + +
+ + {showPersonalAddressField ? ( + + ) : null} +
+ )) + } +
+ + {!hasAdditionalOwners || !hasMaxNumberOfAdditionalOwners ? ( + push('company.additionalOwners', undefined)} + > + + + + + + ) : null} +
+ ) : null} +
+ ) : null} +
+ ); +}; + +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; diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsFormIndividual.js b/src/forms/PayoutDetailsForm/PayoutDetailsFormIndividual.js new file mode 100644 index 0000000000..55ffe0c90c --- /dev/null +++ b/src/forms/PayoutDetailsForm/PayoutDetailsFormIndividual.js @@ -0,0 +1,48 @@ +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 ( + + + + + + ); +}; + +PayoutDetailsFormIndividualComponent.defaultProps = { + disabled: false, +}; + +PayoutDetailsFormIndividualComponent.propTypes = { + disabled: bool, + + // from injectIntl + intl: intlShape.isRequired, +}; + +const PayoutDetailsFormIndividual = compose(injectIntl)(PayoutDetailsFormIndividualComponent); + +export default PayoutDetailsFormIndividual; diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsPersonalDetails.js b/src/forms/PayoutDetailsForm/PayoutDetailsPersonalDetails.js new file mode 100644 index 0000000000..3b4e7026ca --- /dev/null +++ b/src/forms/PayoutDetailsForm/PayoutDetailsPersonalDetails.js @@ -0,0 +1,177 @@ +import React from 'react'; +import { bool, string } from 'prop-types'; +import { FieldBirthdayInput, FieldTextInput } from '../../components'; +import * as validators from '../../util/validators'; +import { intlShape } from 'react-intl'; + +import { stripeCountryConfigs } from './PayoutDetailsForm'; +import css from './PayoutDetailsForm.css'; + +const MIN_STRIPE_ACCOUNT_AGE = 18; + +const PayoutDetailsPersonalDetails = props => { + const { intl, disabled, values, country, fieldId } = props; + + const personalDetailsTitle = intl.formatMessage({ + id: + fieldId === 'company' || fieldId === 'individual' + ? 'PayoutDetailsForm.personalDetailsTitle' + : 'PayoutDetailsForm.personalDetailsAdditionalOwnerTitle', + }); + + const firstNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.firstNameLabel' }); + const firstNamePlaceholder = intl.formatMessage({ + id: 'PayoutDetailsForm.firstNamePlaceholder', + }); + const firstNameRequired = validators.required( + intl.formatMessage({ + id: 'PayoutDetailsForm.firstNameRequired', + }) + ); + + const lastNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.lastNameLabel' }); + const lastNamePlaceholder = intl.formatMessage({ + id: 'PayoutDetailsForm.lastNamePlaceholder', + }); + const lastNameRequired = validators.required( + intl.formatMessage({ + id: 'PayoutDetailsForm.lastNameRequired', + }) + ); + + const birthdayLabel = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabel' }); + const birthdayLabelMonth = intl.formatMessage({ + id: 'PayoutDetailsForm.birthdayLabelMonth', + }); + const birthdayLabelYear = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabelYear' }); + const birthdayRequired = validators.required( + intl.formatMessage({ + id: 'PayoutDetailsForm.birthdayRequired', + }) + ); + const birthdayMinAge = validators.ageAtLeast( + intl.formatMessage( + { + id: 'PayoutDetailsForm.birthdayMinAge', + }, + { + minAge: MIN_STRIPE_ACCOUNT_AGE, + } + ), + MIN_STRIPE_ACCOUNT_AGE + ); + + const showPersonalIdNumber = + (country && stripeCountryConfigs(country).personalIdNumberRequired) || + (country && stripeCountryConfigs(country).ssnLast4Required); + + const personalIdNumberRequired = validators.required( + intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberRequired`, + }) + ); + + let personalIdNumberLabel = null; + let personalIdNumberPlaceholder = null; + let personalIdNumberValid = personalIdNumberRequired; + + if (country === 'US') { + personalIdNumberLabel = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberLabel.US`, + }); + personalIdNumberPlaceholder = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberPlaceholder.US`, + }); + + const validSSN = validators.validSsnLast4( + intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberValid`, + }) + ); + personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validSSN); + } else if (country === 'HK') { + personalIdNumberLabel = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberLabel.HK`, + }); + personalIdNumberPlaceholder = intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberPlaceholder.HK`, + }); + const validHKID = validators.validHKID( + intl.formatMessage({ + id: `PayoutDetailsForm.personalIdNumberValid`, + }) + ); + personalIdNumberValid = validators.composeValidators(personalIdNumberRequired, validHKID); + } + + return ( +
+

{personalDetailsTitle}

+
+ + +
+
+ +
+ + {showPersonalIdNumber ? ( + + ) : null} +
+ ); +}; +PayoutDetailsPersonalDetails.defaultProps = { + country: null, + disabled: false, + fieldId: null, +}; + +PayoutDetailsPersonalDetails.propTypes = { + country: string, + disabled: bool, + fieldId: string, + intl: intlShape.isRequired, +}; + +export default PayoutDetailsPersonalDetails; diff --git a/src/stripe-config.js b/src/stripe-config.js index ec2a94c6c4..611d9bba74 100644 --- a/src/stripe-config.js +++ b/src/stripe-config.js @@ -38,6 +38,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Belgium @@ -51,6 +55,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Canada @@ -80,6 +88,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Finland @@ -93,6 +105,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // France @@ -106,6 +122,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Germany @@ -119,6 +139,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Hong Kong @@ -134,6 +158,9 @@ export const stripeSupportedCountries = [ accountNumber: true, }, personalIdNumberRequired: true, + companyConfig: { + personalAddress: true, + }, }, { // Ireland @@ -147,6 +174,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Italy @@ -160,6 +191,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Luxembourg @@ -173,6 +208,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Netherlands @@ -186,6 +225,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // New Zealand @@ -212,6 +255,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Portugal @@ -225,6 +272,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Spain @@ -238,6 +289,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Sweden @@ -251,6 +306,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // Switzerland @@ -264,6 +323,10 @@ export const stripeSupportedCountries = [ accountConfig: { iban: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // United Kingdom @@ -278,6 +341,10 @@ export const stripeSupportedCountries = [ sortCode: true, accountNumber: true, }, + companyConfig: { + personalAddress: true, + additionalOwners: true, + }, }, { // United States diff --git a/src/translations/en.json b/src/translations/en.json index 4900d5ffdd..c948ddfab5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -452,6 +452,9 @@ "PasswordResetPage.recoveryLinkText": "password recovery page", "PasswordResetPage.resetFailed": "Reset failed. Please try again.", "PasswordResetPage.title": "Reset password", + "PayoutDetailsForm.accountTypeTitle": "Account type", + "PayoutDetailsForm.additionalOwnerLabel": "Add additional owner", + "PayoutDetailsForm.additionalOwnerRemove": "Remove additional owner", "PayoutDetailsForm.addressTitle": "Address", "PayoutDetailsForm.bankDetails": "Bank details", "PayoutDetailsForm.birthdayDatePlaceholder": "dd", @@ -481,6 +484,35 @@ "PayoutDetailsForm.cityLabel": "City", "PayoutDetailsForm.cityPlaceholder": "Helsinki", "PayoutDetailsForm.cityRequired": "This field is required", + "PayoutDetailsForm.companyAccount": "I represent a company", + "PayoutDetailsForm.companyAddressTitle": "Company address", + "PayoutDetailsForm.companyDetailsTitle": "Company details", + "PayoutDetailsForm.companyNameLabel": "Company name", + "PayoutDetailsForm.companyNamePlaceholder": "Enter company name...", + "PayoutDetailsForm.companyNameRequired": "Company name is required", + "PayoutDetailsForm.companyTaxIdLabel.AT": "Firmenbuchnummer (FN)", + "PayoutDetailsForm.companyTaxIdLabel.AU": "Company ACN/ABN - TFN", + "PayoutDetailsForm.companyTaxIdLabel.BE": "TVA/BTW/CBE", + "PayoutDetailsForm.companyTaxIdLabel.CA": "Business Number (Tax ID)", + "PayoutDetailsForm.companyTaxIdLabel.CH": "VAT number UID/MWST/TVA/IVA", + "PayoutDetailsForm.companyTaxIdLabel.DE": "Handelsregisternummer (HRB) ", + "PayoutDetailsForm.companyTaxIdLabel.DK": "Momsregistreringsnummer (CVR)", + "PayoutDetailsForm.companyTaxIdLabel.ES": "Número de Identificación Fiscal (NIF)", + "PayoutDetailsForm.companyTaxIdLabel.FI": "Y-tunnus", + "PayoutDetailsForm.companyTaxIdLabel.FR": "Numéro SIREN", + "PayoutDetailsForm.companyTaxIdLabel.GB": "Companies House Registration Number (CRN)", + "PayoutDetailsForm.companyTaxIdLabel.HK": "Registration Number", + "PayoutDetailsForm.companyTaxIdLabel.IE": "Company Number", + "PayoutDetailsForm.companyTaxIdLabel.IT": "Numero RI/ REA", + "PayoutDetailsForm.companyTaxIdLabel.LU": "Company/RCS number", + "PayoutDetailsForm.companyTaxIdLabel.NL": "KVK number", + "PayoutDetailsForm.companyTaxIdLabel.NO": "Organisasjonsnummer (Orgnr)", + "PayoutDetailsForm.companyTaxIdLabel.NZ": "NZBN", + "PayoutDetailsForm.companyTaxIdLabel.PT": "N.º Contribuinte", + "PayoutDetailsForm.companyTaxIdLabel.SE": "Organisationsnummer", + "PayoutDetailsForm.companyTaxIdLabel.US": "Tax ID", + "PayoutDetailsForm.companyTaxIdPlaceholder": "Enter {idName}...", + "PayoutDetailsForm.companyTaxIdRequired": "{idName} is required", "PayoutDetailsForm.countryLabel": "Country", "PayoutDetailsForm.countryNames.AT": "Austria", "PayoutDetailsForm.countryNames.AU": "Australia", @@ -510,10 +542,12 @@ "PayoutDetailsForm.firstNameLabel": "First name", "PayoutDetailsForm.firstNamePlaceholder": "John", "PayoutDetailsForm.firstNameRequired": "This field is required", + "PayoutDetailsForm.individualAccount": "I'm an individual", "PayoutDetailsForm.information": "Since this was your first listing, we need to know bit more about you in order to send you money. We only ask these once.", "PayoutDetailsForm.lastNameLabel": "Last name", "PayoutDetailsForm.lastNamePlaceholder": "Doe", "PayoutDetailsForm.lastNameRequired": "This field is required", + "PayoutDetailsForm.personalDetailsAdditionalOwnerTitle": "Additional owner details", "PayoutDetailsForm.personalDetailsTitle": "Personal details", "PayoutDetailsForm.personalIdNumberTitle": "Personal id number", "PayoutDetailsForm.personalIdNumberLabel.HK": "Hong Kong Identity Card Number (HKID)", diff --git a/src/translations/fr.json b/src/translations/fr.json index 6d2c20892c..893147a7c1 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -452,6 +452,9 @@ "PasswordResetPage.recoveryLinkText": "la page de réinitialisation du mot de passe", "PasswordResetPage.resetFailed": "La réinitialisation a échoué.", "PasswordResetPage.title": "Réinitialiser le mot de passe", + "PayoutDetailsForm.accountTypeTitle": "Account type", + "PayoutDetailsForm.additionalOwnerLabel": "+ Add additional owner", + "PayoutDetailsForm.additionalOwnerRemove": "Remove additional owner", "PayoutDetailsForm.addressTitle": "Adresse", "PayoutDetailsForm.bankDetails": "Détail du compte bancaire", "PayoutDetailsForm.birthdayDatePlaceholder": "jj", @@ -481,6 +484,35 @@ "PayoutDetailsForm.cityLabel": "Ville", "PayoutDetailsForm.cityPlaceholder": "Helsinki", "PayoutDetailsForm.cityRequired": "Ce champ est requis.", + "PayoutDetailsForm.companyAccount": "I represent a company", + "PayoutDetailsForm.companyAddressTitle": "Company address", + "PayoutDetailsForm.companyDetailsTitle": "Company details", + "PayoutDetailsForm.companyNameLabel": "Company name", + "PayoutDetailsForm.companyNamePlaceholder": "Enter company name...", + "PayoutDetailsForm.companyNameRequired": "Company name is required", + "PayoutDetailsForm.companyTaxIdLabel.AT": "Firmenbuchnummer (FN)", + "PayoutDetailsForm.companyTaxIdLabel.AU": "Company ACN/ABN - TFN", + "PayoutDetailsForm.companyTaxIdLabel.BE": "TVA/BTW/CBE", + "PayoutDetailsForm.companyTaxIdLabel.CA": "Business Number (Tax ID)", + "PayoutDetailsForm.companyTaxIdLabel.CH": "VAT number UID/MWST/TVA/IVA", + "PayoutDetailsForm.companyTaxIdLabel.DE": "Handelsregisternummer (HRB) ", + "PayoutDetailsForm.companyTaxIdLabel.DK": "Momsregistreringsnummer (CVR)", + "PayoutDetailsForm.companyTaxIdLabel.ES": "Número de Identificación Fiscal (NIF)", + "PayoutDetailsForm.companyTaxIdLabel.FI": "Y-tunnus", + "PayoutDetailsForm.companyTaxIdLabel.FR": "Numéro SIREN", + "PayoutDetailsForm.companyTaxIdLabel.GB": "Companies House Registration Number (CRN)", + "PayoutDetailsForm.companyTaxIdLabel.HK": "Registration Number", + "PayoutDetailsForm.companyTaxIdLabel.IE": "Company Number", + "PayoutDetailsForm.companyTaxIdLabel.IT": "Numero RI/ REA", + "PayoutDetailsForm.companyTaxIdLabel.LU": "Company/RCS number", + "PayoutDetailsForm.companyTaxIdLabel.NL": "KVK number", + "PayoutDetailsForm.companyTaxIdLabel.NO": "Organisasjonsnummer (Orgnr)", + "PayoutDetailsForm.companyTaxIdLabel.NZ": "NZBN", + "PayoutDetailsForm.companyTaxIdLabel.PT": "N.º Contribuinte", + "PayoutDetailsForm.companyTaxIdLabel.SE": "Organisationsnummer", + "PayoutDetailsForm.companyTaxIdLabel.US": "Tax ID", + "PayoutDetailsForm.companyTaxIdPlaceholder": "Enter {idName}...", + "PayoutDetailsForm.companyTaxIdRequired": "{idName} is required", "PayoutDetailsForm.countryLabel": "Pays", "PayoutDetailsForm.countryNames.AT": "Autriche", "PayoutDetailsForm.countryNames.AU": "Australie", @@ -510,10 +542,12 @@ "PayoutDetailsForm.firstNameLabel": "Prénom", "PayoutDetailsForm.firstNamePlaceholder": "Prénom", "PayoutDetailsForm.firstNameRequired": "Ce champ est requis.", + "PayoutDetailsForm.individualAccount": "I'm an individual", "PayoutDetailsForm.information": "Puisque c'est votre première annonce, nous devons en savoir un peu plus pour pouvoir vous transférer vos gains. Nous ne vous les demanderons qu'une seule fois.", "PayoutDetailsForm.lastNameLabel": "Nom", "PayoutDetailsForm.lastNamePlaceholder": "Nom", "PayoutDetailsForm.lastNameRequired": "Ce champ est requis.", + "PayoutDetailsForm.personalDetailsAdditionalOwnerTitle": "Additional owner details", "PayoutDetailsForm.personalDetailsTitle": "Détails", "PayoutDetailsForm.personalIdNumberLabel.HK": "Hong Kong Identity Card Number (HKID)", "PayoutDetailsForm.personalIdNumberLabel.US": "Quatre derniers chiffres du numéro de sécurité sociale (SSN)",