Merge pull request #968 from sharetribe/add-missing-stripe-countries

Add missing stripe countries
This commit is contained in:
Jenni Nurmi 2018-12-11 10:49:46 +02:00 committed by GitHub
commit 2625b4427c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 855 additions and 259 deletions

View file

@ -1,6 +1,8 @@
# Mandatory configuration
#
# Note: You also need to set Stripe secret key in Flex Console.
#
REACT_APP_SHARETRIBE_SDK_CLIENT_ID=
REACT_APP_STRIPE_PUBLISHABLE_KEY=
REACT_APP_MAPBOX_ACCESS_TOKEN=

View file

@ -14,10 +14,16 @@ way to update this template, but currently, we follow a pattern:
## Upcoming version 2018-XX-XX
- [add] Add Stripe support for new countries: Canada, New Zealand, Switzerland, Norway, and Hong
Kong. Also add more required fields for US and Australia.
- StripeBankAccountTokenInputField component and PayoutDetailsForm have some changes
- Stripe related configuration is separated to new stripe-config.js file.
- Multiple new translation keys were added and they might not be translated into French yet. If
you use French translation check PR for the changed keys.
[#968](https://github.com/sharetribe/flex-template-web/pull/968)
- [change] Remove generic perUnit translations and replace them with specific night, day and unit
translations depending on booking unit chosen in config.
[#970](https://github.com/sharetribe/flex-template-web/pull/970)
- [fix] Formatting docs with newest Prettier - related commit was lost in #967 at some point.
[#975](https://github.com/sharetribe/flex-template-web/pull/975)
- [change] Improved documents related to onboarding: env.md, deploying-to-production.md,

View file

@ -104,3 +104,20 @@ export const AU_AUD = {
},
group: 'custom inputs',
};
// CA
export const CA_CAD = {
component: formComponent('CA'),
props: {
formName: 'CA_CAD',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('values submitted:', values);
},
},
group: 'custom inputs',
};

View file

@ -134,15 +134,8 @@ class TokenInputFieldComponent extends Component {
};
// Include input values with correct stripe keys
inputsNeeded.forEach(inputType => {
// Stripe fails if there are spaces within the number, this is
// why we have to clean value up first.
const inputValueObj = mapInputsToStripeAccountKeys(
inputType,
cleanedString(values[inputType])
);
accountData = { ...accountData, ...inputValueObj };
});
const inputValueObj = mapInputsToStripeAccountKeys(country, values);
accountData = { ...accountData, ...inputValueObj };
// https://stripe.com/docs/stripe-js/reference#collecting-bank-account-details
this.stripe

View file

@ -6,6 +6,14 @@ import config from '../../config';
export const ACCOUNT_NUMBER = 'accountNumber';
// Australian equivalent for routing number
export const BSB = 'bsb';
// Needed for creating full routing number in Canada
export const INSTITUTION_NUMBER = 'institutionNumber';
// Needed for creating full routing number in Canada
export const TRANSIT_NUMBER = 'transitNumber';
// Needed for creating full routing number in Hong Kong
export const CLEARING_CODE = 'clearingCode';
// Needed for creating full routing number in Hong Kong
export const BRANCH_CODE = 'branchCode';
// International bank account number (e.g. EU countries use this)
export const IBAN = 'iban';
// Routing number to separate bank account in different areas
@ -15,7 +23,17 @@ export const SORT_CODE = 'sortCode';
// Currently supported bank account inputs
// the order here matters: account number input is asked after routing number and its equivalents
export const BANK_ACCOUNT_INPUTS = [BSB, SORT_CODE, ROUTING_NUMBER, ACCOUNT_NUMBER, IBAN];
export const BANK_ACCOUNT_INPUTS = [
BSB,
TRANSIT_NUMBER,
INSTITUTION_NUMBER,
CLEARING_CODE,
BRANCH_CODE,
SORT_CODE,
ROUTING_NUMBER,
ACCOUNT_NUMBER,
IBAN,
];
export const supportedCountries = config.stripe.supportedCountries.map(c => c.code);
@ -117,7 +135,7 @@ export const translateStripeError = (country, intl, stripeError) => {
*
* @return {Object} key - value in Object literal.
*/
export const mapInputsToStripeAccountKeys = (inputType, value) => {
export const mapInputsToStripeAccountKeys = (country, values) => {
// Stripe documentation speaks about actual bank account terms of different countries
// (like BSB, sort code, routing number), but all of those get mapped to one of
// the two different request keys: routing_number or account_number
@ -126,17 +144,63 @@ export const mapInputsToStripeAccountKeys = (inputType, value) => {
// We use those country specific terms since we want to show correct labels and errors for users,
// so this mapping is needed before sending data to Stripe.
switch (inputType) {
case IBAN:
case ACCOUNT_NUMBER:
return { account_number: value };
case BSB:
case SORT_CODE:
case ROUTING_NUMBER:
return { routing_number: value };
// Stripe fails if there are spaces within the number, this is
// why we have to clean value up first.
switch (country) {
case 'AT':
case 'BE':
case 'DK':
case 'FI':
case 'FR':
case 'DE':
case 'IE':
case 'IT':
case 'LU':
case 'NL':
case 'PT':
case 'ES':
case 'SE':
case 'CH':
case 'NO':
return { account_number: cleanedString(values[IBAN]) };
case 'NZ':
// NZ account number is typically presented in the format xx-xxxx-xxxxxxx-xxx
// '-' separators must be removed before sending value to Stripe API
return { account_number: cleanedString(values[ACCOUNT_NUMBER]).replace(/-/g, '') };
case 'AU':
return {
routing_number: cleanedString(values[BSB]),
account_number: cleanedString(values[ACCOUNT_NUMBER]),
};
case 'CA':
return {
routing_number: cleanedString(values[TRANSIT_NUMBER]).concat(
cleanedString(values[INSTITUTION_NUMBER])
),
account_number: cleanedString(values[ACCOUNT_NUMBER]),
};
case 'GB':
return {
routing_number: cleanedString(values[SORT_CODE]),
account_number: cleanedString(values[ACCOUNT_NUMBER]),
};
case 'US':
return {
routing_number: cleanedString(values[ROUTING_NUMBER]),
account_number: cleanedString(values[ACCOUNT_NUMBER]),
};
case 'HK':
return {
routing_number: cleanedString(values[CLEARING_CODE]).concat(
'-',
cleanedString(values[BRANCH_CODE])
),
account_number: cleanedString(values[ACCOUNT_NUMBER]),
};
default:
throw new Error(`Unknown inputType (${inputType}) given to validator`);
throw new Error(`Not supported country (${country}) given to validator`);
}
};

View file

@ -1,5 +1,6 @@
import * as custom from './marketplace-custom-config.js';
import defaultLocationSearches from './default-location-searches';
import { stripePublishableKey, stripeSupportedCountries } from './stripe-config';
const env = process.env.REACT_APP_ENV;
const dev = process.env.REACT_APP_ENV === 'development';
@ -79,162 +80,6 @@ const currencyConfig = {
maximumFractionDigits: 2,
};
const stripePublishableKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY;
// Stripe only supports payments in certain countries, see full list
// at https://stripe.com/global
//
// We currently only support EU countries, US, and AU.
const stripeSupportedCountries = [
{
// Australia
code: 'AU',
currency: 'AUD',
payoutAddressRequired: false,
accountConfig: {
bsb: true,
accountNumber: true,
},
},
{
// Austria
code: 'AT',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Belgium
code: 'BE',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Denmark
code: 'DK',
currency: 'DKK',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Finland
code: 'FI',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// France
code: 'FR',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Germany
code: 'DE',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Ireland
code: 'IE',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Italy
code: 'IT',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Luxembourg
code: 'LU',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Netherlands
code: 'NL',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Portugal
code: 'PT',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Spain
code: 'ES',
currency: 'EUR',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// Sweden
code: 'SE',
currency: 'SEK',
payoutAddressRequired: true,
accountConfig: {
iban: true,
},
},
{
// United Kingdom
code: 'GB',
currency: 'GBP',
payoutAddressRequired: true,
accountConfig: {
sortCode: true,
accountNumber: true,
},
},
{
// United States
code: 'US',
currency: 'USD',
payoutAddressRequired: false,
accountConfig: {
routingNumber: true,
accountNumber: true,
},
},
];
// Address information is used in SEO schema for Organization (http://schema.org/PostalAddress)
const addressCountry = 'FI';
const addressRegion = 'Helsinki';

View file

@ -397,15 +397,24 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) =>
streetAddress,
postalCode,
city,
state,
province,
bankAccountToken,
personalIdNumber,
} = payoutDetails;
const hasProvince = province && !state;
const address = {
city,
line1: streetAddress,
postal_code: postalCode,
state: hasProvince ? province : state,
};
const idNumber =
country === 'US' ? { ssn_last_4: personalIdNumber } : { personal_id_number: personalIdNumber };
// Params for Stripe SDK
const params = {
legal_entity: {
@ -414,6 +423,7 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) =>
address: omitBy(address, isUndefined),
dob: birthDate,
type: 'individual',
...idNumber,
},
tos_shown_and_accepted: true,
};

View file

@ -0,0 +1,186 @@
import React from 'react';
import { bool, object, string } from 'prop-types';
import { FieldSelect, FieldTextInput } from '../../components';
import * as validators from '../../util/validators';
import { stripeCountryConfigs } from './PayoutDetailsForm';
import css from './PayoutDetailsForm.css';
const CANADIAN_PROVINCES = [
'AB',
'BC',
'MB',
'NB',
'NL',
'NS',
'NT',
'NU',
'ON',
'PE',
'QC',
'SK',
'YT',
];
const PayoutDetailsAddress = props => {
const { country, intl, disabled, form } = props;
const countryConfig = country ? stripeCountryConfigs(country).addressConfig : null;
const isRequired = (countryConfig, field) => {
return countryConfig[field];
};
const showAddressLine = country && isRequired(countryConfig, 'addressLine');
const streetAddressLabel = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressLabel',
});
const streetAddressPlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressPlaceholder',
});
const streetAddressRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressRequired',
})
);
const showPostalCode = country && isRequired(countryConfig, 'postalCode');
const postalCodeLabel = intl.formatMessage({ id: 'PayoutDetailsForm.postalCodeLabel' });
const postalCodePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.postalCodePlaceholder',
});
const postalCodeRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.postalCodeRequired',
})
);
const showCity = country && isRequired(countryConfig, 'city');
const cityLabel = intl.formatMessage({ id: 'PayoutDetailsForm.cityLabel' });
const cityPlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.cityPlaceholder' });
const cityRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.cityRequired',
})
);
const showState = country && isRequired(countryConfig, 'state');
const stateLabel = intl.formatMessage({ id: 'PayoutDetailsForm.stateLabel' });
const statePlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.statePlaceholder' });
const stateRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.stateRequired',
})
);
const showProvince = country && isRequired(countryConfig, 'province');
const provinceLabel = intl.formatMessage({ id: 'PayoutDetailsForm.canadianProvinceLabel' });
const provincePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.canadianProvincePlaceholder',
});
const provinceRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.canadianProvinceRequired',
})
);
return (
<div>
{showAddressLine ? (
<FieldTextInput
id="streetAddress"
name="streetAddress"
disabled={disabled}
className={css.field}
type="text"
autoComplete="street-address"
label={streetAddressLabel}
placeholder={streetAddressPlaceholder}
validate={streetAddressRequired}
onUnmount={() => form.change('streetAddress', undefined)}
/>
) : null}
<div className={css.formRow}>
{showPostalCode ? (
<FieldTextInput
id="postalCode"
name="postalCode"
disabled={disabled}
className={css.postalCode}
type="text"
autoComplete="postal-code"
label={postalCodeLabel}
placeholder={postalCodePlaceholder}
validate={postalCodeRequired}
onUnmount={() => form.change('postalCode', undefined)}
/>
) : null}
{showCity ? (
<FieldTextInput
id="city"
name="city"
disabled={disabled}
className={css.city}
type="text"
autoComplete="address-level2"
label={cityLabel}
placeholder={cityPlaceholder}
validate={cityRequired}
onUnmount={() => form.change('city', undefined)}
/>
) : null}
</div>
{showState ? (
<FieldTextInput
id="state"
name="state"
disabled={disabled}
className={css.state}
type="text"
autoComplete="state"
label={stateLabel}
placeholder={statePlaceholder}
validate={stateRequired}
onUnmount={() => form.change('state', undefined)}
/>
) : null}
{showProvince ? (
<FieldSelect
id="province"
name="province"
disabled={disabled}
className={css.selectCountry}
autoComplete="province"
label={provinceLabel}
validate={provinceRequired}
>
<option disabled value="">
{provincePlaceholder}
</option>
{CANADIAN_PROVINCES.map(p => (
<option key={p} value={p}>
{intl.formatMessage({ id: `PayoutDetailsForm.canadianProvinceNames.${p}` })}
</option>
))}
</FieldSelect>
) : null}
</div>
);
};
PayoutDetailsAddress.defaultProps = {
country: null,
disabled: false,
};
PayoutDetailsAddress.propTypes = {
country: string,
disabled: bool,
form: object.isRequired,
};
export default PayoutDetailsAddress;

