From e98222363ed2594e99635b67925edc921d17e9b4 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Fri, 20 Dec 2019 13:06:22 +0200 Subject: [PATCH] Update from upstream --- CHANGELOG.md | 29 +- package.json | 4 +- src/components/Button/Button.js | 20 +- .../EditListingWizard/EditListingWizard.js | 177 ++++- src/components/IconSuccess/IconSuccess.css | 8 + .../IconSuccess/IconSuccess.example.js | 7 + src/components/IconSuccess/IconSuccess.js | 38 + .../LayoutWrapperAccountSettingsSideNav.js | 6 +- .../ModalMissingInformation.js | 2 +- .../StripeAccountReminder.js | 2 +- .../StripeBankAccountRequiredInput.js | 5 +- .../StripeBankAccountTokenInputField.css | 22 +- ...tripeBankAccountTokenInputField.example.js | 18 + .../StripeBankAccountTokenInputField.js | 6 + .../StripeBankAccountTokenInputField.util.js | 14 +- .../StripeConnectAccountStatusBox.css | 115 +++ .../StripeConnectAccountStatusBox.js | 95 +++ src/components/index.js | 2 + src/config.js | 4 +- .../EditListingPage/EditListingPage.duck.js | 92 ++- .../EditListingPage/EditListingPage.js | 65 +- .../EditListingPage.test.js.snap | 1 - .../PayoutPreferencesPage.duck.js | 68 -- .../PayoutPreferencesPage.js | 149 ---- .../StripePayoutPage.css} | 1 + .../StripePayoutPage/StripePayoutPage.duck.js | 85 +++ .../StripePayoutPage/StripePayoutPage.js | 281 +++++++ .../StripePayoutPage.test.js} | 19 +- .../StripePayoutPage.test.js.snap} | 101 ++- src/containers/index.js | 2 +- src/containers/reducers.js | 4 +- src/ducks/index.js | 2 + src/ducks/stripe.duck.js | 403 ---------- src/ducks/stripeConnectAccount.duck.js | 265 +++++++ src/ducks/user.duck.js | 2 +- src/examples.js | 4 +- .../EditListingDescriptionForm.example.js | 2 + .../EditListingDescriptionForm.test.js | 4 +- .../EditListingDescriptionForm.test.js.snap | 31 - .../EditListingFeaturesForm.example.js | 2 + .../EditListingLocationForm.example.js | 2 + .../EditListingLocationForm.test.js | 2 + .../EditListingLocationForm.test.js.snap | 2 + .../EditListingPhotosForm.example.js | 1 + .../EditListingPhotosForm.test.js | 2 + .../EditListingPhotosForm.test.js.snap | 1 + .../EditListingPoliciesForm.example.js | 2 + .../EditListingPoliciesForm.test.js | 2 + .../EditListingPoliciesForm.test.js.snap | 2 + .../EditListingPricingForm.example.js | 2 + .../EditListingPricingForm.test.js | 2 + .../PayoutDetailsForm/PayoutDetailsForm.js | 14 + .../StripeConnectAccountForm.css | 95 +++ .../StripeConnectAccountForm.js | 292 +++++++ src/forms/index.js | 2 +- src/marketplace.css | 3 + src/routeConfiguration.js | 25 +- src/stripe-config.js | 710 ++++++++++++------ src/translations/en.json | 85 ++- src/util/types.js | 1 + yarn.lock | 8 +- 61 files changed, 2401 insertions(+), 1011 deletions(-) create mode 100644 src/components/IconSuccess/IconSuccess.css create mode 100644 src/components/IconSuccess/IconSuccess.example.js create mode 100644 src/components/IconSuccess/IconSuccess.js create mode 100644 src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.css create mode 100644 src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.js delete mode 100644 src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js delete mode 100644 src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js rename src/containers/{PayoutPreferencesPage/PayoutPreferencesPage.css => StripePayoutPage/StripePayoutPage.css} (95%) create mode 100644 src/containers/StripePayoutPage/StripePayoutPage.duck.js create mode 100644 src/containers/StripePayoutPage/StripePayoutPage.js rename src/containers/{PayoutPreferencesPage/PayoutPreferencesPage.test.js => StripePayoutPage/StripePayoutPage.test.js} (77%) rename src/containers/{PayoutPreferencesPage/__snapshots__/PayoutPreferencesPage.test.js.snap => StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap} (59%) create mode 100644 src/ducks/stripeConnectAccount.duck.js create mode 100644 src/forms/StripeConnectAccountForm/StripeConnectAccountForm.css create mode 100644 src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b3761200..1df0872f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,33 @@ https://github.com/sharetribe/flex-template-web/ ## Upcoming version 2019-XX-XX +## [v6.0.0] 2019-12-20 + +This is update from from [upstream](https://github.com/sharetribe/ftw-daily): v4.0.0 + +- [change] Use Stripe's [Connect onboarding](https://stripe.com/docs/connect/connect-onboarding) for + adding and updating the identity information of the Stripe account. + - Before updating to this version you should check + [the related pull request](https://github.com/sharetribe/ftw-daily/pull/1234) + - Read more from documentation: + [How to handle provider onboarding and identity verification on FTW](https://www.sharetribe.com/docs/guides/provider-onboarding-and-identity-verification/) + +**Note:** In this update we have deprecated the old `PayoutDetailsForm` and `PayoutPreferencesPage`. +Form now on Stripe will handle collecting the identity information required for verificating the +Stripe account. On FTW we will only handle creating the new account and adding and updating +information about bank account (e.g. IBAN number). If you want to keep using the custom form inside +your application you need to make sure that you are collecting all the required information and +enabling users to update the account so that it doesn't get restricted. + +- [fix] Add missing props to examples related to EditListingWizard + [#1247](https://github.com/sharetribe/ftw-daily/pull/1247) +- [fix] Add missing props to tests related to EditListingWizard + [#1246](https://github.com/sharetribe/ftw-daily/pull/1246) +- [fix] Update links to API Reference docs. + [#1231](https://github.com/sharetribe/ftw-daily/pull/1231) + +[v6.0.0]: https://github.com/sharetribe/ftw-hourly/compare/v5.1.0...v6.0.0 + ## [v5.1.0] 2019-12-09 - [change] Make it easier to reorder EditListingWizard tabs/panels. @@ -26,7 +53,7 @@ https://github.com/sharetribe/flex-template-web/ https://support.stripe.com/questions/connect-address-validation). - [add] Add IconEdit [#1237](https://github.com/sharetribe/ftw-daily/pull/1237) - [v5.1.0]: https://github.com/sharetribe/flex-template-web/compare/v5.0.3...v5.1.0 + [v5.1.0]: https://github.com/sharetribe/ftw-hourly/compare/v5.0.3...v5.1.0 ## [v5.0.3] 2019-12-09 diff --git a/package.json b/package.json index 2523e82bd..1e84cad8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "v5.1.0", + "version": "v6.0.0", "private": true, "license": "Apache-2.0", "dependencies": { @@ -53,7 +53,7 @@ "redux": "^4.0.1", "redux-thunk": "^2.3.0", "seedrandom": "^3.0.3", - "sharetribe-flex-sdk": "^1.5.0", + "sharetribe-flex-sdk": "^1.8.0", "sharetribe-scripts": "3.1.1", "smoothscroll-polyfill": "^0.4.0", "source-map-support": "^0.5.9", diff --git a/src/components/Button/Button.js b/src/components/Button/Button.js index b009bbf01..f934c25af 100644 --- a/src/components/Button/Button.js +++ b/src/components/Button/Button.js @@ -14,7 +14,17 @@ class Button extends Component { this.setState({ mounted: true }); // eslint-disable-line react/no-did-mount-set-state } render() { - const { children, className, rootClassName, inProgress, ready, disabled, ...rest } = this.props; + const { + children, + className, + rootClassName, + spinnerClassName, + checkmarkClassName, + inProgress, + ready, + disabled, + ...rest + } = this.props; const rootClass = rootClassName || css.root; const classes = classNames(rootClass, className, { @@ -25,9 +35,9 @@ class Button extends Component { let content; if (inProgress) { - content = ; + content = ; } else if (ready) { - content = ; + content = ; } else { content = children; } @@ -50,6 +60,8 @@ const { node, string, bool } = PropTypes; Button.defaultProps = { rootClassName: null, className: null, + spinnerClassName: null, + checkmarkClassName: null, inProgress: false, ready: false, disabled: false, @@ -59,6 +71,8 @@ Button.defaultProps = { Button.propTypes = { rootClassName: string, className: string, + spinnerClassName: string, + checkmarkClassName: string, inProgress: bool, ready: bool, diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index 90904b3fa..411d3f1e8 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -5,15 +5,18 @@ import { compose } from 'redux'; import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; import classNames from 'classnames'; import config from '../../config'; +import routeConfiguration from '../../routeConfiguration'; +import { createResourceLocatorString } from '../../util/routes'; import { withViewport } from '../../util/contextHelpers'; import { LISTING_PAGE_PARAM_TYPE_DRAFT, LISTING_PAGE_PARAM_TYPE_NEW, LISTING_PAGE_PARAM_TYPES, } from '../../util/urlHelpers'; -import { ensureListing, ensureCurrentUser } from '../../util/data'; -import { PayoutDetailsForm } from '../../forms'; -import { Modal, NamedRedirect, Tabs } from '../../components'; +import { ensureCurrentUser, ensureListing } from '../../util/data'; + +import { Modal, NamedRedirect, Tabs, StripeConnectAccountStatusBox } from '../../components'; +import { StripeConnectAccountForm } from '../../forms'; import EditListingWizardTab, { AVAILABILITY, @@ -172,6 +175,43 @@ const scrollToTab = (tabPrefix, tabId) => { } }; +// Create return URL for the Stripe onboarding form +const createReturnURL = (returnURLType, rootURL, routes, pathParams) => { + const path = createResourceLocatorString( + 'EditListingStripeOnboardingPage', + routes, + { ...pathParams, returnURLType }, + {} + ); + const root = rootURL.replace(/\/$/, ''); + return `${root}${path}`; +}; + +// Get attribute: stripeAccountData +const getStripeAccountData = stripeAccount => stripeAccount.attributes.stripeAccountData || null; + +// Get last 4 digits of bank account returned in Stripe account +const getBankAccountLast4Digits = stripeAccountData => + stripeAccountData && stripeAccountData.external_accounts.data.length > 0 + ? stripeAccountData.external_accounts.data[0].last4 + : null; + +// Check if there's requirements on selected type: 'past_due', 'currently_due' etc. +const hasRequirements = (stripeAccountData, requirementType) => + stripeAccountData != null && + stripeAccountData.requirements && + Array.isArray(stripeAccountData.requirements[requirementType]) && + stripeAccountData.requirements[requirementType].length > 0; + +// Redirect user to Stripe's hosted Connect account onboarding form +const handleGetStripeConnectAccountLinkFn = (getLinkFn, commonParams) => type => () => { + getLinkFn({ type, ...commonParams }) + .then(url => { + window.location.href = url; + }) + .catch(err => console.error(err)); +}; + // Create a new or edit listing through EditListingWizard class EditListingWizard extends Component { constructor(props) { @@ -191,15 +231,32 @@ class EditListingWizard extends Component { this.handlePayoutSubmit = this.handlePayoutSubmit.bind(this); } + componentDidMount() { + const { stripeOnboardingReturnURL } = this.props; + + if (stripeOnboardingReturnURL != null) { + this.setState({ showPayoutDetails: true }); + } + } + handleCreateFlowTabScrolling(shouldScroll) { this.hasScrolledToTab = shouldScroll; } handlePublishListing(id) { - const { onPublishListingDraft, currentUser } = this.props; + const { onPublishListingDraft, currentUser, stripeAccount } = this.props; + const stripeConnected = currentUser && currentUser.stripeAccount && !!currentUser.stripeAccount.id; - if (stripeConnected) { + + const stripeAccountData = stripeConnected ? getStripeAccountData(stripeAccount) : null; + + const requirementsMissing = + stripeAccount && + (hasRequirements(stripeAccountData, 'past_due') || + hasRequirements(stripeAccountData, 'currently_due')); + + if (stripeConnected && !requirementsMissing) { onPublishListingDraft(id); } else { this.setState({ @@ -216,10 +273,8 @@ class EditListingWizard extends Component { handlePayoutSubmit(values) { this.props .onPayoutDetailsSubmit(values) - .then(() => { - this.setState({ showPayoutDetails: false }); + .then(response => { this.props.onManageDisableScrolling('EditListingWizard.payoutModal', false); - this.props.onPublishListingDraft(this.state.draftId); }) .catch(() => { // do nothing @@ -237,8 +292,18 @@ class EditListingWizard extends Component { intl, errors, fetchInProgress, + payoutDetailsSaveInProgress, + payoutDetailsSaved, onManageDisableScrolling, onPayoutDetailsFormChange, + onGetStripeConnectAccountLink, + getAccountLinkInProgress, + createStripeAccountError, + updateStripeAccountError, + fetchStripeAccountError, + stripeAccountFetched, + stripeAccount, + currentUser, ...rest } = this.props; @@ -286,6 +351,44 @@ class EditListingWizard extends Component { this.setState({ portalRoot: document.getElementById('portal-root') }); } }; + const formDisabled = getAccountLinkInProgress; + const ensuredCurrentUser = ensureCurrentUser(currentUser); + const currentUserLoaded = !!ensuredCurrentUser.id; + const stripeConnected = currentUserLoaded && !!stripeAccount && !!stripeAccount.id; + + const rootURL = config.canonicalRootURL; + const routes = routeConfiguration(); + const { returnURLType, ...pathParams } = params; + const successURL = createReturnURL('success', rootURL, routes, pathParams); + const failureURL = createReturnURL('failure', rootURL, routes, pathParams); + + const accountId = stripeConnected ? stripeAccount.id : null; + const stripeAccountData = stripeConnected ? getStripeAccountData(stripeAccount) : null; + + const requirementsMissing = + stripeAccount && + (hasRequirements(stripeAccountData, 'past_due') || + hasRequirements(stripeAccountData, 'currently_due')); + + const savedCountry = stripeAccountData ? stripeAccountData.country : null; + + const handleGetStripeConnectAccountLink = handleGetStripeConnectAccountLinkFn( + onGetStripeConnectAccountLink, + { + accountId, + successURL, + failureURL, + } + ); + + const returnedNormallyFromStripe = returnURLType === 'success'; + const showVerificationError = returnURLType === 'failure'; + const showVerificationNeeded = stripeConnected && requirementsMissing; + + // Redirect from success URL to basic path for StripePayoutPage + if (returnedNormallyFromStripe && stripeConnected && !requirementsMissing) { + return ; + } return (
@@ -335,14 +438,49 @@ class EditListingWizard extends Component {

- + {!currentUserLoaded ? ( + + ) : ( + + {stripeConnected && (showVerificationError || showVerificationNeeded) ? ( + + ) : stripeConnected && savedCountry ? ( + + ) : null} + + )}
@@ -369,6 +507,10 @@ EditListingWizard.propTypes = { type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired, tab: oneOf(TABS).isRequired, }).isRequired, + history: shape({ + push: func.isRequired, + replace: func.isRequired, + }).isRequired, // We cannot use propTypes.listing since the listing might be a draft. listing: shape({ @@ -391,8 +533,11 @@ EditListingWizard.propTypes = { createStripeAccountError: object, }).isRequired, fetchInProgress: bool.isRequired, + payoutDetailsSaveInProgress: bool.isRequired, + payoutDetailsSaved: bool.isRequired, onPayoutDetailsFormChange: func.isRequired, onPayoutDetailsSubmit: func.isRequired, + onGetStripeConnectAccountLink: func.isRequired, onManageDisableScrolling: func.isRequired, updateInProgress: bool, diff --git a/src/components/IconSuccess/IconSuccess.css b/src/components/IconSuccess/IconSuccess.css new file mode 100644 index 000000000..f6794b8d2 --- /dev/null +++ b/src/components/IconSuccess/IconSuccess.css @@ -0,0 +1,8 @@ +@import '../../marketplace.css'; + +.root { +} + +.fillColor { + fill: var(--successColor); +} diff --git a/src/components/IconSuccess/IconSuccess.example.js b/src/components/IconSuccess/IconSuccess.example.js new file mode 100644 index 000000000..98f28d104 --- /dev/null +++ b/src/components/IconSuccess/IconSuccess.example.js @@ -0,0 +1,7 @@ +import IconSuccess from './IconSuccess'; + +export const Icon = { + component: IconSuccess, + props: {}, + group: 'icons', +}; diff --git a/src/components/IconSuccess/IconSuccess.js b/src/components/IconSuccess/IconSuccess.js new file mode 100644 index 000000000..414f012b3 --- /dev/null +++ b/src/components/IconSuccess/IconSuccess.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { string } from 'prop-types'; +import classNames from 'classnames'; + +import css from './IconSuccess.css'; + +const IconSuccess = props => { + const { rootClassName, className, fillColor } = props; + const classes = classNames(rootClassName || css.root, className); + return ( + + + + + + + ); +}; + +IconSuccess.defaultProps = { + rootClassName: null, + className: null, + fillColor: null, +}; + +IconSuccess.propTypes = { + rootClassName: string, + className: string, + fillColor: string, +}; + +export default IconSuccess; diff --git a/src/components/LayoutWrapperAccountSettingsSideNav/LayoutWrapperAccountSettingsSideNav.js b/src/components/LayoutWrapperAccountSettingsSideNav/LayoutWrapperAccountSettingsSideNav.js index 86d62d9bb..79a30e9f9 100644 --- a/src/components/LayoutWrapperAccountSettingsSideNav/LayoutWrapperAccountSettingsSideNav.js +++ b/src/components/LayoutWrapperAccountSettingsSideNav/LayoutWrapperAccountSettingsSideNav.js @@ -61,10 +61,10 @@ const LayoutWrapperAccountSettingsSideNavComponent = props => { }, { text: , - selected: currentTab === 'PayoutPreferencesPage', - id: 'PayoutPreferencesPageTab', + selected: currentTab === 'StripePayoutPage', + id: 'StripePayoutPageTab', linkProps: { - name: 'PayoutPreferencesPage', + name: 'StripePayoutPage', }, }, { diff --git a/src/components/ModalMissingInformation/ModalMissingInformation.js b/src/components/ModalMissingInformation/ModalMissingInformation.js index 83ce894de..9a5645fc8 100644 --- a/src/components/ModalMissingInformation/ModalMissingInformation.js +++ b/src/components/ModalMissingInformation/ModalMissingInformation.js @@ -18,7 +18,7 @@ const MISSING_INFORMATION_MODAL_WHITELIST = [ 'ContactDetailsPage', 'EmailVerificationPage', 'PasswordResetPage', - 'PayoutPreferencesPage', + 'StripePayoutPage', ]; const EMAIL_VERIFICATION = 'EMAIL_VERIFICATION'; diff --git a/src/components/ModalMissingInformation/StripeAccountReminder.js b/src/components/ModalMissingInformation/StripeAccountReminder.js index 4b1f2181e..c2372f774 100644 --- a/src/components/ModalMissingInformation/StripeAccountReminder.js +++ b/src/components/ModalMissingInformation/StripeAccountReminder.js @@ -16,7 +16,7 @@ const StripeAccountReminder = props => {

