From a2c21e035b3ec70955e45e6482dcb305515092c3 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Wed, 12 Feb 2020 17:14:34 +0200 Subject: [PATCH 01/17] Add option to render Modal inside Portal --- src/components/Modal/Modal.css | 6 +++ src/components/Modal/Modal.example.js | 75 ++++++++++++++------------- src/components/Modal/Modal.js | 73 +++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 37 deletions(-) diff --git a/src/components/Modal/Modal.css b/src/components/Modal/Modal.css index 97389bb91..ee843b175 100644 --- a/src/components/Modal/Modal.css +++ b/src/components/Modal/Modal.css @@ -76,3 +76,9 @@ color: var(--matterColorLight); } } + +.focusedDiv { + &:focus { + outline: none; + } +} diff --git a/src/components/Modal/Modal.example.js b/src/components/Modal/Modal.example.js index f09f5e2b4..b9d932ea3 100644 --- a/src/components/Modal/Modal.example.js +++ b/src/components/Modal/Modal.example.js @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { Button } from '../../components'; import Modal from './Modal'; @@ -8,44 +8,49 @@ const onManageDisableScrolling = (componentId, scrollingDisabled = true) => { console.log('Toggling Modal - scrollingDisabled currently:', componentId, scrollingDisabled); }; -class ModalWrapper extends Component { - constructor(props) { - super(props); - this.state = { isOpen: false }; - this.handleOpen = this.handleOpen.bind(this); - } - - handleOpen() { - this.setState({ isOpen: true }); - } - - render() { - return ( -
-
Wrapper text before ModalInMobile
- { - this.setState({ isOpen: false }); - console.log('Closing modal'); - }} - onManageDisableScrolling={onManageDisableScrolling} - > -
Some content inside Modal component
-
-
- -
+const ModalWrapper = props => { + const [isOpen, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + return ( +
+
Wrapper text before Modal
+ + { + setOpen(false); + console.log('Closing modal'); + }} + onManageDisableScrolling={onManageDisableScrolling} + > +
Some content inside Modal component
+
+ +
+
- ); - } -} +
+ ); +}; + +export const OldModal = { + component: ModalWrapper, + useDefaultWrapperStyles: false, + props: { + id: 'OldModal', + }, +}; -export const Empty = { +export const ModalWithPortal = { component: ModalWrapper, useDefaultWrapperStyles: false, props: { - id: 'ExampleModal', + id: 'ModalWithPortal', + usePortal: true, }, }; diff --git a/src/components/Modal/Modal.js b/src/components/Modal/Modal.js index b48759926..631512cde 100644 --- a/src/components/Modal/Modal.js +++ b/src/components/Modal/Modal.js @@ -9,6 +9,7 @@ * */ import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage, intlShape, injectIntl } from '../../util/reactIntl'; @@ -18,23 +19,65 @@ import css from './Modal.css'; const KEY_CODE_ESCAPE = 27; +class Portal extends React.Component { + constructor(props) { + super(props); + this.el = document.createElement('div'); + } + + componentDidMount() { + // The portal element is inserted in the DOM tree after + // the Modal's children are mounted, meaning that children + // will be mounted on a detached DOM node. If a child + // component requires to be attached to the DOM tree + // immediately when mounted, for example to measure a + // DOM node, or uses 'autoFocus' in a descendant, add + // state to Modal and only render the children when Modal + // is inserted in the DOM tree. + this.props.portalRoot.appendChild(this.el); + } + + componentWillUnmount() { + this.props.portalRoot.removeChild(this.el); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.el); + } +} + export class ModalComponent extends Component { constructor(props) { super(props); this.handleBodyKeyUp = this.handleBodyKeyUp.bind(this); this.handleClose = this.handleClose.bind(this); + + this.refDiv = React.createRef(); + + this.state = { + portalRoot: null, + }; } componentDidMount() { const { id, isOpen, onManageDisableScrolling } = this.props; onManageDisableScrolling(id, isOpen); document.body.addEventListener('keyup', this.handleBodyKeyUp); + this.setState({ + portalRoot: document.getElementById('portal-root'), + }); } componentDidUpdate(prevProps) { const { id, isOpen, onManageDisableScrolling } = prevProps; if (this.props.isOpen !== isOpen) { onManageDisableScrolling(id, this.props.isOpen); + + // Because we are using portal, + // we need to set the focus inside Modal manually + if (this.props.usePorta && this.props.isOpen) { + this.refDiv.current.focus(); + } } } @@ -69,6 +112,7 @@ export class ModalComponent extends Component { intl, isClosedClassName, isOpen, + usePortal, } = this.props; const closeModalMessage = intl.formatMessage({ id: 'Modal.closeModal' }); @@ -96,7 +140,15 @@ export class ModalComponent extends Component { const classes = classNames(modalClass, className); const scrollLayerClasses = scrollLayerClassName || css.scrollLayer; const containerClasses = containerClassName || css.container; - return ( + const portalRoot = this.state.portalRoot; + + // If you want to use Portal https://reactjs.org/docs/portals.html + // you need to use 'userPortal' flag. + // ModalInMobile component needs to use the old Modal without the portal + // because it's relying that the content is rendered inside + // the DOM hierarchy of the parent component unlike Modal inside Portal. + + return !usePortal ? (
@@ -105,7 +157,22 @@ export class ModalComponent extends Component {
- ); + ) : portalRoot ? ( + +
+
+
+ {closeBtn} +
{children}
+
+
+
+
+ ) : null; } } @@ -120,6 +187,7 @@ ModalComponent.defaultProps = { isClosedClassName: css.isClosed, isOpen: false, onClose: null, + usePortal: false, }; const { bool, func, node, string } = PropTypes; @@ -137,6 +205,7 @@ ModalComponent.propTypes = { isClosedClassName: string, isOpen: bool, onClose: func.isRequired, + usePortal: bool, // eslint-disable-next-line react/no-unused-prop-types onManageDisableScrolling: func.isRequired, From a650d1ea2424c42c8a1950c528c78f06c8fcdc6c Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Wed, 12 Feb 2020 17:15:01 +0200 Subject: [PATCH 02/17] Update ModalInMobile example --- .../ModalInMobile/ModalInMobile.example.js | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/components/ModalInMobile/ModalInMobile.example.js b/src/components/ModalInMobile/ModalInMobile.example.js index dc39025bc..2015ac051 100644 --- a/src/components/ModalInMobile/ModalInMobile.example.js +++ b/src/components/ModalInMobile/ModalInMobile.example.js @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { Button } from '../../components'; import ModalInMobile from './ModalInMobile'; import css from './ModalInMobileExample.css'; @@ -9,41 +9,35 @@ const onManageDisableScrolling = (componentId, scrollingDisabled = true) => { console.log('Toggling ModalInMobile - currently:', componentId, scrollingDisabled); }; -class ModalInMobileWrapper extends Component { - constructor(props) { - super(props); - this.state = { isOpen: false }; - this.handleOpen = this.handleOpen.bind(this); - } +const ModalInMobileWrapper = props => { + const [isOpen, setOpen] = useState(false); - handleOpen() { - this.setState({ isOpen: true }); - } + const handleOpen = () => { + setOpen(true); + }; - render() { - return ( -
-
Wrapper text before ModalInMobile
- { - this.setState({ isOpen: false }); - console.log('Closing modal'); - }} - isModalOpenOnMobile={this.state.isOpen} - onManageDisableScrolling={onManageDisableScrolling} - > -
Some content inside ModalInMobile component
-
-
- -
+ return ( +
+
Wrapper text before ModalInMobile
+ { + setOpen(false); + console.log('Closing modal'); + }} + isModalOpenOnMobile={isOpen} + onManageDisableScrolling={onManageDisableScrolling} + > +
Some content inside ModalInMobile component
+
+
+
- ); - } -} +
+ ); +}; export const Empty = { component: ModalInMobileWrapper, From ea42b7e0fcadbfff7ac621a5aeaa89736113bf49 Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Wed, 12 Feb 2020 17:15:34 +0200 Subject: [PATCH 03/17] Remove Portal from SavedCardDetails and add usePortal flag instead --- .../SavedCardDetails/SavedCardDetails.js | 90 ++++++------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/src/components/SavedCardDetails/SavedCardDetails.js b/src/components/SavedCardDetails/SavedCardDetails.js index 7497d3c1f..858d357f6 100644 --- a/src/components/SavedCardDetails/SavedCardDetails.js +++ b/src/components/SavedCardDetails/SavedCardDetails.js @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import ReactDOM from 'react-dom'; import { bool, func, number, shape, string } from 'prop-types'; import classNames from 'classnames'; import { injectIntl, intlShape } from '../../util/reactIntl'; @@ -21,45 +20,10 @@ import css from './SavedCardDetails.css'; const DEFAULT_CARD = 'defaultCard'; const REPLACE_CARD = 'replaceCard'; -// TODO: change all the modals to use portals at some point. -// Portal is used here to circumvent the problems that rise -// from different levels of z-indexes in DOM tree. In this case -// either TopBar or Menu element were overlapping the modal due -// to stacking context. All the modals should be made with portals -// but portals didn't exist when we originally created modals. - -class Portal extends React.Component { - constructor(props) { - super(props); - this.el = document.createElement('div'); - } - - componentDidMount() { - // The portal element is inserted in the DOM tree after - // the Modal's children are mounted, meaning that children - // will be mounted on a detached DOM node. If a child - // component requires to be attached to the DOM tree - // immediately when mounted, for example to measure a - // DOM node, or uses 'autoFocus' in a descendant, add - // state to Modal and only render the children when Modal - // is inserted in the DOM tree. - this.props.portalRoot.appendChild(this.el); - } - - componentWillUnmount() { - this.props.portalRoot.removeChild(this.el); - } - - render() { - return ReactDOM.createPortal(this.props.children, this.el); - } -} - const SavedCardDetails = props => { const [isModalOpen, setIsModalOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const [active, setActive] = useState(DEFAULT_CARD); - const [portalRoot, setPortalRoot] = useState(null); const { rootClassName, @@ -170,11 +134,8 @@ const SavedCardDetails = props => { const showExpired = isCardExpired && active === DEFAULT_CARD; - const setPortalRootAfterInitialRender = () => { - setPortalRoot(document.getElementById('portal-root')); - }; return ( -
+
@@ -226,31 +187,34 @@ const SavedCardDetails = props => { ) : null} - {portalRoot && onManageDisableScrolling ? ( - - { - setIsModalOpen(false); - }} - contentClassName={css.modalContent} - onManageDisableScrolling={onManageDisableScrolling} - > -
-
{removeCardModalTitle}
-

{removeCardModalContent}

-
-
setIsModalOpen(false)} className={css.cancelCardDelete}> - {cancel} -
- + {onManageDisableScrolling ? ( + { + setIsModalOpen(false); + }} + usePortal + contentClassName={css.modalContent} + onManageDisableScrolling={onManageDisableScrolling} + > +
+
{removeCardModalTitle}
+

{removeCardModalContent}

+
+
setIsModalOpen(false)} + className={css.cancelCardDelete} + tabIndex="0" + > + {cancel}
+
- - +
+
) : null}
); From f5d5283a2e4c1485744e52699961038abfc5a08f Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Wed, 12 Feb 2020 17:23:49 +0200 Subject: [PATCH 04/17] Add usePortal flag to Modals --- src/components/EditListingWizard/EditListingWizard.js | 1 + .../ModalMissingInformation/ModalMissingInformation.js | 1 + src/components/ReviewModal/ReviewModal.js | 1 + src/components/Topbar/Topbar.js | 2 ++ src/containers/AuthenticationPage/AuthenticationPage.js | 1 + .../__snapshots__/AuthenticationPage.test.js.snap | 1 + src/containers/ListingPage/SectionHostMaybe.js | 1 + src/containers/ListingPage/SectionImages.js | 1 + 8 files changed, 9 insertions(+) diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index 634c7427d..c10d81f45 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -380,6 +380,7 @@ class EditListingWizard extends Component { isOpen={this.state.showPayoutDetails} onClose={this.handlePayoutModalClose} onManageDisableScrolling={onManageDisableScrolling} + usePortal >

diff --git a/src/components/ModalMissingInformation/ModalMissingInformation.js b/src/components/ModalMissingInformation/ModalMissingInformation.js index 9a5645fc8..a5910fd59 100644 --- a/src/components/ModalMissingInformation/ModalMissingInformation.js +++ b/src/components/ModalMissingInformation/ModalMissingInformation.js @@ -139,6 +139,7 @@ class ModalMissingInformation extends Component { hasSeenMissingInformationReminder: true, }); }} + usePortal onManageDisableScrolling={onManageDisableScrolling} closeButtonMessage={closeButtonMessage} > diff --git a/src/components/ReviewModal/ReviewModal.js b/src/components/ReviewModal/ReviewModal.js index 213adcf7f..dba365ece 100644 --- a/src/components/ReviewModal/ReviewModal.js +++ b/src/components/ReviewModal/ReviewModal.js @@ -36,6 +36,7 @@ const ReviewModal = props => { isOpen={isOpen} onClose={onCloseModal} onManageDisableScrolling={onManageDisableScrolling} + usePortal closeButtonMessage={closeButtonMessage} > diff --git a/src/components/Topbar/Topbar.js b/src/components/Topbar/Topbar.js index 786b69ea1..553f05933 100644 --- a/src/components/Topbar/Topbar.js +++ b/src/components/Topbar/Topbar.js @@ -240,6 +240,7 @@ class TopbarComponent extends Component { id="TopbarMobileMenu" isOpen={isMobileMenuOpen} onClose={this.handleMobileMenuClose} + usePortal onManageDisableScrolling={onManageDisableScrolling} > {authInProgress ? null : mobileMenu} @@ -249,6 +250,7 @@ class TopbarComponent extends Component { containerClassName={css.modalContainer} isOpen={isMobileSearchOpen} onClose={this.handleMobileSearchClose} + usePortal onManageDisableScrolling={onManageDisableScrolling} >
diff --git a/src/containers/AuthenticationPage/AuthenticationPage.js b/src/containers/AuthenticationPage/AuthenticationPage.js index 44fb476fa..477937ad1 100644 --- a/src/containers/AuthenticationPage/AuthenticationPage.js +++ b/src/containers/AuthenticationPage/AuthenticationPage.js @@ -241,6 +241,7 @@ export class AuthenticationPageComponent extends Component { id="AuthenticationPage.tos" isOpen={this.state.tosModalOpen} onClose={() => this.setState({ tosModalOpen: false })} + usePortal onManageDisableScrolling={onManageDisableScrolling} >
diff --git a/src/containers/AuthenticationPage/__snapshots__/AuthenticationPage.test.js.snap b/src/containers/AuthenticationPage/__snapshots__/AuthenticationPage.test.js.snap index 2e5641451..7de9b0e28 100644 --- a/src/containers/AuthenticationPage/__snapshots__/AuthenticationPage.test.js.snap +++ b/src/containers/AuthenticationPage/__snapshots__/AuthenticationPage.test.js.snap @@ -86,6 +86,7 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = ` isOpen={false} onClose={[Function]} onManageDisableScrolling={[Function]} + usePortal={true} >

diff --git a/src/containers/ListingPage/SectionHostMaybe.js b/src/containers/ListingPage/SectionHostMaybe.js index 2c958134a..94e5ae874 100644 --- a/src/containers/ListingPage/SectionHostMaybe.js +++ b/src/containers/ListingPage/SectionHostMaybe.js @@ -35,6 +35,7 @@ const SectionHostMaybe = props => { contentClassName={css.enquiryModalContent} isOpen={isEnquiryModalOpen} onClose={onCloseEnquiryModal} + usePortal onManageDisableScrolling={onManageDisableScrolling} > { lightCloseButton isOpen={imageCarouselOpen} onClose={onImageCarouselClose} + usePortal onManageDisableScrolling={onManageDisableScrolling} > From e62e09cfe1dff16cea12378afb2bd86b4f2b6e2f Mon Sep 17 00:00:00 2001 From: Jenni Nurmi Date: Mon, 24 Feb 2020 09:25:54 +0200 Subject: [PATCH 05/17] Update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17320aff..86d20623b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +- [change] Update `Modal` component to have option to use `Portal` with `usePortal` flag. Keep also + possibility to use modals without Portal because of `ModalInMobile` component. + [#1258](https://github.com/sharetribe/ftw-daily/pull/1258) + ## [v4.2.0] 2020-02-18 - [add] Show a banner when a user is logged in with limited access. @@ -24,6 +28,8 @@ way to update this template, but currently, we follow a pattern: - [change] Add `handlebars` 4.5.3 and `serialize-javascript` 2.1.1 to resolutions in `package.json`. [#1251](https://github.com/sharetribe/ftw-daily/pull/1251) + [v4.2.0]: https://github.com/sharetribe/flex-template-web/compare/v4.1.0...v4.2.0 + ## [v4.1.0] 2020-02-03 - [fix] Remove unused 'invalid' prop that breaks some versions of Final Form @@ -32,7 +38,7 @@ way to update this template, but currently, we follow a pattern: - [add] Add missing countries (e.g. MX and JP) to `StripeBankAccountTokenInput` validations. [#1250](https://github.com/sharetribe/ftw-daily/pull/1250) - [v4.0.1]: https://github.com/sharetribe/flex-template-web/compare/v4.0.0...v4.0.1 + [v4.0.1]: https://github.com/sharetribe/flex-template-web/compare/v4.0.0...v4.1.0 ## [v4.0.0] 2019-12-19 From b4c29c502b436bc510bb73622f6e61681a1cbb04 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Mon, 9 Mar 2020 15:34:10 +0200 Subject: [PATCH 06/17] Add default MCC to stripe-config --- src/config.js | 3 ++- src/stripe-config.js | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index 905d46561..78b58df24 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, stripeCountryDetails } from './stripe-config'; +import { defaultMCC, stripePublishableKey, stripeCountryDetails } from './stripe-config'; import { currencyConfiguration } from './currency-config'; const env = process.env.REACT_APP_ENV; @@ -200,6 +200,7 @@ const config = { currencyConfig, listingMinimumPriceSubUnits, stripe: { + defaultMCC: defaultMCC, publishableKey: stripePublishableKey, supportedCountries: stripeCountryDetails, }, diff --git a/src/stripe-config.js b/src/stripe-config.js index 562d74502..f656058a6 100644 --- a/src/stripe-config.js +++ b/src/stripe-config.js @@ -7,6 +7,16 @@ To make Stripe connection work, you also need to set Stripe's private key in the export const stripePublishableKey = process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY; +/** + * Default merchant category code (MCC) + * MCCs are used to classify businesses by the type of goods or services they provide. + * + * In FTW we use code 5734 Computer Software Stores as a default for all the connected accounts. + * + * See the whole list of MCC codes from https://stripe.com/docs/connect/setting-mcc#list + */ +export const defaultMCC = '5734'; + /* Stripe only supports payments in certain countries, see full list at https://stripe.com/global @@ -281,13 +291,13 @@ export const stripeCountryDetails = [ /* NOTE: This configuration will not be updated! -We might remove this code in the later releases. +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! +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. From 4dfe748fc2a1673ca23bbc24311bcd9abcc4df08 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Mon, 9 Mar 2020 16:00:49 +0200 Subject: [PATCH 07/17] Pass some default values to Stripe when creating Stripe account --- .../EditListingWizard/EditListingWizard.js | 1 + .../StripePayoutPage/StripePayoutPage.js | 1 + .../StripePayoutPage.test.js.snap | 86 +++++++++++++ src/ducks/stripeConnectAccount.duck.js | 43 +++++-- .../StripeConnectAccountForm.css | 32 +++++ .../StripeConnectAccountForm.js | 116 ++++++++++++++++-- src/translations/en.json | 5 + 7 files changed, 264 insertions(+), 20 deletions(-) diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index c10d81f45..07caf3214 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -398,6 +398,7 @@ class EditListingWizard extends Component { disabled={formDisabled} inProgress={payoutDetailsSaveInProgress} ready={payoutDetailsSaved} + currentUser={ensuredCurrentUser} stripeBankAccountLastDigits={getBankAccountLast4Digits(stripeAccountData)} savedCountry={savedCountry} submitButtonText={intl.formatMessage({ diff --git a/src/containers/StripePayoutPage/StripePayoutPage.js b/src/containers/StripePayoutPage/StripePayoutPage.js index e2b66e2c7..f5ee31b8f 100644 --- a/src/containers/StripePayoutPage/StripePayoutPage.js +++ b/src/containers/StripePayoutPage/StripePayoutPage.js @@ -159,6 +159,7 @@ export const StripePayoutPageComponent = props => { disabled={formDisabled} inProgress={payoutDetailsSaveInProgress} ready={payoutDetailsSaved} + currentUser={ensuredCurrentUser} stripeBankAccountLastDigits={getBankAccountLast4Digits(stripeAccountData)} savedCountry={savedCountry} submitButtonText={intl.formatMessage({ diff --git a/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap b/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap index f38e7a38d..a8ed97b74 100644 --- a/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap +++ b/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap @@ -38,6 +38,38 @@ exports[`StripePayoutPage matches snapshot with Stripe connected 1`] = ` />

({ // ================ Thunks ================ // export const createStripeAccount = params => (dispatch, getState, sdk) => { - const country = params.country; - const bankAccountToken = params.bankAccountToken; + 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, accountType, bankAccountToken, businessProfileMCC, businessProfileURL } = params; + + // Capabilities are a collection of settings that can be requested for each provider. + // What Capabilities are required determines what information Stripe requires to be + // collected from the providers. + // You can read more from here: https://stripe.com/docs/connect/capabilities-overview + // In Flex both 'card_payments' and 'transfers' are required. + const requestedCapabilities = ['card_payments', 'transfers']; + + const accountInfo = { + business_type: accountType, + tos_shown_and_accepted: true, + }; dispatch(stripeAccountCreateRequest()); - return sdk.stripeAccount - .create( - { country, bankAccountToken, requestedCapabilities: ['card_payments', 'transfers'] }, - { expand: true } - ) + return stripe + .createToken('account', accountInfo) + .then(response => { + const accountToken = response.token.id; + return sdk.stripeAccount.create( + { + country, + accountToken, + bankAccountToken, + requestedCapabilities, + businessProfileMCC, + businessProfileURL, + }, + { expand: true } + ); + }) .then(response => { const stripeAccount = response.data.data; dispatch(stripeAccountCreateSuccess(stripeAccount)); diff --git a/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.css b/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.css index 48b181112..8a58d1d38 100644 --- a/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.css +++ b/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.css @@ -29,6 +29,19 @@ margin-bottom: 24px; } +.radioButtonRow { + display: flex; + justify-content: left; + flex-wrap: wrap; + width: 100%; + margin-bottom: 24px; + white-space: nowrap; +} + +.radioButtonRow > :first-child { + margin-right: 36px; +} + .selectCountry { margin-bottom: 24px; } @@ -37,6 +50,25 @@ @apply --marketplaceModalErrorStyles; } +.termsText { + @apply --marketplaceModalHelperText; + margin-bottom: 12px; + text-align: center; + + @media (--viewportMedium) { + margin-bottom: 16px; + } +} + +.termsLink { + @apply --marketplaceModalHelperLink; + + &:hover { + text-decoration: underline; + cursor: pointer; + } +} + .bankDetailsStripeField p { @apply --marketplaceH4FontStyles; } diff --git a/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js b/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js index db764ff7a..ce910f4f9 100644 --- a/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js +++ b/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js @@ -6,12 +6,17 @@ import { Form as FinalForm } from 'react-final-form'; import arrayMutators from 'final-form-arrays'; import classNames from 'classnames'; import config from '../../config'; +import routeConfiguration from '../../routeConfiguration'; +import { createResourceLocatorString } from '../../util/routes'; import { isStripeError } from '../../util/errors'; import * as validators from '../../util/validators'; +import { propTypes } from '../../util/types'; import { Button, + ExternalLink, InlineTextButton, FieldSelect, + FieldRadioButton, Form, StripeBankAccountTokenInputField, } from '../../components'; @@ -35,15 +40,76 @@ const countryCurrency = countryCode => { }; const CreateStripeAccountFields = props => { - const { disabled, countryLabel, values, intl } = props; + const { disabled, countryLabel, showAsRequired, form, values, intl, currentUserId } = props; + + /* + We pass some default values to Stripe when creating a new Stripe account in order to reduce couple of steps from Connect Onboarding form. + - businessProfileURL: user's profile URL + - businessProfileMCC: default MCC code from stripe-config.js + - accountToken (https://stripe.com/docs/connect/account-tokens) with following information: + * accountType: individual or business + * tos_shown_and_accepted: true + Only country and bank account token are mandatory values. If you decide to remove the additional default values listed here, remember to update the `createStripeAccount` function in `ducks/stripeConnectAccount.duck.js`. + */ + + const individualAccountLabel = intl.formatMessage({ + id: 'StripeConnectAccountForm.individualAccount', + }); + + const companyAccountLabel = intl.formatMessage({ id: 'StripeConnectAccountForm.companyAccount' }); + + const hasBusinessURL = values && values.businessProfileURL; + // Use user profile page as business_url on this marketplace + // or just fake it if it's dev environment using Stripe test endpoints + // because Stripe will not allow passing a localhost URL + if (!hasBusinessURL && currentUserId) { + const pathToProfilePage = uuid => + createResourceLocatorString('ProfilePage', routeConfiguration(), { id: uuid }, {}); + const hasCanonicalRootUrl = config && config.canonicalRootURL; + const rootUrl = hasCanonicalRootUrl ? config.canonicalRootURL.replace(/\/$/, '') : null; + const defaultBusinessURL = + hasCanonicalRootUrl && !rootUrl.includes('localhost') + ? `${rootUrl}${pathToProfilePage(currentUserId.uuid)}` + : `https://test-marketplace.com${pathToProfilePage(currentUserId.uuid)}`; + form.change('businessProfileURL', defaultBusinessURL); + } + + const hasMCC = values && values.businessProfileMCC; + // Use default merchant category code (MCC) from stripe-config.js + if (!hasMCC && config.stripe.defaultMCC) { + const defaultBusinessProfileMCC = config.stripe.defaultMCC; + form.change('businessProfileMCC', defaultBusinessProfileMCC); + } + const country = values.country; const countryRequired = validators.required( intl.formatMessage({ id: 'StripeConnectAccountForm.countryRequired', }) ); + return (
+

+ +

+
+ + +
+ { stripeAccountFetched, stripeBankAccountLastDigits, submitButtonText, + form, values, stripeConnected, + currentUser, } = fieldRenderProps; const accountDataLoaded = stripeConnected && stripeAccountFetched && savedCountry; @@ -184,6 +252,10 @@ const StripeConnectAccountFormComponent = props => { [css.disabled]: disabled, }); + const showAsRequired = pristine; + + const currentUserId = currentUser ? currentUser.id : null; + // If the user doesn't have Stripe connected account, // show fields for country and bank account. // Otherwise, show only possibility the edit bank account @@ -192,8 +264,11 @@ const StripeConnectAccountFormComponent = props => { @@ -211,20 +286,35 @@ const StripeConnectAccountFormComponent = props => { /> ); + const stripeConnectedAccountTermsLink = ( + + + + ); + // Don't show the submit button while fetching the Stripe account data const submitButtonMaybe = !stripeConnected || accountDataLoaded ? ( - + <> +

+ +

+ + + ) : null; // If the Stripe publishable key is not set up, don't show the form @@ -256,6 +346,7 @@ const StripeConnectAccountFormComponent = props => { StripeConnectAccountFormComponent.defaultProps = { className: null, + currentUser: null, stripeAccountError: null, disabled: false, inProgress: false, @@ -267,6 +358,7 @@ StripeConnectAccountFormComponent.defaultProps = { }; StripeConnectAccountFormComponent.propTypes = { + currentUser: propTypes.currentUser, className: string, stripeAccountError: object, disabled: bool, diff --git a/src/translations/en.json b/src/translations/en.json index 8aaea9f14..e07b83b87 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -877,7 +877,9 @@ "StripePaymentForm.stripe.validation_error.processing_error": "An error occurred while processing the card.", "StripePaymentForm.submitConfirmPaymentInfo": "Confirm request", "StripePaymentForm.submitPaymentInfo": "Send request", + "StripeConnectAccountForm.accountTypeTitle": "Account type", "StripeConnectAccountForm.bankAccountLabel": "Bank account", + "StripeConnectAccountForm.companyAccount": "I represent a company", "StripeConnectAccountForm.countryLabel": "Country", "StripeConnectAccountForm.countryNames.AT": "Austria", "StripeConnectAccountForm.countryNames.AU": "Australia", @@ -914,7 +916,10 @@ "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.individualAccount": "I'm an individual", "StripeConnectAccountForm.loadingStripeAccountData": "Fetching payout details…", + "StripeConnectAccountForm.stripeToSText": "By saving details, you agree to the {stripeConnectedAccountTermsLink}", + "StripeConnectAccountForm.stripeConnectedAccountTermsLink": "Stripe Connected Account Agreement", "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.", From d25b9964ac52ff5f36f0de869e2017502dcbcb79 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Wed, 11 Mar 2020 11:36:51 +0200 Subject: [PATCH 08/17] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d20623b..9ba3dd3a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +- [change] Use some default values to improve Stripe Connect onboarding. When creating a new Stripe + the account we will pass the account type, business URL and MCC to Stripe in order to avoid a + couple of steps in Connect Onboarding. We will also pass `tos_shown_and_accepted` flag. This PR + will bring back the previously used `accountToken` which is now used for passing e.g. the account + type to Stripe. [#1267](https://github.com/sharetribe/ftw-daily/pull/1267) - [change] Update `Modal` component to have option to use `Portal` with `usePortal` flag. Keep also possibility to use modals without Portal because of `ModalInMobile` component. [#1258](https://github.com/sharetribe/ftw-daily/pull/1258) From d6b82fef2ad035ecfd9bce09dbd5419f84b19c2f Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Wed, 11 Mar 2020 15:25:37 +0200 Subject: [PATCH 09/17] Check that listing closed is shown only for closed listings --- src/components/BookingPanel/BookingPanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/BookingPanel/BookingPanel.js b/src/components/BookingPanel/BookingPanel.js index 5f6552862..62d9f0102 100644 --- a/src/components/BookingPanel/BookingPanel.js +++ b/src/components/BookingPanel/BookingPanel.js @@ -145,11 +145,11 @@ const BookingPanel = props => { > - ) : ( + ) : isClosed ? (
- )} + ) : null}
); From 700df4b63155b1e8f7d9e6b92282ca115cb8dbe1 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Wed, 11 Mar 2020 15:25:50 +0200 Subject: [PATCH 10/17] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba3dd3a0..5cb6663db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +- [fix] Don't flash listing closed text on mobile view of `BookingPanel` when the listing data is + not loaded yet. Instead, check that text is shown only for closed listings. + [#1268](https://github.com/sharetribe/ftw-daily/pull/1268) - [change] Use some default values to improve Stripe Connect onboarding. When creating a new Stripe the account we will pass the account type, business URL and MCC to Stripe in order to avoid a couple of steps in Connect Onboarding. We will also pass `tos_shown_and_accepted` flag. This PR From 07625a54fa502c1d4f9c63aa37245992ab1b693a Mon Sep 17 00:00:00 2001 From: sktoiva Date: Thu, 12 Mar 2020 10:21:13 +0200 Subject: [PATCH 11/17] Redirect user back to Stripe on returning to failure url --- .../EditListingWizard/EditListingWizard.css | 4 + .../EditListingWizard/EditListingWizard.js | 149 +++++++++++------- .../EditListingPage/EditListingPage.js | 24 ++- .../StripePayoutPage/StripePayoutPage.js | 23 ++- src/ducks/stripeConnectAccount.duck.js | 9 +- .../StripeConnectAccountForm.js | 12 +- src/translations/en.json | 11 +- 7 files changed, 162 insertions(+), 70 deletions(-) diff --git a/src/components/EditListingWizard/EditListingWizard.css b/src/components/EditListingWizard/EditListingWizard.css index 2c1ed4c04..1a4f8d116 100644 --- a/src/components/EditListingWizard/EditListingWizard.css +++ b/src/components/EditListingWizard/EditListingWizard.css @@ -112,3 +112,7 @@ padding-top: 11px; } } + +.modalMessage { + @apply --marketplaceModalParagraphStyles; +} diff --git a/src/components/EditListingWizard/EditListingWizard.js b/src/components/EditListingWizard/EditListingWizard.js index 07caf3214..52bb439c1 100644 --- a/src/components/EditListingWizard/EditListingWizard.js +++ b/src/components/EditListingWizard/EditListingWizard.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, useEffect } from 'react'; import { array, bool, func, number, object, oneOf, shape, string } from 'prop-types'; import { compose } from 'redux'; import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; @@ -7,6 +7,7 @@ import config from '../../config'; import routeConfiguration from '../../routeConfiguration'; import { createResourceLocatorString } from '../../util/routes'; import { withViewport } from '../../util/contextHelpers'; +import { propTypes } from '../../util/types'; import { LISTING_PAGE_PARAM_TYPE_DRAFT, LISTING_PAGE_PARAM_TYPE_NEW, @@ -48,6 +49,9 @@ export const TABS = [ // Tabs are horizontal in small screens const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023; +const STRIPE_ONBOARDING_RETURN_URL_SUCCESS = 'success'; +const STRIPE_ONBOARDING_RETURN_URL_FAILURE = 'failure'; + const tabLabel = (intl, tab) => { let key = null; if (tab === DESCRIPTION) { @@ -173,6 +177,11 @@ const handleGetStripeConnectAccountLinkFn = (getLinkFn, commonParams) => type => .catch(err => console.error(err)); }; +const RedirectToStripe = ({ redirectFn }) => { + useEffect(redirectFn('custom_account_verification'), []); + return ; +}; + // Create a new or edit listing through EditListingWizard class EditListingWizard extends Component { constructor(props) { @@ -194,7 +203,7 @@ class EditListingWizard extends Component { componentDidMount() { const { stripeOnboardingReturnURL } = this.props; - if (stripeOnboardingReturnURL != null) { + if (stripeOnboardingReturnURL != null && !this.showPayoutDetails) { this.setState({ showPayoutDetails: true }); } } @@ -263,6 +272,8 @@ class EditListingWizard extends Component { fetchStripeAccountError, stripeAccountFetched, stripeAccount, + stripeAccountError, + stripeAccountLinkError, currentUser, ...rest } = this.props; @@ -314,8 +325,18 @@ class EditListingWizard extends Component { 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 successURL = createReturnURL( + STRIPE_ONBOARDING_RETURN_URL_SUCCESS, + rootURL, + routes, + pathParams + ); + const failureURL = createReturnURL( + STRIPE_ONBOARDING_RETURN_URL_FAILURE, + rootURL, + routes, + pathParams + ); const accountId = stripeConnected ? stripeAccount.id : null; const stripeAccountData = stripeConnected ? getStripeAccountData(stripeAccount) : null; @@ -336,8 +357,8 @@ class EditListingWizard extends Component { } ); - const returnedNormallyFromStripe = returnURLType === 'success'; - const showVerificationError = returnURLType === 'failure'; + const returnedNormallyFromStripe = returnURLType === STRIPE_ONBOARDING_RETURN_URL_SUCCESS; + const returnedAbnormallyFromStripe = returnURLType === STRIPE_ONBOARDING_RETURN_URL_FAILURE; const showVerificationNeeded = stripeConnected && requirementsMissing; // Redirect from success URL to basic path for StripePayoutPage @@ -384,54 +405,59 @@ class EditListingWizard extends Component { >