View file

@ -17,6 +17,7 @@ import {
import * as validators from '../../util/validators';
import { isStripeInvalidPostalCode } from '../../util/errors';
import PayoutDetailsAddress from './PayoutDetailsAddress';
import css from './PayoutDetailsForm.css';
const MIN_STRIPE_ACCOUNT_AGE = 18;
@ -32,11 +33,6 @@ export const stripeCountryConfigs = countryCode => {
return country;
};
const requiresAddress = countryCode => {
const country = stripeCountryConfigs(countryCode);
return country.payoutAddressRequired;
};
const countryCurrency = countryCode => {
const country = stripeCountryConfigs(countryCode);
return country.currency;
@ -114,43 +110,54 @@ const PayoutDetailsFormComponent = props => (
})
);
const streetAddressLabel = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressLabel',
});
const streetAddressPlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressPlaceholder',
});
const streetAddressRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressRequired',
})
);
const postalCodeLabel = intl.formatMessage({ id: 'PayoutDetailsForm.postalCodeLabel' });
const postalCodePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.postalCodePlaceholder',
});
const postalCodeRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.postalCodeRequired',
})
);
const cityLabel = intl.formatMessage({ id: 'PayoutDetailsForm.cityLabel' });
const cityPlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.cityPlaceholder' });
const cityRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.cityRequired',
})
);
const showAddressFields = country && requiresAddress(country);
// 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,
});
@ -178,7 +185,6 @@ const PayoutDetailsFormComponent = props => (
<FormattedMessage id="PayoutDetailsForm.stripeConnectedAccountTermsLink" />
</ExternalLink>
);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
@ -245,48 +251,8 @@ const PayoutDetailsFormComponent = props => (
</option>
))}
</FieldSelect>
{showAddressFields ? (
<div>
<FieldTextInput
id="streetAddress"
name="streetAddress"
disabled={disabled}
className={css.field}
type="text"
autoComplete="street-address"
label={streetAddressLabel}
placeholder={streetAddressPlaceholder}
validate={streetAddressRequired}
onUnmount={() => form.change('streetAddress', undefined)}
/>
<div className={css.formRow}>
<FieldTextInput
id="postalCode"
name="postalCode"
disabled={disabled}
className={css.postalCode}
type="text"
autoComplete="postal-code"
label={postalCodeLabel}
placeholder={postalCodePlaceholder}
validate={postalCodeRequired}
onUnmount={() => form.change('postalCode', undefined)}
/>
<FieldTextInput
id="city"
name="city"
disabled={disabled}
className={css.city}
type="text"
autoComplete="address-level2"
label={cityLabel}
placeholder={cityPlaceholder}
validate={cityRequired}
onUnmount={() => form.change('city', undefined)}
/>
</div>
</div>
) : null}
<PayoutDetailsAddress country={country} intl={intl} disabled={disabled} form={form} />
</div>
{country ? (
<div className={css.sectionContainer}>
@ -303,7 +269,27 @@ const PayoutDetailsFormComponent = props => (
/>
</div>
) : null}
{showPersonalIdNumber ? (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.personalIdNumberTitle" />
</h3>
<FieldTextInput
id="personalIdNumber"
name="personalIdNumber"
disabled={disabled}
className={css.personalIdNumber}
type="text"
label={personalIdNumberLabel}
placeholder={personalIdNumberPlaceholder}
validate={personalIdNumberValid}
/>
</div>
) : null}
{error}
<p className={css.termsText}>
<FormattedMessage
id="PayoutDetailsForm.stripeToSText"

298
src/stripe-config.js Normal file
View file

@ -0,0 +1,298 @@
/*
* Stripe related configuration.
*/
// NOTE: REACT_APP_STRIPE_PUBLISHABLE_KEY is mandatory environment variable.
// This variable is set in a hidden file: .env
// 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;
// Stripe only supports payments in certain countries, see full list
// at https://stripe.com/global
//
export const stripeSupportedCountries = [
{
// Australia
code: 'AU',
currency: 'AUD',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
state: true,
},
accountConfig: {
bsb: true,
accountNumber: true,
},
},
{
// Austria
code: 'AT',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Belgium
code: 'BE',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Canada
code: 'CA',
currency: 'CAD',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
province: true,
},
accountConfig: {
transitNumber: true,
institutionNumber: true,
accountNumber: true,
},
},
{
// Denmark
code: 'DK',
currency: 'DKK',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Finland
code: 'FI',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// France
code: 'FR',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Germany
code: 'DE',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Hong Kong
code: 'HK',
currency: 'HKD',
addressConfig: {
addressLine: true,
city: true,
},
accountConfig: {
clearingCode: true,
branchCode: true,
accountNumber: true,
},
personalIdNumberRequired: true,
},
{
// Ireland
code: 'IE',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Italy
code: 'IT',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Luxembourg
code: 'LU',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Netherlands
code: 'NL',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// New Zealand
code: 'NZ',
currency: 'NZD',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
accountNumber: true,
},
},
{
// Norway
code: 'NO',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Portugal
code: 'PT',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Spain
code: 'ES',
currency: 'EUR',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Sweden
code: 'SE',
currency: 'SEK',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// Switzerland
code: 'CH',
currency: 'CHF',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
iban: true,
},
},
{
// United Kingdom
code: 'GB',
currency: 'GBP',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
},
accountConfig: {
sortCode: true,
accountNumber: true,
},
},
{
// United States
code: 'US',
currency: 'USD',
addressConfig: {
addressLine: true,
city: true,
postalCode: true,
state: true,
},
accountConfig: {
routingNumber: true,
accountNumber: true,
},
ssnLast4Required: true,
},
];