- +
diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountRequiredInput.js b/src/components/StripeBankAccountTokenInputField/StripeBankAccountRequiredInput.js index 155ba94d4..7e8729d17 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountRequiredInput.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountRequiredInput.js @@ -20,6 +20,7 @@ const StripeBankAccountRequiredInput = props => { showStripeError, inputError, disabled, + showInColumns, } = props; const showInputError = isTouched && !!inputError; @@ -29,6 +30,8 @@ const StripeBankAccountRequiredInput = props => { [css.inputError]: showInputError || showStripeError, }); + const columnsClass = showInColumns ? css.longForm : null; + const inputProps = { className: classes, id: `${formName}.bankAccountToken.${inputType}`, @@ -43,7 +46,7 @@ const StripeBankAccountRequiredInput = props => { const errorMessage =

{inputError}

; return ( -
+
diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.css b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.css index c6b2e1ef9..db5f79ffd 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.css +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.css @@ -5,17 +5,37 @@ } .root { + /* Parent component should not shrink this field */ + flex-shrink: 0; + + /* This component uses flexbox layout too */ + display: flex; + justify-content: space-between; + flex-wrap: wrap; } .input { + /* Parent component should not shrink this field */ + flex-shrink: 0; border-bottom-color: var(--attentionColor); margin-bottom: 24px; - + width: 100%; &:last-of-type { margin-bottom: 0; } } +.longForm { + width: 100%; + margin-bottom: 24px; +} + +@media (--viewportSmall) { + .longForm { + width: calc(50% - 9px); + } +} + .inputSuccess { border-bottom-color: var(--successColor); } diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js index c791f0735..2b8fc236e 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.example.js @@ -12,6 +12,7 @@ const formComponent = country => props => ( render={fieldRenderProps => { const { formName, handleSubmit, onChange } = fieldRenderProps; const currency = stripeCountryConfigs(country).currency; + return (
{ @@ -121,3 +122,20 @@ export const CA_CAD = { }, group: 'custom inputs', }; + +// JP +export const JP_JPY = { + component: formComponent('JP'), + props: { + formName: 'JP_JPY', + 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 201b75c8e..4f8d3d85a 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.js @@ -28,6 +28,7 @@ import css from './StripeBankAccountTokenInputField.css'; // value and moves on to another input within this component. const BLUR_TIMEOUT = 100; const DEBOUNCE_WAIT_TIME = 1000; +const MIN_INPUT_COUNT_FOR_TWO_COLUMNS = 6; class TokenInputFieldComponent extends Component { constructor(props) { @@ -260,6 +261,10 @@ class TokenInputFieldComponent extends Component { const inputConfiguration = requiredInputs(country); + // E.g. Japan has 6 fields in the bank account details so we want to + // show the inputs in two columns on bigger screens + const showInColumns = inputConfiguration.length >= MIN_INPUT_COUNT_FOR_TWO_COLUMNS; + return (
{inputConfiguration.map(inputType => { @@ -277,6 +282,7 @@ class TokenInputFieldComponent extends Component { isTouched={this.state[inputType].touched || formMeta.touched} showStripeError={showStripeError} inputError={this.state[inputType].error} + showInColumns={showInColumns} /> ); })} diff --git a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js index 7b479d95d..345269145 100644 --- a/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js +++ b/src/components/StripeBankAccountTokenInputField/StripeBankAccountTokenInputField.util.js @@ -4,6 +4,8 @@ import config from '../../config'; // Bank account number (used in countries where IBAN is not in use) export const ACCOUNT_NUMBER = 'accountNumber'; +// Required for Japan +export const ACCOUNT_OWNER_NAME = 'accountOwnerName'; // Australian equivalent for routing number export const BSB = 'bsb'; // Needed for creating full routing number in Canada @@ -14,8 +16,14 @@ export const TRANSIT_NUMBER = 'transitNumber'; export const CLEARING_CODE = 'clearingCode'; // Needed for creating full routing number in Hong Kong and Singapore export const BRANCH_CODE = 'branchCode'; -// Needed for creating full routing number in Singapore +// Required for Japan +export const BRANCH_NAME = 'branchName'; +// Required for Japan +export const BANK_NAME = 'bankName'; +// Needed for creating full routing number in e.g. Singapore export const BANK_CODE = 'bankCode'; +// Clave Bancaria Estandarizada (standardized banking cipher) used in Mexico +export const CLABE = 'clabe'; // International bank account number (e.g. EU countries use this) export const IBAN = 'iban'; // Routing number to separate bank account in different areas @@ -30,12 +38,16 @@ export const BANK_ACCOUNT_INPUTS = [ TRANSIT_NUMBER, INSTITUTION_NUMBER, CLEARING_CODE, + BRANCH_NAME, BRANCH_CODE, + BANK_NAME, BANK_CODE, SORT_CODE, ROUTING_NUMBER, + ACCOUNT_OWNER_NAME, ACCOUNT_NUMBER, IBAN, + CLABE, ]; export const supportedCountries = config.stripe.supportedCountries.map(c => c.code); diff --git a/src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.css b/src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.css new file mode 100644 index 000000000..af1118e95 --- /dev/null +++ b/src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.css @@ -0,0 +1,115 @@ +@import '../../marketplace.css'; + +.root { +} + +.verificiationBox { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + + padding: 15px 32px 23px 32px; + margin-bottom: 24px; + + border-radius: 4px; +} + +@media (--viewportMedium) { + .verificiationBox { + flex-wrap: nowrap; + } +} + +.verificiationBoxTextWrapper { + display: flex; + flex-wrap: wrap; + padding: 5px 24px 3px 0px; + margin-bottom: 16px; +} + +@media (--viewportMedium) { + .verificiationBoxTextWrapper { + margin-bottom: 0px; + } +} + +.verificationBoxTitle { + @apply --marketplaceH4FontStyles; + font-weight: var(--fontWeightSemiBold); + + align-self: center; + + width: 100%; + margin-top: 0; + margin-bottom: 0; +} + +.verificationBoxText { + @apply --marketplaceH4FontStyles; + font-weight: var(--fontWeightRegular); + margin-top: 0; + margin-bottom: 0; +} + +.getVerifiedButton { + @apply --marketplaceH5FontStyles; + font-weight: var(--fontWeightSemiBold); + + border-radius: 4px; + min-height: 38px; + width: 300px; + max-width: 110px; +} + +/* Verification required box */ +.verficiationNeededBox { + border: 1px solid var(--attentionColor); + background: var(--attentionColorLight); +} + +/* Verification error box */ +.verficiationErrorBox { + background: var(--failColorLight); + border: 1px solid var(--failColor); +} + +/* Verification success box */ +.verficiationSuccessBox { + background: var(--successColorLight); + border: 1px solid var(--successColor); + padding: 8px 24px; +} + +.verificationBoxSuccessTextWrapper { + margin-bottom: 0; +} +.editVerificationButton { + @apply --marketplaceH4FontStyles; + color: var(--matterColor); + font-weight: var(--fontWeightNormal); + min-height: 46px; + margin: 0; + padding-top: 3px; + + &:hover, + &:focus { + outline: none; + color: var(--matterColorDark); + } +} + +.icon { + margin-right: 10px; +} + +.iconEditPencil { + stroke: var(--matterColor); +} + +.spinner { + width: 24px; + height: 24px; + stroke: var(--successColor); + stroke-width: 3px; +} diff --git a/src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.js b/src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.js new file mode 100644 index 000000000..3b808565b --- /dev/null +++ b/src/components/StripeConnectAccountStatusBox/StripeConnectAccountStatusBox.js @@ -0,0 +1,95 @@ +import React from 'react'; +import classNames from 'classnames'; +import { FormattedMessage } from '../../util/reactIntl'; +import { IconEdit, IconSuccess, PrimaryButton, InlineTextButton } from '../../components'; +import css from './StripeConnectAccountStatusBox.css'; + +const STATUS_VERIFICATION_NEEDED = 'verificationNeeded'; +const STATUS_VERIFICATION_SUCCESS = 'verificationSuccess'; +const STATUS_VERIFICATION_ERROR = 'verificationError'; + +const StripeConnectAccountStatusBox = props => { + const { type, onGetStripeConnectAccountLink, inProgress, disabled } = props; + + if (type === STATUS_VERIFICATION_NEEDED) { + return ( +
+
+
+ +
+
+ +
+
+ + + + +
+ ); + } else if (type === STATUS_VERIFICATION_SUCCESS) { + return ( +
+
+
+ + +
+
+ + + + + +
+ ); + } else if (type === STATUS_VERIFICATION_ERROR) { + return ( +
+
+
+ +
+
+ +
+
+ + + + +
+ ); + } else { + throw new Error(`Unknown type ${type} for StripeConnectAccountStatusBox`); + } +}; + +export default StripeConnectAccountStatusBox; diff --git a/src/components/index.js b/src/components/index.js index fa447260e..4d2d0ffd3 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -31,6 +31,7 @@ export { default as IconSocialMediaFacebook } from './IconSocialMediaFacebook/Ic export { default as IconSocialMediaInstagram } from './IconSocialMediaInstagram/IconSocialMediaInstagram'; export { default as IconSocialMediaTwitter } from './IconSocialMediaTwitter/IconSocialMediaTwitter'; export { default as IconSpinner } from './IconSpinner/IconSpinner'; +export { default as IconSuccess } from './IconSuccess/IconSuccess'; // Other independent components export { default as ExternalLink } from './ExternalLink/ExternalLink'; @@ -144,6 +145,7 @@ export { default as SearchMapPriceLabel } from './SearchMapPriceLabel/SearchMapP export { default as SearchResultsPanel } from './SearchResultsPanel/SearchResultsPanel'; export { default as SelectMultipleFilter } from './SelectMultipleFilter/SelectMultipleFilter'; export { default as SelectSingleFilter } from './SelectSingleFilter/SelectSingleFilter'; +export { default as StripeConnectAccountStatusBox } from './StripeConnectAccountStatusBox/StripeConnectAccountStatusBox'; export { default as StripePaymentAddress } from './StripePaymentAddress/StripePaymentAddress'; export { default as UserCard } from './UserCard/UserCard'; diff --git a/src/config.js b/src/config.js index 2d9867f06..5f46d67bc 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,6 @@ import * as custom from './marketplace-custom-config.js'; import defaultLocationSearches from './default-location-searches'; -import { stripePublishableKey, stripeSupportedCountries } from './stripe-config'; +import { stripePublishableKey, stripeCountryDetails } from './stripe-config'; import { currencyConfiguration } from './currency-config'; const env = process.env.REACT_APP_ENV; @@ -201,7 +201,7 @@ const config = { listingMinimumPriceSubUnits, stripe: { publishableKey: stripePublishableKey, - supportedCountries: stripeSupportedCountries, + supportedCountries: stripeCountryDetails, }, canonicalRootURL, address: { diff --git a/src/containers/EditListingPage/EditListingPage.duck.js b/src/containers/EditListingPage/EditListingPage.duck.js index 406ec78dd..911becbc3 100644 --- a/src/containers/EditListingPage/EditListingPage.duck.js +++ b/src/containers/EditListingPage/EditListingPage.duck.js @@ -4,6 +4,12 @@ import { resetToStartOfDay } from '../../util/dates'; import { denormalisedResponseEntities } from '../../util/data'; import { storableError } from '../../util/errors'; import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; +import { + createStripeAccount, + updateStripeAccount, + fetchStripeAccount, +} from '../../ducks/stripeConnectAccount.duck'; +import { fetchCurrentUser } from '../../ducks/user.duck'; import * as log from '../../util/log'; const { UUID } = sdkTypes; @@ -55,6 +61,10 @@ export const DELETE_EXCEPTION_REQUEST = 'app/EditListingPage/DELETE_AVAILABILITY export const DELETE_EXCEPTION_SUCCESS = 'app/EditListingPage/DELETE_AVAILABILITY_EXCEPTION_SUCCESS'; export const DELETE_EXCEPTION_ERROR = 'app/EditListingPage/DELETE_AVAILABILITY_EXCEPTION_ERROR'; +export const SAVE_PAYOUT_DETAILS_REQUEST = 'app/EditListingPage/SAVE_PAYOUT_DETAILS_REQUEST'; +export const SAVE_PAYOUT_DETAILS_SUCCESS = 'app/EditListingPage/SAVE_PAYOUT_DETAILS_SUCCESS'; +export const SAVE_PAYOUT_DETAILS_ERROR = 'app/EditListingPage/SAVE_PAYOUT_DETAILS_ERROR'; + // ================ Reducer ================ // const initialState = { @@ -81,6 +91,8 @@ const initialState = { listingDraft: null, updatedTab: null, updateInProgress: false, + payoutDetailsSaveInProgress: false, + payoutDetailsSaved: false, }; export default function reducer(state = initialState, action = {}) { @@ -273,6 +285,13 @@ export default function reducer(state = initialState, action = {}) { deleteExceptionInProgress: false, }; + case SAVE_PAYOUT_DETAILS_REQUEST: + return { ...state, payoutDetailsSaveInProgress: true }; + case SAVE_PAYOUT_DETAILS_ERROR: + return { ...state, payoutDetailsSaveInProgress: false }; + case SAVE_PAYOUT_DETAILS_SUCCESS: + return { ...state, payoutDetailsSaveInProgress: false, payoutDetailsSaved: true }; + default: return state; } @@ -344,6 +363,11 @@ export const addAvailabilityExceptionError = errorAction(ADD_EXCEPTION_ERROR); export const deleteAvailabilityExceptionRequest = requestAction(DELETE_EXCEPTION_REQUEST); export const deleteAvailabilityExceptionSuccess = successAction(DELETE_EXCEPTION_SUCCESS); export const deleteAvailabilityExceptionError = errorAction(DELETE_EXCEPTION_ERROR); + +export const savePayoutDetailsRequest = requestAction(SAVE_PAYOUT_DETAILS_REQUEST); +export const savePayoutDetailsSuccess = successAction(SAVE_PAYOUT_DETAILS_SUCCESS); +export const savePayoutDetailsError = errorAction(SAVE_PAYOUT_DETAILS_ERROR); + // ================ Thunk ================ // export function requestShowListing(actionPayload) { @@ -495,22 +519,55 @@ export const requestFetchAvailabilityExceptions = fetchParams => (dispatch, getS }); }; +export const savePayoutDetails = (values, isUpdateCall) => (dispatch, getState, sdk) => { + const upsertThunk = isUpdateCall ? updateStripeAccount : createStripeAccount; + dispatch(savePayoutDetailsRequest()); + + return dispatch(upsertThunk(values, { expand: true })) + .then(response => { + dispatch(savePayoutDetailsSuccess()); + return response; + }) + .catch(() => dispatch(savePayoutDetailsError())); +}; + // loadData is run for each tab of the wizard. When editing an // existing listing, the listing must be fetched first. -export function loadData(params) { - return dispatch => { - dispatch(clearUpdatedTab()); - const { id, type } = params; - if (type === 'new') { - // No need to fetch anything when creating a new listing - return Promise.resolve(null); - } - const payload = { - id: new UUID(id), - include: ['author', 'images'], - 'fields.image': ['variants.landscape-crop', 'variants.landscape-crop2x'], - }; - return dispatch(requestShowListing(payload)).then(response => { + +// loadData is run for each tab of the wizard. When editing an +// existing listing, the listing must be fetched first. +export const loadData = params => (dispatch, getState, sdk) => { + dispatch(clearUpdatedTab()); + const { id, type } = params; + + if (type === 'new') { + // No need to listing data when creating a new listing + return Promise.all([dispatch(fetchCurrentUser())]) + .then(response => { + const currentUser = getState().user.currentUser; + if (currentUser && currentUser.stripeAccount) { + dispatch(fetchStripeAccount()); + } + return response; + }) + .catch(e => { + throw e; + }); + } + + const payload = { + id: new UUID(id), + include: ['author', 'images'], + 'fields.image': ['variants.landscape-crop', 'variants.landscape-crop2x'], + }; + + return Promise.all([dispatch(requestShowListing(payload)), dispatch(fetchCurrentUser())]) + .then(response => { + const currentUser = getState().user.currentUser; + if (currentUser && currentUser.stripeAccount) { + dispatch(fetchStripeAccount()); + } + if (response.data && response.data.data) { const listing = response.data.data; const tz = listing.attributes.availabilityPlan.timezone; @@ -530,7 +587,10 @@ export function loadData(params) { }; dispatch(requestFetchAvailabilityExceptions(params)); } + return response; + }) + .catch(e => { + throw e; }); - }; -} +}; diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js index b0df645db..20a80d03c 100644 --- a/src/containers/EditListingPage/EditListingPage.js +++ b/src/containers/EditListingPage/EditListingPage.js @@ -17,7 +17,11 @@ import { LISTING_STATE_DRAFT, LISTING_STATE_PENDING_APPROVAL, propTypes } from ' import { ensureOwnListing } from '../../util/data'; import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; -import { stripeAccountClearError, createStripeAccount } from '../../ducks/stripe.duck'; +import { + stripeAccountClearError, + createStripeAccount, + getStripeConnectAccountLink, +} from '../../ducks/stripeConnectAccount.duck'; import { EditListingWizard, Footer, NamedRedirect, Page, UserNav } from '../../components'; import { TopbarContainer } from '../../containers'; @@ -32,10 +36,18 @@ import { removeListingImage, loadData, clearUpdatedTab, + savePayoutDetails, } from './EditListingPage.duck'; import css from './EditListingPage.css'; +const STRIPE_ONBOARDING_RETURN_URL_SUCCESS = 'success'; +const STRIPE_ONBOARDING_RETURN_URL_FAILURE = 'failure'; +const STRIPE_ONBOARDING_RETURN_URL_TYPES = [ + STRIPE_ONBOARDING_RETURN_URL_SUCCESS, + STRIPE_ONBOARDING_RETURN_URL_FAILURE, +]; + const { UUID } = sdkTypes; // N.B. All the presentational content needs to be extracted to their own components @@ -57,19 +69,23 @@ export const EditListingPageComponent = props => { onImageUpload, onRemoveListingImage, onManageDisableScrolling, - onPayoutDetailsSubmit, + onPayoutDetailsFormSubmit, onPayoutDetailsFormChange, + onGetStripeConnectAccountLink, onUpdateImageOrder, onChange, page, params, scrollingDisabled, allowOnlyOneListing, + stripeAccountFetched, + stripeAccount, } = props; - const { id, type } = params; + const { id, type, returnURLType } = params; const isNewURI = type === LISTING_PAGE_PARAM_TYPE_NEW; const isDraftURI = type === LISTING_PAGE_PARAM_TYPE_DRAFT; + const isNewListingFlow = isNewURI || isDraftURI; const listingId = page.submittedListingId || (id ? new UUID(id) : null); const listing = getOwnListing(listingId); @@ -77,8 +93,10 @@ export const EditListingPageComponent = props => { const { state: currentListingState } = currentListing.attributes; const isPastDraft = currentListingState && currentListingState !== LISTING_STATE_DRAFT; - const shouldRedirect = (isNewURI || isDraftURI) && listingId && isPastDraft; - const showForm = isNewURI || currentListing.id; + const shouldRedirect = isNewListingFlow && listingId && isPastDraft; + + const hasStripeOnboardingDataIfNeeded = returnURLType ? !!(currentUser && currentUser.id) : true; + const showForm = hasStripeOnboardingDataIfNeeded && (isNewURI || currentListing.id); if (shouldRedirect) { const isPendingApproval = @@ -141,6 +159,7 @@ export const EditListingPageComponent = props => { addExceptionError, deleteExceptionError, }; + // TODO: is this dead code? (shouldRedirect is checked before) const newListingPublished = isDraftURI && currentListing && currentListingState !== LISTING_STATE_DRAFT; @@ -161,10 +180,9 @@ export const EditListingPageComponent = props => { return !removedImageIds.includes(img.id); }); - const title = - isNewURI || isDraftURI - ? intl.formatMessage({ id: 'EditListingPage.titleCreateListing' }) - : intl.formatMessage({ id: 'EditListingPage.titleEditListing' }); + const title = isNewListingFlow + ? intl.formatMessage({ id: 'EditListingPage.titleCreateListing' }) + : intl.formatMessage({ id: 'EditListingPage.titleEditListing' }); return ( @@ -195,17 +213,23 @@ export const EditListingPageComponent = props => { onCreateListingDraft={onCreateListingDraft} onPublishListingDraft={onPublishListingDraft} onPayoutDetailsFormChange={onPayoutDetailsFormChange} - onPayoutDetailsSubmit={onPayoutDetailsSubmit} + onPayoutDetailsSubmit={onPayoutDetailsFormSubmit} + onGetStripeConnectAccountLink={onGetStripeConnectAccountLink} onImageUpload={onImageUpload} onUpdateImageOrder={onUpdateImageOrder} onRemoveImage={onRemoveListingImage} onChange={onChange} currentUser={currentUser} onManageDisableScrolling={onManageDisableScrolling} + stripeOnboardingReturnURL={params.returnURLType} updatedTab={page.updatedTab} updateInProgress={page.updateInProgress || page.createListingDraftInProgress} fetchExceptionsInProgress={page.fetchExceptionsInProgress} availabilityExceptions={page.availabilityExceptions} + payoutDetailsSaveInProgress={page.payoutDetailsSaveInProgress} + payoutDetailsSaved={page.payoutDetailsSaved} + stripeAccountFetched={stripeAccountFetched} + stripeAccount={stripeAccount} />
@@ -271,6 +295,7 @@ EditListingPageComponent.propTypes = { slug: string.isRequired, type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired, tab: string.isRequired, + returnURLType: oneOf(STRIPE_ONBOARDING_RETURN_URL_TYPES), }).isRequired, scrollingDisabled: bool.isRequired, @@ -285,7 +310,17 @@ EditListingPageComponent.propTypes = { const mapStateToProps = state => { const page = state.EditListingPage; - const { createStripeAccountInProgress, createStripeAccountError } = state.stripe; + + const { + getAccountLinkInProgress, + createStripeAccountInProgress, + createStripeAccountError, + updateStripeAccountError, + fetchStripeAccountError, + stripeAccount, + stripeAccountFetched, + } = state.stripeConnectAccount; + const { currentUser, currentUserListing, currentUserListingFetched } = state.user; const fetchInProgress = createStripeAccountInProgress; @@ -296,7 +331,12 @@ const mapStateToProps = state => { return listings.length === 1 ? listings[0] : null; }; return { + getAccountLinkInProgress, createStripeAccountError, + updateStripeAccountError, + fetchStripeAccountError, + stripeAccount, + stripeAccountFetched, currentUser, currentUserListing, currentUserListingFetched, @@ -318,6 +358,9 @@ const mapDispatchToProps = dispatch => ({ dispatch(manageDisableScrolling(componentId, disableScrolling)), onPayoutDetailsFormChange: () => dispatch(stripeAccountClearError()), onPayoutDetailsSubmit: values => dispatch(createStripeAccount(values)), + onPayoutDetailsFormSubmit: (values, isUpdateCall) => + dispatch(savePayoutDetails(values, isUpdateCall)), + onGetStripeConnectAccountLink: params => dispatch(getStripeConnectAccountLink(params)), onUpdateImageOrder: imageOrder => dispatch(updateImageOrder(imageOrder)), onRemoveListingImage: imageId => dispatch(removeListingImage(imageId)), onChange: () => dispatch(clearUpdatedTab()), diff --git a/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap b/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap index 3bcb2852a..5d370abb4 100644 --- a/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap +++ b/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap @@ -53,7 +53,6 @@ exports[`EditListingPageComponent matches snapshot 1`] = ` onImageUpload={[Function]} onManageDisableScrolling={[Function]} onPayoutDetailsFormChange={[Function]} - onPayoutDetailsSubmit={[Function]} onPublishListingDraft={[Function]} onRemoveImage={[Function]} onUpdateImageOrder={[Function]} diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js deleted file mode 100644 index 9d0972f46..000000000 --- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.duck.js +++ /dev/null @@ -1,68 +0,0 @@ -import { createStripeAccount } from '../../ducks/stripe.duck'; -import { fetchCurrentUser } from '../../ducks/user.duck'; - -// ================ Action types ================ // - -export const SET_INITIAL_STATE = 'app/PayoutPreferencesPage/SET_INITIAL_STATE'; -export const SAVE_PAYOUT_DETAILS_REQUEST = 'app/PayoutPreferencesPage/SAVE_PAYOUT_DETAILS_REQUEST'; -export const SAVE_PAYOUT_DETAILS_SUCCESS = 'app/PayoutPreferencesPage/SAVE_PAYOUT_DETAILS_SUCCESS'; -export const SAVE_PAYOUT_DETAILS_ERROR = 'app/PayoutPreferencesPage/SAVE_PAYOUT_DETAILS_ERROR'; - -// ================ Reducer ================ // - -const initialState = { - payoutDetailsSaveInProgress: false, - payoutDetailsSaved: false, -}; - -export default function payoutPreferencesPageReducer(state = initialState, action = {}) { - const { type } = action; - switch (type) { - case SET_INITIAL_STATE: - return initialState; - - case SAVE_PAYOUT_DETAILS_REQUEST: - return { ...state, payoutDetailsSaveInProgress: true }; - case SAVE_PAYOUT_DETAILS_ERROR: - return { ...state, payoutDetailsSaveInProgress: false }; - case SAVE_PAYOUT_DETAILS_SUCCESS: - return { ...state, payoutDetailsSaveInProgress: false, payoutDetailsSaved: true }; - - default: - return state; - } -} - -// ================ Action creators ================ // - -export const setInitialState = () => ({ - type: SET_INITIAL_STATE, -}); - -export const savePayoutDetailsRequest = () => ({ - type: SAVE_PAYOUT_DETAILS_REQUEST, -}); -export const savePayoutDetailsError = () => ({ - type: SAVE_PAYOUT_DETAILS_ERROR, -}); -export const savePayoutDetailsSuccess = () => ({ - type: SAVE_PAYOUT_DETAILS_SUCCESS, -}); - -// ================ Thunks ================ // - -export const savePayoutDetails = values => (dispatch, getState, sdk) => { - dispatch(savePayoutDetailsRequest()); - - return dispatch(createStripeAccount(values)) - .then(() => dispatch(savePayoutDetailsSuccess())) - .catch(() => dispatch(savePayoutDetailsError())); -}; - -export const loadData = () => (dispatch, getState, sdk) => { - // Clear state so that previously loaded data is not visible - // in case this page load fails. - dispatch(setInitialState()); - - return dispatch(fetchCurrentUser()); -}; diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js b/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js deleted file mode 100644 index ee5cda05e..000000000 --- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.js +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react'; -import { bool, func } from 'prop-types'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; -import { ensureCurrentUser } from '../../util/data'; -import { propTypes } from '../../util/types'; -import { isScrollingDisabled } from '../../ducks/UI.duck'; -import { stripeAccountClearError } from '../../ducks/stripe.duck'; -import { - LayoutSideNavigation, - LayoutWrapperMain, - LayoutWrapperAccountSettingsSideNav, - LayoutWrapperTopbar, - LayoutWrapperFooter, - Footer, - Page, - UserNav, -} from '../../components'; -import { PayoutDetailsForm } from '../../forms'; -import { TopbarContainer } from '../../containers'; -import { savePayoutDetails, loadData } from './PayoutPreferencesPage.duck'; - -import css from './PayoutPreferencesPage.css'; - -export const PayoutPreferencesPageComponent = props => { - const { - currentUser, - scrollingDisabled, - createStripeAccountError, - onPayoutDetailsFormChange, - onPayoutDetailsFormSubmit, - payoutDetailsSaveInProgress, - payoutDetailsSaved, - intl, - } = props; - - const ensuredCurrentUser = ensureCurrentUser(currentUser); - const currentUserLoaded = !!ensuredCurrentUser.id; - const stripeConnected = - currentUserLoaded && - !!ensuredCurrentUser.stripeAccount && - !!ensuredCurrentUser.stripeAccount.id; - - const title = intl.formatMessage({ id: 'PayoutPreferencesPage.title' }); - const formDisabled = !currentUserLoaded || stripeConnected || payoutDetailsSaved; - - let message = ; - - if (currentUserLoaded && payoutDetailsSaved) { - message = ; - } else if (currentUserLoaded && stripeConnected) { - message = ; - } else if (currentUserLoaded && !stripeConnected) { - message = ; - } - - const showForm = - currentUserLoaded && (payoutDetailsSaveInProgress || payoutDetailsSaved || !stripeConnected); - const form = showForm ? ( - - ) : null; - - return ( - - - - - - - - -
-

- -

-

{message}

- {form} -
-
- -
- - - - ); -}; - -PayoutPreferencesPageComponent.defaultProps = { - currentUser: null, - createStripeAccountError: null, -}; - -PayoutPreferencesPageComponent.propTypes = { - currentUser: propTypes.currentUser, - scrollingDisabled: bool.isRequired, - payoutDetailsSaveInProgress: bool.isRequired, - createStripeAccountError: propTypes.error, - payoutDetailsSaved: bool.isRequired, - - onPayoutDetailsFormChange: func.isRequired, - onPayoutDetailsFormSubmit: func.isRequired, - - // from injectIntl - intl: intlShape.isRequired, -}; - -const mapStateToProps = state => { - const { createStripeAccountError } = state.stripe; - const { currentUser } = state.user; - const { payoutDetailsSaveInProgress, payoutDetailsSaved } = state.PayoutPreferencesPage; - return { - currentUser, - createStripeAccountError, - payoutDetailsSaveInProgress, - payoutDetailsSaved, - scrollingDisabled: isScrollingDisabled(state), - }; -}; - -const mapDispatchToProps = dispatch => ({ - onPayoutDetailsFormChange: () => dispatch(stripeAccountClearError()), - onPayoutDetailsFormSubmit: values => dispatch(savePayoutDetails(values)), -}); - -const PayoutPreferencesPage = compose( - connect( - mapStateToProps, - mapDispatchToProps - ), - injectIntl -)(PayoutPreferencesPageComponent); - -PayoutPreferencesPage.loadData = loadData; - -export default PayoutPreferencesPage; diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.css b/src/containers/StripePayoutPage/StripePayoutPage.css similarity index 95% rename from src/containers/PayoutPreferencesPage/PayoutPreferencesPage.css rename to src/containers/StripePayoutPage/StripePayoutPage.css index b16ae38a7..360e74b27 100644 --- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.css +++ b/src/containers/StripePayoutPage/StripePayoutPage.css @@ -3,6 +3,7 @@ .content { @media (--viewportMedium) { margin: 32px auto 0 auto; + width: 100%; max-width: 564px; } diff --git a/src/containers/StripePayoutPage/StripePayoutPage.duck.js b/src/containers/StripePayoutPage/StripePayoutPage.duck.js new file mode 100644 index 000000000..4647f4ffc --- /dev/null +++ b/src/containers/StripePayoutPage/StripePayoutPage.duck.js @@ -0,0 +1,85 @@ +import pick from 'lodash/pick'; +import { + createStripeAccount, + updateStripeAccount, + fetchStripeAccount, +} from '../../ducks/stripeConnectAccount.duck'; +import { fetchCurrentUser } from '../../ducks/user.duck'; + +// ================ Action types ================ // + +export const SET_INITIAL_VALUES = 'app/StripePayoutPage/SET_INITIAL_VALUES'; +export const SAVE_PAYOUT_DETAILS_REQUEST = 'app/StripePayoutPage/SAVE_PAYOUT_DETAILS_REQUEST'; +export const SAVE_PAYOUT_DETAILS_SUCCESS = 'app/StripePayoutPage/SAVE_PAYOUT_DETAILS_SUCCESS'; +export const SAVE_PAYOUT_DETAILS_ERROR = 'app/StripePayoutPage/SAVE_PAYOUT_DETAILS_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + payoutDetailsSaveInProgress: false, + payoutDetailsSaved: false, + fromReturnURL: false, +}; + +export default function reducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case SET_INITIAL_VALUES: + return { ...initialState, ...payload }; + + case SAVE_PAYOUT_DETAILS_REQUEST: + return { ...state, payoutDetailsSaveInProgress: true }; + case SAVE_PAYOUT_DETAILS_ERROR: + return { ...state, payoutDetailsSaveInProgress: false }; + case SAVE_PAYOUT_DETAILS_SUCCESS: + return { ...state, payoutDetailsSaveInProgress: false, payoutDetailsSaved: true }; + + default: + return state; + } +} + +// ================ Action creators ================ // + +export const setInitialValues = initialValues => ({ + type: SET_INITIAL_VALUES, + payload: pick(initialValues, Object.keys(initialState)), +}); + +export const savePayoutDetailsRequest = () => ({ + type: SAVE_PAYOUT_DETAILS_REQUEST, +}); +export const savePayoutDetailsError = () => ({ + type: SAVE_PAYOUT_DETAILS_ERROR, +}); +export const savePayoutDetailsSuccess = () => ({ + type: SAVE_PAYOUT_DETAILS_SUCCESS, +}); + +// ================ Thunks ================ // + +export const savePayoutDetails = (values, isUpdateCall) => (dispatch, getState, sdk) => { + const upsertThunk = isUpdateCall ? updateStripeAccount : createStripeAccount; + dispatch(savePayoutDetailsRequest()); + + return dispatch(upsertThunk(values, { expand: true })) + .then(response => { + dispatch(savePayoutDetailsSuccess()); + return response; + }) + .catch(() => dispatch(savePayoutDetailsError())); +}; + +export const loadData = () => (dispatch, getState, sdk) => { + // Clear state so that previously loaded data is not visible + // in case this page load fails. + dispatch(setInitialValues()); + + return dispatch(fetchCurrentUser()).then(response => { + const currentUser = getState().user.currentUser; + if (currentUser && currentUser.stripeAccount) { + dispatch(fetchStripeAccount()); + } + return response; + }); +}; diff --git a/src/containers/StripePayoutPage/StripePayoutPage.js b/src/containers/StripePayoutPage/StripePayoutPage.js new file mode 100644 index 000000000..e2b66e2c7 --- /dev/null +++ b/src/containers/StripePayoutPage/StripePayoutPage.js @@ -0,0 +1,281 @@ +import React from 'react'; +import { bool, func, oneOf, shape } from 'prop-types'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import routeConfiguration from '../../routeConfiguration'; +import config from '../../config'; +import { createResourceLocatorString } from '../../util/routes'; +import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; +import { ensureCurrentUser } from '../../util/data'; +import { propTypes } from '../../util/types'; +import { isScrollingDisabled } from '../../ducks/UI.duck'; +import { + stripeAccountClearError, + getStripeConnectAccountLink, +} from '../../ducks/stripeConnectAccount.duck'; +import { + NamedRedirect, + LayoutSideNavigation, + LayoutWrapperMain, + LayoutWrapperAccountSettingsSideNav, + LayoutWrapperTopbar, + LayoutWrapperFooter, + Footer, + Page, + StripeConnectAccountStatusBox, + UserNav, +} from '../../components'; +import { StripeConnectAccountForm } from '../../forms'; +import { TopbarContainer } from '..'; +import { savePayoutDetails, loadData } from './StripePayoutPage.duck'; + +import css from './StripePayoutPage.css'; + +const STRIPE_ONBOARDING_RETURN_URL_SUCCESS = 'success'; +const STRIPE_ONBOARDING_RETURN_URL_FAILURE = 'failure'; +const STRIPE_ONBOARDING_RETURN_URL_TYPES = [ + STRIPE_ONBOARDING_RETURN_URL_SUCCESS, + STRIPE_ONBOARDING_RETURN_URL_FAILURE, +]; + +// Create return URL for the Stripe onboarding form +const createReturnURL = (returnURLType, rootURL, routes) => { + const path = createResourceLocatorString( + 'StripePayoutOnboardingPage', + routes, + { returnURLType }, + {} + ); + const root = rootURL.replace(/\/$/, ''); + return `${root}${path}`; +}; + +// Get attribute: stripeAccountData +const getStripeAccountData = stripeAccount => stripeAccount.attributes.stripeAccountData || null; + +// Get last 4 digits of bank account returned in Stripe account +const getBankAccountLast4Digits = stripeAccountData => + stripeAccountData && stripeAccountData.external_accounts.data.length > 0 + ? stripeAccountData.external_accounts.data[0].last4 + : null; + +// Check if there's requirements on selected type: 'past_due', 'currently_due' etc. +const hasRequirements = (stripeAccountData, requirementType) => + stripeAccountData != null && + stripeAccountData.requirements && + Array.isArray(stripeAccountData.requirements[requirementType]) && + stripeAccountData.requirements[requirementType].length > 0; + +// Redirect user to Stripe's hosted Connect account onboarding form +const handleGetStripeConnectAccountLinkFn = (getLinkFn, commonParams) => type => () => { + getLinkFn({ type, ...commonParams }) + .then(url => { + window.location.href = url; + }) + .catch(err => console.error(err)); +}; + +export const StripePayoutPageComponent = props => { + const { + currentUser, + scrollingDisabled, + getAccountLinkInProgress, + createStripeAccountError, + updateStripeAccountError, + fetchStripeAccountError, + stripeAccountFetched, + stripeAccount, + onPayoutDetailsFormChange, + onPayoutDetailsFormSubmit, + onGetStripeConnectAccountLink, + payoutDetailsSaveInProgress, + payoutDetailsSaved, + params, + intl, + } = props; + + const { returnURLType } = params; + const ensuredCurrentUser = ensureCurrentUser(currentUser); + const currentUserLoaded = !!ensuredCurrentUser.id; + const stripeConnected = currentUserLoaded && !!stripeAccount && !!stripeAccount.id; + + const title = intl.formatMessage({ id: 'StripePayoutPage.title' }); + + const formDisabled = getAccountLinkInProgress; + + const rootURL = config.canonicalRootURL; + const routes = routeConfiguration(); + const successURL = createReturnURL(STRIPE_ONBOARDING_RETURN_URL_SUCCESS, rootURL, routes); + const failureURL = createReturnURL(STRIPE_ONBOARDING_RETURN_URL_FAILURE, rootURL, routes); + + const accountId = stripeConnected ? stripeAccount.id : null; + const stripeAccountData = stripeConnected ? getStripeAccountData(stripeAccount) : null; + const requirementsMissing = + stripeAccount && + (hasRequirements(stripeAccountData, 'past_due') || + hasRequirements(stripeAccountData, 'currently_due')); + + const savedCountry = stripeAccountData ? stripeAccountData.country : null; + + const handleGetStripeConnectAccountLink = handleGetStripeConnectAccountLinkFn( + onGetStripeConnectAccountLink, + { + accountId, + successURL, + failureURL, + } + ); + + const returnedNormallyFromStripe = returnURLType === STRIPE_ONBOARDING_RETURN_URL_SUCCESS; + const showVerificationError = returnURLType === STRIPE_ONBOARDING_RETURN_URL_FAILURE; + const showVerificationNeeded = stripeConnected && requirementsMissing; + + // Redirect from success URL to basic path for StripePayoutPage + if (returnedNormallyFromStripe && stripeConnected && !requirementsMissing) { + return ; + } + + return ( + + + + + + + + +
+

+ +

+ {!currentUserLoaded ? ( + + ) : ( + + {stripeConnected && (showVerificationError || showVerificationNeeded) ? ( + + ) : stripeConnected && savedCountry ? ( + + ) : null} + + )} +
+
+ +
+ + + + ); +}; + +StripePayoutPageComponent.defaultProps = { + currentUser: null, + createStripeAccountError: null, + updateStripeAccountError: null, + fetchStripeAccountError: null, + stripeAccount: null, + params: { + returnURLType: null, + }, +}; + +StripePayoutPageComponent.propTypes = { + currentUser: propTypes.currentUser, + scrollingDisabled: bool.isRequired, + getAccountLinkInProgress: bool.isRequired, + payoutDetailsSaveInProgress: bool.isRequired, + createStripeAccountError: propTypes.error, + updateStripeAccountError: propTypes.error, + fetchStripeAccountError: propTypes.error, + stripeAccount: propTypes.stripeAccount, + payoutDetailsSaved: bool.isRequired, + + onPayoutDetailsFormChange: func.isRequired, + onPayoutDetailsFormSubmit: func.isRequired, + onGetStripeConnectAccountLink: func.isRequired, + params: shape({ + returnURLType: oneOf(STRIPE_ONBOARDING_RETURN_URL_TYPES), + }), + + // from injectIntl + intl: intlShape.isRequired, +}; + +const mapStateToProps = state => { + const { + getAccountLinkInProgress, + createStripeAccountError, + updateStripeAccountError, + fetchStripeAccountError, + stripeAccount, + stripeAccountFetched, + } = state.stripeConnectAccount; + const { currentUser } = state.user; + const { payoutDetailsSaveInProgress, payoutDetailsSaved } = state.StripePayoutPage; + return { + currentUser, + getAccountLinkInProgress, + createStripeAccountError, + updateStripeAccountError, + fetchStripeAccountError, + stripeAccount, + stripeAccountFetched, + payoutDetailsSaveInProgress, + payoutDetailsSaved, + scrollingDisabled: isScrollingDisabled(state), + }; +}; + +const mapDispatchToProps = dispatch => ({ + onPayoutDetailsFormChange: () => dispatch(stripeAccountClearError()), + onPayoutDetailsFormSubmit: (values, isUpdateCall) => + dispatch(savePayoutDetails(values, isUpdateCall)), + onGetStripeConnectAccountLink: params => dispatch(getStripeConnectAccountLink(params)), +}); + +const StripePayoutPage = compose( + connect( + mapStateToProps, + mapDispatchToProps + ), + injectIntl +)(StripePayoutPageComponent); + +StripePayoutPage.loadData = loadData; + +export default StripePayoutPage; diff --git a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js b/src/containers/StripePayoutPage/StripePayoutPage.test.js similarity index 77% rename from src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js rename to src/containers/StripePayoutPage/StripePayoutPage.test.js index 5211503f0..b118d16f1 100644 --- a/src/containers/PayoutPreferencesPage/PayoutPreferencesPage.test.js +++ b/src/containers/StripePayoutPage/StripePayoutPage.test.js @@ -1,23 +1,26 @@ import React from 'react'; import { renderShallow } from '../../util/test-helpers'; import { fakeIntl, createCurrentUser, createStripeAccount } from '../../util/test-data'; -import { PayoutPreferencesPageComponent } from './PayoutPreferencesPage'; +import { StripePayoutPageComponent } from './StripePayoutPage'; const noop = () => null; -describe('PayoutPreferencesPage', () => { +describe('StripePayoutPage', () => { it('matches snapshot with Stripe not connected', () => { const currentUser = createCurrentUser('stripe-not-connected'); expect(currentUser.stripeAccount).toBeUndefined(); const tree = renderShallow( - ); expect(tree).toMatchSnapshot(); @@ -32,14 +35,17 @@ describe('PayoutPreferencesPage', () => { ); expect(currentUser.stripeAccount).toBeDefined(); const tree = renderShallow( - ); expect(tree).toMatchSnapshot(); @@ -54,14 +60,17 @@ describe('PayoutPreferencesPage', () => { ); expect(currentUser.stripeAccount).toBeDefined(); const tree = renderShallow( - ); expect(tree).toMatchSnapshot(); diff --git a/src/containers/PayoutPreferencesPage/__snapshots__/PayoutPreferencesPage.test.js.snap b/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap similarity index 59% rename from src/containers/PayoutPreferencesPage/__snapshots__/PayoutPreferencesPage.test.js.snap rename to src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap index d9d5a6e59..f38e7a38d 100644 --- a/src/containers/PayoutPreferencesPage/__snapshots__/PayoutPreferencesPage.test.js.snap +++ b/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PayoutPreferencesPage matches snapshot with Stripe connected 1`] = ` +exports[`StripePayoutPage matches snapshot with Stripe connected 1`] = `

-

- -

+
`; -exports[`PayoutPreferencesPage matches snapshot with Stripe not connected 1`] = ` +exports[`StripePayoutPage matches snapshot with Stripe not connected 1`] = `

-

- -

-
@@ -125,10 +124,10 @@ exports[`PayoutPreferencesPage matches snapshot with Stripe not connected 1`] = `; -exports[`PayoutPreferencesPage matches snapshot with details submitted 1`] = ` +exports[`StripePayoutPage matches snapshot with details submitted 1`] = `

-

- -

-
diff --git a/src/containers/index.js b/src/containers/index.js index b3b71691f..a2fc3c3ec 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -12,7 +12,7 @@ export { default as PasswordChangePage } from './PasswordChangePage/PasswordChan export { default as PasswordRecoveryPage } from './PasswordRecoveryPage/PasswordRecoveryPage'; export { default as PasswordResetPage } from './PasswordResetPage/PasswordResetPage'; export { default as PaymentMethodsPage } from './PaymentMethodsPage/PaymentMethodsPage'; -export { default as PayoutPreferencesPage } from './PayoutPreferencesPage/PayoutPreferencesPage'; +export { default as StripePayoutPage } from './StripePayoutPage/StripePayoutPage' export { default as PrivacyPolicyPage } from './PrivacyPolicyPage/PrivacyPolicyPage'; export { default as ProfilePage } from './ProfilePage/ProfilePage'; export { default as ProfileSettingsPage } from './ProfileSettingsPage/ProfileSettingsPage'; diff --git a/src/containers/reducers.js b/src/containers/reducers.js index aaff15057..be5755e0d 100644 --- a/src/containers/reducers.js +++ b/src/containers/reducers.js @@ -12,11 +12,11 @@ import ManageListingsPage from './ManageListingsPage/ManageListingsPage.duck'; import PasswordChangePage from './PasswordChangePage/PasswordChangePage.duck'; import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.duck'; import PasswordResetPage from './PasswordResetPage/PasswordResetPage.duck'; -import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage.duck'; import PaymentMethodsPage from './PaymentMethodsPage/PaymentMethodsPage.duck'; import ProfilePage from './ProfilePage/ProfilePage.duck'; import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck'; import SearchPage from './SearchPage/SearchPage.duck'; +import StripePayoutPage from './StripePayoutPage/StripePayoutPage.duck'; import TransactionPage from './TransactionPage/TransactionPage.duck'; export { @@ -29,10 +29,10 @@ export { PasswordChangePage, PasswordRecoveryPage, PasswordResetPage, - PayoutPreferencesPage, PaymentMethodsPage, ProfilePage, ProfileSettingsPage, SearchPage, + StripePayoutPage, TransactionPage, }; diff --git a/src/ducks/index.js b/src/ducks/index.js index fa18d646d..e9d298f71 100644 --- a/src/ducks/index.js +++ b/src/ducks/index.js @@ -13,6 +13,7 @@ import UI from './UI.duck'; import marketplaceData from './marketplaceData.duck'; import paymentMethods from './paymentMethods.duck'; import stripe from './stripe.duck'; +import stripeConnectAccount from './stripeConnectAccount.duck'; import user from './user.duck'; export { @@ -25,5 +26,6 @@ export { marketplaceData, paymentMethods, stripe, + stripeConnectAccount, user, }; diff --git a/src/ducks/stripe.duck.js b/src/ducks/stripe.duck.js index cd533edb2..eea94e466 100644 --- a/src/ducks/stripe.duck.js +++ b/src/ducks/stripe.duck.js @@ -1,13 +1,8 @@ -import config from '../config'; import { storableError } from '../util/errors'; import * as log from '../util/log'; // ================ Action types ================ // -export const STRIPE_ACCOUNT_CREATE_REQUEST = 'app/stripe/STRIPE_ACCOUNT_CREATE_REQUEST'; -export const STRIPE_ACCOUNT_CREATE_SUCCESS = 'app/stripe/STRIPE_ACCOUNT_CREATE_SUCCESS'; -export const STRIPE_ACCOUNT_CREATE_ERROR = 'app/stripe/STRIPE_ACCOUNT_CREATE_ERROR'; - export const STRIPE_ACCOUNT_CLEAR_ERROR = 'app/stripe/STRIPE_ACCOUNT_CLEAR_ERROR'; export const ACCOUNT_OPENER_CREATE_REQUEST = 'app/stripe/ACCOUNT_OPENER_CREATE_REQUEST'; @@ -37,14 +32,6 @@ export const RETRIEVE_PAYMENT_INTENT_ERROR = 'app/stripe/RETRIEVE_PAYMENT_INTENT // ================ Reducer ================ // const initialState = { - createStripeAccountInProgress: false, - createStripeAccountError: null, - createAccountOpenerInProgress: false, - createAccountOpenerError: false, - personAccountOpener: null, - persons: [], - stripeAccount: null, - stripeAccountFetched: false, handleCardPaymentInProgress: false, handleCardPaymentError: null, handleCardSetupInProgress: false, @@ -58,19 +45,6 @@ const initialState = { export default function reducer(state = initialState, action = {}) { const { type, payload } = action; switch (type) { - case STRIPE_ACCOUNT_CREATE_REQUEST: - return { ...state, createStripeAccountError: null, createStripeAccountInProgress: true }; - case STRIPE_ACCOUNT_CREATE_SUCCESS: - return { - ...state, - createStripeAccountInProgress: false, - stripeAccount: payload, - stripeAccountFetched: true, - }; - case STRIPE_ACCOUNT_CREATE_ERROR: - console.error(payload); - return { ...state, createStripeAccountError: payload, createStripeAccountInProgress: false }; - case STRIPE_ACCOUNT_CLEAR_ERROR: return { ...initialState }; @@ -173,55 +147,10 @@ export default function reducer(state = initialState, action = {}) { // ================ Action creators ================ // -export const stripeAccountCreateRequest = () => ({ type: STRIPE_ACCOUNT_CREATE_REQUEST }); - -export const stripeAccountCreateSuccess = stripeAccount => ({ - type: STRIPE_ACCOUNT_CREATE_SUCCESS, - payload: stripeAccount, -}); - -export const stripeAccountCreateError = e => ({ - type: STRIPE_ACCOUNT_CREATE_ERROR, - payload: e, - error: true, -}); - export const stripeAccountClearError = () => ({ type: STRIPE_ACCOUNT_CLEAR_ERROR, }); -export const accountOpenerCreateRequest = personToken => ({ - type: ACCOUNT_OPENER_CREATE_REQUEST, - payload: personToken, -}); - -export const accountOpenerCreateSuccess = payload => ({ - type: ACCOUNT_OPENER_CREATE_SUCCESS, - payload, -}); - -export const accountOpenerCreateError = payload => ({ - type: ACCOUNT_OPENER_CREATE_ERROR, - payload, - error: true, -}); - -export const personCreateRequest = personToken => ({ - type: PERSON_CREATE_REQUEST, - payload: personToken, -}); - -export const personCreateSuccess = payload => ({ - type: PERSON_CREATE_SUCCESS, - payload, -}); - -export const personCreateError = payload => ({ - type: PERSON_CREATE_ERROR, - payload, - error: true, -}); - export const handleCardPaymentRequest = () => ({ type: HANDLE_CARD_PAYMENT_REQUEST, }); @@ -273,338 +202,6 @@ export const retrievePaymentIntentError = payload => ({ // ================ Thunks ================ // -// Util: rename address fields to match Stripe API specifications -const formatAddress = address => { - const { city, streetAddress, postalCode, state, province } = address; - const cityMaybe = city ? { city } : {}; - const streetAddressMaybe = streetAddress ? { line1: streetAddress } : {}; - const postalCodeMaybe = postalCode ? { postal_code: postalCode } : {}; - const stateMaybe = state ? { state } : province ? { state: province } : {}; - - return { - ...cityMaybe, - ...streetAddressMaybe, - ...postalCodeMaybe, - ...stateMaybe, - }; -}; - -// Util: rename personToken params to match Stripe API specifications -const personTokenParams = (personData, companyConfig) => { - const { - isAccountOpener, - fname: firstName, - lname: lastName, - birthDate, - address, - personalIdNumber, - email, - phone, - role, - ownershipPercentage, - title, - } = personData; - - const addressMaybe = address ? { address: formatAddress(address) } : {}; - const emailMaybe = email ? { email } : {}; - const phoneMaybe = phone ? { phone } : {}; - - const personalIdNumberRequired = companyConfig && companyConfig.personalIdNumberRequired; - const ssnLast4Required = companyConfig && companyConfig.ssnLast4Required; - - const idNumberMaybe = ssnLast4Required - ? { ssn_last_4: personalIdNumber } - : personalIdNumberRequired - ? { id_number: personalIdNumber } - : {}; - - const accountOpenerMaybe = isAccountOpener ? { account_opener: true } : {}; - const jobTitleMaybe = title ? { title } : {}; - const ownerMaybe = role && role.find(r => r === 'owner') ? { owner: true } : {}; - const ownershipPercentageMaybe = ownershipPercentage - ? { percent_ownership: Number.parseFloat(ownershipPercentage) } - : {}; - - const relationshipMaybe = - isAccountOpener || title || role - ? { - relationship: { - ...accountOpenerMaybe, - ...jobTitleMaybe, - ...ownerMaybe, - ...ownershipPercentageMaybe, - }, - } - : {}; - - return { - person: { - first_name: firstName, - last_name: lastName, - dob: birthDate, - ...addressMaybe, - ...idNumberMaybe, - ...emailMaybe, - ...phoneMaybe, - ...relationshipMaybe, - }, - }; -}; - -const createStripePerson = (personParams, companyConfig, stripe) => (dispatch, getState, sdk) => { - const { isAccountOpener } = personParams; - let personToken = 'no-token'; - return stripe - .createToken('person', personTokenParams(personParams, companyConfig)) - .then(response => { - personToken = response.token.id; - - // Request to create person in progress - // Account opener is mandatory for all - so it's handled separately - const createPersonRequest = isAccountOpener - ? accountOpenerCreateRequest - : personCreateRequest; - dispatch(createPersonRequest({ personToken })); - - return sdk.stripePersons.create({ personToken }, { expand: true }); - }) - .then(response => { - // Stripe person created successfully - const createPersonSuccess = isAccountOpener - ? accountOpenerCreateSuccess - : personCreateSuccess; - dispatch(createPersonSuccess({ personToken, stripePerson: response.data.data })); - - return response; - }) - .catch(err => { - const e = storableError(err); - - // Stripe person creation failed - const createPersonError = isAccountOpener ? accountOpenerCreateError : personCreateError; - dispatch(createPersonError({ personToken, error: e })); - - const stripeMessage = - e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta - ? e.apiErrors[0].meta.stripeMessage - : null; - log.error(err, 'create-stripe-person-failed', { stripeMessage }); - throw e; - }); -}; - -// accountData should be either individual or company -const bankAccountTokenParams = accountData => accountData.bankAccountToken; -const businessProfileParams = (accountData, accountConfig) => { - const businessProfileRequired = - accountConfig && accountConfig.mccForUS && accountConfig.businessURL; - - const { mcc, url } = - accountData && accountData.businessProfile ? accountData.businessProfile : {}; - - const hasInformation = mcc && url; - - return businessProfileRequired && hasInformation - ? { - businessProfileMCC: mcc, - businessProfileURL: url, - } - : {}; -}; - -// Util: rename accountToken params to match Stripe API specifications -const accountTokenParamsForCompany = company => { - const { address, name, phone, taxId } = company; - const addressMaybe = address ? { address: formatAddress(address) } : {}; - const phoneMaybe = phone ? { phone } : {}; - return { - business_type: 'company', - company: { - name, - tax_id: taxId, - ...addressMaybe, - ...phoneMaybe, - }, - tos_shown_and_accepted: true, - }; -}; - -export const createStripeCompanyAccount = (payoutDetails, companyConfig, stripe) => ( - dispatch, - getState, - sdk -) => { - const { company, country, accountOpener, persons = [] } = payoutDetails; - const state = getState(); - let stripeAccount = - state.stripe && state.stripe.stripeAccount ? state.stripe.stripeAccount : null; - - dispatch(stripeAccountCreateRequest()); - - const createPersons = () => { - return Promise.all([ - dispatch( - createStripePerson({ ...accountOpener, isAccountOpener: true }, companyConfig, stripe) - ), - ...persons.map(p => dispatch(createStripePerson(p, companyConfig, stripe))), - ]); - }; - - // If stripeAccount exists, stripePersons call must have failed. - // Retry person creation - if (stripeAccount) { - return createPersons() - .then(response => { - // Return created stripe account from this thunk function - return stripeAccount; - }) - .catch(err => { - const e = storableError(err); - dispatch(stripeAccountCreateError(e)); - const stripeMessage = - e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta - ? e.apiErrors[0].meta.stripeMessage - : null; - log.error(err, 'create-stripe-company-persons-failed', { stripeMessage }); - throw e; - }); - } - - return stripe - .createToken('account', accountTokenParamsForCompany(company)) - .then(response => { - const accountToken = response.token.id; - const bankAccountToken = bankAccountTokenParams(company); - const stripeAccountParams = { - accountToken, - bankAccountToken, - country, - ...businessProfileParams(company, companyConfig), - }; - return sdk.stripeAccount.create(stripeAccountParams, { expand: true }); - }) - .then(response => { - stripeAccount = response; - dispatch(stripeAccountCreateSuccess(response.data.data)); - return createPersons(); - }) - .then(response => { - // Return created stripe account from this thunk function - return stripeAccount; - }) - .catch(err => { - const e = storableError(err); - dispatch(stripeAccountCreateError(e)); - const stripeMessage = - e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta - ? e.apiErrors[0].meta.stripeMessage - : null; - const errorFlag = !stripeAccount - ? 'create-stripe-company-account-failed' - : 'create-stripe-company-persons-failed'; - log.error(err, errorFlag, { stripeMessage }); - throw e; - }); -}; - -const accountTokenParamsForIndividual = (individual, individualConfig) => { - const { - fname: firstName, - lname: lastName, - birthDate, - address, - phone, - email, - personalIdNumber, - } = individual; - - const addressMaybe = address ? { address: formatAddress(address) } : {}; - const dobMaybe = birthDate ? { dob: birthDate } : {}; - const emailMaybe = email ? { email } : {}; - const phoneMaybe = phone ? { phone } : {}; - - const personalIdNumberRequired = individualConfig && individualConfig.personalIdNumberRequired; - const ssnLast4Required = individualConfig && individualConfig.ssnLast4Required; - - const idNumberMaybe = ssnLast4Required - ? { ssn_last_4: personalIdNumber } - : personalIdNumberRequired - ? { id_number: personalIdNumber } - : {}; - - return { - business_type: 'individual', - individual: { - first_name: firstName, - last_name: lastName, - ...dobMaybe, - ...addressMaybe, - ...emailMaybe, - ...phoneMaybe, - ...idNumberMaybe, - }, - tos_shown_and_accepted: true, - }; -}; - -export const createStripeIndividualAccount = (payoutDetails, individualConfig, stripe) => ( - dispatch, - getState, - sdk -) => { - const { country, individual } = payoutDetails; - let stripeAccount; - dispatch(stripeAccountCreateRequest()); - - return stripe - .createToken('account', accountTokenParamsForIndividual(individual, individualConfig)) - .then(response => { - const accountToken = response.token.id; - const bankAccountToken = bankAccountTokenParams(individual); - const stripeAccountParams = { - accountToken, - bankAccountToken, - country, - ...businessProfileParams(individual, individualConfig), - }; - return sdk.stripeAccount.create(stripeAccountParams, { expand: true }); - }) - .then(response => { - stripeAccount = response; - dispatch(stripeAccountCreateSuccess(response.data.data)); - return stripeAccount; - }) - .catch(err => { - const e = storableError(err); - dispatch(stripeAccountCreateError(e)); - const stripeMessage = - e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta - ? e.apiErrors[0].meta.stripeMessage - : null; - log.error(err, 'create-stripe-individual-account-failed', { stripeMessage }); - throw e; - }); -}; - -export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) => { - if (typeof window === 'undefined' || !window.Stripe) { - throw new Error('Stripe must be loaded for submitting PayoutPreferences'); - } - - const stripe = window.Stripe(config.stripe.publishableKey); - - const country = payoutDetails.country; - const countryConfig = config.stripe.supportedCountries.find(c => c.code === country); - const individualConfig = countryConfig.individualConfig; - const companyConfig = countryConfig.companyConfig; - - if (payoutDetails.accountType === 'individual') { - return dispatch(createStripeIndividualAccount(payoutDetails, individualConfig, stripe)); - } else { - return dispatch(createStripeCompanyAccount(payoutDetails, companyConfig, stripe)); - } -}; - export const retrievePaymentIntent = params => dispatch => { const { stripe, stripePaymentIntentClientSecret } = params; dispatch(retrievePaymentIntentRequest()); diff --git a/src/ducks/stripeConnectAccount.duck.js b/src/ducks/stripeConnectAccount.duck.js new file mode 100644 index 000000000..ec624c098 --- /dev/null +++ b/src/ducks/stripeConnectAccount.duck.js @@ -0,0 +1,265 @@ +// This file deals with Flex API which will create Stripe Custom Connect accounts +// from given bank_account tokens. + +import { storableError } from '../util/errors'; +import * as log from '../util/log'; + +// ================ Action types ================ // + +export const STRIPE_ACCOUNT_CREATE_REQUEST = 'app/stripe/STRIPE_ACCOUNT_CREATE_REQUEST'; +export const STRIPE_ACCOUNT_CREATE_SUCCESS = 'app/stripe/STRIPE_ACCOUNT_CREATE_SUCCESS'; +export const STRIPE_ACCOUNT_CREATE_ERROR = 'app/stripe/STRIPE_ACCOUNT_CREATE_ERROR'; + +export const STRIPE_ACCOUNT_UPDATE_REQUEST = 'app/stripe/STRIPE_ACCOUNT_UPDATE_REQUEST'; +export const STRIPE_ACCOUNT_UPDATE_SUCCESS = 'app/stripe/STRIPE_ACCOUNT_UPDATE_SUCCESS'; +export const STRIPE_ACCOUNT_UPDATE_ERROR = 'app/stripe/STRIPE_ACCOUNT_UPDATE_ERROR'; + +export const STRIPE_ACCOUNT_FETCH_REQUEST = 'app/stripe/STRIPE_ACCOUNT_FETCH_REQUEST'; +export const STRIPE_ACCOUNT_FETCH_SUCCESS = 'app/stripe/STRIPE_ACCOUNT_FETCH_SUCCESS'; +export const STRIPE_ACCOUNT_FETCH_ERROR = 'app/stripe/STRIPE_ACCOUNT_FETCH_ERROR'; + +export const STRIPE_ACCOUNT_CLEAR_ERROR = 'app/stripe/STRIPE_ACCOUNT_CLEAR_ERROR'; + +export const GET_ACCOUNT_LINK_REQUEST = 'app/stripeConnectAccount.duck.js/GET_ACCOUNT_LINK_REQUEST'; +export const GET_ACCOUNT_LINK_SUCCESS = 'app/stripeConnectAccount.duck.js/GET_ACCOUNT_LINK_SUCCESS'; +export const GET_ACCOUNT_LINK_ERROR = 'app/stripeConnectAccount.duck.js/GET_ACCOUNT_LINK_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + createStripeAccountInProgress: false, + createStripeAccountError: null, + updateStripeAccountInProgress: false, + updateStripeAccountError: null, + fetchStripeAccountInProgress: false, + fetchStripeAccountError: null, + getAccountLinkInProgress: false, + getAccountLinkError: false, + stripeAccount: null, + stripeAccountFetched: false, +}; + +export default function reducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case STRIPE_ACCOUNT_CREATE_REQUEST: + return { ...state, createStripeAccountError: null, createStripeAccountInProgress: true }; + case STRIPE_ACCOUNT_CREATE_SUCCESS: + return { + ...state, + createStripeAccountInProgress: false, + stripeAccount: payload, + stripeAccountFetched: true, + }; + case STRIPE_ACCOUNT_CREATE_ERROR: + console.error(payload); + return { ...state, createStripeAccountError: payload, createStripeAccountInProgress: false }; + + case STRIPE_ACCOUNT_UPDATE_REQUEST: + return { ...state, updateStripeAccountError: null, updateStripeAccountInProgress: true }; + case STRIPE_ACCOUNT_UPDATE_SUCCESS: + return { + ...state, + updateStripeAccountInProgress: false, + stripeAccount: payload, + stripeAccountFetched: true, + }; + case STRIPE_ACCOUNT_UPDATE_ERROR: + console.error(payload); + return { ...state, updateStripeAccountError: payload, createStripeAccountInProgress: false }; + + case STRIPE_ACCOUNT_FETCH_REQUEST: + return { ...state, fetchStripeAccountError: null, fetchStripeAccountInProgress: true }; + case STRIPE_ACCOUNT_FETCH_SUCCESS: + return { + ...state, + fetchStripeAccountInProgress: false, + stripeAccount: payload, + stripeAccountFetched: true, + }; + case STRIPE_ACCOUNT_FETCH_ERROR: + console.error(payload); + return { ...state, fetchStripeAccountError: payload, fetchStripeAccountInProgress: false }; + + case STRIPE_ACCOUNT_CLEAR_ERROR: + return { ...initialState }; + + case GET_ACCOUNT_LINK_REQUEST: + return { ...state, getAccountLinkError: null, getAccountLinkInProgress: true }; + case GET_ACCOUNT_LINK_ERROR: + return { ...state, getAccountLinkInProgress: false, getAccountLinkError: true }; + case GET_ACCOUNT_LINK_SUCCESS: + return { ...state, getAccountLinkInProgress: false }; + + default: + return state; + } +} + +// ================ Action creators ================ // + +export const stripeAccountCreateRequest = () => ({ type: STRIPE_ACCOUNT_CREATE_REQUEST }); + +export const stripeAccountCreateSuccess = stripeAccount => ({ + type: STRIPE_ACCOUNT_CREATE_SUCCESS, + payload: stripeAccount, +}); + +export const stripeAccountCreateError = e => ({ + type: STRIPE_ACCOUNT_CREATE_ERROR, + payload: e, + error: true, +}); + +export const stripeAccountUpdateRequest = () => ({ type: STRIPE_ACCOUNT_UPDATE_REQUEST }); + +export const stripeAccountUpdateSuccess = stripeAccount => ({ + type: STRIPE_ACCOUNT_UPDATE_SUCCESS, + payload: stripeAccount, +}); + +export const stripeAccountUpdateError = e => ({ + type: STRIPE_ACCOUNT_UPDATE_ERROR, + payload: e, + error: true, +}); + +export const stripeAccountFetchRequest = () => ({ type: STRIPE_ACCOUNT_FETCH_REQUEST }); + +export const stripeAccountFetchSuccess = stripeAccount => ({ + type: STRIPE_ACCOUNT_FETCH_SUCCESS, + payload: stripeAccount, +}); + +export const stripeAccountFetchError = e => ({ + type: STRIPE_ACCOUNT_FETCH_ERROR, + payload: e, + error: true, +}); + +export const stripeAccountClearError = () => ({ + type: STRIPE_ACCOUNT_CLEAR_ERROR, +}); + +export const getAccountLinkRequest = () => ({ + type: GET_ACCOUNT_LINK_REQUEST, +}); +export const getAccountLinkError = () => ({ + type: GET_ACCOUNT_LINK_ERROR, +}); +export const getAccountLinkSuccess = () => ({ + type: GET_ACCOUNT_LINK_SUCCESS, +}); + +// ================ Thunks ================ // + +export const createStripeAccount = params => (dispatch, getState, sdk) => { + const country = params.country; + const bankAccountToken = params.bankAccountToken; + + dispatch(stripeAccountCreateRequest()); + + return sdk.stripeAccount + .create( + { country, bankAccountToken, requestedCapabilities: ['card_payments', 'transfers'] }, + { expand: true } + ) + .then(response => { + const stripeAccount = response.data.data; + dispatch(stripeAccountCreateSuccess(stripeAccount)); + return stripeAccount; + }) + .catch(err => { + const e = storableError(err); + dispatch(stripeAccountCreateError(e)); + const stripeMessage = + e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta + ? e.apiErrors[0].meta.stripeMessage + : null; + log.error(err, 'create-stripe-account-failed', { stripeMessage }); + throw e; + }); +}; + +// This function is used for updating the bank account token +// but could be expanded to other information as well. +// +// If the Stripe account has been created with account token, +// you need to use account token also to update the account. +// By default the account token will not be used. +// See API reference for more information: +// https://www.sharetribe.com/api-reference/?javascript#update-stripe-account +export const updateStripeAccount = params => (dispatch, getState, sdk) => { + const bankAccountToken = params.bankAccountToken; + + dispatch(stripeAccountUpdateRequest()); + return sdk.stripeAccount + .update( + { bankAccountToken, requestedCapabilities: ['card_payments', 'transfers'] }, + { expand: true } + ) + .then(response => { + const stripeAccount = response.data.data; + dispatch(stripeAccountUpdateSuccess(stripeAccount)); + return stripeAccount; + }) + .catch(err => { + const e = storableError(err); + dispatch(stripeAccountUpdateError(e)); + const stripeMessage = + e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta + ? e.apiErrors[0].meta.stripeMessage + : null; + log.error(err, 'update-stripe-account-failed', { stripeMessage }); + throw e; + }); +}; + +export const fetchStripeAccount = params => (dispatch, getState, sdk) => { + dispatch(stripeAccountFetchRequest()); + + return sdk.stripeAccount + .fetch() + .then(response => { + const stripeAccount = response.data.data; + dispatch(stripeAccountFetchSuccess(stripeAccount)); + return stripeAccount; + }) + .catch(err => { + const e = storableError(err); + dispatch(stripeAccountFetchError(e)); + const stripeMessage = + e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta + ? e.apiErrors[0].meta.stripeMessage + : null; + log.error(err, 'fetch-stripe-account-failed', { stripeMessage }); + throw e; + }); +}; + +export const getStripeConnectAccountLink = params => (dispatch, getState, sdk) => { + const { failureURL, successURL, type } = params; + dispatch(getAccountLinkRequest()); + + return sdk.stripeAccountLinks + .create({ + failureURL, + successURL, + type, + collect: 'currently_due', + }) + .then(response => { + // Return the account link + return response.data.data.attributes.url; + }) + .catch(err => { + const e = storableError(err); + dispatch(getAccountLinkError(e)); + const stripeMessage = + e.apiErrors && e.apiErrors.length > 0 && e.apiErrors[0].meta + ? e.apiErrors[0].meta.stripeMessage + : null; + log.error(err, 'get-stripe-account-link-failed', { stripeMessage }); + throw e; + }); +}; diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index a94ae4e1d..a9f7f7d4d 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -4,7 +4,7 @@ import { transitionsToRequested } from '../util/transaction'; import { LISTING_STATE_DRAFT } from '../util/types'; import * as log from '../util/log'; import { authInfo } from './Auth.duck'; -import { stripeAccountCreateSuccess } from './stripe.duck.js'; +import { stripeAccountCreateSuccess } from './stripeConnectAccount.duck'; // ================ Action types ================ // diff --git a/src/examples.js b/src/examples.js index 1c4a7b112..717cb995e 100644 --- a/src/examples.js +++ b/src/examples.js @@ -44,6 +44,7 @@ import * as IconSocialMediaFacebook from './components/IconSocialMediaFacebook/I import * as IconSocialMediaInstagram from './components/IconSocialMediaInstagram/IconSocialMediaInstagram.example'; import * as IconSocialMediaTwitter from './components/IconSocialMediaTwitter/IconSocialMediaTwitter.example'; import * as IconSpinner from './components/IconSpinner/IconSpinner.example'; +import * as IconSuccess from './components/IconSuccess/IconSuccess.example'; import * as ImageCarousel from './components/ImageCarousel/ImageCarousel.example'; import * as KeywordFilter from './components/KeywordFilter/KeywordFilter.example'; import * as ListingCard from './components/ListingCard/ListingCard.example'; @@ -90,7 +91,6 @@ import * as FilterForm from './forms/FilterForm/FilterForm.example'; import * as LoginForm from './forms/LoginForm/LoginForm.example'; import * as PasswordRecoveryForm from './forms/PasswordRecoveryForm/PasswordRecoveryForm.example'; import * as PasswordResetForm from './forms/PasswordResetForm/PasswordResetForm.example'; -import * as PayoutDetailsForm from './forms/PayoutDetailsForm/PayoutDetailsForm.example'; import * as ReviewForm from './forms/ReviewForm/ReviewForm.example'; import * as SendMessageForm from './forms/SendMessageForm/SendMessageForm.example'; import * as SignupForm from './forms/SignupForm/SignupForm.example'; @@ -160,6 +160,7 @@ export { IconSocialMediaInstagram, IconSocialMediaTwitter, IconSpinner, + IconSuccess, ImageCarousel, KeywordFilter, ListingCard, @@ -175,7 +176,6 @@ export { PaginationLinks, PasswordRecoveryForm, PasswordResetForm, - PayoutDetailsForm, PriceFilter, PropertyGroup, RangeSlider, diff --git a/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.example.js b/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.example.js index cd92b871c..d316fdf72 100644 --- a/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.example.js +++ b/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.example.js @@ -10,6 +10,8 @@ export const Empty = { saveActionMsg: 'Save description', updated: false, updateInProgress: false, + disabled: false, + ready: false, }, group: 'forms', }; diff --git a/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.test.js b/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.test.js index 0caa59d11..8ed278aef 100644 --- a/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.test.js +++ b/src/forms/EditListingDescriptionForm/EditListingDescriptionForm.test.js @@ -15,7 +15,9 @@ describe('EditListingDescriptionForm', () => { saveActionMsg="Save description" updated={false} updateInProgress={false} - certificate={[{ key: 'cat1', label: 'Cat 1' }, { key: 'cat2', label: 'Cat 2' }]} + disabled={false} + ready={false} + categories={[{ key: 'cat1', label: 'Cat 1' }, { key: 'cat2', label: 'Cat 2' }]} /> ); expect(tree).toMatchSnapshot(); diff --git a/src/forms/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap b/src/forms/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap index 401c9b71b..0a422f896 100644 --- a/src/forms/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap +++ b/src/forms/EditListingDescriptionForm/__snapshots__/EditListingDescriptionForm.test.js.snap @@ -50,37 +50,6 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = ` value="" /> -
- - -
+ ) : null; + + // If the Stripe publishable key is not set up, don't show the form + return config.stripe.publishableKey ? ( + + {!stripeConnected || accountDataLoaded ? ( + stripeAccountFields + ) : ( +
+ +
+ )} + + + + {children} + + {submitButtonMaybe} + + ) : ( +
+ +
+ ); + }} + /> + ); +}; + +StripeConnectAccountFormComponent.defaultProps = { + className: null, + stripeAccountError: null, + disabled: false, + inProgress: false, + ready: false, + savedCountry: null, + stripeBankAccountLastDigits: null, + submitButtonText: null, + fieldRenderProps: null, +}; + +StripeConnectAccountFormComponent.propTypes = { + className: string, + stripeAccountError: object, + disabled: bool, + inProgress: bool, + ready: bool, + savedCountry: string, + stripeBankAccountLastDigits: string, + stripeAccountFetched: bool.isRequired, + submitButtonText: string, + fieldRenderProps: shape({ + handleSubmit: func, + invalid: bool, + pristine: bool, + values: object, + }), + + // from injectIntl + intl: intlShape.isRequired, +}; + +const StripeConnectAccountForm = compose(injectIntl)(StripeConnectAccountFormComponent); + +export default StripeConnectAccountForm; diff --git a/src/forms/index.js b/src/forms/index.js index 540ab883f..bfd4fe821 100644 --- a/src/forms/index.js +++ b/src/forms/index.js @@ -17,11 +17,11 @@ export { default as PasswordChangeForm } from './PasswordChangeForm/PasswordChan export { default as PasswordRecoveryForm } from './PasswordRecoveryForm/PasswordRecoveryForm'; export { default as PasswordResetForm } from './PasswordResetForm/PasswordResetForm'; export { default as PaymentMethodsForm } from './PaymentMethodsForm/PaymentMethodsForm'; -export { default as PayoutDetailsForm } from './PayoutDetailsForm/PayoutDetailsForm'; export { default as PriceFilterForm } from './PriceFilterForm/PriceFilterForm'; export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSettingsForm'; export { default as ReviewForm } from './ReviewForm/ReviewForm'; export { default as SendMessageForm } from './SendMessageForm/SendMessageForm'; export { default as SignupForm } from './SignupForm/SignupForm'; export { default as StripePaymentForm } from './StripePaymentForm/StripePaymentForm'; +export { default as StripeConnectAccountForm } from './StripeConnectAccountForm/StripeConnectAccountForm'; export { default as TopbarSearchForm } from './TopbarSearchForm/TopbarSearchForm'; diff --git a/src/marketplace.css b/src/marketplace.css index e001c4967..47b1487ab 100644 --- a/src/marketplace.css +++ b/src/marketplace.css @@ -46,8 +46,11 @@ --successColor: #2ecc71; --successColorDark: #239954; + --successColorLight: #f0fff6; --failColor: #ff0000; + --failColorLight: #fff0f0; --attentionColor: #ffaa00; + --attentionColorLight: #fff7f0; --bannedColorLight: var(--marketplaceColorLight); --bannedColorDark: var(--marketplaceColor); diff --git a/src/routeConfiguration.js b/src/routeConfiguration.js index f661fad59..adf19585d 100644 --- a/src/routeConfiguration.js +++ b/src/routeConfiguration.js @@ -13,7 +13,7 @@ import { PasswordChangePage, PasswordRecoveryPage, PasswordResetPage, - PayoutPreferencesPage, + StripePayoutPage, PaymentMethodsPage, PrivacyPolicyPage, ProfilePage, @@ -32,7 +32,7 @@ import { NamedRedirect } from './components'; export const ACCOUNT_SETTINGS_PAGES = [ 'ContactDetailsPage', 'PasswordChangePage', - 'PayoutPreferencesPage', + 'StripePayoutPage', 'PaymentMethodsPage', ]; @@ -124,6 +124,13 @@ const routeConfiguration = () => { component: props => , loadData: EditListingPage.loadData, }, + { + path: '/l/:slug/:id/:type/:tab/:returnURLType', + name: 'EditListingStripeOnboardingPage', + auth: true, + component: props => , + loadData: EditListingPage.loadData, + }, // Canonical path should be after the `/l/new` path since they // conflict and `new` is not a valid listing UUID. @@ -236,11 +243,19 @@ const routeConfiguration = () => { }, { path: '/account/payments', - name: 'PayoutPreferencesPage', + name: 'StripePayoutPage', + auth: true, + authPage: 'LoginPage', + component: props => , + loadData: StripePayoutPage.loadData, + }, + { + path: '/account/payments/:returnURLType', + name: 'StripePayoutOnboardingPage', auth: true, authPage: 'LoginPage', - component: props => , - loadData: PayoutPreferencesPage.loadData, + component: props => , + loadData: StripePayoutPage.loadData, }, { path: '/account/payment-methods', diff --git a/src/stripe-config.js b/src/stripe-config.js index 98b8d6033..562d74502 100644 --- a/src/stripe-config.js +++ b/src/stripe-config.js @@ -1,30 +1,24 @@ -/* - * Stripe related configuration. - * - * Note: this setup is for API version '2019-02-19' and later. - * If you have an older API version in use, you need to update your Stripe API. - * You can check your Stripe API version from Stripe Dashboard -> Developers. - */ +/* 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. +*/ -// 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 = [ +/* +Stripe only supports payments in certain countries, see full list +at https://stripe.com/global + +You can find the bank account formats from https://stripe.com/docs/connect/payouts#formats +*/ + +export const stripeCountryDetails = [ { - // Australia + //Australia code: 'AU', currency: 'AUD', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - stateAU: true, - }, accountConfig: { bsb: true, accountNumber: true, @@ -34,46 +28,22 @@ export const stripeSupportedCountries = [ // Austria code: 'AT', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Belgium code: 'BE', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Canada code: 'CA', currency: 'CAD', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - provinceCA: true, - }, accountConfig: { transitNumber: true, institutionNumber: true, @@ -84,168 +54,133 @@ export const stripeSupportedCountries = [ // Denmark code: 'DK', currency: 'DKK', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, + }, + { + // Estionia + code: 'EE', + currency: 'EUR', + accountConfig: { + iban: true, }, }, { // Finland code: 'FI', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // France code: 'FR', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Germany code: 'DE', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, + }, + { + // Greece + code: 'GR', + currency: 'EUR', + accountConfig: { + iban: true, }, }, { // Hong Kong code: 'HK', currency: 'HKD', - addressConfig: { - addressLine: true, - city: true, - }, accountConfig: { clearingCode: true, branchCode: true, accountNumber: true, }, - companyConfig: { - personalAddress: true, - personalIdNumberRequired: true, - }, - individualConfig: { - personalIdNumberRequired: true, - }, }, { // Ireland code: 'IE', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Italy code: 'IT', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, + accountConfig: { + iban: true, }, + }, + { + // Japan + code: 'JP', + currency: 'JPY', + accountConfig: { + bankName: true, + branchName: true, + bankCode: true, + branchCode: true, + accountNumber: true, + accountOwnerName: true, + }, + }, + { + // Latvia + code: 'LV', + currency: 'EUR', accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, + }, + { + // Lithuania + code: 'LT', + currency: 'EUR', + accountConfig: { + iban: true, }, }, { // Luxembourg code: 'LU', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, + }, + { + // Mexico + code: 'MX', + currency: 'MXN', + accountConfig: { + clabe: true, }, }, { // Netherlands code: 'NL', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // New Zealand code: 'NZ', currency: 'NZD', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { accountNumber: true, }, @@ -254,157 +189,504 @@ export const stripeSupportedCountries = [ // Norway code: 'NO', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, + }, + { + // Poland + code: 'PL', + currency: 'EUR', + accountConfig: { + iban: true, }, }, { // Portugal code: 'PT', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Singapore code: 'SG', currency: 'SGD', - addressConfig: { - addressLine: true, - postalCode: true, - }, accountConfig: { bankCode: true, branchCode: true, accountNumber: true, }, - companyConfig: { - personalAddress: true, - owners: true, - personalIdNumberRequired: true, + }, + { + // Slovakia + code: 'SK', + currency: 'EUR', + accountConfig: { + iban: true, }, - individualConfig: { - personalIdNumberRequired: true, + }, + { + // Slovenia + code: 'SI', + currency: 'EUR', + accountConfig: { + iban: true, }, }, { // Spain code: 'ES', currency: 'EUR', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Sweden code: 'SE', currency: 'SEK', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // Switzerland code: 'CH', currency: 'CHF', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { iban: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // United Kingdom code: 'GB', currency: 'GBP', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - }, accountConfig: { sortCode: true, accountNumber: true, }, - companyConfig: { - personalAddress: true, - owners: true, - }, }, { // United States code: 'US', currency: 'USD', - addressConfig: { - addressLine: true, - city: true, - postalCode: true, - stateUS: true, - }, accountConfig: { routingNumber: true, accountNumber: true, }, - companyConfig: { - businessURL: true, - companyPhone: true, - mccForUS: true, - owners: true, - personalAddress: true, - personalEmail: true, - personalPhone: true, - ssnLast4Required: true, - }, - individualConfig: { - businessURL: true, - mccForUS: true, - ssnLast4Required: true, - personalEmail: true, - personalPhone: true, - }, }, ]; + +/* +NOTE: This configuration will not be updated! +We might remove this code in the later releases. + +With Connect Onboarding Stripe will handle collecting most of the information about user. For new setup we only need the list of countries and accountConfig. +If you want to handle the whole onboarding flow on your on application, you can use the old PayoutDetailsForm as a starting point. That form uses this configuration option. +You should make sure that the list of countries is up-to-date and that the config contains all the required infomation you need to collect. + +Remember to change the import from config.js if you want to use this config! + +This setup is for API version '2019-02-19' and later. +If you have an older API version in use, you need to update your Stripe API. +You can check your Stripe API version from Stripe Dashboard -> Developers. +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, +// stateAU: true, +// }, +// accountConfig: { +// bsb: true, +// accountNumber: true, +// }, +// }, +// { +// // Austria +// code: 'AT', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Belgium +// code: 'BE', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Canada +// code: 'CA', +// currency: 'CAD', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// provinceCA: true, +// }, +// accountConfig: { +// transitNumber: true, +// institutionNumber: true, +// accountNumber: true, +// }, +// }, +// { +// // Denmark +// code: 'DK', +// currency: 'DKK', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Finland +// code: 'FI', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // France +// code: 'FR', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Germany +// code: 'DE', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Hong Kong +// code: 'HK', +// currency: 'HKD', +// addressConfig: { +// addressLine: true, +// city: true, +// }, +// accountConfig: { +// clearingCode: true, +// branchCode: true, +// accountNumber: true, +// }, +// companyConfig: { +// personalAddress: true, +// personalIdNumberRequired: true, +// }, +// individualConfig: { +// personalIdNumberRequired: true, +// }, +// }, +// { +// // Ireland +// code: 'IE', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Italy +// code: 'IT', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Luxembourg +// code: 'LU', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Netherlands +// code: 'NL', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: 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, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Portugal +// code: 'PT', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Singapore +// code: 'SG', +// currency: 'SGD', +// addressConfig: { +// addressLine: true, +// postalCode: true, +// }, +// accountConfig: { +// bankCode: true, +// branchCode: true, +// accountNumber: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// personalIdNumberRequired: true, +// }, +// individualConfig: { +// personalIdNumberRequired: true, +// }, +// }, +// { +// // Spain +// code: 'ES', +// currency: 'EUR', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Sweden +// code: 'SE', +// currency: 'SEK', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // Switzerland +// code: 'CH', +// currency: 'CHF', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// iban: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // United Kingdom +// code: 'GB', +// currency: 'GBP', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// }, +// accountConfig: { +// sortCode: true, +// accountNumber: true, +// }, +// companyConfig: { +// personalAddress: true, +// owners: true, +// }, +// }, +// { +// // United States +// code: 'US', +// currency: 'USD', +// addressConfig: { +// addressLine: true, +// city: true, +// postalCode: true, +// stateUS: true, +// }, +// accountConfig: { +// routingNumber: true, +// accountNumber: true, +// }, +// companyConfig: { +// businessURL: true, +// companyPhone: true, +// mccForUS: true, +// owners: true, +// personalAddress: true, +// personalEmail: true, +// personalPhone: true, +// ssnLast4Required: true, +// }, +// individualConfig: { +// businessURL: true, +// mccForUS: true, +// ssnLast4Required: true, +// personalEmail: true, +// personalPhone: true, +// }, +// }, +// ]; diff --git a/src/translations/en.json b/src/translations/en.json index 68d671125..69f47e29b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -233,7 +233,7 @@ "EditListingPhotosForm.showListingFailed": "Fetching teacher profile data failed", "EditListingPhotosForm.updateFailed": "Failed to update. Please try again.", "EditListingPhotosPanel.createListingTitle": "Add a few photos", - "EditListingPhotosPanel.payoutModalInfo": "Since you now have finished your teacher profile, we need to know bit more about you in order to send you money. We only ask these once.", + "EditListingPhotosPanel.payoutModalInfo": "Almost done! In order to send you money, we need to know a bit more about you. Next, we will guide you through verifying your account. The details can also be edited from Account Setting page on Payout details tab. ", "EditListingPhotosPanel.payoutModalTitleOneMoreThing": "One more thing:", "EditListingPhotosPanel.payoutModalTitlePayoutPreferences": "Payout preferences", "EditListingPhotosPanel.listingTitle": "your teacher profile", @@ -602,20 +602,28 @@ "PayoutDetailsForm.countryNames.CH": "Switzerland", "PayoutDetailsForm.countryNames.DE": "Germany", "PayoutDetailsForm.countryNames.DK": "Denmark", + "PayoutDetailsForm.countryNames.EE": "Estonia", "PayoutDetailsForm.countryNames.ES": "Spain", "PayoutDetailsForm.countryNames.FI": "Finland", "PayoutDetailsForm.countryNames.FR": "France", + "PayoutDetailsForm.countryNames.GR": "Greece", "PayoutDetailsForm.countryNames.GB": "United Kingdom", "PayoutDetailsForm.countryNames.HK": "Hong Kong", "PayoutDetailsForm.countryNames.IE": "Ireland", "PayoutDetailsForm.countryNames.IT": "Italy", + "PayoutDetailsForm.countryNames.JP": "Japan", "PayoutDetailsForm.countryNames.LU": "Luxembourg", + "PayoutDetailsForm.countryNames.LT": "Lithuania", + "PayoutDetailsForm.countryNames.LV": "Latvia", + "PayoutDetailsForm.countryNames.MX": "Mexico", "PayoutDetailsForm.countryNames.NL": "Netherlands", "PayoutDetailsForm.countryNames.NO": "Norway", "PayoutDetailsForm.countryNames.NZ": "New Zealand", "PayoutDetailsForm.countryNames.PT": "Portugal", "PayoutDetailsForm.countryNames.SE": "Sweden", "PayoutDetailsForm.countryNames.SG": "Singapore", + "PayoutDetailsForm.countryNames.SI": "Slovenia", + "PayoutDetailsForm.countryNames.SK": "Slovakia", "PayoutDetailsForm.countryNames.US": "United States", "PayoutDetailsForm.countryPlaceholder": "Select your country…", "PayoutDetailsForm.countryRequired": "This field is required", @@ -626,7 +634,7 @@ "PayoutDetailsForm.firstNamePlaceholder": "John", "PayoutDetailsForm.firstNameRequired": "This field is required", "PayoutDetailsForm.individualAccount": "I'm an individual", - "PayoutDetailsForm.information": "Since you have now completed you teacher profile, we need to know bit more about you in order to send you money. We only ask these once.", + "PayoutDetailsForm.information": "Almost done! In order to send you money, we need to know a bit more about you. Next, we will guide you through verifying your account. The details can also be edited from Account Setting page on Payout details tab.", "PayoutDetailsForm.lastNameLabel": "Last name", "PayoutDetailsForm.lastNamePlaceholder": "Doe", "PayoutDetailsForm.lastNameRequired": "This field is required", @@ -667,13 +675,6 @@ "PayoutDetailsForm.stripeToSText": "By saving details, you agree to the {stripeConnectedAccountTermsLink}", "PayoutDetailsForm.submitButtonText": "Save details & publish listing", "PayoutDetailsForm.title": "One more thing: payout preferences", - "PayoutPreferencesPage.heading": "Payout details", - "PayoutPreferencesPage.loadingData": "Loading data…", - "PayoutPreferencesPage.payoutDetailsSaved": "Payment information successfully saved! If you want to change your payment details, please contact the marketplace admins.", - "PayoutPreferencesPage.stripeAlreadyConnected": "You’ve already entered your payment details. If you want to change your payment details, please contact the marketplace admins.", - "PayoutPreferencesPage.stripeNotConnected": "Payment information not saved. Please fill in the form to accept payments from your listings.", - "PayoutPreferencesPage.submitButtonText": "Save details", - "PayoutPreferencesPage.title": "Payout details", "PriceFilter.clear": "Clear", "PriceFilter.label": "Price per hour", "PriceFilter.labelSelectedPlain": "Price per hour: {minPrice} - {maxPrice}", @@ -812,15 +813,31 @@ "StripeBankAccountTokenInputField.accountNumber.label": "Bank account number", "StripeBankAccountTokenInputField.accountNumber.placeholder": "Type in bank account number…", "StripeBankAccountTokenInputField.accountNumber.required": "Bank account number is required", + "StripeBankAccountTokenInputField.accountOwnerName.inline": "account owner name", + "StripeBankAccountTokenInputField.accountOwnerName.label": "Bank account owner name", + "StripeBankAccountTokenInputField.accountOwnerName.placeholder": "John Doe", + "StripeBankAccountTokenInputField.accountOwnerName.required": "Bank account owner name is required", "StripeBankAccountTokenInputField.andBeforeLastItemInAList": " and", "StripeBankAccountTokenInputField.bankCode.inline": "bank code", "StripeBankAccountTokenInputField.bankCode.label": "Bank code", "StripeBankAccountTokenInputField.bankCode.placeholder": "Type in bank code…", "StripeBankAccountTokenInputField.bankCode.required": "Bank code is required", + "StripeBankAccountTokenInputField.bankName.inline": "bank name", + "StripeBankAccountTokenInputField.bankName.label": "Bank name", + "StripeBankAccountTokenInputField.bankName.placeholder": "Type in bank name…", + "StripeBankAccountTokenInputField.bankName.required": "Bank name is required", "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.branchName.inline": "branch name", + "StripeBankAccountTokenInputField.branchName.label": "Branch name", + "StripeBankAccountTokenInputField.branchName.placeholder": "Type in branch name…", + "StripeBankAccountTokenInputField.branchName.required": "Branch name is required", + "StripeBankAccountTokenInputField.clabe.inline": "CLABE", + "StripeBankAccountTokenInputField.clabe.label": "CLABE", + "StripeBankAccountTokenInputField.clabe.placeholder": "Type in CLABE…", + "StripeBankAccountTokenInputField.clabe.required": "CLABE is required", "StripeBankAccountTokenInputField.bsb.inline": "BSB", "StripeBankAccountTokenInputField.bsb.label": "BSB", "StripeBankAccountTokenInputField.bsb.placeholder": "Type in BSB…", @@ -908,6 +925,56 @@ "StripePaymentForm.stripe.validation_error.processing_error": "An error occurred while processing the card.", "StripePaymentForm.submitConfirmPaymentInfo": "Confirm request", "StripePaymentForm.submitPaymentInfo": "Send request", + "StripeConnectAccountForm.bankAccountLabel": "Bank account", + "StripeConnectAccountForm.countryLabel": "Country", + "StripeConnectAccountForm.countryNames.AT": "Austria", + "StripeConnectAccountForm.countryNames.AU": "Australia", + "StripeConnectAccountForm.countryNames.BE": "Belgium", + "StripeConnectAccountForm.countryNames.CA": "Canada", + "StripeConnectAccountForm.countryNames.CH": "Switzerland", + "StripeConnectAccountForm.countryNames.DE": "Germany", + "StripeConnectAccountForm.countryNames.GR": "Greece", + "StripeConnectAccountForm.countryNames.DK": "Denmark", + "StripeConnectAccountForm.countryNames.EE": "Estonia", + "StripeConnectAccountForm.countryNames.ES": "Spain", + "StripeConnectAccountForm.countryNames.FI": "Finland", + "StripeConnectAccountForm.countryNames.FR": "France", + "StripeConnectAccountForm.countryNames.GB": "United Kingdom", + "StripeConnectAccountForm.countryNames.HK": "Hong Kong", + "StripeConnectAccountForm.countryNames.IE": "Ireland", + "StripeConnectAccountForm.countryNames.IT": "Italy", + "StripeConnectAccountForm.countryNames.JP": "Japan", + "StripeConnectAccountForm.countryNames.LT": "Lithuania", + "StripeConnectAccountForm.countryNames.LV": "Latvia", + "StripeConnectAccountForm.countryNames.LU": "Luxembourg", + "StripeConnectAccountForm.countryNames.MX": "Mexico", + "StripeConnectAccountForm.countryNames.NL": "Netherlands", + "StripeConnectAccountForm.countryNames.NO": "Norway", + "StripeConnectAccountForm.countryNames.NZ": "New Zealand", + "StripeConnectAccountForm.countryNames.PL": "Poland", + "StripeConnectAccountForm.countryNames.PT": "Portugal", + "StripeConnectAccountForm.countryNames.SE": "Sweden", + "StripeConnectAccountForm.countryNames.SI": "Slovenia", + "StripeConnectAccountForm.countryNames.SK": "Slovakia", + "StripeConnectAccountForm.countryNames.SG": "Singapore", + "StripeConnectAccountForm.countryNames.US": "United States", + "StripeConnectAccountForm.countryPlaceholder": "Select your country…", + "StripeConnectAccountForm.countryRequired": "This field is required", + "StripeConnectAccountForm.createStripeAccountFailed": "Whoops, something went wrong. Please try again.", + "StripeConnectAccountForm.createStripeAccountFailedWithStripeError": "Whoops, something went wrong. Stripe returned an error message: \"{stripeMessage}\"", + "StripeConnectAccountForm.loadingStripeAccountData": "Fetching payout details…", + "StripePayoutPage.heading": "Payout details", + "StripePayoutPage.loadingData": "Loading data…", + "StripePayoutPage.stripeNotConnected": "Payment information not saved. Please fill in the form to accept payments from your listings.", + "StripePayoutPage.submitButtonText": "Save details", + "StripePayoutPage.title": "Payout details", + "StripeConnectAccountStatusBox.editAccountButton": "Edit Stripe account", + "StripeConnectAccountStatusBox.getVerifiedButton": "Get verified", + "StripeConnectAccountStatusBox.verificationFailedText": "In order for you to receive payments, you need to add your banking details and verify your account.", + "StripeConnectAccountStatusBox.verificationFailedTitle": "Something went wrong - please try again", + "StripeConnectAccountStatusBox.verificationNeededText": "In order for you to receive payments you need to add few more details to your Stripe account to verify your account.", + "StripeConnectAccountStatusBox.verificationNeededTitle": "Stripe needs more information", + "StripeConnectAccountStatusBox.verificationSuccessTitle": "Your Stripe account is up to date!", "TermsOfServicePage.heading": "Terms of Service", "TermsOfServicePage.privacyTabTitle": "Privacy Policy", "TermsOfServicePage.schemaTitle": "Terms of Service | {siteTitle}", diff --git a/src/util/types.js b/src/util/types.js index 8d692268f..fd1e85e6d 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -308,6 +308,7 @@ propTypes.stripeAccount = shape({ type: propTypes.value('stripeAccount').isRequired, attributes: shape({ stripeAccountId: string.isRequired, + stripeAccountData: object, }), }); diff --git a/yarn.lock b/yarn.lock index 6dcc40e1a..00a0e26e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10144,10 +10144,10 @@ shallowequal@1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharetribe-flex-sdk@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.5.0.tgz#a1bb9c603f8a52a1f53053cb610fdf15ff223288" - integrity sha512-l/3vX9GEz9Uz9z+9+WDHH6/O1GMSh8sa1kQpEHBjypx3kDjFT2a6SSxEzk3YQ/qIaUEHvGj9G+Xy3AszF6pCrA== +sharetribe-flex-sdk@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.8.0.tgz#d7f24bf416f15c5f20d7b6830e0d6e5832082b43" + integrity sha512-600NLlvQamxQP8hKhyWlytl5cycoYxAMce1xXAFxTMAoNiOyLEh9K/6I4nrf9AowpDL+2IoPvWGZd9ftl4xeGQ== dependencies: axios "^0.19.0" js-cookie "^2.1.3"