- +
- +

-

- -

{!currentUserLoaded ? ( + ) : returnedAbnormallyFromStripe && !stripeAccountLinkError ? ( +

+ +

) : ( - - {stripeConnected && (showVerificationError || showVerificationNeeded) ? ( - - ) : stripeConnected && savedCountry ? ( - - ) : null} - + <> +

+ +

+ + {stripeConnected && !returnedAbnormallyFromStripe && showVerificationNeeded ? ( + + ) : stripeConnected && savedCountry && !returnedAbnormallyFromStripe ? ( + + ) : null} + + )}
@@ -442,14 +468,23 @@ class EditListingWizard extends Component { EditListingWizard.defaultProps = { className: null, + currentUser: null, rootClassName: null, listing: null, + stripeAccount: null, + stripeAccountFetched: null, updateInProgress: false, + createStripeAccountError: null, + updateStripeAccountError: null, + fetchStripeAccountError: null, + stripeAccountError: null, + stripeAccountLinkError: null, }; EditListingWizard.propTypes = { id: string.isRequired, className: string, + currentUser: propTypes.currentUser, rootClassName: string, params: shape({ id: string.isRequired, @@ -457,10 +492,8 @@ EditListingWizard.propTypes = { type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired, tab: oneOf(TABS).isRequired, }).isRequired, - history: shape({ - push: func.isRequired, - replace: func.isRequired, - }).isRequired, + stripeAccount: object, + stripeAccountFetched: bool, // We cannot use propTypes.listing since the listing might be a draft. listing: shape({ @@ -480,16 +513,20 @@ EditListingWizard.propTypes = { publishListingError: object, showListingsError: object, uploadImageError: object, - createStripeAccountError: object, }).isRequired, + createStripeAccountError: propTypes.error, + updateStripeAccountError: propTypes.error, + fetchStripeAccountError: propTypes.error, + stripeAccountError: propTypes.error, + stripeAccountLinkError: propTypes.error, + fetchInProgress: bool.isRequired, + getAccountLinkInProgress: bool.isRequired, payoutDetailsSaveInProgress: bool.isRequired, payoutDetailsSaved: bool.isRequired, onPayoutDetailsFormChange: func.isRequired, - onPayoutDetailsSubmit: func.isRequired, onGetStripeConnectAccountLink: func.isRequired, onManageDisableScrolling: func.isRequired, - updateInProgress: bool, // from withViewport viewport: shape({ diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js index 17bf28ced..e29884fcf 100644 --- a/src/containers/EditListingPage/EditListingPage.js +++ b/src/containers/EditListingPage/EditListingPage.js @@ -58,7 +58,9 @@ export const EditListingPageComponent = props => { currentUser, createStripeAccountError, fetchInProgress, + fetchStripeAccountError, getOwnListing, + getAccountLinkError, history, intl, onFetchAvailabilityExceptions, @@ -81,6 +83,7 @@ export const EditListingPageComponent = props => { scrollingDisabled, stripeAccountFetched, stripeAccount, + updateStripeAccountError, } = props; const { id, type, returnURLType } = params; @@ -197,6 +200,7 @@ export const EditListingPageComponent = props => { onPayoutDetailsFormChange={onPayoutDetailsFormChange} onPayoutDetailsSubmit={onPayoutDetailsFormSubmit} onGetStripeConnectAccountLink={onGetStripeConnectAccountLink} + getAccountLinkInProgress={getAccountLinkInProgress} onImageUpload={onImageUpload} onUpdateImageOrder={onUpdateImageOrder} onRemoveImage={onRemoveListingImage} @@ -210,6 +214,10 @@ export const EditListingPageComponent = props => { payoutDetailsSaved={page.payoutDetailsSaved} stripeAccountFetched={stripeAccountFetched} stripeAccount={stripeAccount} + stripeAccountError={ + createStripeAccountError || updateStripeAccountError || fetchStripeAccountError + } + stripeAccountLinkError={getAccountLinkError} /> ); @@ -227,7 +235,11 @@ export const EditListingPageComponent = props => { EditListingPageComponent.defaultProps = { createStripeAccountError: null, + fetchStripeAccountError: null, + getAccountLinkError: null, + stripeAccountFetched: null, currentUser: null, + stripeAccount: null, currentUserHasOrders: null, listing: null, listingDraft: null, @@ -237,17 +249,23 @@ EditListingPageComponent.defaultProps = { EditListingPageComponent.propTypes = { createStripeAccountError: propTypes.error, + fetchStripeAccountError: propTypes.error, + getAccountLinkError: propTypes.error, + updateStripeAccountError: propTypes.error, currentUser: propTypes.currentUser, fetchInProgress: bool.isRequired, getOwnListing: func.isRequired, onFetchAvailabilityExceptions: func.isRequired, onCreateAvailabilityException: func.isRequired, + onDeleteAvailabilityException: func.isRequired, + onFetchBookings: func.isRequired, + onGetStripeConnectAccountLink: func.isRequired, onCreateListingDraft: func.isRequired, onPublishListingDraft: func.isRequired, onImageUpload: func.isRequired, onManageDisableScrolling: func.isRequired, onPayoutDetailsFormChange: func.isRequired, - onPayoutDetailsSubmit: func.isRequired, + onPayoutDetailsFormSubmit: func.isRequired, onUpdateImageOrder: func.isRequired, onRemoveListingImage: func.isRequired, onUpdateListing: func.isRequired, @@ -260,6 +278,8 @@ EditListingPageComponent.propTypes = { tab: string.isRequired, returnURLType: oneOf(STRIPE_ONBOARDING_RETURN_URL_TYPES), }).isRequired, + stripeAccountFetched: bool, + stripeAccount: object, scrollingDisabled: bool.isRequired, /* from withRouter */ @@ -275,6 +295,7 @@ const mapStateToProps = state => { const page = state.EditListingPage; const { getAccountLinkInProgress, + getAccountLinkError, createStripeAccountInProgress, createStripeAccountError, updateStripeAccountError, @@ -294,6 +315,7 @@ const mapStateToProps = state => { }; return { getAccountLinkInProgress, + getAccountLinkError, createStripeAccountError, updateStripeAccountError, fetchStripeAccountError, diff --git a/src/containers/StripePayoutPage/StripePayoutPage.js b/src/containers/StripePayoutPage/StripePayoutPage.js index f5ee31b8f..19341e219 100644 --- a/src/containers/StripePayoutPage/StripePayoutPage.js +++ b/src/containers/StripePayoutPage/StripePayoutPage.js @@ -80,6 +80,7 @@ export const StripePayoutPageComponent = props => { currentUser, scrollingDisabled, getAccountLinkInProgress, + getAccountLinkError, createStripeAccountError, updateStripeAccountError, fetchStripeAccountError, @@ -127,7 +128,7 @@ export const StripePayoutPageComponent = props => { ); const returnedNormallyFromStripe = returnURLType === STRIPE_ONBOARDING_RETURN_URL_SUCCESS; - const showVerificationError = returnURLType === STRIPE_ONBOARDING_RETURN_URL_FAILURE; + const returnedAbnormallyFromStripe = returnURLType === STRIPE_ONBOARDING_RETURN_URL_FAILURE; const showVerificationNeeded = stripeConnected && requirementsMissing; // Redirect from success URL to basic path for StripePayoutPage @@ -135,6 +136,12 @@ export const StripePayoutPageComponent = props => { return ; } + // Failure url should redirect back to Stripe since it's most likely due to page reload + // Account link creation will fail if the account is the reason + if (returnedAbnormallyFromStripe && !getAccountLinkError) { + handleGetStripeConnectAccountLink('custom_account_verification')(); + } + return ( @@ -154,6 +161,8 @@ export const StripePayoutPageComponent = props => { {!currentUserLoaded ? ( + ) : returnedAbnormallyFromStripe && !getAccountLinkError ? ( + ) : ( { stripeAccountError={ createStripeAccountError || updateStripeAccountError || fetchStripeAccountError } + stripeAccountLinkError={getAccountLinkError} stripeAccountFetched={stripeAccountFetched} onChange={onPayoutDetailsFormChange} onSubmit={onPayoutDetailsFormSubmit} onGetStripeConnectAccountLink={handleGetStripeConnectAccountLink} stripeConnected={stripeConnected} > - {stripeConnected && (showVerificationError || showVerificationNeeded) ? ( + {stripeConnected && !returnedAbnormallyFromStripe && showVerificationNeeded ? ( - ) : stripeConnected && savedCountry ? ( + ) : stripeConnected && savedCountry && !returnedAbnormallyFromStripe ? ( { const { getAccountLinkInProgress, + getAccountLinkError, createStripeAccountError, updateStripeAccountError, fetchStripeAccountError, @@ -251,6 +265,7 @@ const mapStateToProps = state => { return { currentUser, getAccountLinkInProgress, + getAccountLinkError, createStripeAccountError, updateStripeAccountError, fetchStripeAccountError, diff --git a/src/ducks/stripeConnectAccount.duck.js b/src/ducks/stripeConnectAccount.duck.js index 6605dbb49..51ad9cae0 100644 --- a/src/ducks/stripeConnectAccount.duck.js +++ b/src/ducks/stripeConnectAccount.duck.js @@ -34,7 +34,7 @@ const initialState = { fetchStripeAccountInProgress: false, fetchStripeAccountError: null, getAccountLinkInProgress: false, - getAccountLinkError: false, + getAccountLinkError: null, stripeAccount: null, stripeAccountFetched: false, }; @@ -87,7 +87,8 @@ export default function reducer(state = initialState, action = {}) { case GET_ACCOUNT_LINK_REQUEST: return { ...state, getAccountLinkError: null, getAccountLinkInProgress: true }; case GET_ACCOUNT_LINK_ERROR: - return { ...state, getAccountLinkInProgress: false, getAccountLinkError: true }; + console.error(payload); + return { ...state, getAccountLinkInProgress: false, getAccountLinkError: payload }; case GET_ACCOUNT_LINK_SUCCESS: return { ...state, getAccountLinkInProgress: false }; @@ -144,8 +145,10 @@ export const stripeAccountClearError = () => ({ export const getAccountLinkRequest = () => ({ type: GET_ACCOUNT_LINK_REQUEST, }); -export const getAccountLinkError = () => ({ +export const getAccountLinkError = e => ({ type: GET_ACCOUNT_LINK_ERROR, + payload: e, + error: true, }); export const getAccountLinkSuccess = () => ({ type: GET_ACCOUNT_LINK_SUCCESS, diff --git a/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js b/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js index ce910f4f9..d4933e490 100644 --- a/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js +++ b/src/forms/StripeConnectAccountForm/StripeConnectAccountForm.js @@ -188,7 +188,7 @@ const UpdateStripeAccountFields = props => { }; const ErrorsMaybe = props => { - const { stripeAccountError } = props; + const { stripeAccountError, stripeAccountLinkError } = props; return isStripeError(stripeAccountError) ? (
{
+ ) : stripeAccountLinkError ? ( +
+ +
) : null; }; @@ -220,6 +224,7 @@ const StripeConnectAccountFormComponent = props => { className, children, stripeAccountError, + stripeAccountLinkError, disabled, handleSubmit, inProgress, @@ -328,7 +333,10 @@ const StripeConnectAccountFormComponent = props => {
)} - + {children} diff --git a/src/translations/en.json b/src/translations/en.json index e07b83b87..175eae458 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -187,9 +187,6 @@ "EditListingPhotosForm.showListingFailed": "Fetching listing data failed", "EditListingPhotosForm.updateFailed": "Failed to update listing. Please try again.", "EditListingPhotosPanel.createListingTitle": "Add a few photos", - "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.title": "Edit the photos of {listingTitle}", "EditListingPoliciesForm.rulesLabel": "Sauna rules", "EditListingPoliciesForm.rulesPlaceholder": "Describe the no-nos", @@ -208,6 +205,10 @@ "EditListingPricingPanel.createListingTitle": "How much does it cost?", "EditListingPricingPanel.listingPriceCurrencyInvalid": "Listing currency is different from the marketplace currency. You cannot edit the price.", "EditListingPricingPanel.title": "Edit the pricing of {listingTitle}", + "EditListingWizard.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. ", + "EditListingWizard.payoutModalTitleOneMoreThing": "One more thing:", + "EditListingWizard.payoutModalTitlePayoutPreferences": "Payout preferences", + "EditListingWizard.redirectingToStripe": "You returned early from Stripe. Redirecting you back to Stripe…", "EditListingWizard.saveEditDescription": "Save description", "EditListingWizard.saveEditFeatures": "Save amenities", "EditListingWizard.saveEditLocation": "Save location", @@ -914,14 +915,16 @@ "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.createStripeAccountFailed": "Whoops, something went wrong. Please try again. If the problem persists, contact your marketplace administrators.", "StripeConnectAccountForm.createStripeAccountFailedWithStripeError": "Whoops, something went wrong. Stripe returned an error message: \"{stripeMessage}\"", "StripeConnectAccountForm.individualAccount": "I'm an individual", + "StripeConnectAccountForm.createStripeAccountLinkFailed": "Whoops, something went wrong. Please contact your marketplace administrators.", "StripeConnectAccountForm.loadingStripeAccountData": "Fetching payout details…", "StripeConnectAccountForm.stripeToSText": "By saving details, you agree to the {stripeConnectedAccountTermsLink}", "StripeConnectAccountForm.stripeConnectedAccountTermsLink": "Stripe Connected Account Agreement", "StripePayoutPage.heading": "Payout details", "StripePayoutPage.loadingData": "Loading data…", + "StripePayoutPage.redirectingToStripe": "You returned early from Stripe. Redirecting you back to Stripe…", "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", From 86b4e27a14b2eca66cd5129f5a5c132a427bab3f Mon Sep 17 00:00:00 2001 From: sktoiva Date: Thu, 12 Mar 2020 14:47:30 +0200 Subject: [PATCH 12/17] Fix typo --- src/components/Modal/Modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/Modal.js b/src/components/Modal/Modal.js index 631512cde..3f77a8d8c 100644 --- a/src/components/Modal/Modal.js +++ b/src/components/Modal/Modal.js @@ -75,7 +75,7 @@ export class ModalComponent extends Component { // Because we are using portal, // we need to set the focus inside Modal manually - if (this.props.usePorta && this.props.isOpen) { + if (this.props.usePortal && this.props.isOpen) { this.refDiv.current.focus(); } } From 79fca2beba97c1e1a0774f72116fce8765dfb31b Mon Sep 17 00:00:00 2001 From: sktoiva Date: Mon, 16 Mar 2020 09:10:13 +0200 Subject: [PATCH 13/17] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb6663db..262ff5552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +- [change] Redirect user back to Stripe during Connect Onboarding Flow when user is returned to + failure URL provided that the Account Link generation is successful. + [#1269](https://github.com/sharetribe/ftw-daily/pull/1269) - [fix] Don't flash listing closed text on mobile view of `BookingPanel` when the listing data is not loaded yet. Instead, check that text is shown only for closed listings. [#1268](https://github.com/sharetribe/ftw-daily/pull/1268) From 2276f362f4f750c69d8e887e631a39e05bd805bc Mon Sep 17 00:00:00 2001 From: sktoiva Date: Mon, 16 Mar 2020 09:32:46 +0200 Subject: [PATCH 14/17] Update tests and snapshots for failure url handling --- src/containers/EditListingPage/EditListingPage.js | 3 +++ src/containers/EditListingPage/EditListingPage.test.js | 2 ++ .../__snapshots__/EditListingPage.test.js.snap | 7 +++++++ src/containers/StripePayoutPage/StripePayoutPage.test.js | 3 +++ .../__snapshots__/StripePayoutPage.test.js.snap | 6 ++++++ 5 files changed, 21 insertions(+) diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js index e29884fcf..509cff025 100644 --- a/src/containers/EditListingPage/EditListingPage.js +++ b/src/containers/EditListingPage/EditListingPage.js @@ -61,6 +61,7 @@ export const EditListingPageComponent = props => { fetchStripeAccountError, getOwnListing, getAccountLinkError, + getAccountLinkInProgress, history, intl, onFetchAvailabilityExceptions, @@ -237,6 +238,7 @@ EditListingPageComponent.defaultProps = { createStripeAccountError: null, fetchStripeAccountError: null, getAccountLinkError: null, + getAccountLinkInProgress: null, stripeAccountFetched: null, currentUser: null, stripeAccount: null, @@ -251,6 +253,7 @@ EditListingPageComponent.propTypes = { createStripeAccountError: propTypes.error, fetchStripeAccountError: propTypes.error, getAccountLinkError: propTypes.error, + getAccountLinkInProgress: bool, updateStripeAccountError: propTypes.error, currentUser: propTypes.currentUser, fetchInProgress: bool.isRequired, diff --git a/src/containers/EditListingPage/EditListingPage.test.js b/src/containers/EditListingPage/EditListingPage.test.js index b85c65b25..0f2e3d82a 100644 --- a/src/containers/EditListingPage/EditListingPage.test.js +++ b/src/containers/EditListingPage/EditListingPage.test.js @@ -20,6 +20,8 @@ describe('EditListingPageComponent', () => { getOwnListing={getOwnListing} images={[]} intl={fakeIntl} + onGetStripeConnectAccountLink={noop} + onPayoutDetailsFormSubmit={noop} onLogout={noop} onManageDisableScrolling={noop} onFetchBookings={noop} diff --git a/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap b/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap index 46edaa4cd..6959a1504 100644 --- a/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap +++ b/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap @@ -28,6 +28,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = ` } } fetchInProgress={false} + getAccountLinkInProgress={null} history={ Object { "push": [Function], @@ -48,9 +49,11 @@ exports[`EditListingPageComponent matches snapshot 1`] = ` newListingPublished={false} onChange={[Function]} onCreateListingDraft={[Function]} + onGetStripeConnectAccountLink={[Function]} onImageUpload={[Function]} onManageDisableScrolling={[Function]} onPayoutDetailsFormChange={[Function]} + onPayoutDetailsSubmit={[Function]} onPublishListingDraft={[Function]} onRemoveImage={[Function]} onUpdateImageOrder={[Function]} @@ -63,6 +66,10 @@ exports[`EditListingPageComponent matches snapshot 1`] = ` "type": "new", } } + stripeAccount={null} + stripeAccountError={null} + stripeAccountFetched={null} + stripeAccountLinkError={null} />
`; diff --git a/src/containers/StripePayoutPage/StripePayoutPage.test.js b/src/containers/StripePayoutPage/StripePayoutPage.test.js index b118d16f1..e503726a8 100644 --- a/src/containers/StripePayoutPage/StripePayoutPage.test.js +++ b/src/containers/StripePayoutPage/StripePayoutPage.test.js @@ -18,6 +18,7 @@ describe('StripePayoutPage', () => { onPayoutDetailsFormChange={noop} onPayoutDetailsFormSubmit={noop} onGetStripeConnectAccountLink={noop} + stripeAccountFetched={false} getAccountLinkInProgress={false} intl={fakeIntl} history={{ replace: noop }} @@ -43,6 +44,7 @@ describe('StripePayoutPage', () => { onPayoutDetailsFormChange={noop} onPayoutDetailsFormSubmit={noop} onGetStripeConnectAccountLink={noop} + stripeAccountFetched={false} getAccountLinkInProgress={false} intl={fakeIntl} history={{ replace: noop }} @@ -68,6 +70,7 @@ describe('StripePayoutPage', () => { onPayoutDetailsFormChange={noop} onPayoutDetailsFormSubmit={noop} onGetStripeConnectAccountLink={noop} + stripeAccountFetched={false} getAccountLinkInProgress={false} intl={fakeIntl} history={{ replace: noop }} diff --git a/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap b/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap index a8ed97b74..88899e539 100644 --- a/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap +++ b/src/containers/StripePayoutPage/__snapshots__/StripePayoutPage.test.js.snap @@ -78,6 +78,8 @@ exports[`StripePayoutPage matches snapshot with Stripe connected 1`] = ` ready={false} savedCountry={null} stripeAccountError={null} + stripeAccountFetched={false} + stripeAccountLinkError={null} stripeBankAccountLastDigits={null} stripeConnected={false} submitButtonText="StripePayoutPage.submitButtonText" @@ -162,6 +164,8 @@ exports[`StripePayoutPage matches snapshot with Stripe not connected 1`] = ` ready={false} savedCountry={null} stripeAccountError={null} + stripeAccountFetched={false} + stripeAccountLinkError={null} stripeBankAccountLastDigits={null} stripeConnected={false} submitButtonText="StripePayoutPage.submitButtonText" @@ -256,6 +260,8 @@ exports[`StripePayoutPage matches snapshot with details submitted 1`] = ` ready={true} savedCountry={null} stripeAccountError={null} + stripeAccountFetched={false} + stripeAccountLinkError={null} stripeBankAccountLastDigits={null} stripeConnected={false} submitButtonText="StripePayoutPage.submitButtonText" From 22cf71997f6b40669089c2b056dc8bb24222a663 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Mon, 16 Mar 2020 10:39:30 +0200 Subject: [PATCH 15/17] Release v4.3.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 262ff5552..5f1c9dec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2020-XX-XX +## [v4.3.0] 2020-03-16 + - [change] Redirect user back to Stripe during Connect Onboarding Flow when user is returned to failure URL provided that the Account Link generation is successful. [#1269](https://github.com/sharetribe/ftw-daily/pull/1269) @@ -29,6 +31,8 @@ way to update this template, but currently, we follow a pattern: possibility to use modals without Portal because of `ModalInMobile` component. [#1258](https://github.com/sharetribe/ftw-daily/pull/1258) + [v4.3.0]: https://github.com/sharetribe/flex-template-web/compare/v4.2.0...v4.3.0 + ## [v4.2.0] 2020-02-18 - [add] Show a banner when a user is logged in with limited access. diff --git a/package.json b/package.json index 0aa2eb4c5..591119f3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.0.0", + "version": "4.3.0", "private": true, "license": "Apache-2.0", "dependencies": { From 9356b528bed6cff29d24d75f95cc552330854b1f Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Mon, 16 Mar 2020 11:14:11 +0200 Subject: [PATCH 16/17] Remove Portal from EditListingAvailabilityPanel and use usePortal flag in Modal --- .../EditListingAvailabilityPanel.js | 122 ++++++------------ 1 file changed, 39 insertions(+), 83 deletions(-) diff --git a/src/components/EditListingAvailabilityPanel/EditListingAvailabilityPanel.js b/src/components/EditListingAvailabilityPanel/EditListingAvailabilityPanel.js index 2c75ec27f..fe07fdad7 100644 --- a/src/components/EditListingAvailabilityPanel/EditListingAvailabilityPanel.js +++ b/src/components/EditListingAvailabilityPanel/EditListingAvailabilityPanel.js @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import ReactDOM from 'react-dom'; import { arrayOf, bool, func, object, string } from 'prop-types'; import classNames from 'classnames'; import { FormattedMessage } from '../../util/reactIntl'; @@ -29,42 +28,6 @@ const MAX_EXCEPTIONS_COUNT = 100; const defaultTimeZone = () => typeof window !== 'undefined' ? getDefaultTimeZoneOnBrowser() : 'Etc/UTC'; -//////////// -// Portal // -//////////// - -// TODO: change all the modals to use portals at some point. -// Portal is used here to circumvent the problems that rise -// from different levels of z-indexes in DOM tree. -// Note: React Portal didn't exist when we originally created modals. - -class Portal extends React.Component { - constructor(props) { - super(props); - this.el = document.createElement('div'); - } - - componentDidMount() { - // The portal element is inserted in the DOM tree after - // the Modal's children are mounted, meaning that children - // will be mounted on a detached DOM node. If a child - // component requires to be attached to the DOM tree - // immediately when mounted, for example to measure a - // DOM node, or uses 'autoFocus' in a descendant, add - // state to Modal and only render the children when Modal - // is inserted in the DOM tree. - this.props.portalRoot.appendChild(this.el); - } - - componentWillUnmount() { - this.props.portalRoot.removeChild(this.el); - } - - render() { - return ReactDOM.createPortal(this.props.children, this.el); - } -} - ///////////// // Weekday // ///////////// @@ -194,13 +157,8 @@ const EditListingAvailabilityPanel = props => { // Hooks const [isEditPlanModalOpen, setIsEditPlanModalOpen] = useState(false); const [isEditExceptionsModalOpen, setIsEditExceptionsModalOpen] = useState(false); - const [portalRoot, setPortalRoot] = useState(null); const [valuesFromLastSubmit, setValuesFromLastSubmit] = useState(null); - const setPortalRootAfterInitialRender = () => { - setPortalRoot(document.getElementById('portal-root')); - }; - const classes = classNames(rootClassName || css.root, className); const currentListing = ensureOwnListing(listing); const isNextButtonDisabled = !currentListing.attributes.availabilityPlan; @@ -261,7 +219,7 @@ const EditListingAvailabilityPanel = props => { }; return ( -
+

{isPublished ? ( { {submitButtonText} ) : null} - {portalRoot && onManageDisableScrolling ? ( - - setIsEditPlanModalOpen(false)} - onManageDisableScrolling={onManageDisableScrolling} - containerClassName={css.modalContainer} - > - - - + {onManageDisableScrolling ? ( + setIsEditPlanModalOpen(false)} + onManageDisableScrolling={onManageDisableScrolling} + containerClassName={css.modalContainer} + usePortal + > + + ) : null} - {portalRoot && onManageDisableScrolling ? ( - - setIsEditExceptionsModalOpen(false)} - onManageDisableScrolling={onManageDisableScrolling} - containerClassName={css.modalContainer} - > - - - + {onManageDisableScrolling ? ( + setIsEditExceptionsModalOpen(false)} + onManageDisableScrolling={onManageDisableScrolling} + containerClassName={css.modalContainer} + usePortal + > + + ) : null}

); From 2603c88ea7359f808264d0e65c8d264a23d151f4 Mon Sep 17 00:00:00 2001 From: Jenni Laakso Date: Mon, 16 Mar 2020 11:15:40 +0200 Subject: [PATCH 17/17] Update changelog --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba641a30c..bfa1b29b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,27 @@ https://github.com/sharetribe/flex-template-web/ ## Upcoming version 2020-XX-XX +## [v6.3.0] 2020-03-16 + +This is update from from [upstream](https://github.com/sharetribe/ftw-daily): v4.3.0 + +- [change] Redirect user back to Stripe during Connect Onboarding Flow when user is returned to + failure URL provided that the Account Link generation is successful. + [#1269](https://github.com/sharetribe/ftw-daily/pull/1269) +- [fix] Don't flash listing closed text on mobile view of `BookingPanel` when the listing data is + not loaded yet. Instead, check that text is shown only for closed listings. + [#1268](https://github.com/sharetribe/ftw-daily/pull/1268) +- [change] Use some default values to improve Stripe Connect onboarding. When creating a new Stripe + the account we will pass the account type, business URL and MCC to Stripe in order to avoid a + couple of steps in Connect Onboarding. We will also pass `tos_shown_and_accepted` flag. This PR + will bring back the previously used `accountToken` which is now used for passing e.g. the account + type to Stripe. [#1267](https://github.com/sharetribe/ftw-daily/pull/1267) +- [change] Update `Modal` component to have option to use `Portal` with `usePortal` flag. Keep also + possibility to use modals without Portal because of `ModalInMobile` component. + [#1258](https://github.com/sharetribe/ftw-daily/pull/1258) + +[v6.3.0]: https://github.com/sharetribe/ftw-hourly/compare/v6.2.0...v6.3.0 + ## [v6.2.0] 2020-02-18 This is update from from [upstream](https://github.com/sharetribe/ftw-daily): v4.2.0