From abb68832ff18c47f502cd2ab097b5b4b9fc3a509 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 18 Sep 2018 17:43:42 +0200 Subject: [PATCH] SONARCLOUD-43 Allow users to select the plan when creating an org (#705) --- .../sonar-docs/src/tooltips/billing/coupon.md | 1 + server/sonar-web/src/main/js/api/billing.ts | 29 +++ .../js/app/components/GlobalContainer.tsx | 2 +- .../main/js/app/components/StartupModal.tsx | 14 +- .../__tests__/StartupModal-test.tsx | 11 +- .../{Extension.js => Extension.tsx} | 52 ++-- ...ension.js => GlobalAdminPageExtension.tsx} | 25 +- ...geExtension.js => GlobalPageExtension.tsx} | 25 +- .../extensions/OrganizationPageExtension.tsx | 1 - .../{PortfoliosPage.js => PortfoliosPage.tsx} | 14 +- ...nsion.js => ProjectAdminPageExtension.tsx} | 28 +- .../extensions/{utils.js => utils.ts} | 8 +- .../src/main/js/app/styles/init/misc.css | 4 - server/sonar-web/src/main/js/app/types.ts | 5 + .../create/organization/BillingFormShim.tsx | 57 +++++ .../js/apps/create/organization/CardForm.tsx | 89 +++++++ .../apps/create/organization/CouponForm.tsx | 82 ++++++ .../organization/CreateOrganization.tsx | 154 ++++++++--- .../organization/OrganizationDetailsStep.tsx | 47 +++- .../organization/PaymentMethodSelect.tsx | 57 +++++ .../apps/create/organization/PlanSelect.tsx | 87 +++++++ .../js/apps/create/organization/PlanStep.tsx | 145 +++++++++++ .../__mocks__/BillingFormShim.tsx | 47 ++++ .../__tests__/BillingFormShim-test.tsx | 49 ++++ .../organization/__tests__/CardForm-test.tsx | 37 +++ .../__tests__/CouponForm-test.tsx | 35 +++ .../__tests__/CreateOrganization-test.tsx | 46 +++- .../OrganizationDetailsStep-test.tsx | 33 ++- .../__tests__/PaymentMethodSelect-test.tsx | 34 +++ .../__tests__/PlanSelect-test.tsx | 34 +++ .../organization/__tests__/PlanStep-test.tsx | 132 ++++++++++ .../BillingFormShim-test.tsx.snap | 15 ++ .../__snapshots__/CardForm-test.tsx.snap | 106 ++++++++ .../__snapshots__/CouponForm-test.tsx.snap | 19 ++ .../CreateOrganization-test.tsx.snap | 105 +++++++- .../OrganizationDetailsStep-test.tsx.snap | 46 +++- .../PaymentMethodSelect-test.tsx.snap | 79 ++++++ .../__snapshots__/PlanSelect-test.tsx.snap | 123 +++++++++ .../__snapshots__/PlanStep-test.tsx.snap | 242 ++++++++++++++++++ .../__tests__/whenLoggedIn-test.tsx | 1 + .../__tests__/withCurrentUser-test.tsx | 40 +++ .../main/js/apps/create/organization/utils.ts | 28 ++ .../apps/create/organization/whenLoggedIn.tsx | 13 +- .../create/organization/withCurrentUser.tsx | 43 ++++ .../tutorials/onboarding/OnboardingPage.tsx | 16 +- .../src/main/js/components/controls/Radio.tsx | 56 ++++ .../js/components/controls/ValidationForm.tsx | 26 +- .../controls/__tests__/Radio-test.tsx | 34 +++ .../__snapshots__/Radio-test.tsx.snap | 29 +++ .../resources/org/sonar/l10n/core.properties | 5 + 50 files changed, 2200 insertions(+), 210 deletions(-) create mode 100644 server/sonar-docs/src/tooltips/billing/coupon.md create mode 100644 server/sonar-web/src/main/js/api/billing.ts rename server/sonar-web/src/main/js/app/components/extensions/{Extension.js => Extension.tsx} (71%) rename server/sonar-web/src/main/js/app/components/extensions/{GlobalAdminPageExtension.js => GlobalAdminPageExtension.tsx} (74%) rename server/sonar-web/src/main/js/app/components/extensions/{GlobalPageExtension.js => GlobalPageExtension.tsx} (74%) rename server/sonar-web/src/main/js/app/components/extensions/{PortfoliosPage.js => PortfoliosPage.tsx} (76%) rename server/sonar-web/src/main/js/app/components/extensions/{ProjectAdminPageExtension.js => ProjectAdminPageExtension.tsx} (78%) rename server/sonar-web/src/main/js/app/components/extensions/{utils.js => utils.ts} (87%) create mode 100644 server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/CardForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/CouponForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/PaymentMethodSelect.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/CardForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/CouponForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/PaymentMethodSelect-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CardForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CouponForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PaymentMethodSelect-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/create/organization/utils.ts create mode 100644 server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/Radio.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/Radio-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap diff --git a/server/sonar-docs/src/tooltips/billing/coupon.md b/server/sonar-docs/src/tooltips/billing/coupon.md new file mode 100644 index 00000000000..50319dbc179 --- /dev/null +++ b/server/sonar-docs/src/tooltips/billing/coupon.md @@ -0,0 +1 @@ +A coupon is a way to pay for yearly subscriptions or to use other payment methods than card. Contact us for more information. diff --git a/server/sonar-web/src/main/js/api/billing.ts b/server/sonar-web/src/main/js/api/billing.ts new file mode 100644 index 00000000000..f0a20dca756 --- /dev/null +++ b/server/sonar-web/src/main/js/api/billing.ts @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { getJSON } from '../helpers/request'; +import throwGlobalError from '../app/utils/throwGlobalError'; +import { SubscriptionPlan } from '../app/types'; + +export function getSubscriptionPlans(): Promise { + return getJSON('/api/billing/show_subscription_plans').then( + ({ subscriptionPlans }) => subscriptionPlans, + throwGlobalError + ); +} diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index d924a697abf..32ee2c13079 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -37,7 +37,7 @@ export default function GlobalContainer(props: Props) { return ( {({ suggestions }) => ( - +
diff --git a/server/sonar-web/src/main/js/app/components/StartupModal.tsx b/server/sonar-web/src/main/js/app/components/StartupModal.tsx index 8481e4dc86f..49863fa9ffc 100644 --- a/server/sonar-web/src/main/js/app/components/StartupModal.tsx +++ b/server/sonar-web/src/main/js/app/components/StartupModal.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { withRouter, WithRouterProps } from 'react-router'; import { CurrentUser, isLoggedIn } from '../types'; import { differenceInDays, parseDate, toShortNotSoISOString } from '../../helpers/dates'; import { EditionKey } from '../../apps/marketplace/utils'; @@ -56,11 +57,10 @@ interface DispatchProps { } interface OwnProps { - location: { pathname: string }; children?: React.ReactNode; } -type Props = StateProps & DispatchProps & OwnProps; +type Props = StateProps & DispatchProps & OwnProps & WithRouterProps; enum ModalKey { license, @@ -77,10 +77,6 @@ interface State { const LICENSE_PROMPT = 'sonarqube.license.prompt'; export class StartupModal extends React.PureComponent { - static contextTypes = { - router: PropTypes.object.isRequired - }; - static childContextTypes = { openProjectOnboarding: PropTypes.func }; @@ -121,13 +117,13 @@ export class StartupModal extends React.PureComponent { openOrganizationOnboarding = () => { this.closeOnboarding(); - this.context.router.push('/create-organization'); + this.props.router.push({ pathname: '/create-organization', state: { paid: true } }); }; openProjectOnboarding = () => { if (isSonarCloud()) { this.setState({ automatic: false, modal: undefined }); - this.context.router.push(`/projects/create`); + this.props.router.push(`/projects/create`); } else { this.setState({ modal: ModalKey.projectOnboarding }); } @@ -212,4 +208,4 @@ const mapDispatchToProps: DispatchProps = { skipOnboardingAction }; export default connect( mapStateToProps, mapDispatchToProps -)(StartupModal); +)(withRouter(StartupModal)); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx index 7e076dd9410..62feff5e458 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx @@ -19,11 +19,13 @@ */ import * as React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { Location } from 'history'; +import { InjectedRouter } from 'react-router'; import { StartupModal } from '../StartupModal'; import { showLicense } from '../../../api/marketplace'; import { save, get } from '../../../helpers/storage'; import { hasMessage } from '../../../helpers/l10n'; -import { waitAndUpdate } from '../../../helpers/testUtils'; +import { waitAndUpdate, mockRouter } from '../../../helpers/testUtils'; import { differenceInDays, toShortNotSoISOString } from '../../../helpers/dates'; import { LoggedInUser } from '../../types'; import { EditionKey } from '../../../apps/marketplace/utils'; @@ -136,15 +138,16 @@ async function shouldDisplayLicense(wrapper: ShallowWrapper) { function getWrapper(props = {}) { return shallow( + // @ts-ignore avoid passing everything from WithRouterProps
- , - { context: { router: { push: jest.fn() } } } + ); } diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.js b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx similarity index 71% rename from server/sonar-web/src/main/js/app/components/extensions/Extension.js rename to server/sonar-web/src/main/js/app/components/extensions/Extension.tsx index d2934d30980..7aadebe3108 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/Extension.js +++ b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx @@ -17,36 +17,28 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import Helmet from 'react-helmet'; import * as PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import { injectIntl } from 'react-intl'; +import { withRouter, WithRouterProps } from 'react-router'; +import { injectIntl, InjectedIntlProps } from 'react-intl'; import { getExtensionStart } from './utils'; import { translate } from '../../../helpers/l10n'; import getStore from '../../utils/getStore'; +import { CurrentUser } from '../../types'; -/*:: -type Props = { - currentUser: Object, - extension: { - key: string, - name: string - }, - intl: Object, - location: { hash: string }, - onFail: string => void, - options?: {}, - router: Object -}; -*/ +interface OwnProps { + currentUser: CurrentUser; + extension: { key: string; name: string }; + onFail: (message: string) => void; + options?: {}; +} + +type Props = OwnProps & WithRouterProps & InjectedIntlProps; -class Extension extends React.PureComponent { - /*:: container: Object; */ - /*:: props: Props; */ - /*:: stop: ?Function; */ +class Extension extends React.PureComponent { + container?: HTMLElement | null; + stop?: Function; static contextTypes = { suggestions: PropTypes.object.isRequired @@ -56,15 +48,11 @@ class Extension extends React.PureComponent { this.startExtension(); } - componentDidUpdate(prevProps /*: Props */) { + componentDidUpdate(prevProps: Props) { if (prevProps.extension !== this.props.extension) { this.stopExtension(); this.startExtension(); - } else if ( - prevProps.location !== this.props.location && - // old router from backbone app updates hash, don't react in this case - prevProps.location.hash === this.props.location.hash - ) { + } else if (prevProps.location !== this.props.location) { this.startExtension(); } } @@ -73,7 +61,7 @@ class Extension extends React.PureComponent { this.stopExtension(); } - handleStart = (start /*: Function */) => { + handleStart = (start: Function) => { const store = getStore(); this.stop = start({ store, @@ -99,7 +87,7 @@ class Extension extends React.PureComponent { stopExtension() { if (this.stop) { this.stop(); - this.stop = null; + this.stop = undefined; } } @@ -113,4 +101,4 @@ class Extension extends React.PureComponent { } } -export default injectIntl(withRouter(Extension)); +export default injectIntl(withRouter(Extension)); diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx similarity index 74% rename from server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js rename to server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx index 640ef485c36..e479fbc7537 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx @@ -17,26 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; import ExtensionContainer from './ExtensionContainer'; import NotFound from '../NotFound'; -import { getAppState } from '../../../store/rootReducer'; +import { Extension } from '../../types'; +import { getAppState, Store } from '../../../store/rootReducer'; -/*:: -type Props = { - adminPages: Array<{ key: string }>, - params: { - extensionKey: string, - pluginKey: string - } -}; -*/ +interface Props { + adminPages: Extension[] | undefined; + params: { extensionKey: string; pluginKey: string }; +} -function GlobalAdminPageExtension(props /*: Props */) { +function GlobalAdminPageExtension(props: Props) { const { extensionKey, pluginKey } = props.params; - const extension = props.adminPages.find(p => p.key === `${pluginKey}/${extensionKey}`); + const extension = (props.adminPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? ( ) : ( @@ -44,7 +39,7 @@ function GlobalAdminPageExtension(props /*: Props */) { ); } -const mapStateToProps = state => ({ +const mapStateToProps = (state: Store) => ({ adminPages: getAppState(state).adminPages }); diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx similarity index 74% rename from server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js rename to server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx index ede95cb92ed..8d7d535333b 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js +++ b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx @@ -17,26 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; import ExtensionContainer from './ExtensionContainer'; import NotFound from '../NotFound'; -import { getAppState } from '../../../store/rootReducer'; +import { getAppState, Store } from '../../../store/rootReducer'; +import { Extension } from '../../types'; -/*:: -type Props = { - globalPages: Array<{ key: string }>, - params: { - extensionKey: string, - pluginKey: string - } -}; -*/ +interface Props { + globalPages: Extension[] | undefined; + params: { extensionKey: string; pluginKey: string }; +} -function GlobalPageExtension(props /*: Props */) { +function GlobalPageExtension(props: Props) { const { extensionKey, pluginKey } = props.params; - const extension = props.globalPages.find(p => p.key === `${pluginKey}/${extensionKey}`); + const extension = (props.globalPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? ( ) : ( @@ -44,7 +39,7 @@ function GlobalPageExtension(props /*: Props */) { ); } -const mapStateToProps = state => ({ +const mapStateToProps = (state: Store) => ({ globalPages: getAppState(state).globalPages }); diff --git a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx index a12e39986cd..d1b1b181a8f 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx @@ -65,7 +65,6 @@ class OrganizationPageExtension extends React.PureComponent { return extension ? ( ) : ( diff --git a/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.js b/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx similarity index 76% rename from server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.js rename to server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx index 5f34a72964f..182fad8c030 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.js +++ b/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx @@ -17,17 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import GlobalPageExtension from './GlobalPageExtension'; -export default function PortfoliosPage(props /*: Object */) { - return ( -
- -
- ); +export default function PortfoliosPage() { + return ; } diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx similarity index 78% rename from server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js rename to server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx index e39394c6d91..d4fb48cc14d 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js +++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx @@ -17,34 +17,26 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; +import { Location } from 'history'; import ExtensionContainer from './ExtensionContainer'; import NotFound from '../NotFound'; import { addGlobalErrorMessage } from '../../../store/globalMessages'; +import { Component } from '../../types'; -/*:: -type Props = { - component: { - configuration?: { - extensions: Array<{ key: string }> - } - }, - location: { query: { id: string } }, - params: { - extensionKey: string, - pluginKey: string - } -}; -*/ +interface Props { + component: Component; + location: Location; + params: { extensionKey: string; pluginKey: string }; +} -function ProjectAdminPageExtension(props /*: Props */) { +function ProjectAdminPageExtension(props: Props) { const { extensionKey, pluginKey } = props.params; const { component } = props; const extension = component.configuration && - component.configuration.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`); + (component.configuration.extensions || []).find(p => p.key === `${pluginKey}/${extensionKey}`); return extension ? ( ) : ( diff --git a/server/sonar-web/src/main/js/app/components/extensions/utils.js b/server/sonar-web/src/main/js/app/components/extensions/utils.ts similarity index 87% rename from server/sonar-web/src/main/js/app/components/extensions/utils.js rename to server/sonar-web/src/main/js/app/components/extensions/utils.ts index 98600e25581..84c9dccd659 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/utils.js +++ b/server/sonar-web/src/main/js/app/components/extensions/utils.ts @@ -17,21 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow import exposeLibraries from './exposeLibraries'; import { getExtensionFromCache } from '../../utils/installExtensionsHandler'; +import { getBaseUrl } from '../../../helpers/urls'; -function installScript(key /*: string */) { +function installScript(key: string) { return new Promise(resolve => { exposeLibraries(); const scriptTag = document.createElement('script'); - scriptTag.src = `${window.baseUrl}/static/${key}.js`; + scriptTag.src = `${getBaseUrl()}/static/${key}.js`; scriptTag.onload = resolve; document.getElementsByTagName('body')[0].appendChild(scriptTag); }); } -export function getExtensionStart(key /*: string */) { +export function getExtensionStart(key: string): Promise { return new Promise((resolve, reject) => { const fromCache = getExtensionFromCache(key); if (fromCache) { diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index dcd5aff9e14..b13e0a463a7 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -57,10 +57,6 @@ th.nowrap { font-size: var(--smallFontSize); } -.note a { - color: var(--secondFontColor); -} - .spacer-left { margin-left: 8px !important; } diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 4e4153ad236..1ddd5c5e0d5 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -703,6 +703,11 @@ export interface SourceViewerFile { uuid: string; } +export interface SubscriptionPlan { + maxNcloc: number; + price: number; +} + export interface TestCase { coveredLines: number; durationInMs: number; diff --git a/server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx b/server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx new file mode 100644 index 00000000000..8c83ec676b3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { CurrentUser, SubscriptionPlan } from '../../../app/types'; + +interface ChildrenProps { + alertError: string | undefined; + couponValue: string; + onSubmit: React.FormEventHandler; + renderAdditionalInfo: () => React.ReactNode; + renderBillingNameInput: () => React.ReactNode; + renderBraintreeClient: () => React.ReactNode; + renderCountrySelect: () => React.ReactNode; + renderCouponInput: (children?: React.ReactNode) => React.ReactNode; + renderEmailInput: () => React.ReactNode; + renderNextCharge: () => React.ReactNode; + renderPlanSelect: () => React.ReactNode; + renderResetButton: () => React.ReactNode; + renderSpinner: () => React.ReactNode; + renderSubmitButton: (text?: string) => React.ReactNode; + renderTermsOfService: () => React.ReactNode; + renderTypeOfUseSelect: () => React.ReactNode; +} + +interface Props { + children: (props: ChildrenProps) => React.ReactElement; + country?: string; + currentUser: CurrentUser; + onClose: () => void; + onCommit: () => void; + organizationKey: string | (() => Promise); + subscriptionPlans: SubscriptionPlan[]; +} + +export default class BillingFormShim extends React.Component { + render() { + const { BillingForm } = (window as any).SonarBilling; + return ; + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/CardForm.tsx b/server/sonar-web/src/main/js/apps/create/organization/CardForm.tsx new file mode 100644 index 00000000000..221214668da --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/CardForm.tsx @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import BillingFormShim from './BillingFormShim'; +import { withCurrentUser } from './withCurrentUser'; +import { CurrentUser, SubscriptionPlan } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + createOrganization: () => Promise; + currentUser: CurrentUser; + onSubmit: () => void; + subscriptionPlans: SubscriptionPlan[]; +} + +export class CardForm extends React.PureComponent { + handleClose = () => { + // do nothing + }; + + render() { + return ( +
+ + {form => ( +
+
+
+

{translate('billing.upgrade.billing_info')}

+ {form.renderEmailInput()} + {form.renderTypeOfUseSelect()} + {form.renderBillingNameInput()} + {form.renderCountrySelect()} + {form.renderAdditionalInfo()} +
+
+

{translate('billing.upgrade.plan')}

+ {form.renderPlanSelect()} +

{translate('billing.upgrade.card_info')}

+ {form.renderBraintreeClient()} +
+
+
+ {form.renderNextCharge()} +
+ {form.alertError &&

{form.alertError}

} +
+
+ {form.renderSpinner()} + {form.renderSubmitButton( + translate('onboarding.create_organization.create_and_upgrade') + )} +
+ {form.renderTermsOfService()} +
+ )} +
+
+ ); + } +} + +export default withCurrentUser(CardForm); diff --git a/server/sonar-web/src/main/js/apps/create/organization/CouponForm.tsx b/server/sonar-web/src/main/js/apps/create/organization/CouponForm.tsx new file mode 100644 index 00000000000..4747165561d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/CouponForm.tsx @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import BillingFormShim from './BillingFormShim'; +import { withCurrentUser } from './withCurrentUser'; +import { CurrentUser } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; +import DocTooltip from '../../../components/docs/DocTooltip'; + +interface Props { + createOrganization: () => Promise; + currentUser: CurrentUser; + onSubmit: () => void; +} + +export class CouponForm extends React.PureComponent { + handleClose = () => { + // do nothing + }; + + render() { + return ( +
+ + {form => ( +
+
{form.renderBraintreeClient()}
+ {form.renderCouponInput( + + )} +

{translate('billing.upgrade.billing_info')}

+ {form.renderEmailInput()} + {form.renderTypeOfUseSelect()} + {form.renderBillingNameInput()} + {form.renderCountrySelect()} + {form.renderAdditionalInfo()} + {form.alertError &&

{form.alertError}

} +
+ {form.renderSpinner()} + {form.renderSubmitButton( + translate('onboarding.create_organization.create_and_upgrade') + )} +
+ {form.renderTermsOfService()} +
+ )} +
+
+ ); + } +} + +export default withCurrentUser(CouponForm); diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx index cf61cd192ed..7837907b045 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx @@ -23,10 +23,13 @@ import { FormattedMessage } from 'react-intl'; import { Link, withRouter, WithRouterProps } from 'react-router'; import { connect } from 'react-redux'; import OrganizationDetailsStep from './OrganizationDetailsStep'; +import PlanStep from './PlanStep'; +import { formatPrice } from './utils'; import { whenLoggedIn } from './whenLoggedIn'; -import { translate } from '../../../helpers/l10n'; -import { OrganizationBase, Organization } from '../../../app/types'; import { createOrganization } from '../../organizations/actions'; +import { getSubscriptionPlans } from '../../../api/billing'; +import { OrganizationBase, Organization, SubscriptionPlan } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; import { getOrganizationUrl } from '../../../helpers/urls'; import '../../../app/styles/sonarcloud.css'; import '../../tutorials/styles.css'; // TODO remove me @@ -35,13 +38,30 @@ interface Props { createOrganization: (organization: OrganizationBase) => Promise; } -export class CreateOrganization extends React.PureComponent { +enum Step { + OrganizationDetails, + Plan +} + +interface State { + loading: boolean; + organization?: Organization; + step: Step; + subscriptionPlans?: SubscriptionPlan[]; +} + +export class CreateOrganization extends React.PureComponent { mounted = false; + state: State = { + loading: true, + step: Step.OrganizationDetails + }; componentDidMount() { this.mounted = true; document.body.classList.add('white-page'); document.documentElement.classList.add('white-page'); + this.fetchSubscriptionPlans(); } componentWillUnmount() { @@ -49,22 +69,66 @@ export class CreateOrganization extends React.PureComponent) => { - return this.props - .createOrganization({ - avatar: organization.avatar, - description: organization.description, - key: organization.key, - name: organization.name || organization.key, - url: organization.url - }) - .then(organization => { - this.props.router.push(getOrganizationUrl(organization.key)); - }); + fetchSubscriptionPlans = () => { + getSubscriptionPlans().then( + subscriptionPlans => { + if (this.mounted) { + this.setState({ loading: false, subscriptionPlans }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleOrganizationDetailsStepOpen = () => { + this.setState({ step: Step.OrganizationDetails }); + }; + + handleOrganizationDetailsFinish = (organization: Required) => { + this.setState({ organization, step: Step.Plan }); + return Promise.resolve(); + }; + + handlePaidPlanChoose = () => { + if (this.state.organization) { + this.props.router.push(getOrganizationUrl(this.state.organization.key)); + } + }; + + handleFreePlanChoose = () => { + this.createOrganization().then( + key => this.props.router.push(getOrganizationUrl(key)), + () => {} + ); + }; + + createOrganization = () => { + const { organization } = this.state; + if (organization) { + return this.props + .createOrganization({ + avatar: organization.avatar, + description: organization.description, + key: organization.key, + name: organization.name || organization.key, + url: organization.url + }) + .then(({ key }) => key); + } else { + return Promise.reject(); + } }; render() { + const { location } = this.props; + const { loading, subscriptionPlans } = this.state; const header = translate('onboarding.create_organization.page.header'); + const startedPrice = subscriptionPlans && subscriptionPlans[0] && subscriptionPlans[0].price; + const formattedPrice = formatPrice(startedPrice); return ( <> @@ -72,25 +136,51 @@ export class CreateOrganization extends React.PureComponent

{header}

-
- {translate('cancel')} -
-

- , - price: '€10', // TODO - more: ( - {translate('learn_more')} - ) - }} - /> -

+ {startedPrice !== undefined && ( +

+ , + price: formattedPrice, + more: ( + + {translate('learn_more')} + + ) + }} + /> +

+ )}
- + {loading ? ( + + ) : ( + <> + + + {subscriptionPlans !== undefined && + this.state.organization && ( + + )} + + )}
); diff --git a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx index ca40212da76..4c7f28a20a6 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx @@ -23,6 +23,7 @@ import Step from '../../tutorials/components/Step'; import ValidationForm, { ChildrenProps } from '../../../components/controls/ValidationForm'; import { translate } from '../../../helpers/l10n'; import { ResetButtonLink, SubmitButton } from '../../../components/ui/buttons'; +import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; import DropdownIcon from '../../../components/icons-components/DropdownIcon'; import { isUrl } from '../../../helpers/urls'; import { OrganizationBase } from '../../../app/types'; @@ -39,7 +40,11 @@ const initialValues: Values = { }; interface Props { + finished: boolean; onContinue: (organization: Required) => Promise; + onOpen: () => void; + open: boolean; + organization?: OrganizationBase & { key: string }; } interface State { @@ -49,6 +54,21 @@ interface State { export default class OrganizationDetailsStep extends React.PureComponent { state: State = { additional: false }; + getInitialValues = (): Values => { + const { organization } = this.props; + if (organization) { + return { + avatar: organization.avatar || '', + description: organization.description || '', + name: organization.name, + key: organization.key, + url: organization.url || '' + }; + } else { + return initialValues; + } + }; + handleAdditionalClick = () => { this.setState(state => ({ additional: !state.additional })); }; @@ -81,6 +101,7 @@ export default class OrganizationDetailsStep extends React.PureComponent { if (!free) { errors.key = translate('onboarding.create_organization.organization_name.taken'); @@ -178,10 +199,7 @@ export default class OrganizationDetailsStep extends React.PureComponent
- - {/* // TODO change me */} - {translate('onboarding.create_organization.page.header')} - + {translate('continue')}
); @@ -191,7 +209,8 @@ export default class OrganizationDetailsStep extends React.PureComponent - initialValues={initialValues} + initialValues={this.getInitialValues()} + isInitialValid={this.props.organization !== undefined} onSubmit={this.props.onContinue} validate={this.handleValidate}> {this.renderInnerForm} @@ -200,14 +219,24 @@ export default class OrganizationDetailsStep extends React.PureComponent { + const { organization } = this.props; + return organization ? ( +
+ + {organization.key} +
+ ) : null; + }; + render() { return ( {}} - open={true} + finished={this.props.finished} + onOpen={this.props.onOpen} + open={this.props.open} renderForm={this.renderForm} - renderResult={() =>
} + renderResult={this.renderResult} stepNumber={1} stepTitle={translate('onboarding.create_organization.enter_org_details')} /> diff --git a/server/sonar-web/src/main/js/apps/create/organization/PaymentMethodSelect.tsx b/server/sonar-web/src/main/js/apps/create/organization/PaymentMethodSelect.tsx new file mode 100644 index 00000000000..ddf53728c55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/PaymentMethodSelect.tsx @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import RadioToggle from '../../../components/controls/RadioToggle'; +import { translate } from '../../../helpers/l10n'; + +export enum PaymentMethod { + Card = 'card', + Coupon = 'coupon' +} + +interface Props { + onChange: (paymentMethod: PaymentMethod) => void; + paymentMethod: PaymentMethod | undefined; +} + +export default class PaymentMethodSelect extends React.PureComponent { + render() { + const options = Object.values(PaymentMethod).map(value => ({ + label: translate('billing', value), + value + })); + + return ( +
+ +
+ +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx new file mode 100644 index 00000000000..c5310dcedf8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router'; +import Radio from '../../../components/controls/Radio'; +import { translate } from '../../../helpers/l10n'; + +export enum Plan { + Free = 'free', + Paid = 'paid' +} + +interface Props { + onChange: (plan: Plan) => void; + plan: Plan; + startingPrice: string; +} + +export default class PlanSelect extends React.PureComponent { + handleFreePlanClick = () => { + this.props.onChange(Plan.Free); + }; + + handlePaidPlanClick = () => { + this.props.onChange(Plan.Paid); + }; + + render() { + const { plan } = this.props; + return ( +
+
+ + {translate('billing.free_plan.title')} + +

+ {translate('billing.free_plan.description')} +

+
+
+ + {translate('billing.paid_plan.title')} + +

+ + {' '} + + {translate('learn_more')} + +
+ + ) + }} + /> +

+
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx new file mode 100644 index 00000000000..fc7050b6042 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import PaymentMethodSelect, { PaymentMethod } from './PaymentMethodSelect'; +import CardForm from './CardForm'; +import CouponForm from './CouponForm'; +import PlanSelect, { Plan } from './PlanSelect'; +import Step from '../../tutorials/components/Step'; +import { translate } from '../../../helpers/l10n'; +import { getExtensionStart } from '../../../app/components/extensions/utils'; +import { SubscriptionPlan } from '../../../app/types'; +import { SubmitButton } from '../../../components/ui/buttons'; + +interface Props { + createOrganization: () => Promise; + onFreePlanChoose: () => void; + onPaidPlanChoose: () => void; + onlyPaid?: boolean; + open: boolean; + startingPrice: string; + subscriptionPlans: SubscriptionPlan[]; +} + +interface State { + paymentMethod?: PaymentMethod; + plan: Plan; + ready: boolean; +} + +export default class PlanStep extends React.PureComponent { + mounted = false; + + constructor(props: Props) { + super(props); + this.state = { + plan: props.onlyPaid ? Plan.Paid : Plan.Free, + ready: false + }; + } + + componentDidMount() { + this.mounted = true; + getExtensionStart('billing/billing').then( + () => { + if (this.mounted) { + this.setState({ ready: true }); + } + }, + () => {} + ); + } + + componentWillUnmount() { + this.mounted = false; + } + + handlePlanChange = (plan: Plan) => { + this.setState({ plan }); + }; + + handlePaymentMethodChange = (paymentMethod: PaymentMethod) => { + this.setState({ paymentMethod }); + }; + + renderForm = () => { + return ( +
+ {this.state.ready && ( + <> + {!this.props.onlyPaid && ( + + )} + + {this.state.plan === Plan.Paid ? ( + <> + + {this.state.paymentMethod === PaymentMethod.Card && ( + + )} + {this.state.paymentMethod === PaymentMethod.Coupon && ( + + )} + + ) : ( + + {translate('my_account.create_organization')} + + )} + + )} +
+ ); + }; + + render() { + const stepTitle = translate( + this.props.onlyPaid + ? 'onboarding.create_organization.enter_payment_details' + : 'onboarding.create_organization.choose_plan' + ); + + return ( + {}} + open={this.props.open} + renderForm={this.renderForm} + renderResult={() => null} + stepNumber={2} + stepTitle={stepTitle} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx b/server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx new file mode 100644 index 00000000000..4c9b4e3596a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; + +export default class BillingFormShim extends React.Component<{ children: any }> { + render() { + return ( +
+ {this.props.children({ + alertError: undefined, + couponValue: '', + onSubmit: jest.fn(), + renderAdditionalInfo: () =>
, + renderBillingNameInput: () =>
, + renderBraintreeClient: () =>
, + renderCountrySelect: () =>
, + renderCouponInput: () =>
, + renderEmailInput: () =>
, + renderNextCharge: () =>
, + renderPlanSelect: () =>
, + renderResetButton: () =>
, + renderSpinner: () =>
, + renderSubmitButton: () =>
, + renderTermsOfService: () =>
, + renderTypeOfUseSelect: () =>
+ })} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx new file mode 100644 index 00000000000..91e2eb8a4ef --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import BillingFormShim from '../BillingFormShim'; + +beforeAll(() => { + function BillingForm() { + return
; + } + + (window as any).SonarBilling = { BillingForm }; +}); + +afterAll(() => { + delete (window as any).SonarBilling; +}); + +it('should render', () => { + expect( + shallow( + + {() =>
} + + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CardForm-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CardForm-test.tsx new file mode 100644 index 00000000000..be130c5f74b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CardForm-test.tsx @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { CardForm } from '../CardForm'; + +jest.mock('../BillingFormShim'); + +it('should render', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('BillingFormShim').dive()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CouponForm-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CouponForm-test.tsx new file mode 100644 index 00000000000..4b963011d94 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CouponForm-test.tsx @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { CouponForm } from '../CouponForm'; + +jest.mock('../BillingFormShim'); + +it('should render', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index 09a6cef02ef..3dce4baf976 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -20,26 +20,52 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import { CreateOrganization } from '../CreateOrganization'; -import { mockRouter } from '../../../../helpers/testUtils'; +import { mockRouter, waitAndUpdate } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/billing', () => ({ + getSubscriptionPlans: jest + .fn() + .mockResolvedValue([{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]) +})); + +const organization = { + avatar: 'http://example.com/avatar', + description: 'description-foo', + key: 'key-foo', + name: 'name-foo', + url: 'http://example.com/foo' +}; it('should render and create organization', async () => { const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' }); const router = mockRouter(); const wrapper = shallow( // @ts-ignore avoid passing everything from WithRouterProps - + ); + await waitAndUpdate(wrapper); expect(wrapper).toMatchSnapshot(); - const organization = { - avatar: 'http://example.com/avatar', - description: 'description-foo', - key: 'key-foo', - name: 'name-foo', - url: 'http://example.com/foo' - }; wrapper.find('OrganizationDetailsStep').prop('onContinue')(organization); - await new Promise(setImmediate); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('PlanStep').prop('onFreePlanChoose')(); + await waitAndUpdate(wrapper); expect(createOrganization).toBeCalledWith(organization); expect(router.push).toBeCalledWith('/organizations/foo'); }); + +it('should preselect paid plan', async () => { + const router = mockRouter(); + const location = { state: { paid: true } }; + const wrapper = shallow( + // @ts-ignore avoid passing everything from WithRouterProps + + ); + await waitAndUpdate(wrapper); + wrapper.find('OrganizationDetailsStep').prop('onContinue')(organization); + await waitAndUpdate(wrapper); + + expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx index 8d6ddf788aa..056856e7c6d 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx @@ -31,8 +31,15 @@ beforeEach(() => { (getOrganization as jest.Mock).mockResolvedValue(undefined); }); -it('should render', () => { - const wrapper = shallow(); +it('should render form', () => { + const wrapper = shallow( + + ); expect(wrapper).toMatchSnapshot(); expect(wrapper.dive()).toMatchSnapshot(); expect(getForm(wrapper)).toMatchSnapshot(); @@ -52,7 +59,14 @@ it('should render', () => { }); it('should validate', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); const instance = wrapper.instance() as OrganizationDetailsStep; expect( @@ -91,6 +105,19 @@ it('should validate', () => { ).rejects.toEqual({ key: 'onboarding.create_organization.organization_name.taken' }); }); +it('should render result', () => { + const wrapper = shallow( + + ); + expect(wrapper.dive()).toMatchSnapshot(); +}); + function getForm(wrapper: ShallowWrapper) { return wrapper .dive() diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PaymentMethodSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PaymentMethodSelect-test.tsx new file mode 100644 index 00000000000..fea97e71637 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PaymentMethodSelect-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import PaymentMethodSelect, { PaymentMethod } from '../PaymentMethodSelect'; + +it('should render and change', () => { + const onChange = jest.fn(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('RadioToggle').prop('onCheck')(PaymentMethod.Card); + expect(onChange).toBeCalledWith(PaymentMethod.Card); + + wrapper.setProps({ paymentMethod: PaymentMethod.Card }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx new file mode 100644 index 00000000000..ffe6c520eec --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import PlanSelect, { Plan } from '../PlanSelect'; + +it('should render and select', () => { + const onChange = jest.fn(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('Radio[checked=false]').prop('onCheck')(); + expect(onChange).toBeCalledWith(Plan.Paid); + + wrapper.setProps({ plan: Plan.Paid }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx new file mode 100644 index 00000000000..e1360eebab5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx @@ -0,0 +1,132 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import PlanStep from '../PlanStep'; +import { waitAndUpdate, click } from '../../../../helpers/testUtils'; +import { Plan } from '../PlanSelect'; +import { PaymentMethod } from '../PaymentMethodSelect'; + +jest.mock('../../../../app/components/extensions/utils', () => ({ + getExtensionStart: jest.fn().mockResolvedValue(undefined) +})); + +it('should render and use free plan', async () => { + const onFreePlanChoose = jest.fn(); + const wrapper = shallow( + + ); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.dive()).toMatchSnapshot(); + + click(wrapper.dive().find('SubmitButton')); + expect(onFreePlanChoose).toBeCalled(); +}); + +it('should upgrade using card', async () => { + const onPaidPlanChoose = jest.fn(); + const wrapper = shallow( + + ); + await waitAndUpdate(wrapper); + + wrapper + .dive() + .find('PlanSelect') + .prop('onChange')(Plan.Paid); + expect(wrapper.dive()).toMatchSnapshot(); + + wrapper + .dive() + .find('PaymentMethodSelect') + .prop('onChange')(PaymentMethod.Card); + expect(wrapper.dive()).toMatchSnapshot(); + + wrapper + .dive() + .find('Connect(withCurrentUser(CardForm))') + .prop('onSubmit')(); + expect(onPaidPlanChoose).toBeCalled(); +}); + +it('should upgrade using coupon', async () => { + const onPaidPlanChoose = jest.fn(); + const wrapper = shallow( + + ); + await waitAndUpdate(wrapper); + + wrapper + .dive() + .find('PlanSelect') + .prop('onChange')(Plan.Paid); + expect(wrapper.dive()).toMatchSnapshot(); + + wrapper + .dive() + .find('PaymentMethodSelect') + .prop('onChange')(PaymentMethod.Coupon); + expect(wrapper.dive()).toMatchSnapshot(); + + wrapper + .dive() + .find('Connect(withCurrentUser(CouponForm))') + .prop('onSubmit')(); + expect(onPaidPlanChoose).toBeCalled(); +}); + +it('should preselect paid plan', async () => { + const wrapper = shallow( + + ); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.dive()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap new file mode 100644 index 00000000000..5ed8a8cf920 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CardForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CardForm-test.tsx.snap new file mode 100644 index 00000000000..91a6ae106f1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CardForm-test.tsx.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +
+ +
+`; + +exports[`should render 2`] = ` +
+
+
+
+

+ billing.upgrade.billing_info +

+
+
+
+
+
+
+
+

+ billing.upgrade.plan +

+
+

+ billing.upgrade.card_info +

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CouponForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CouponForm-test.tsx.snap new file mode 100644 index 00000000000..738f9080adb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CouponForm-test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap index 96117496248..7892d99c530 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap @@ -19,17 +19,60 @@ exports[`should render and create organization 1`] = ` > onboarding.create_organization.page.header -
- - cancel - -
+ , + "more": + learn_more + , + "price": "billing.price_format.10", + } + } + /> +

+ + + + +
+ +`; + +exports[`should render and create organization 2`] = ` + + +
+
+

+ onboarding.create_organization.page.header +

@@ -42,19 +85,53 @@ exports[`should render and create organization 1`] = ` "more": learn_more , - "price": "€10", + "price": "billing.price_format.10", } } />

- + + + +
`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap index 71347ae70ab..d122f4cc2fb 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render 1`] = ` +exports[`should render form 1`] = ` `; -exports[`should render 2`] = ` +exports[`should render form 2`] = `
@@ -41,6 +41,7 @@ exports[`should render 2`] = ` "url": "", } } + isInitialValid={false} onSubmit={[MockFunction]} validate={[Function]} /> @@ -48,7 +49,7 @@ exports[`should render 2`] = `
`; -exports[`should render 3`] = ` +exports[`should render form 3`] = `
@@ -147,9 +148,44 @@ exports[`should render 3`] = ` - onboarding.create_organization.page.header + continue
`; + +exports[`should render result 1`] = ` +
+
+ 1 +
+
+ + + org + +
+
+

+ onboarding.create_organization.enter_org_details +

+
+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PaymentMethodSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PaymentMethodSelect-test.tsx.snap new file mode 100644 index 00000000000..101582f6012 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PaymentMethodSelect-test.tsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render and change 1`] = ` +
+ +
+ +
+
+`; + +exports[`should render and change 2`] = ` +
+ +
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap new file mode 100644 index 00000000000..53c6eb02270 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render and select 1`] = ` +
+
+ + + billing.free_plan.title + + +

+ billing.free_plan.description +

+
+
+ + + billing.paid_plan.title + + +

+ + + + learn_more + +
+ , + "price": "10", + } + } + /> +

+
+
+`; + +exports[`should render and select 2`] = ` +
+
+ + + billing.free_plan.title + + +

+ billing.free_plan.description +

+
+
+ + + billing.paid_plan.title + + +

+ + + + learn_more + +
+ , + "price": "10", + } + } + /> +

+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap new file mode 100644 index 00000000000..28b0d36d4f4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should preselect paid plan 1`] = ` + +`; + +exports[`should preselect paid plan 2`] = ` +
+
+ 2 +
+
+

+ onboarding.create_organization.enter_payment_details +

+
+
+ + + + + +
+
+`; + +exports[`should render and use free plan 1`] = ` + +`; + +exports[`should render and use free plan 2`] = ` +
+
+ 2 +
+
+

+ onboarding.create_organization.choose_plan +

+
+
+ + + + my_account.create_organization + + +
+
+`; + +exports[`should upgrade using card 1`] = ` +
+
+ 2 +
+
+

+ onboarding.create_organization.choose_plan +

+
+
+ + + + + + +
+
+`; + +exports[`should upgrade using card 2`] = ` +
+
+ 2 +
+
+

+ onboarding.create_organization.choose_plan +

+
+
+ + + + + + + +
+
+`; + +exports[`should upgrade using coupon 1`] = ` +
+
+ 2 +
+
+

+ onboarding.create_organization.choose_plan +

+
+
+ + + + + + +
+
+`; + +exports[`should upgrade using coupon 2`] = ` +
+
+ 2 +
+
+

+ onboarding.create_organization.choose_plan +

+
+
+ + + + + + + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx index c3c1fdeab24..4fc1ee27506 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx @@ -47,6 +47,7 @@ it('should not render for anonymous user', () => { function getRenderedType(wrapper: ShallowWrapper) { return wrapper + .dive() .dive() .dive() .type(); diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx new file mode 100644 index 00000000000..142f6e94f67 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { createStore } from 'redux'; +import { withCurrentUser } from '../withCurrentUser'; +import { CurrentUser } from '../../../../app/types'; + +class X extends React.Component<{ currentUser: CurrentUser }> { + render() { + return
; + } +} + +const UnderTest = withCurrentUser(X); + +it('should pass logged in user', () => { + const currentUser = { isLoggedIn: false }; + const store = createStore(state => state, { users: { currentUser } }); + const wrapper = shallow(, { context: { store } }); + expect(wrapper.dive().type()).toBe(X); + expect(wrapper.dive().prop('currentUser')).toBe(currentUser); +}); diff --git a/server/sonar-web/src/main/js/apps/create/organization/utils.ts b/server/sonar-web/src/main/js/apps/create/organization/utils.ts new file mode 100644 index 00000000000..29fc906c0d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/utils.ts @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { translateWithParameters } from '../../../helpers/l10n'; +import { formatMeasure } from '../../../helpers/measures'; + +export function formatPrice(price?: number, noSign?: boolean) { + const priceFormatted = formatMeasure(price, 'FLOAT') + .replace(/[.|,]0$/, '') + .replace(/([.|,]\d)$/, '$10'); + return noSign ? priceFormatted : translateWithParameters('billing.price_format', priceFormatted); +} diff --git a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx b/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx index 1535c852d2a..ae6e431535d 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx @@ -18,14 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { connect } from 'react-redux'; import { withRouter, WithRouterProps } from 'react-router'; +import { withCurrentUser } from './withCurrentUser'; import { CurrentUser, isLoggedIn } from '../../../app/types'; -import { Store, getCurrentUser } from '../../../store/rootReducer'; export function whenLoggedIn

(WrappedComponent: React.ComponentClass

) { + const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + class Wrapper extends React.Component

{ - static displayName = `whenLoggedIn(${WrappedComponent.displayName})`; + static displayName = `whenLoggedIn(${wrappedDisplayName})`; componentDidMount() { if (!isLoggedIn(this.props.currentUser)) { @@ -46,9 +47,5 @@ export function whenLoggedIn

(WrappedComponent: React.ComponentClass

) { } } - function mapStateToProps(state: Store) { - return { currentUser: getCurrentUser(state) }; - } - - return connect(mapStateToProps)(withRouter(Wrapper)); + return withCurrentUser(withRouter(Wrapper)); } diff --git a/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx b/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx new file mode 100644 index 00000000000..117af6607dc --- /dev/null +++ b/server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { CurrentUser } from '../../../app/types'; +import { Store, getCurrentUser } from '../../../store/rootReducer'; + +export function withCurrentUser

( + WrappedComponent: React.ComponentClass

+) { + const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + class Wrapper extends React.Component

{ + static displayName = `withCurrentUser(${wrappedDisplayName})`; + + render() { + return ; + } + } + + function mapStateToProps(state: Store) { + return { currentUser: getCurrentUser(state) }; + } + + return connect(mapStateToProps)(Wrapper); +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx index baca2d16467..039574a03fb 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { InjectedRouter } from 'react-router'; import OnboardingModal from './OnboardingModal'; import { skipOnboarding } from '../../../api/users'; import { skipOnboarding as skipOnboardingAction } from '../../../store/users'; @@ -30,6 +31,10 @@ interface DispatchProps { skipOnboardingAction: () => void; } +interface OwnProps { + router: InjectedRouter; +} + enum ModalKey { onboarding, teamOnboarding @@ -39,10 +44,9 @@ interface State { modal?: ModalKey; } -export class OnboardingPage extends React.PureComponent { +export class OnboardingPage extends React.PureComponent { static contextTypes = { - openProjectOnboarding: PropTypes.func.isRequired, - router: PropTypes.object.isRequired + openProjectOnboarding: PropTypes.func.isRequired }; state: State = { modal: ModalKey.onboarding }; @@ -50,16 +54,16 @@ export class OnboardingPage extends React.PureComponent { closeOnboarding = () => { skipOnboarding(); this.props.skipOnboardingAction(); - this.context.router.replace('/'); + this.props.router.replace('/'); }; closeOrganizationOnboarding = ({ key }: Pick) => { this.closeOnboarding(); - this.context.router.push(`/organizations/${key}`); + this.props.router.push(`/organizations/${key}`); }; openOrganizationOnboarding = () => { - this.context.router.push('/create-organizations'); + this.props.router.push({ pathname: '/create-organization', state: { paid: true } }); }; openTeamOnboarding = () => { diff --git a/server/sonar-web/src/main/js/components/controls/Radio.tsx b/server/sonar-web/src/main/js/components/controls/Radio.tsx new file mode 100644 index 00000000000..9209ad32c68 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/Radio.tsx @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; + +interface Props { + checked: boolean; + className?: string; + onCheck: () => void; +} + +export default class Radio extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onCheck(); + }; + + render() { + return ( + + + {this.props.children} + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/controls/ValidationForm.tsx b/server/sonar-web/src/main/js/components/controls/ValidationForm.tsx index 5f33868b67b..b4e061b3924 100644 --- a/server/sonar-web/src/main/js/components/controls/ValidationForm.tsx +++ b/server/sonar-web/src/main/js/components/controls/ValidationForm.tsx @@ -32,20 +32,28 @@ interface Props { } export default class ValidationForm extends React.Component> { + mounted = false; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + handleSubmit = (data: V, { setSubmitting }: FormikActions) => { const result = this.props.onSubmit(data); + const stopSubmitting = () => { + if (this.mounted) { + setSubmitting(false); + } + }; if (result) { - result.then( - () => { - setSubmitting(false); - }, - () => { - setSubmitting(false); - } - ); + result.then(stopSubmitting, stopSubmitting); } else { - setSubmitting(false); + stopSubmitting(); } }; diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Radio-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Radio-test.tsx new file mode 100644 index 00000000000..8d4a4f79aa8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Radio-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import Radio from '../Radio'; +import { click } from '../../../helpers/testUtils'; + +it('should render and check', () => { + const onCheck = jest.fn(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + + click(wrapper); + expect(onCheck).toBeCalled(); + wrapper.setProps({ checked: true }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap new file mode 100644 index 00000000000..92e8076ce5e --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render and check 1`] = ` + + + +`; + +exports[`should render and check 2`] = ` + + + +`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index d901aca6df8..c63dff4130a 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2717,6 +2717,11 @@ onboarding.create_organization.url=URL onboarding.create_organization.url.error=The value must be a valid url. onboarding.create_organization.description=Description onboarding.create_organization.enter_org_details=Enter your organization details +onboarding.create_organization.enter_payment_details=Enter payment details +onboarding.create_organization.choose_plan=Choose a plan +onboarding.create_organization.choose_payment_method=Choose payment solution +onboarding.create_organization.enter_your_coupon=Enter your coupon +onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade onboarding.team.header=Join a team onboarding.team.first_step=Well congrats, the first step is done! -- 2.39.5