]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-43 Allow users to select the plan when creating an org (#705)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 18 Sep 2018 15:43:42 +0000 (17:43 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 25 Sep 2018 18:21:00 +0000 (20:21 +0200)
56 files changed:
server/sonar-docs/src/tooltips/billing/coupon.md [new file with mode: 0644]
server/sonar-web/src/main/js/api/billing.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/app/components/StartupModal.tsx
server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
server/sonar-web/src/main/js/app/components/extensions/Extension.js [deleted file]
server/sonar-web/src/main/js/app/components/extensions/Extension.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.js [deleted file]
server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.js [deleted file]
server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx
server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.js [deleted file]
server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js [deleted file]
server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/utils.js [deleted file]
server/sonar-web/src/main/js/app/components/extensions/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/styles/init/misc.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/create/organization/BillingFormShim.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/CardForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/CouponForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx
server/sonar-web/src/main/js/apps/create/organization/OrganizationDetailsStep.tsx
server/sonar-web/src/main/js/apps/create/organization/PaymentMethodSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/PlanSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/PlanStep.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__mocks__/BillingFormShim.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/BillingFormShim-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/CardForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/CouponForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/OrganizationDetailsStep-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/PaymentMethodSelect-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanSelect-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/PlanStep-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/BillingFormShim-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CardForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CouponForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/CreateOrganization-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/OrganizationDetailsStep-test.tsx.snap
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PaymentMethodSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/__snapshots__/PlanStep-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/__tests__/whenLoggedIn-test.tsx
server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/create/organization/whenLoggedIn.tsx
server/sonar-web/src/main/js/apps/create/organization/withCurrentUser.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingPage.tsx
server/sonar-web/src/main/js/components/controls/Radio.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/ValidationForm.tsx
server/sonar-web/src/main/js/components/controls/__tests__/Radio-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-docs/src/tooltips/billing/coupon.md b/server/sonar-docs/src/tooltips/billing/coupon.md
new file mode 100644 (file)
index 0000000..50319db
--- /dev/null
@@ -0,0 +1 @@
+A coupon is a way to pay for yearly subscriptions or to use other payment methods than card. Contact us for more information.
diff --git a/server/sonar-web/src/main/js/api/billing.ts b/server/sonar-web/src/main/js/api/billing.ts
new file mode 100644 (file)
index 0000000..f0a20dc
--- /dev/null
@@ -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
+  );
+}
index d924a697abf4bf3958cafea3f8089ca93932de9d..32ee2c1307971842a4314159f4f46cf2b7e048df 100644 (file)
@@ -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">
index 8481e4dc86ff8fc8f1330cd27eb31dbb7f7edae6..49863fa9ffc5ed86b519c1c03fb1119c373d952f 100644 (file)
@@ -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));
index 7e076dd9410981c9ada18cd1bbd87b95a2c34021..62feff5e4583a845ee3e7cce9622262cf7c17a6d 100644 (file)
  */
 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.js