View file

@ -445,6 +445,22 @@
"PayoutDetailsForm.birthdayMonthPlaceholder": "mm",
"PayoutDetailsForm.birthdayRequired": "Birthday is required and must be a valid date.",
"PayoutDetailsForm.birthdayYearPlaceholder": "yyyy",
"PayoutDetailsForm.canadianProvinceLabel": "Province",
"PayoutDetailsForm.canadianProvinceNames.AB": "Alberta",
"PayoutDetailsForm.canadianProvinceNames.BC": "British Columbia",
"PayoutDetailsForm.canadianProvinceNames.MB": "Manitoba",
"PayoutDetailsForm.canadianProvinceNames.NB": "New Brunswick",
"PayoutDetailsForm.canadianProvinceNames.NL": "Newfoundland and Labrador",
"PayoutDetailsForm.canadianProvinceNames.NS": "Nova Scotia",
"PayoutDetailsForm.canadianProvinceNames.NT": "Northwest Territories",
"PayoutDetailsForm.canadianProvinceNames.NU": "Nunavut",
"PayoutDetailsForm.canadianProvinceNames.ON": "Ontario",
"PayoutDetailsForm.canadianProvinceNames.PE": "Prince Edward Island",
"PayoutDetailsForm.canadianProvinceNames.QC": "Quebec",
"PayoutDetailsForm.canadianProvinceNames.SK": "Saskatchewan",
"PayoutDetailsForm.canadianProvinceNames.YT": "Yukon",
"PayoutDetailsForm.canadianProvincePlaceholder": "Province",
"PayoutDetailsForm.canadianProvinceRequired": "This field is required",
"PayoutDetailsForm.cityLabel": "City",
"PayoutDetailsForm.cityPlaceholder": "Helsinki",
"PayoutDetailsForm.cityRequired": "This field is required",
@ -452,16 +468,21 @@
"PayoutDetailsForm.countryNames.AT": "Austria",
"PayoutDetailsForm.countryNames.AU": "Australia",
"PayoutDetailsForm.countryNames.BE": "Belgium",
"PayoutDetailsForm.countryNames.CA": "Canada",
"PayoutDetailsForm.countryNames.CH": "Switzerland",
"PayoutDetailsForm.countryNames.DE": "Germany",
"PayoutDetailsForm.countryNames.DK": "Denmark",
"PayoutDetailsForm.countryNames.ES": "Spain",
"PayoutDetailsForm.countryNames.FI": "Finland",
"PayoutDetailsForm.countryNames.FR": "France",
"PayoutDetailsForm.countryNames.GB": "United Kingdom",
"PayoutDetailsForm.countryNames.HK": "Hong Kong",
"PayoutDetailsForm.countryNames.IE": "Ireland",
"PayoutDetailsForm.countryNames.IT": "Italy",
"PayoutDetailsForm.countryNames.LU": "Luxembourg",
"PayoutDetailsForm.countryNames.NL": "Netherlands",
"PayoutDetailsForm.countryNames.NO": "Norway",
"PayoutDetailsForm.countryNames.NZ": "New Zealand",
"PayoutDetailsForm.countryNames.PT": "Portugal",
"PayoutDetailsForm.countryNames.SE": "Sweden",
"PayoutDetailsForm.countryNames.US": "United States",
@ -477,9 +498,19 @@
"PayoutDetailsForm.lastNamePlaceholder": "Doe",
"PayoutDetailsForm.lastNameRequired": "This field is required",
"PayoutDetailsForm.personalDetailsTitle": "Personal details",
"PayoutDetailsForm.personalIdNumberTitle": "Personal id number",
"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.personalIdNumberValid": "Invalid value",
"PayoutDetailsForm.postalCodeLabel": "Postal code",
"PayoutDetailsForm.postalCodePlaceholder": "00100",
"PayoutDetailsForm.postalCodeRequired": "This field is required",
"PayoutDetailsForm.stateLabel": "State",
"PayoutDetailsForm.statePlaceholder": "Enter your state",
"PayoutDetailsForm.stateRequired": "This field is required",
"PayoutDetailsForm.streetAddressLabel": "Street address",
"PayoutDetailsForm.streetAddressPlaceholder": "Enter your street address…",
"PayoutDetailsForm.streetAddressRequired": "This field is required",
@ -629,16 +660,28 @@
"StripeBankAccountTokenInputField.accountNumber.placeholder": "Type in bank account number…",
"StripeBankAccountTokenInputField.accountNumber.required": "Bank account number is required",
"StripeBankAccountTokenInputField.andBeforeLastItemInAList": " and",
"StripeBankAccountTokenInputField.branchCode.inline": "Branch code",
"StripeBankAccountTokenInputField.branchCode.label": "Branch code",
"StripeBankAccountTokenInputField.branchCode.placeholder": "Type in branch code…",
"StripeBankAccountTokenInputField.branchCode.required": "Branch code is required",
"StripeBankAccountTokenInputField.bsb.inline": "BSB",
"StripeBankAccountTokenInputField.bsb.label": "BSB",
"StripeBankAccountTokenInputField.bsb.placeholder": "Type in BSB…",
"StripeBankAccountTokenInputField.bsb.required": "BSB is required",
"StripeBankAccountTokenInputField.clearingCode.inline": "Clearing code",
"StripeBankAccountTokenInputField.clearingCode.label": "Clearing code",
"StripeBankAccountTokenInputField.clearingCode.placeholder": "Type in clearing code…",
"StripeBankAccountTokenInputField.clearingCode.required": "Clearing code is required",
"StripeBankAccountTokenInputField.genericStripeError": "Could not connect account number. Please double-check that your {inputs} are valid in {country}",
"StripeBankAccountTokenInputField.genericStripeErrorIban": "Could not connect account number. Please double-check that your account number is valid in {country}",
"StripeBankAccountTokenInputField.iban.inline": "IBAN",
"StripeBankAccountTokenInputField.iban.label": "Bank account number (IBAN)",
"StripeBankAccountTokenInputField.iban.placeholder": "DE89 3704 0044 0532 0130 00",
"StripeBankAccountTokenInputField.iban.required": "Bank account number (IBAN) is required",
"StripeBankAccountTokenInputField.institutionNumber.inline": "institution number",
"StripeBankAccountTokenInputField.institutionNumber.label": "Institution number",
"StripeBankAccountTokenInputField.institutionNumber.placeholder": "Type in institution number",
"StripeBankAccountTokenInputField.institutionNumber.required": "Institution number is required",
"StripeBankAccountTokenInputField.routingNumber.inline": "routing number",
"StripeBankAccountTokenInputField.routingNumber.label": "Routing number",
"StripeBankAccountTokenInputField.routingNumber.placeholder": "Type in routing number…",
@ -647,6 +690,10 @@
"StripeBankAccountTokenInputField.sortCode.label": "Sort code",
"StripeBankAccountTokenInputField.sortCode.placeholder": "Type in sort code…",
"StripeBankAccountTokenInputField.sortCode.required": "Sort code is required",
"StripeBankAccountTokenInputField.transitNumber.inline": "transit number",
"StripeBankAccountTokenInputField.transitNumber.label": "Transit number",
"StripeBankAccountTokenInputField.transitNumber.placeholder": "Type in transit number",
"StripeBankAccountTokenInputField.transitNumber.required": "Transit number is required",
"StripeBankAccountTokenInputField.unsupportedCountry": "Country not supported: {country}",
"StripePaymentForm.creditCardDetails": "Credit card details",
"StripePaymentForm.genericError": "Could not handle payment data. Please try again.",

