onClose: () => void;
onCommit: () => void;
onCouponUpdate?: (coupon?: Coupon) => void;
+ onFailToUpgrade?: () => void;
organizationKey: string | (() => Promise<string>);
skipBraintreeInit?: boolean;
subscriptionPlans: SubscriptionPlan[];
interface Props {
createOrganization: () => Promise<string>;
currentUser: CurrentUser;
+ onFailToUpgrade: () => void;
onSubmit: () => void;
subscriptionPlans: SubscriptionPlan[];
}
currentUser={this.props.currentUser}
onClose={this.handleClose}
onCommit={this.props.onSubmit}
+ onFailToUpgrade={this.props.onFailToUpgrade}
organizationKey={this.props.createOrganization}
subscriptionPlans={this.props.subscriptionPlans}>
{form => (
interface Props {
createOrganization: () => Promise<string>;
currentUser: CurrentUser;
+ onFailToUpgrade: () => void;
onSubmit: () => void;
}
onClose={this.handleClose}
onCommit={this.props.onSubmit}
onCouponUpdate={this.handleCouponUpdate}
+ onFailToUpgrade={this.props.onFailToUpgrade}
organizationKey={this.props.createOrganization}
skipBraintreeInit={true}
subscriptionPlans={[]}>
import { FormattedMessage } from 'react-intl';
import { Link, withRouter, WithRouterProps } from 'react-router';
import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
import OrganizationDetailsStep from './OrganizationDetailsStep';
import PlanStep from './PlanStep';
import { formatPrice } from './utils';
import { whenLoggedIn } from './whenLoggedIn';
-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 * as api from '../../../api/organizations';
+import * as actions from '../../../store/organizations';
+import { Store } from '../../../store/rootReducer';
import '../../../app/styles/sonarcloud.css';
import '../../tutorials/styles.css'; // TODO remove me
interface Props {
createOrganization: (organization: OrganizationBase) => Promise<Organization>;
+ deleteOrganization: (key: string) => Promise<void>;
}
enum Step {
};
handleFreePlanChoose = () => {
- this.createOrganization().then(
- key => this.props.router.push(getOrganizationUrl(key)),
- () => {}
- );
+ return this.createOrganization().then(key => {
+ this.props.router.push(getOrganizationUrl(key));
+ });
};
createOrganization = () => {
}
};
+ deleteOrganization = () => {
+ const { organization } = this.state;
+ if (organization) {
+ this.props.deleteOrganization(organization.key).catch(() => {});
+ }
+ };
+
render() {
const { location } = this.props;
const { loading, subscriptionPlans } = this.state;
this.state.organization && (
<PlanStep
createOrganization={this.createOrganization}
+ deleteOrganization={this.deleteOrganization}
onFreePlanChoose={this.handleFreePlanChoose}
onPaidPlanChoose={this.handlePaidPlanChoose}
onlyPaid={location.state && location.state.paid === true}
}
}
-const mapDispatchToProps = { createOrganization: createOrganization as any };
+function createOrganization(organization: OrganizationBase) {
+ return (dispatch: Dispatch<Store>) => {
+ return api.createOrganization(organization).then((organization: Organization) => {
+ dispatch(actions.createOrganization(organization));
+ return organization;
+ });
+ };
+}
+
+function deleteOrganization(key: string) {
+ return (dispatch: Dispatch<Store>) => {
+ return api.deleteOrganization(key).then(() => {
+ dispatch(actions.deleteOrganization(key));
+ });
+ };
+}
+
+const mapDispatchToProps = {
+ createOrganization: createOrganization as any,
+ deleteOrganization: deleteOrganization as any
+};
export default whenLoggedIn(
connect(
import { getExtensionStart } from '../../../app/components/extensions/utils';
import { SubscriptionPlan } from '../../../app/types';
import { SubmitButton } from '../../../components/ui/buttons';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
interface Props {
createOrganization: () => Promise<string>;
- onFreePlanChoose: () => void;
+ deleteOrganization: () => void;
+ onFreePlanChoose: () => Promise<void>;
onPaidPlanChoose: () => void;
onlyPaid?: boolean;
open: boolean;
paymentMethod?: PaymentMethod;
plan: Plan;
ready: boolean;
+ submitting: boolean;
}
export default class PlanStep extends React.PureComponent<Props, State> {
super(props);
this.state = {
plan: props.onlyPaid ? Plan.Paid : Plan.Free,
- ready: false
+ ready: false,
+ submitting: false
};
}
this.setState({ paymentMethod });
};
+ stopSubmitting = () => {
+ if (this.mounted) {
+ this.setState({ submitting: false });
+ }
+ };
+
+ handleFreePlanSubmit = () => {
+ this.setState({ submitting: true });
+ this.props.onFreePlanChoose().then(this.stopSubmitting, this.stopSubmitting);
+ };
+
renderForm = () => {
return (
<div className="boxed-group-inner">
{this.state.paymentMethod === PaymentMethod.Card && (
<CardForm
createOrganization={this.props.createOrganization}
+ onFailToUpgrade={this.props.deleteOrganization}
onSubmit={this.props.onPaidPlanChoose}
subscriptionPlans={this.props.subscriptionPlans}
/>
{this.state.paymentMethod === PaymentMethod.Coupon && (
<CouponForm
createOrganization={this.props.createOrganization}
+ onFailToUpgrade={this.props.deleteOrganization}
onSubmit={this.props.onPaidPlanChoose}
/>
)}
</>
) : (
- <SubmitButton className="big-spacer-top" onClick={this.props.onFreePlanChoose}>
- {translate('my_account.create_organization')}
- </SubmitButton>
+ <div className="display-flex-center big-spacer-top">
+ <SubmitButton disabled={this.state.submitting} onClick={this.handleFreePlanSubmit}>
+ {translate('my_account.create_organization')}
+ </SubmitButton>
+ {this.state.submitting && <DeferredSpinner className="spacer-left" />}
+ </div>
)}
</>
)}
<CardForm
createOrganization={jest.fn()}
currentUser={{ isLoggedIn: false }}
+ onFailToUpgrade={jest.fn()}
onSubmit={jest.fn()}
subscriptionPlans={[{ maxNcloc: 100000, price: 10 }, { maxNcloc: 250000, price: 75 }]}
/>
<CouponForm
createOrganization={jest.fn()}
currentUser={{ isLoggedIn: false }}
+ onFailToUpgrade={jest.fn()}
onSubmit={jest.fn()}
/>
);
expect(wrapper.find('PlanStep').prop('onlyPaid')).toBe(true);
});
+
+it('should roll back after upgrade failure', async () => {
+ const createOrganization = jest.fn().mockResolvedValue({ key: 'foo' });
+ const deleteOrganization = jest.fn().mockResolvedValue(undefined);
+ const router = mockRouter();
+ const wrapper = shallow(
+ <CreateOrganization
+ createOrganization={createOrganization}
+ deleteOrganization={deleteOrganization}
+ // @ts-ignore avoid passing everything from WithRouterProps
+ location={{}}
+ // @ts-ignore avoid passing everything from WithRouterProps
+ router={router}
+ />
+ );
+ await waitAndUpdate(wrapper);
+
+ wrapper.find('OrganizationDetailsStep').prop<Function>('onContinue')(organization);
+ await waitAndUpdate(wrapper);
+
+ wrapper.find('PlanStep').prop<Function>('createOrganization')();
+ expect(createOrganization).toBeCalledWith(organization);
+
+ wrapper.find('PlanStep').prop<Function>('deleteOrganization')();
+ expect(deleteOrganization).toBeCalledWith(organization.key);
+});
}));
it('should render and use free plan', async () => {
- const onFreePlanChoose = jest.fn();
+ const onFreePlanChoose = jest.fn().mockResolvedValue(undefined);
const wrapper = shallow(
<PlanStep
createOrganization={jest.fn().mockResolvedValue('org')}
+ deleteOrganization={jest.fn().mockResolvedValue(undefined)}
onFreePlanChoose={onFreePlanChoose}
onPaidPlanChoose={jest.fn()}
open={true}
const wrapper = shallow(
<PlanStep
createOrganization={jest.fn().mockResolvedValue('org')}
- onFreePlanChoose={jest.fn()}
+ deleteOrganization={jest.fn().mockResolvedValue(undefined)}
+ onFreePlanChoose={jest.fn().mockResolvedValue(undefined)}
onPaidPlanChoose={onPaidPlanChoose}
open={true}
startingPrice="10"
const wrapper = shallow(
<PlanStep
createOrganization={jest.fn().mockResolvedValue('org')}
- onFreePlanChoose={jest.fn()}
+ deleteOrganization={jest.fn().mockResolvedValue(undefined)}
+ onFreePlanChoose={jest.fn().mockResolvedValue(undefined)}
onPaidPlanChoose={onPaidPlanChoose}
open={true}
startingPrice="10"
const wrapper = shallow(
<PlanStep
createOrganization={jest.fn()}
- onFreePlanChoose={jest.fn()}
+ deleteOrganization={jest.fn().mockResolvedValue(undefined)}
+ onFreePlanChoose={jest.fn().mockResolvedValue(undefined)}
onPaidPlanChoose={jest.fn()}
onlyPaid={true}
open={true}
}
onClose={[Function]}
onCommit={[MockFunction]}
+ onFailToUpgrade={[MockFunction]}
organizationKey={[MockFunction]}
subscriptionPlans={
Array [
onClose={[Function]}
onCommit={[MockFunction]}
onCouponUpdate={[Function]}
+ onFailToUpgrade={[MockFunction]}
organizationKey={[MockFunction]}
skipBraintreeInit={true}
subscriptionPlans={Array []}
/>
<PlanStep
createOrganization={[Function]}
+ deleteOrganization={[Function]}
onFreePlanChoose={[Function]}
onPaidPlanChoose={[Function]}
open={true}
plan="free"
startingPrice="10"
/>
- <SubmitButton
- className="big-spacer-top"
- onClick={[MockFunction]}
+ <div
+ className="display-flex-center big-spacer-top"
>
- my_account.create_organization
- </SubmitButton>
+ <SubmitButton
+ disabled={false}
+ onClick={[Function]}
+ >
+ my_account.create_organization
+ </SubmitButton>
+ </div>
</React.Fragment>
</div>
</div>
/>
<Connect(withCurrentUser(CardForm))
createOrganization={[MockFunction]}
+ onFailToUpgrade={[MockFunction]}
onSubmit={[MockFunction]}
subscriptionPlans={Array []}
/>
/>
<Connect(withCurrentUser(CouponForm))
createOrganization={[MockFunction]}
+ onFailToUpgrade={[MockFunction]}
onSubmit={[MockFunction]}
/>
</React.Fragment>