deleted file mode 100644 (file)
index d2934d3..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import 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 { getExtensionStart } from './utils';
-import { translate } from '../../../helpers/l10n';
-import getStore from '../../utils/getStore';
-
-/*::
-type Props = {
-  currentUser: Object,
-  extension: {
-    key: string,
-    name: string
-  },
-  intl: Object,
-  location: { hash: string },
-  onFail: string => void,
-  options?: {},
-  router: Object
-};
-*/
-
-class Extension extends React.PureComponent {
-  /*:: container: Object; */
-  /*:: props: Props; */
-  /*:: stop: ?Function; */
-
-  static contextTypes = {
-    suggestions: PropTypes.object.isRequired
-  };
-
-  componentDidMount() {
-    this.startExtension();
-  }
-
-  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
-    ) {
-      this.startExtension();
-    }
-  }
-
-  componentWillUnmount() {
-    this.stopExtension();
-  }
-
-  handleStart = (start /*: Function */) => {
-    const store = getStore();
-    this.stop = start({
-      store,
-      el: this.container,
-      currentUser: this.props.currentUser,
-      intl: this.props.intl,
-      location: this.props.location,
-      router: this.props.router,
-      suggestions: this.context.suggestions,
-      ...this.props.options
-    });
-  };
-
-  handleFailure = () => {
-    this.props.onFail(translate('page_extension_failed'));
-  };
-
-  startExtension() {
-    const { extension } = this.props;
-    getExtensionStart(extension.key).then(this.handleStart, this.handleFailure);
-  }
-
-  stopExtension() {
-    if (this.stop) {
-      this.stop();
-      this.stop = null;
-    }
-  }
-
-  render() {
-    return (
-      <div>
-        <Helmet title={this.props.extension.name} />
-        <div ref={container => (this.container = container)} />
-      </div>
-    );
-  }
-}
-
-export default injectIntl(withRouter(Extension));
diff --git a/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx b/server/sonar-web/src/main/js/app/components/extensions/Extension.tsx
new file mode 100644 (file)
index 0000000..7aadebe
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import * as PropTypes from 'prop-types';
+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';
+
+interface OwnProps {
+  currentUser: CurrentUser;
+  extension: { key: string; name: string };
+  onFail: (message: string) => void;
+  options?: {};
+}
+
+type Props = OwnProps & WithRouterProps & InjectedIntlProps;
+
+class Extension extends React.PureComponent<Props> {
+  container?: HTMLElement | null;
+  stop?: Function;
+
+  static contextTypes = {
+    suggestions: PropTypes.object.isRequired
+  };
+
+  componentDidMount() {
+    this.startExtension();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.extension !== this.props.extension) {
+      this.stopExtension();
+      this.startExtension();
+    } else if (prevProps.location !== this.props.location) {
+      this.startExtension();
+    }
+  }
+
+  componentWillUnmount() {
+    this.stopExtension();
+  }
+
+  handleStart = (start: Function) => {
+    const store = getStore();
+    this.stop = start({
+      store,
+      el: this.container,
+      currentUser: this.props.currentUser,
+      intl: this.props.intl,
+      location: this.props.location,
+      router: this.props.router,
+      suggestions: this.context.suggestions,
+      ...this.props.options
+    });
+  };
+
+  handleFailure = () => {
+    this.props.onFail(translate('page_extension_failed'));
+  };
+
+  startExtension() {
+    const { extension } = this.props;
+    getExtensionStart(extension.key).then(this.handleStart, this.handleFailure);
+  }
+
+  stopExtension() {
+    if (this.stop) {
+      this.stop();
+      this.stop = undefined;
+    }
+  }
+
+  render() {
+    return (
+      <div>
+        <Helmet title={this.props.extension.name} />
+        <div ref={container => (this.container = container)} />
+      </div>
+    );
+  }
+}
+
+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.js
deleted file mode 100644 (file)
index 640ef48..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { connect } from 'react-redux';
-import ExtensionContainer from './ExtensionContainer';
-import NotFound from '../NotFound';
-import { getAppState } from '../../../store/rootReducer';
-
-/*::
-type Props = {
-  adminPages: Array<{ key: string }>,
-  params: {
-    extensionKey: string,
-    pluginKey: string
-  }
-};
-*/
-
-function GlobalAdminPageExtension(props /*: Props */) {
-  const { extensionKey, pluginKey } = props.params;
-  const extension = props.adminPages.find(p => p.key === `${pluginKey}/${extensionKey}`);
-  return extension ? (
-    <ExtensionContainer extension={extension} />
-  ) : (
-    <NotFound withContainer={false} />
-  );
-}
-
-const mapStateToProps = state => ({
-  adminPages: getAppState(state).adminPages
-});
-
-export default connect(mapStateToProps)(GlobalAdminPageExtension);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/GlobalAdminPageExtension.tsx
new file mode 100644 (file)
index 0000000..e479fbc
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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 ExtensionContainer from './ExtensionContainer';
+import NotFound from '../NotFound';
+import { Extension } from '../../types';
+import { getAppState, Store } from '../../../store/rootReducer';
+
+interface Props {
+  adminPages: Extension[] | undefined;
+  params: { extensionKey: string; pluginKey: string };
+}
+
+function GlobalAdminPageExtension(props: Props) {
+  const { extensionKey, pluginKey } = props.params;
+  const extension = (props.adminPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`);
+  return extension ? (
+    <ExtensionContainer extension={extension} />
+  ) : (
+    <NotFound withContainer={false} />
+  );
+}
+
+const mapStateToProps = (state: Store) => ({
+  adminPages: getAppState(state).adminPages
+});
+
+export default connect(mapStateToProps)(GlobalAdminPageExtension);
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.js
deleted file mode 100644 (file)
index ede95cb..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { connect } from 'react-redux';
-import ExtensionContainer from './ExtensionContainer';
-import NotFound from '../NotFound';
-import { getAppState } from '../../../store/rootReducer';
-
-/*::
-type Props = {
-  globalPages: Array<{ key: string }>,
-  params: {
-    extensionKey: string,
-    pluginKey: string
-  }
-};
-*/
-
-function GlobalPageExtension(props /*: Props */) {
-  const { extensionKey, pluginKey } = props.params;
-  const extension = props.globalPages.find(p => p.key === `${pluginKey}/${extensionKey}`);
-  return extension ? (
-    <ExtensionContainer extension={extension} />
-  ) : (
-    <NotFound withContainer={false} />
-  );
-}
-
-const mapStateToProps = state => ({
-  globalPages: getAppState(state).globalPages
-});
-
-export default connect(mapStateToProps)(GlobalPageExtension);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/GlobalPageExtension.tsx
new file mode 100644 (file)
index 0000000..8d7d535
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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 ExtensionContainer from './ExtensionContainer';
+import NotFound from '../NotFound';
+import { getAppState, Store } from '../../../store/rootReducer';
+import { Extension } from '../../types';
+
+interface Props {
+  globalPages: Extension[] | undefined;
+  params: { extensionKey: string; pluginKey: string };
+}
+
+function GlobalPageExtension(props: Props) {
+  const { extensionKey, pluginKey } = props.params;
+  const extension = (props.globalPages || []).find(p => p.key === `${pluginKey}/${extensionKey}`);
+  return extension ? (
+    <ExtensionContainer extension={extension} />
+  ) : (
+    <NotFound withContainer={false} />
+  );
+}
+
+const mapStateToProps = (state: Store) => ({
+  globalPages: getAppState(state).globalPages
+});
+
+export default connect(mapStateToProps)(GlobalPageExtension);
index a12e39986cd271fe067a2a98348d8e7cb9e78730..d1b1b181a8fde6a7a6c801b1f96a1b2082f0ba9c 100644 (file)
@@ -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.js
deleted file mode 100644 (file)
index 5f34a72..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import 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>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx b/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx
new file mode 100644 (file)
index 0000000..182fad8
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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 GlobalPageExtension from './GlobalPageExtension';
+
+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.js
deleted file mode 100644 (file)
index e39394c..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { connect } from 'react-redux';
-import ExtensionContainer from './ExtensionContainer';
-import NotFound from '../NotFound';
-import { addGlobalErrorMessage } from '../../../store/globalMessages';
-
-/*::
-type Props = {
-  component: {
-    configuration?: {
-      extensions: Array<{ key: string }>
-    }
-  },
-  location: { query: { id: string } },
-  params: {
-    extensionKey: string,
-    pluginKey: string
-  }
-};
-*/
-
-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}`);
-  return extension ? (
-    <ExtensionContainer extension={extension} options={{ component }} />
-  ) : (
-    <NotFound withContainer={false} />
-  );
-}
-
-const mapDispatchToProps = { onFail: addGlobalErrorMessage };
-
-export default connect(
-  null,
-  mapDispatchToProps
-)(ProjectAdminPageExtension);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.tsx
new file mode 100644 (file)
index 0000000..d4fb48c
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 { Location } from 'history';
+import ExtensionContainer from './ExtensionContainer';
+import NotFound from '../NotFound';
+import { addGlobalErrorMessage } from '../../../store/globalMessages';
+import { Component } from '../../types';
+
+interface Props {
+  component: Component;
+  location: Location;
+  params: { extensionKey: string; pluginKey: string };
+}
+
+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}`);
+  return extension ? (
+    <ExtensionContainer extension={extension} options={{ component }} />
+  ) : (
+    <NotFound withContainer={false} />
+  );
+}
+
+const mapDispatchToProps = { onFail: addGlobalErrorMessage };
+
+export default connect(
+  null,
+  mapDispatchToProps
+)(ProjectAdminPageExtension);
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.js
deleted file mode 100644 (file)
index 98600e2..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import exposeLibraries from './exposeLibraries';
-import { getExtensionFromCache } from '../../utils/installExtensionsHandler';
-
-function installScript(key /*: string */) {
-  return new Promise(resolve => {
-    exposeLibraries();
-    const scriptTag = document.createElement('script');
-    scriptTag.src = `${window.baseUrl}/static/${key}.js`;
-    scriptTag.onload = resolve;
-    document.getElementsByTagName('body')[0].appendChild(scriptTag);
-  });
-}
-
-export function getExtensionStart(key /*: string */) {
-  return new Promise((resolve, reject) => {
-    const fromCache = getExtensionFromCache(key);
-    if (fromCache) {
-      resolve(fromCache);
-      return;
-    }
-
-    installScript(key).then(() => {
-      const start = getExtensionFromCache(key);
-      if (start) {
-        resolve(start);
-      } else {
-        reject();
-      }
-    }, reject);
-  });
-}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/utils.ts b/server/sonar-web/src/main/js/app/components/extensions/utils.ts
new file mode 100644 (file)
index 0000000..84c9dcc
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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 exposeLibraries from './exposeLibraries';
+import { getExtensionFromCache } from '../../utils/installExtensionsHandler';
+import { getBaseUrl } from '../../../helpers/urls';
+
+function installScript(key: string) {
+  return new Promise(resolve => {
+    exposeLibraries();
+    const scriptTag = document.createElement('script');
+    scriptTag.src = `${getBaseUrl()}/static/${key}.js`;
+    scriptTag.onload = resolve;
+    document.getElementsByTagName('body')[0].appendChild(scriptTag);
+  });
+}
+
+export function getExtensionStart(key: string): Promise<Function> {
+  return new Promise((resolve, reject) => {
+    const fromCache = getExtensionFromCache(key);
+    if (fromCache) {
+      resolve(fromCache);
+      return;
+    }
+
+    installScript(key).then(() => {
+      const start = getExtensionFromCache(key);
+      if (start) {
+        resolve(start);
+      } else {
+        reject();
+      }
+    }, reject);
+  });
+}
index dcd5aff9e147ac47bd080268bb5ac5db39ed931e..b13e0a463a78e6506858b477b392209a1049abf1 100644 (file)
@@ -57,10 +57,6 @@ th.nowrap {
   font-size: var(--smallFontSize);
 }
 
-.note a {
-  color: var(--secondFontColor);
-}
-
 .spacer-left {
   margin-left: 8px !important;
 }
index 4e4153ad2365de9ac6684304843d5fc006cacb97..1ddd5c5e0d5eb8f74f96e11bd77cfaeb10a355fc 100644 (file)
@@ -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 (file)
index 0000000..8c83ec6
--- /dev/null
@@ -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 (file)
index 0000000..2212146
--- /dev/null
@@ -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 (file)
index 0000000..4747165
--- /dev/null
@@ -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);
index cf61cd192eda0e548b3ee8bfa0f7c6c8cf148c9f..7837907b045c1dbbf815dc32a17700b8a7f74832 100644 (file)
@@ -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>
       </>
     );
index ca40212da761cc42ab40bf2e7818b6467158b2a5..4c7f28a20a607a38d48e6752b356ada530441b35 100644 (file)
@@ -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 (file)
index 0000000..ddf5372
--- /dev/null
@@ -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 (file)
index 0000000..c5310dc
--- /dev/null
@@ -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 (file)
index 0000000..fc7050b
--- /dev/null
@@ -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 (file)
index 0000000..4c9b4e3
--- /dev/null
@@ -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 (file)
index 0000000..91e2eb8
--- /dev/null
@@ -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 (file)
index 0000000..be130c5
--- /dev/null
@@ -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 (file)
index 0000000..4b96301
--- /dev/null
@@ -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();
+});
index 09a6cef02ef50289063178a49ae33186c5445863..3dce4baf976e134742a530f5eec7a8d46ac949a4 100644 (file)
 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);
+});
index 8d6ddf788aad62ead6195cd7fe983d325ba37b11..056856e7c6d1f0f55a150e6ad8efdfdc2a73c531 100644 (file)
@@ -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 (file)
index 0000000..fea97e7
--- /dev/null
@@ -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 (file)
index 0000000..ffe6c52
--- /dev/null
@@ -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 (file)
index 0000000..e1360ee
--- /dev/null
@@ -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 (file)
index 0000000..5ed8a8c
--- /dev/null
@@ -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 (file)
index 0000000..91a6ae1
--- /dev/null
@@ -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 (file)
index 0000000..738f908
--- /dev/null
@@ -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>
+`;
index 96117496248a0f7e78de39d6d424d85010c39531..7892d99c5301725e9bb264248a6383045407d586 100644 (file)
@@ -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>
 `;
index 71347ae70abec4208d97ad01a58a479581dd27d9..d122f4cc2fbcb99948cb0370dd1622feacc15d5a 100644 (file)
@@ -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 (file)
index 0000000..101582f
--- /dev/null
@@ -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 (file)
index 0000000..53c6eb0
--- /dev/null
@@ -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 (file)
index 0000000..28b0d36
--- /dev/null
@@ -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>
+`;
index c3c1fdeab24aee95b4f17d67ca54a22448cc2563..4fc1ee27506f4c6b5bcf6496f448beeae1998dea 100644 (file)
@@ -47,6 +47,7 @@ it('should not render for anonymous user', () => {
 
 function getRenderedType(wrapper: ShallowWrapper) {
   return wrapper
+    .dive()
     .dive()
     .dive()
     .type();
diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/withCurrentUser-test.tsx
new file mode 100644 (file)
index 0000000..142f6e9
--- /dev/null
@@ -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 (file)
index 0000000..29fc906
--- /dev/null
@@ -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);
+}
index 1535c852d2a77e9dabc216b61714b0d85412e929..ae6e431535d0af74a3c1e39928a4a2962fdd2f80 100644 (file)
  * 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 (file)
index 0000000..117af66
--- /dev/null
@@ -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);
+}
index baca2d164672abb257576679baeb29922805aea6..039574a03fbf4858bdabc6be374a707194fca6c7 100644 (file)
@@ -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 (file)
index 0000000..9209ad3
--- /dev/null
@@ -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>
+    );
+  }
+}
index 5f33868b67b39271f0eba7d8aea46439c923ef07..b4e061b39247f6daefdc60647ff742d8f7f3d229 100644 (file)
@@ -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 (file)
index 0000000..8d4a4f7
--- /dev/null
@@ -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 (file)
index 0000000..92e8076
--- /dev/null
@@ -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>
+`;
index d901aca6df8bfd861926fe6295b41266df0a4888..c63dff4130a96d45a195365e92af40103c4600b6 100644 (file)
@@ -2717,6 +2717,11 @@ onboarding.create_organization.url=URL
 onboarding.create_organization.url.error=The value must be a valid url.
 onboarding.create_organization.description=Description
 onboarding.create_organization.enter_org_details=Enter your organization details
+onboarding.create_organization.enter_payment_details=Enter payment details
+onboarding.create_organization.choose_plan=Choose a plan
+onboarding.create_organization.choose_payment_method=Choose payment solution
+onboarding.create_organization.enter_your_coupon=Enter your coupon
+onboarding.create_organization.create_and_upgrade=Create Organization and Upgrade
 
 onboarding.team.header=Join a team
 onboarding.team.first_step=Well congrats, the first step is done!