View file

@ -445,6 +445,22 @@
"PayoutDetailsForm.birthdayMonthPlaceholder": "mm",
"PayoutDetailsForm.birthdayRequired": "La date de naissance est requise et doit être valide.",
"PayoutDetailsForm.birthdayYearPlaceholder": "aaaa",
"PayoutDetailsForm.canadianProvinceLabel": "Province",
"PayoutDetailsForm.canadianProvinceNames.AB": "Alberta",
"PayoutDetailsForm.canadianProvinceNames.BC": "British Columbia",
"PayoutDetailsForm.canadianProvinceNames.MB": "Manitoba",
"PayoutDetailsForm.canadianProvinceNames.NB": "New Brunswick",
"PayoutDetailsForm.canadianProvinceNames.NL": "Newfoundland and Labrador",
"PayoutDetailsForm.canadianProvinceNames.NS": "Nova Scotia",
"PayoutDetailsForm.canadianProvinceNames.NT": "Northwest Territories",
"PayoutDetailsForm.canadianProvinceNames.NU": "Nunavut",
"PayoutDetailsForm.canadianProvinceNames.ON": "Ontario",
"PayoutDetailsForm.canadianProvinceNames.PE": "Prince Edward Island",
"PayoutDetailsForm.canadianProvinceNames.QC": "Quebec",
"PayoutDetailsForm.canadianProvinceNames.SK": "Saskatchewan",
"PayoutDetailsForm.canadianProvinceNames.YT": "Yukon",
"PayoutDetailsForm.canadianProvincePlaceholder": "Province",
"PayoutDetailsForm.canadianProvinceRequired": "This field is required",
"PayoutDetailsForm.cityLabel": "Ville",
"PayoutDetailsForm.cityPlaceholder": "Helsinki",
"PayoutDetailsForm.cityRequired": "Ce champ est requis.",
@ -452,16 +468,21 @@
"PayoutDetailsForm.countryNames.AT": "Autriche",
"PayoutDetailsForm.countryNames.AU": "Australie",
"PayoutDetailsForm.countryNames.BE": "Belgique",
"PayoutDetailsForm.countryNames.CA": "Canada",
"PayoutDetailsForm.countryNames.CH": "Switzerland",
"PayoutDetailsForm.countryNames.DE": "Allemagne",
"PayoutDetailsForm.countryNames.DK": "Danemark",
"PayoutDetailsForm.countryNames.ES": "Espagne",
"PayoutDetailsForm.countryNames.FI": "Finlande",
"PayoutDetailsForm.countryNames.FR": "France",
"PayoutDetailsForm.countryNames.GB": "Royaune-Uni",
"PayoutDetailsForm.countryNames.HK": "Hong Kong",
"PayoutDetailsForm.countryNames.IE": "Irelande",
"PayoutDetailsForm.countryNames.IT": "Italie",
"PayoutDetailsForm.countryNames.LU": "Luxembourg",
"PayoutDetailsForm.countryNames.NL": "Pays-Bas",
"PayoutDetailsForm.countryNames.NO": "Norway",
"PayoutDetailsForm.countryNames.NZ": "New Zealand",
"PayoutDetailsForm.countryNames.PT": "Portugal",
"PayoutDetailsForm.countryNames.SE": "Suède",
"PayoutDetailsForm.countryNames.US": "États-Unis",
@ -477,9 +498,19 @@
"PayoutDetailsForm.lastNamePlaceholder": "Nom",
"PayoutDetailsForm.lastNameRequired": "Ce champ est requis.",
"PayoutDetailsForm.personalDetailsTitle": "Détails",
"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.postalCodeLabel": "Code postal",
"PayoutDetailsForm.postalCodePlaceholder": "00100",
"PayoutDetailsForm.postalCodeRequired": "Ce champ est requis.",
"PayoutDetailsForm.stateLabel": "State",
"PayoutDetailsForm.statePlaceholder": "Enter your state",
"PayoutDetailsForm.stateRequired": "This field is required",
"PayoutDetailsForm.streetAddressLabel": "Adresse",
"PayoutDetailsForm.streetAddressPlaceholder": "Entrez votre adresse",
"PayoutDetailsForm.streetAddressRequired": "Ce champ est requis.",
@ -499,8 +530,8 @@
"PayoutPreferencesPage.title": "Paiements",
"PriceFilter.clear": "Effacer",
"PriceFilter.label": "Prix",
"PriceFilter.labelSelectedPlain": "Prix : {minPrice} - {maxPrice}",
"PriceFilter.labelSelectedButton": "{minPrice} - {maxPrice}",
"PriceFilter.labelSelectedPlain": "Prix : {minPrice} - {maxPrice}",
"PriceFilterForm.cancel": "Annuler",
"PriceFilterForm.clear": "Effacer",
"PriceFilterForm.label": "Gamme de prix :",
@ -629,16 +660,28 @@
"StripeBankAccountTokenInputField.accountNumber.placeholder": "Entrez votre numéro de compte bancaire",
"StripeBankAccountTokenInputField.accountNumber.required": "Le numéro de compte bancaire est requis.",
"StripeBankAccountTokenInputField.andBeforeLastItemInAList": " et",
"StripeBankAccountTokenInputField.branchCode.inline": "Branch code",
"StripeBankAccountTokenInputField.branchCode.label": "Branch code",
"StripeBankAccountTokenInputField.branchCode.placeholder": "Type in branch code…",
"StripeBankAccountTokenInputField.branchCode.required": "Branch code is required",
"StripeBankAccountTokenInputField.bsb.inline": "BSB",
"StripeBankAccountTokenInputField.bsb.label": "BSB",
"StripeBankAccountTokenInputField.bsb.placeholder": "Entrez le BSB…",
"StripeBankAccountTokenInputField.bsb.required": "Le BSB est requis.",
"StripeBankAccountTokenInputField.clearingCode.inline": "Clearing code",
"StripeBankAccountTokenInputField.clearingCode.label": "Clearing code",
"StripeBankAccountTokenInputField.clearingCode.placeholder": "Type in clearing code…",
"StripeBankAccountTokenInputField.clearingCode.required": "Clearing code is required",
"StripeBankAccountTokenInputField.genericStripeError": "Impossible de connecter ce numéro de compte bancaire. Veuillez vérifier que {inputs} est valide en {country}",
"StripeBankAccountTokenInputField.genericStripeErrorIban": "Impossible de connecter ce numéro de compte bancaire. Veuillez vérifier ce numéro est valide en {country}",
"StripeBankAccountTokenInputField.iban.inline": "IBAN",
"StripeBankAccountTokenInputField.iban.label": "Numéro de compte bancaire (IBAN)",
"StripeBankAccountTokenInputField.iban.placeholder": "Par exemple FR76 3000 6000 0112 3456 7890 189",
"StripeBankAccountTokenInputField.iban.required": "Le numéro de compte bancaire (IBAN) est requis.",
"StripeBankAccountTokenInputField.institutionNumber.inline": "institution number",
"StripeBankAccountTokenInputField.institutionNumber.label": "Institution number",
"StripeBankAccountTokenInputField.institutionNumber.placeholder": "Type in institution number",
"StripeBankAccountTokenInputField.institutionNumber.required": "Institution number is required",
"StripeBankAccountTokenInputField.routingNumber.inline": "numéro de routage number",
"StripeBankAccountTokenInputField.routingNumber.label": "Numéro de routage",
"StripeBankAccountTokenInputField.routingNumber.placeholder": "Entrez le numéro de routage…",
@ -647,6 +690,10 @@
"StripeBankAccountTokenInputField.sortCode.label": "Code de tri",
"StripeBankAccountTokenInputField.sortCode.placeholder": "Entrez un code de tri…",
"StripeBankAccountTokenInputField.sortCode.required": "Un code de tri est requis.",
"StripeBankAccountTokenInputField.transitNumber.inline": "transit number",
"StripeBankAccountTokenInputField.transitNumber.label": "Transit number",
"StripeBankAccountTokenInputField.transitNumber.placeholder": "Type in transit number",
"StripeBankAccountTokenInputField.transitNumber.required": "Transit number is required",
"StripeBankAccountTokenInputField.unsupportedCountry": "Pays non supporté : {country}",
"StripePaymentForm.creditCardDetails": "Votre carte bancaire",
"StripePaymentForm.genericError": "Impossible de traiter ces détails de paiement. Veuillez essayer de nouveau.",
@ -749,4 +796,4 @@
"UserCard.heading": "Bonjour, je suis {name}.",
"UserCard.showFullBioLink": "plus",
"UserCard.viewProfileLink": "Voir le profil"
}
}

