diff --git a/.env-template b/.env-template index 82e4be59..24066181 100644 --- a/.env-template +++ b/.env-template @@ -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= diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e97ad6..5758c6cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js index f0b95162..c791f073 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js @@ -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', +}; diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.js b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.js index 3046759c..93eff0ff 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.js @@ -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 diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js index 1a20b72a..c095265f 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js @@ -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`); } }; diff --git a/src/config.js b/src/config.js index 725b72da..b42c7a43 100644 --- a/src/config.js +++ b/src/config.js @@ -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'; diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index 7dbe3691..2f110e82 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -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, }; diff --git a/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js b/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js new file mode 100644 index 00000000..0e9ed0e1 --- /dev/null +++ b/src/forms/PayoutDetailsForm/PayoutDetailsAddress.js @@ -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 ( +