aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-09-18 17:43:42 +0200
committerSonarTech <sonartech@sonarsource.com>2018-09-25 20:21:00 +0200
commitabb68832ff18c47f502cd2ab097b5b4b9fc3a509 (patch)
treedacf53d56390dc2855fa3d8c04ec02c25000beb9 /server/sonar-web/src/main/js
parentc003387eb63a644d9e887dcb7799d962ec27310c (diff)
downloadsonarqube-abb68832ff18c47f502cd2ab097b5b4b9fc3a509.tar.gz
sonarqube-abb68832ff18c47f502cd2ab097b5b4b9fc3a509.zip
SONARCLOUD-43 Allow users to select the plan when creating an org (#705)
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/api/billing.ts29
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/StartupModal.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx11
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/Extension.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/Extension.js)52
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js)25
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js)25
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx1
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.js)14
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js)28
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/utils.ts (renamed from server/sonar-web/src/main/js/app/components/extensions/utils.js)8
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css4
-rw-r--r--server/sonar-web/src/main/js/app/types.ts5
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CardForm.tsx89
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CouponForm.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx154
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PaymentMethodSelect.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx145
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx47
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CardForm-test.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CouponForm-test.tsx35
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx46
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/PaymentMethodSelect-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx132
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CardForm-test.tsx.snap106
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CouponForm-test.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap105
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap46
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PaymentMethodSelect-test.tsx.snap79
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap123
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap242
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx40
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/utils.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx16
-rw-r--r--server/sonar-web/src/main/js/components/controls/Radio.tsx56
-rw-r--r--server/sonar-web/src/main/js/components/controls/ValidationForm.tsx26
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Radio-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap29
48 files changed, 2194 insertions, 210 deletions
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<SubscriptionPlan[]> {
+ 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 (
<SuggestionsProvider>
{({ suggestions }) => (
- <StartupModal location={props.location}>
+ <StartupModal>
<div className="global-container">
<div className="page-wrapper" id="container">
<div className="page-container">
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<Props, State> {
- static contextTypes = {
- router: PropTypes.object.isRequired
- };
-
static childContextTypes = {
openProjectOnboarding: PropTypes.func
};
@@ -121,13 +117,13 @@ export class StartupModal extends React.PureComponent<Props, State> {
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
<StartupModal
canAdmin={true}
currentEdition={EditionKey.enterprise}
currentUser={LOGGED_IN_USER}
- location={{ pathname: 'foo/bar' }}
+ location={{ pathname: 'foo/bar' } as Location}
+ router={mockRouter() as InjectedRouter}
skipOnboardingAction={jest.fn()}
{...props}>
<div />
- </StartupModal>,
- { context: { router: { push: jest.fn() } } }
+ </StartupModal>
);
}
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
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<Props> {
+ 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<OwnProps>(withRouter<OwnProps & InjectedIntlProps>(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
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 ? (
<ExtensionContainer extension={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
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 ? (
<ExtensionContainer extension={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<Props> {
return extension ? (
<ExtensionContainer
extension={extension}
- location={this.props.location}
options={{ organization, refreshOrganization: this.refreshOrganization }}
/>
) : (
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
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 (
- <div>
- <GlobalPageExtension
- location={props.location}
- params={{ pluginKey: 'governance', extensionKey: 'portfolios' }}
- />
- </div>
- );
+export default function PortfoliosPage() {
+ return <GlobalPageExtension params={{ pluginKey: 'governance', extensionKey: 'portfolios' }} />;
}
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
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 ? (
<ExtensionContainer extension={extension} options={{ component }} />
) : (
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
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<Function> {
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<any>;
+ country?: string;
+ currentUser: CurrentUser;
+ onClose: () => void;
+ onCommit: () => void;
+ organizationKey: string | (() => Promise<string>);
+ subscriptionPlans: SubscriptionPlan[];
+}
+
+export default class BillingFormShim extends React.Component<Props> {
+ render() {
+ const { BillingForm } = (window as any).SonarBilling;
+ return <BillingForm {...this.props} />;
+ }
+}
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<string>;
+ currentUser: CurrentUser;
+ onSubmit: () => void;
+ subscriptionPlans: SubscriptionPlan[];
+}
+
+export class CardForm extends React.PureComponent<Props> {
+ handleClose = () => {
+ // do nothing
+ };
+
+ render() {
+ return (
+ <div className="huge-spacer-top">
+ <BillingFormShim
+ currentUser={this.props.currentUser}
+ onClose={this.handleClose}
+ onCommit={this.props.onSubmit}
+ organizationKey={this.props.createOrganization}
+ subscriptionPlans={this.props.subscriptionPlans}>
+ {form => (
+ <form onSubmit={form.onSubmit}>
+ <div className="columns column-show-overflow">
+ <div className="column-half">
+ <h3>{translate('billing.upgrade.billing_info')}</h3>
+ {form.renderEmailInput()}
+ {form.renderTypeOfUseSelect()}
+ {form.renderBillingNameInput()}
+ {form.renderCountrySelect()}
+ {form.renderAdditionalInfo()}
+ </div>
+ <div className="column-half">
+ <h3>{translate('billing.upgrade.plan')}</h3>
+ {form.renderPlanSelect()}
+ <h3>{translate('billing.upgrade.card_info')}</h3>
+ {form.renderBraintreeClient()}
+ </div>
+ </div>
+ <div className="upgrade-footer big-spacer-top">
+ {form.renderNextCharge()}
+ <hr className="big-spacer-bottom" />
+ {form.alertError && <p className="alert alert-danger">{form.alertError}</p>}
+ </div>
+ <div
+ className={classNames({
+ 'big-spacer-top': form.alertError !== undefined
+ })}>
+ {form.renderSpinner()}
+ {form.renderSubmitButton(
+ translate('onboarding.create_organization.create_and_upgrade')
+ )}
+ </div>
+ {form.renderTermsOfService()}
+ </form>
+ )}
+ </BillingFormShim>
+ </div>
+ );
+ }
+}
+
+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<string>;
+ currentUser: CurrentUser;
+ onSubmit: () => void;
+}
+
+export class CouponForm extends React.PureComponent<Props> {
+ handleClose = () => {
+ // do nothing
+ };
+
+ render() {
+ return (
+ <div className="huge-spacer-top">
+ <BillingFormShim
+ currentUser={this.props.currentUser}
+ onClose={this.handleClose}
+ onCommit={this.props.onSubmit}
+ organizationKey={this.props.createOrganization}
+ subscriptionPlans={[]}>
+ {form => (
+ <form onSubmit={form.onSubmit}>
+ <div className="hidden">{form.renderBraintreeClient()}</div>
+ {form.renderCouponInput(
+ <label htmlFor="coupon">
+ {translate('billing.upgrade.coupon')}
+ <DocTooltip
+ className="little-spacer-left"
+ doc={import(/* webpackMode: "eager" */ 'Docs/tooltips/billing/coupon.md')}
+ />
+ </label>
+ )}
+ <h3 className="big-spacer-top">{translate('billing.upgrade.billing_info')}</h3>
+ {form.renderEmailInput()}
+ {form.renderTypeOfUseSelect()}
+ {form.renderBillingNameInput()}
+ {form.renderCountrySelect()}
+ {form.renderAdditionalInfo()}
+ {form.alertError && <p className="alert alert-danger">{form.alertError}</p>}
+ <div className={classNames({ 'big-spacer-top': form.alertError !== undefined })}>
+ {form.renderSpinner()}
+ {form.renderSubmitButton(
+ translate('onboarding.create_organization.create_and_upgrade')
+ )}
+ </div>
+ {form.renderTermsOfService()}
+ </form>
+ )}
+ </BillingFormShim>
+ </div>
+ );
+ }
+}
+
+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<Organization>;
}
-export class CreateOrganization extends React.PureComponent<Props & WithRouterProps> {
+enum Step {
+ OrganizationDetails,
+ Plan
+}
+
+interface State {
+ loading: boolean;
+ organization?: Organization;
+ step: Step;
+ subscriptionPlans?: SubscriptionPlan[];
+}
+
+export class CreateOrganization extends React.PureComponent<Props & WithRouterProps, State> {
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<Props & WithRouterPr
document.body.classList.remove('white-page');
}
- handleOrganizationCreate = (organization: Required<OrganizationBase>) => {
- 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<OrganizationBase>) => {
+ 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<Props & WithRouterPr
<div className="sonarcloud page page-limited">
<header className="page-header">
<h1 className="page-title big-spacer-bottom">{header}</h1>
- <div className="page-actions">
- <Link to="/">{translate('cancel')}</Link>
- </div>
- <p className="page-description">
- <FormattedMessage
- defaultMessage={translate('onboarding.create_organization.page.description')}
- id="onboarding.create_organization.page.description"
- values={{
- break: <br />,
- price: '€10', // TODO
- more: (
- <Link to="/documentation/sonarcloud-pricing">{translate('learn_more')}</Link>
- )
- }}
- />
- </p>
+ {startedPrice !== undefined && (
+ <p className="page-description">
+ <FormattedMessage
+ defaultMessage={translate('onboarding.create_organization.page.description')}
+ id="onboarding.create_organization.page.description"
+ values={{
+ break: <br />,
+ price: formattedPrice,
+ more: (
+ <Link target="_blank" to="/documentation/sonarcloud-pricing">
+ {translate('learn_more')}
+ </Link>
+ )
+ }}
+ />
+ </p>
+ )}
</header>
- <OrganizationDetailsStep onContinue={this.handleOrganizationCreate} />
+ {loading ? (
+ <i className="spinner" />
+ ) : (
+ <>
+ <OrganizationDetailsStep
+ finished={this.state.organization !== undefined}
+ onContinue={this.handleOrganizationDetailsFinish}
+ onOpen={this.handleOrganizationDetailsStepOpen}
+ open={this.state.step === Step.OrganizationDetails}
+ organization={this.state.organization}
+ />
+
+ {subscriptionPlans !== undefined &&
+ this.state.organization && (
+ <PlanStep
+ createOrganization={this.createOrganization}
+ onFreePlanChoose={this.handleFreePlanChoose}
+ onPaidPlanChoose={this.handlePaidPlanChoose}
+ onlyPaid={location.state && location.state.paid === true}
+ open={this.state.step === Step.Plan}
+ startingPrice={formattedPrice}
+ subscriptionPlans={subscriptionPlans}
+ />
+ )}
+ </>
+ )}
</div>
</>
);
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<OrganizationBase>) => Promise<void>;
+ onOpen: () => void;
+ open: boolean;
+ organization?: OrganizationBase & { key: string };
}
interface State {
@@ -49,6 +54,21 @@ interface State {
export default class OrganizationDetailsStep extends React.PureComponent<Props, State> {
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<Props,
return Promise.reject(errors);
}
+ // TODO debounce
return this.checkFreeKey(key).then(free => {
if (!free) {
errors.key = translate('onboarding.create_organization.organization_name.taken');
@@ -178,10 +199,7 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
</div>
</div>
<div className="big-spacer-top">
- <SubmitButton disabled={isSubmitting || !isValid || !dirty}>
- {/* // TODO change me */}
- {translate('onboarding.create_organization.page.header')}
- </SubmitButton>
+ <SubmitButton disabled={isSubmitting || !isValid}>{translate('continue')}</SubmitButton>
</div>
</>
);
@@ -191,7 +209,8 @@ export default class OrganizationDetailsStep extends React.PureComponent<Props,
return (
<div className="boxed-group-inner">
<ValidationForm<Values>
- 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<Props,
);
};
+ renderResult = () => {
+ const { organization } = this.props;
+ return organization ? (
+ <div className="boxed-group-actions display-flex-center">
+ <AlertSuccessIcon className="spacer-right" />
+ <strong>{organization.key}</strong>
+ </div>
+ ) : null;
+ };
+
render() {
return (
<Step
- finished={false}
- onOpen={() => {}}
- open={true}
+ finished={this.props.finished}
+ onOpen={this.props.onOpen}
+ open={this.props.open}
renderForm={this.renderForm}
- renderResult={() => <div />}
+ 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<Props> {
+ render() {
+ const options = Object.values(PaymentMethod).map(value => ({
+ label: translate('billing', value),
+ value
+ }));
+
+ return (
+ <div>
+ <label className="spacer-bottom">
+ {translate('onboarding.create_organization.choose_payment_method')}
+ </label>
+ <div className="little-spacer-top">
+ <RadioToggle
+ name="payment-method"
+ onCheck={this.props.onChange}
+ options={options}
+ value={this.props.paymentMethod}
+ />
+ </div>
+ </div>
+ );
+ }
+}
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<Props> {
+ handleFreePlanClick = () => {
+ this.props.onChange(Plan.Free);
+ };
+
+ handlePaidPlanClick = () => {
+ this.props.onChange(Plan.Paid);
+ };
+
+ render() {
+ const { plan } = this.props;
+ return (
+ <div
+ aria-label={translate('onboarding.create_organization.choose_plan')}
+ className="huge-spacer-bottom"
+ role="radiogroup">
+ <div>
+ <Radio checked={plan === Plan.Free} onCheck={this.handleFreePlanClick}>
+ <span>{translate('billing.free_plan.title')}</span>
+ </Radio>
+ <p className="note markdown little-spacer-top">
+ {translate('billing.free_plan.description')}
+ </p>
+ </div>
+ <div className="big-spacer-top">
+ <Radio checked={plan === Plan.Paid} onCheck={this.handlePaidPlanClick}>
+ <span>{translate('billing.paid_plan.title')}</span>
+ </Radio>
+ <p className="note markdown little-spacer-top">
+ <FormattedMessage
+ defaultMessage={translate('billing.paid_plan.description')}
+ id="billing.paid_plan.description"
+ values={{
+ price: this.props.startingPrice,
+ more: (
+ <>
+ {' '}
+ <Link target="_blank" to="/documentation/sonarcloud-pricing">
+ {translate('learn_more')}
+ </Link>
+ <br />
+ </>
+ )
+ }}
+ />
+ </p>
+ </div>
+ </div>
+ );
+ }
+}
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<string>;
+ 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<Props, State> {
+ 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 (
+ <div className="boxed-group-inner">
+ {this.state.ready && (
+ <>
+ {!this.props.onlyPaid && (
+ <PlanSelect
+ onChange={this.handlePlanChange}
+ plan={this.state.plan}
+ startingPrice={this.props.startingPrice}
+ />
+ )}
+
+ {this.state.plan === Plan.Paid ? (
+ <>
+ <PaymentMethodSelect
+ onChange={this.handlePaymentMethodChange}
+ paymentMethod={this.state.paymentMethod}
+ />
+ {this.state.paymentMethod === PaymentMethod.Card && (
+ <CardForm
+ createOrganization={this.props.createOrganization}
+ onSubmit={this.props.onPaidPlanChoose}
+ subscriptionPlans={this.props.subscriptionPlans}
+ />
+ )}
+ {this.state.paymentMethod === PaymentMethod.Coupon && (
+ <CouponForm
+ createOrganization={this.props.createOrganization}
+ onSubmit={this.props.onPaidPlanChoose}
+ />
+ )}
+ </>
+ ) : (
+ <SubmitButton className="big-spacer-top" onClick={this.props.onFreePlanChoose}>
+ {translate('my_account.create_organization')}
+ </SubmitButton>
+ )}
+ </>
+ )}
+ </div>
+ );
+ };
+
+ render() {
+ const stepTitle = translate(
+ this.props.onlyPaid
+ ? 'onboarding.create_organization.enter_payment_details'
+ : 'onboarding.create_organization.choose_plan'
+ );
+
+ return (
+ <Step
+ finished={false}
+ onOpen={() => {}}
+ 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 (
+ <div id="BillingFormShim">
+ {this.props.children({
+ alertError: undefined,
+ couponValue: '',
+ onSubmit: jest.fn(),
+ renderAdditionalInfo: () => <div id="additional-info" />,
+ renderBillingNameInput: () => <div id="billing-name" />,
+ renderBraintreeClient: () => <div id="braintree-client" />,
+ renderCountrySelect: () => <div id="country-select" />,
+ renderCouponInput: () => <div id="coupon-input" />,
+ renderEmailInput: () => <div id="email-input" />,
+ renderNextCharge: () => <div id="next-charge" />,
+ renderPlanSelect: () => <div id="plan-select" />,
+ renderResetButton: () => <div id="reset-button" />,
+ renderSpinner: () => <div id="spinner" />,
+ renderSubmitButton: () => <div id="submit-button" />,
+ renderTermsOfService: () => <div id="terms-of-service" />,
+ renderTypeOfUseSelect: () => <div id="type-of-use-select" />
+ })}
+ </div>
+ );
+ }
+}
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 <div id="billing-form" />;
+ }
+
+ (window as any).SonarBilling = { BillingForm };
+});
+
+afterAll(() => {
+ delete (window as any).SonarBilling;
+});
+
+it('should render', () => {
+ expect(
+ shallow(
+ <BillingFormShim
+ currentUser={{ isLoggedIn: false }}
+ onClose={jest.fn()}
+ onCommit={jest.fn()}
+ organizationKey="org"
+ subscriptionPlans={[]}>
+ {() => <div id="inner-billing-form" />}
+ </BillingFormShim>
+ )
+ ).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(
+ <CardForm
+ createOrganization={jest.fn()}
+ currentUser={{ isLoggedIn: false }}
+ onSubmit={jest.fn()}
+ subscriptionPlans={[{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]}
+ />
+ );
+ 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(
+ <CouponForm
+ createOrganization={jest.fn()}
+ currentUser={{ isLoggedIn: false }}
+ onSubmit={jest.fn()}
+ />
+ );
+ 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
- <CreateOrganization createOrganization={createOrganization} router={router} />
+ <CreateOrganization createOrganization={createOrganization} location={{}} router={router} />
);
+ 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<Function>('onContinue')(organization);
- await new Promise(setImmediate);
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('PlanStep').prop<Function>('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
+ <CreateOrganization createOrganization={jest.fn()} location={location} router={router} />
+ );
+ await waitAndUpdate(wrapper);
+ wrapper.find('OrganizationDetailsStep').prop<Function>('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(<OrganizationDetailsStep onContinue={jest.fn()} />);
+it('should render form', () => {
+ const wrapper = shallow(
+ <OrganizationDetailsStep
+ finished={false}
+ onContinue={jest.fn()}
+ onOpen={jest.fn()}
+ open={true}
+ />
+ );
expect(wrapper).toMatchSnapshot();
expect(wrapper.dive()).toMatchSnapshot();
expect(getForm(wrapper)).toMatchSnapshot();
@@ -52,7 +59,14 @@ it('should render', () => {
});
it('should validate', () => {
- const wrapper = shallow(<OrganizationDetailsStep onContinue={jest.fn()} />);
+ const wrapper = shallow(
+ <OrganizationDetailsStep
+ finished={false}
+ onContinue={jest.fn()}
+ onOpen={jest.fn()}
+ open={true}
+ />
+ );
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(
+ <OrganizationDetailsStep
+ finished={true}
+ onContinue={jest.fn()}
+ onOpen={jest.fn()}
+ open={false}
+ organization={{ avatar: '', description: '', key: 'org', name: 'Organization', url: '' }}
+ />
+ );
+ 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(<PaymentMethodSelect onChange={onChange} paymentMethod={undefined} />);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('RadioToggle').prop<Function>('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(<PlanSelect onChange={onChange} plan={Plan.Free} startingPrice="10" />);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('Radio[checked=false]').prop<Function>('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(
+ <PlanStep
+ createOrganization={jest.fn().mockResolvedValue('org')}
+ onFreePlanChoose={onFreePlanChoose}
+ onPaidPlanChoose={jest.fn()}
+ open={true}
+ startingPrice="10"
+ subscriptionPlans={[]}
+ />
+ );
+ 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(
+ <PlanStep
+ createOrganization={jest.fn().mockResolvedValue('org')}
+ onFreePlanChoose={jest.fn()}
+ onPaidPlanChoose={onPaidPlanChoose}
+ open={true}
+ startingPrice="10"
+ subscriptionPlans={[]}
+ />
+ );
+ await waitAndUpdate(wrapper);
+
+ wrapper
+ .dive()
+ .find('PlanSelect')
+ .prop<Function>('onChange')(Plan.Paid);
+ expect(wrapper.dive()).toMatchSnapshot();
+
+ wrapper
+ .dive()
+ .find('PaymentMethodSelect')
+ .prop<Function>('onChange')(PaymentMethod.Card);
+ expect(wrapper.dive()).toMatchSnapshot();
+
+ wrapper
+ .dive()
+ .find('Connect(withCurrentUser(CardForm))')
+ .prop<Function>('onSubmit')();
+ expect(onPaidPlanChoose).toBeCalled();
+});
+
+it('should upgrade using coupon', async () => {
+ const onPaidPlanChoose = jest.fn();
+ const wrapper = shallow(
+ <PlanStep
+ createOrganization={jest.fn().mockResolvedValue('org')}
+ onFreePlanChoose={jest.fn()}
+ onPaidPlanChoose={onPaidPlanChoose}
+ open={true}
+ startingPrice="10"
+ subscriptionPlans={[]}
+ />
+ );
+ await waitAndUpdate(wrapper);
+
+ wrapper
+ .dive()
+ .find('PlanSelect')
+ .prop<Function>('onChange')(Plan.Paid);
+ expect(wrapper.dive()).toMatchSnapshot();
+
+ wrapper
+ .dive()
+ .find('PaymentMethodSelect')
+ .prop<Function>('onChange')(PaymentMethod.Coupon);
+ expect(wrapper.dive()).toMatchSnapshot();
+
+ wrapper
+ .dive()
+ .find('Connect(withCurrentUser(CouponForm))')
+ .prop<Function>('onSubmit')();
+ expect(onPaidPlanChoose).toBeCalled();
+});
+
+it('should preselect paid plan', async () => {
+ const wrapper = shallow(
+ <PlanStep
+ createOrganization={jest.fn()}
+ onFreePlanChoose={jest.fn()}
+ onPaidPlanChoose={jest.fn()}
+ onlyPaid={true}
+ open={true}
+ startingPrice="10"
+ subscriptionPlans={[]}
+ />
+ );
+ 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`] = `
+<BillingForm
+ currentUser={
+ Object {
+ "isLoggedIn": false,
+ }
+ }
+ onClose={[MockFunction]}
+ onCommit={[MockFunction]}
+ organizationKey="org"
+ subscriptionPlans={Array []}
+/>
+`;
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`] = `
+<div
+ className="huge-spacer-top"
+>
+ <BillingFormShim
+ currentUser={
+ Object {
+ "isLoggedIn": false,
+ }
+ }
+ onClose={[Function]}
+ onCommit={[MockFunction]}
+ organizationKey={[MockFunction]}
+ subscriptionPlans={
+ Array [
+ Object {
+ "maxNcloc": 100000,
+ "price": 10,
+ },
+ Object {
+ "maxNcloc": 250000,
+ "price": 75,
+ },
+ ]
+ }
+ />
+</div>
+`;
+
+exports[`should render 2`] = `
+<div
+ id="BillingFormShim"
+>
+ <form
+ onSubmit={[MockFunction]}
+ >
+ <div
+ className="columns column-show-overflow"
+ >
+ <div
+ className="column-half"
+ >
+ <h3>
+ billing.upgrade.billing_info
+ </h3>
+ <div
+ id="email-input"
+ />
+ <div
+ id="type-of-use-select"
+ />
+ <div
+ id="billing-name"
+ />
+ <div
+ id="country-select"
+ />
+ <div
+ id="additional-info"
+ />
+ </div>
+ <div
+ className="column-half"
+ >
+ <h3>
+ billing.upgrade.plan
+ </h3>
+ <div
+ id="plan-select"
+ />
+ <h3>
+ billing.upgrade.card_info
+ </h3>
+ <div
+ id="braintree-client"
+ />
+ </div>
+ </div>
+ <div
+ className="upgrade-footer big-spacer-top"
+ >
+ <div
+ id="next-charge"
+ />
+ <hr
+ className="big-spacer-bottom"
+ />
+ </div>
+ <div
+ className=""
+ >
+ <div
+ id="spinner"
+ />
+ <div
+ id="submit-button"
+ />
+ </div>
+ <div
+ id="terms-of-service"
+ />
+ </form>
+</div>
+`;
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`] = `
+<div
+ className="huge-spacer-top"
+>
+ <BillingFormShim
+ currentUser={
+ Object {
+ "isLoggedIn": false,
+ }
+ }
+ onClose={[Function]}
+ onCommit={[MockFunction]}
+ organizationKey={[MockFunction]}
+ subscriptionPlans={Array []}
+ />
+</div>
+`;
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
</h1>
- <div
- className="page-actions"
+ <p
+ className="page-description"
>
- <Link
- onlyActiveOnIndex={false}
- style={Object {}}
- to="/"
- >
- cancel
- </Link>
- </div>
+ <FormattedMessage
+ defaultMessage="onboarding.create_organization.page.description"
+ id="onboarding.create_organization.page.description"
+ values={
+ Object {
+ "break": <br />,
+ "more": <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/sonarcloud-pricing"
+ >
+ learn_more
+ </Link>,
+ "price": "billing.price_format.10",
+ }
+ }
+ />
+ </p>
+ </header>
+ <React.Fragment>
+ <OrganizationDetailsStep
+ finished={false}
+ onContinue={[Function]}
+ onOpen={[Function]}
+ open={true}
+ />
+ </React.Fragment>
+ </div>
+</React.Fragment>
+`;
+
+exports[`should render and create organization 2`] = `
+<React.Fragment>
+ <HelmetWrapper
+ defer={true}
+ encodeSpecialCharacters={true}
+ title="onboarding.create_organization.page.header"
+ titleTemplate="%s"
+ />
+ <div
+ className="sonarcloud page page-limited"
+ >
+ <header
+ className="page-header"
+ >
+ <h1
+ className="page-title big-spacer-bottom"
+ >
+ onboarding.create_organization.page.header
+ </h1>
<p
className="page-description"
>
@@ -42,19 +85,53 @@ exports[`should render and create organization 1`] = `
"more": <Link
onlyActiveOnIndex={false}
style={Object {}}
+ target="_blank"
to="/documentation/sonarcloud-pricing"
>
learn_more
</Link>,
- "price": "€10",
+ "price": "billing.price_format.10",
}
}
/>
</p>
</header>
- <OrganizationDetailsStep
- onContinue={[Function]}
- />
+ <React.Fragment>
+ <OrganizationDetailsStep
+ finished={true}
+ onContinue={[Function]}
+ onOpen={[Function]}
+ open={false}
+ organization={
+ Object {
+ "avatar": "http://example.com/avatar",
+ "description": "description-foo",
+ "key": "key-foo",
+ "name": "name-foo",
+ "url": "http://example.com/foo",
+ }
+ }
+ />
+ <PlanStep
+ createOrganization={[Function]}
+ onFreePlanChoose={[Function]}
+ onPaidPlanChoose={[Function]}
+ open={true}
+ startingPrice="billing.price_format.10"
+ subscriptionPlans={
+ Array [
+ Object {
+ "maxNcloc": 100000,
+ "price": 10,
+ },
+ Object {
+ "maxNcloc": 250000,
+ "price": 75,
+ },
+ ]
+ }
+ />
+ </React.Fragment>
</div>
</React.Fragment>
`;
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`] = `
<Step
finished={false}
- onOpen={[Function]}
+ onOpen={[MockFunction]}
open={true}
renderForm={[Function]}
renderResult={[Function]}
@@ -12,7 +12,7 @@ exports[`should render 1`] = `
/>
`;
-exports[`should render 2`] = `
+exports[`should render form 2`] = `
<div
className="boxed-group onboarding-step is-open"
>
@@ -41,6 +41,7 @@ exports[`should render 2`] = `
"url": "",
}
}
+ isInitialValid={false}
onSubmit={[MockFunction]}
validate={[Function]}
/>
@@ -48,7 +49,7 @@ exports[`should render 2`] = `
</div>
`;
-exports[`should render 3`] = `
+exports[`should render form 3`] = `
<form
onSubmit={[Function]}
>
@@ -147,9 +148,44 @@ exports[`should render 3`] = `
<SubmitButton
disabled={true}
>
- onboarding.create_organization.page.header
+ continue
</SubmitButton>
</div>
</React.Fragment>
</form>
`;
+
+exports[`should render result 1`] = `
+<div
+ className="boxed-group onboarding-step is-finished"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 1
+ </div>
+ <div
+ className="boxed-group-actions display-flex-center"
+ >
+ <AlertSuccessIcon
+ className="spacer-right"
+ />
+ <strong>
+ org
+ </strong>
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.enter_org_details
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ />
+</div>
+`;
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`] = `
+<div>
+ <label
+ className="spacer-bottom"
+ >
+ onboarding.create_organization.choose_payment_method
+ </label>
+ <div
+ className="little-spacer-top"
+ >
+ <RadioToggle
+ disabled={false}
+ name="payment-method"
+ onCheck={[MockFunction]}
+ options={
+ Array [
+ Object {
+ "label": "billing.card",
+ "value": "card",
+ },
+ Object {
+ "label": "billing.coupon",
+ "value": "coupon",
+ },
+ ]
+ }
+ value={null}
+ />
+ </div>
+</div>
+`;
+
+exports[`should render and change 2`] = `
+<div>
+ <label
+ className="spacer-bottom"
+ >
+ onboarding.create_organization.choose_payment_method
+ </label>
+ <div
+ className="little-spacer-top"
+ >
+ <RadioToggle
+ disabled={false}
+ name="payment-method"
+ onCheck={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ "card",
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ options={
+ Array [
+ Object {
+ "label": "billing.card",
+ "value": "card",
+ },
+ Object {
+ "label": "billing.coupon",
+ "value": "coupon",
+ },
+ ]
+ }
+ value="card"
+ />
+ </div>
+</div>
+`;
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`] = `
+<div
+ aria-label="onboarding.create_organization.choose_plan"
+ className="huge-spacer-bottom"
+ role="radiogroup"
+>
+ <div>
+ <Radio
+ checked={true}
+ onCheck={[Function]}
+ >
+ <span>
+ billing.free_plan.title
+ </span>
+ </Radio>
+ <p
+ className="note markdown little-spacer-top"
+ >
+ billing.free_plan.description
+ </p>
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <Radio
+ checked={false}
+ onCheck={[Function]}
+ >
+ <span>
+ billing.paid_plan.title
+ </span>
+ </Radio>
+ <p
+ className="note markdown little-spacer-top"
+ >
+ <FormattedMessage
+ defaultMessage="billing.paid_plan.description"
+ id="billing.paid_plan.description"
+ values={
+ Object {
+ "more": <React.Fragment>
+
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/sonarcloud-pricing"
+ >
+ learn_more
+ </Link>
+ <br />
+ </React.Fragment>,
+ "price": "10",
+ }
+ }
+ />
+ </p>
+ </div>
+</div>
+`;
+
+exports[`should render and select 2`] = `
+<div
+ aria-label="onboarding.create_organization.choose_plan"
+ className="huge-spacer-bottom"
+ role="radiogroup"
+>
+ <div>
+ <Radio
+ checked={false}
+ onCheck={[Function]}
+ >
+ <span>
+ billing.free_plan.title
+ </span>
+ </Radio>
+ <p
+ className="note markdown little-spacer-top"
+ >
+ billing.free_plan.description
+ </p>
+ </div>
+ <div
+ className="big-spacer-top"
+ >
+ <Radio
+ checked={true}
+ onCheck={[Function]}
+ >
+ <span>
+ billing.paid_plan.title
+ </span>
+ </Radio>
+ <p
+ className="note markdown little-spacer-top"
+ >
+ <FormattedMessage
+ defaultMessage="billing.paid_plan.description"
+ id="billing.paid_plan.description"
+ values={
+ Object {
+ "more": <React.Fragment>
+
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation/sonarcloud-pricing"
+ >
+ learn_more
+ </Link>
+ <br />
+ </React.Fragment>,
+ "price": "10",
+ }
+ }
+ />
+ </p>
+ </div>
+</div>
+`;
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`] = `
+<Step
+ finished={false}
+ onOpen={[Function]}
+ open={true}
+ renderForm={[Function]}
+ renderResult={[Function]}
+ stepNumber={2}
+ stepTitle="onboarding.create_organization.enter_payment_details"
+/>
+`;
+
+exports[`should preselect paid plan 2`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 2
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.enter_payment_details
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <React.Fragment>
+ <React.Fragment>
+ <PaymentMethodSelect
+ onChange={[Function]}
+ />
+ </React.Fragment>
+ </React.Fragment>
+ </div>
+</div>
+`;
+
+exports[`should render and use free plan 1`] = `
+<Step
+ finished={false}
+ onOpen={[Function]}
+ open={true}
+ renderForm={[Function]}
+ renderResult={[Function]}
+ stepNumber={2}
+ stepTitle="onboarding.create_organization.choose_plan"
+/>
+`;
+
+exports[`should render and use free plan 2`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 2
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.choose_plan
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <React.Fragment>
+ <PlanSelect
+ onChange={[Function]}
+ plan="free"
+ startingPrice="10"
+ />
+ <SubmitButton
+ className="big-spacer-top"
+ onClick={[MockFunction]}
+ >
+ my_account.create_organization
+ </SubmitButton>
+ </React.Fragment>
+ </div>
+</div>
+`;
+
+exports[`should upgrade using card 1`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 2
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.choose_plan
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <React.Fragment>
+ <PlanSelect
+ onChange={[Function]}
+ plan="paid"
+ startingPrice="10"
+ />
+ <React.Fragment>
+ <PaymentMethodSelect
+ onChange={[Function]}
+ />
+ </React.Fragment>
+ </React.Fragment>
+ </div>
+</div>
+`;
+
+exports[`should upgrade using card 2`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 2
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.choose_plan
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <React.Fragment>
+ <PlanSelect
+ onChange={[Function]}
+ plan="paid"
+ startingPrice="10"
+ />
+ <React.Fragment>
+ <PaymentMethodSelect
+ onChange={[Function]}
+ paymentMethod="card"
+ />
+ <Connect(withCurrentUser(CardForm))
+ createOrganization={[MockFunction]}
+ onSubmit={[MockFunction]}
+ subscriptionPlans={Array []}
+ />
+ </React.Fragment>
+ </React.Fragment>
+ </div>
+</div>
+`;
+
+exports[`should upgrade using coupon 1`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 2
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.choose_plan
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <React.Fragment>
+ <PlanSelect
+ onChange={[Function]}
+ plan="paid"
+ startingPrice="10"
+ />
+ <React.Fragment>
+ <PaymentMethodSelect
+ onChange={[Function]}
+ />
+ </React.Fragment>
+ </React.Fragment>
+ </div>
+</div>
+`;
+
+exports[`should upgrade using coupon 2`] = `
+<div
+ className="boxed-group onboarding-step is-open"
+>
+ <div
+ className="onboarding-step-number"
+ >
+ 2
+ </div>
+ <div
+ className="boxed-group-header"
+ >
+ <h2>
+ onboarding.create_organization.choose_plan
+ </h2>
+ </div>
+ <div
+ className="boxed-group-inner"
+ >
+ <React.Fragment>
+ <PlanSelect
+ onChange={[Function]}
+ plan="paid"
+ startingPrice="10"
+ />
+ <React.Fragment>
+ <PaymentMethodSelect
+ onChange={[Function]}
+ paymentMethod="coupon"
+ />
+ <Connect(withCurrentUser(CouponForm))
+ createOrganization={[MockFunction]}
+ onSubmit={[MockFunction]}
+ />
+ </React.Fragment>
+ </React.Fragment>
+ </div>
+</div>
+`;
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
@@ -49,5 +49,6 @@ 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 <div />;
+ }
+}
+
+const UnderTest = withCurrentUser(X);
+
+it('should pass logged in user', () => {
+ const currentUser = { isLoggedIn: false };
+ const store = createStore(state => state, { users: { currentUser } });
+ const wrapper = shallow(<UnderTest />, { 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<P>(WrappedComponent: React.ComponentClass<P>) {
+ const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
class Wrapper extends React.Component<P & { currentUser: CurrentUser } & WithRouterProps> {
- static displayName = `whenLoggedIn(${WrappedComponent.displayName})`;
+ static displayName = `whenLoggedIn(${wrappedDisplayName})`;
componentDidMount() {
if (!isLoggedIn(this.props.currentUser)) {
@@ -46,9 +47,5 @@ export function whenLoggedIn<P>(WrappedComponent: React.ComponentClass<P>) {
}
}
- 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<P>(
+ WrappedComponent: React.ComponentClass<P & { currentUser: CurrentUser }>
+) {
+ const wrappedDisplayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
+
+ class Wrapper extends React.Component<P & { currentUser: CurrentUser }> {
+ static displayName = `withCurrentUser(${wrappedDisplayName})`;
+
+ render() {
+ return <WrappedComponent {...this.props} />;
+ }
+ }
+
+ 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<DispatchProps, State> {
+export class OnboardingPage extends React.PureComponent<OwnProps & DispatchProps, State> {
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<DispatchProps, State> {
closeOnboarding = () => {
skipOnboarding();
this.props.skipOnboardingAction();
- this.context.router.replace('/');
+ this.props.router.replace('/');
};
closeOrganizationOnboarding = ({ key }: Pick<Organization, 'key'>) => {
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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onCheck();
+ };
+
+ render() {
+ return (
+ <a
+ aria-checked={this.props.checked}
+ className={classNames(
+ 'display-inline-flex-center link-base-color link-no-underline',
+ this.props.className
+ )}
+ href="#"
+ onClick={this.handleClick}
+ role="radio">
+ <i
+ className={classNames('icon-radio', 'spacer-right', {
+ 'is-checked': this.props.checked
+ })}
+ />
+ {this.props.children}
+ </a>
+ );
+ }
+}
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<V> {
}
export default class ValidationForm<V> extends React.Component<Props<V>> {
+ mounted = false;
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
handleSubmit = (data: V, { setSubmitting }: FormikActions<V>) => {
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(<Radio checked={false} onCheck={onCheck} />);
+ 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`] = `
+<a
+ aria-checked={false}
+ className="display-inline-flex-center link-base-color link-no-underline"
+ href="#"
+ onClick={[Function]}
+ role="radio"
+>
+ <i
+ className="icon-radio spacer-right"
+ />
+</a>
+`;
+
+exports[`should render and check 2`] = `
+<a
+ aria-checked={true}
+ className="display-inline-flex-center link-base-color link-no-underline"
+ href="#"
+ onClick={[Function]}
+ role="radio"
+>
+ <i
+ className="icon-radio spacer-right is-checked"
+ />
+</a>
+`;