View file

@ -136,5 +136,70 @@ export const ageAtLeast = (message, minYears) => value => {
return message;
};
export const validSsnLast4 = message => value => {
return value.length === 4 ? VALID : message;
};
export const validHKID = message => value => {
// Accept value 000000000 for testing Stripe
if (value.length === 9 && value.match(/([0]{9})/)) {
return VALID;
}
// HKID format example: AB364912(5)
// ID can start with one or two letters and the check digit in the end can be in brackets or not
if (value.length < 8) {
return message;
}
// Handle possible brackets in value
if (value.charAt(value.length - 3) === '(' && value.charAt(value.length - 1) === ')') {
value = value.substring(0, value.length - 3) + value.charAt(value.length - 2);
}
value = value.toUpperCase();
// Check that pattern is correct and split value to array
const hkidPattern = /^([A-Z]{1,2})([0-9]{6})([A0-9])$/;
const matchArray = value.match(hkidPattern);
if (!matchArray) {
return message;
}
const charPart = matchArray[1];
const numPart = matchArray[2];
const checkDigit = matchArray[3];
// Calculate the checksum for character part.
// Transfer letters to numbers so that A=10, B=11, C=12 etc.
// If there is only one letter in the ID use 36 as the first value
// Total calculation is weighted so that 1st digit is x9, 2nd digit x8, 3rd digit x7 etc.
const strValidChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let checkSum = 0;
if (charPart.length === 2) {
checkSum += 9 * (10 + strValidChars.indexOf(charPart.charAt(0)));
checkSum += 8 * (10 + strValidChars.indexOf(charPart.charAt(1)));
} else {
checkSum += 9 * 36;
checkSum += 8 * (10 + strValidChars.indexOf(charPart));
}
// Calculate the checksum for numeric part
for (let i = 0, j = 7; i < numPart.length; i++, j--) {
checkSum += j * numPart.charAt(i);
}
// Verify the check digit
const remaining = checkSum % 11;
let verify = remaining === 0 ? 0 : 11 - remaining;
verify = verify.toString();
const isValid = verify === checkDigit || (verify === 10 && checkDigit === 'A');
return isValid ? VALID : message;
};
export const composeValidators = (...validators) => value =>
validators.reduce((error, validator) => error || validator(value), VALID);

View file

@ -6,6 +6,7 @@ import {
maxLength,
moneySubUnitAmountAtLeast,
composeValidators,
validHKID,
} from './validators';
const { Money } = sdkTypes;
@ -184,4 +185,33 @@ describe('validators', () => {
expect(validateLength('')).toEqual('minLength');
});
});
describe('validHKID()', () => {
it('should fail on too short HKID', () => {
expect(validHKID('fail')('AB987')).toEqual('fail');
});
it('should fail if value is in wrong format: no letters', () => {
expect(validHKID('fail')('13278240')).toEqual('fail');
});
it('should fail if value is in wrong format: too many letters', () => {
expect(validHKID('fail')('ABE9876543')).toEqual('fail');
});
it('should fail if value is in wrong format: too many numbers', () => {
expect(validHKID('fail')('E13278240')).toEqual('fail');
});
it('should fail if check digit is wrong', () => {
expect(validHKID('fail')('E327824(3)')).toEqual('fail');
});
it('should allow string of nine digits with all zeros', () => {
expect(validHKID('pass')('000000000')).toBeUndefined();
});
it('should allow string with valid HKID using two letters', () => {
expect(validHKID('pass')('AB9876543')).toBeUndefined();
});
it('should allow string with valid HKID using one letter', () => {
expect(validHKID('pass')('E3278240')).toBeUndefined();
});
it('should allow string with valid HKID using brackets in check digit', () => {
expect(validHKID('pass')('E327824(0)')).toBeUndefined();
});
});
});