123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- import React, {Fragment} from 'react';
- import classNames from 'classnames';
- import {addLoadingMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {Client} from 'sentry/api';
- import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
- import NavTabs from 'sentry/components/navTabs';
- import type {AdminConfirmRenderProps} from 'admin/components/adminConfirmationModal';
- import PlanList from 'admin/components/planList';
- import {ANNUAL, MONTHLY} from 'getsentry/constants';
- import type {BillingConfig} from 'getsentry/types';
- import {CheckoutType, PlanTier} from 'getsentry/types';
- import {getAmPlanTier} from 'getsentry/utils/billing';
- type Props = DeprecatedAsyncComponent['props'] &
- AdminConfirmRenderProps & {
- orgId: string;
- partnerPlanId: string | null;
- };
- type State = DeprecatedAsyncComponent['state'] & {
- activeTier: Exclude<PlanTier, PlanTier.MM1>;
- am1BillingConfig: BillingConfig | null;
- am2BillingConfig: BillingConfig | null;
- am3BillingConfig: BillingConfig | null;
- billingInterval: 'monthly' | 'annual';
- contractInterval: 'monthly' | 'annual';
- mm2BillingConfig: BillingConfig | null;
- plan: null | string;
- reservedAttachments: null | number;
- reservedErrors: null | number;
- reservedMonitorSeats: null | number;
- reservedReplays: null | number;
- reservedSpans: null | number;
- reservedTransactions: null | number;
- reservedUptime: null | number;
- };
- /**
- * Rendered as part of a openAdminConfirmModal call
- */
- class ChangePlanAction extends DeprecatedAsyncComponent<Props, State> {
- componentDidMount() {
- super.componentDidMount();
- this.props.setConfirmCallback(this.handleConfirm);
- }
- getDefaultState() {
- return {
- ...super.getDefaultState(),
- plan: this.props.partnerPlanId,
- reservedErrors: null,
- reservedTransactions: null,
- reservedReplays: null,
- reservedAttachments: null,
- reservedMonitorSeats: null,
- reservedUptime: null,
- reservedSpans: null,
- activeTier: this.props.partnerPlanId
- ? getAmPlanTier(this.props.partnerPlanId)
- : PlanTier.AM3,
- billingInterval: MONTHLY,
- contractInterval: MONTHLY,
- am1BillingConfig: null,
- mm2BillingConfig: null,
- };
- }
- getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
- return [
- ['mm2BillingConfig', `/customers/${this.props.orgId}/billing-config/?tier=mm2`],
- ['am1BillingConfig', `/customers/${this.props.orgId}/billing-config/?tier=am1`],
- ['am2BillingConfig', `/customers/${this.props.orgId}/billing-config/?tier=am2`],
- ['am3BillingConfig', `/customers/${this.props.orgId}/billing-config/?tier=am3`],
- ];
- }
- handleConfirm = async () => {
- const {onConfirm, orgId} = this.props;
- const {
- activeTier,
- plan,
- reservedErrors,
- reservedTransactions,
- reservedReplays,
- reservedAttachments,
- reservedMonitorSeats,
- reservedUptime,
- reservedSpans,
- reservedProfileDuration,
- } = this.state;
- const api = new Client();
- addLoadingMessage('Updating plan\u2026');
- if (activeTier === PlanTier.MM2) {
- const data = {plan};
- try {
- await api.requestPromise(`/customers/${orgId}/`, {
- method: 'PUT',
- data,
- });
- addSuccessMessage(
- `Customer account has been updated with ${JSON.stringify(data)}.`
- );
- onConfirm?.(data);
- } catch (error) {
- onConfirm?.({error});
- }
- return;
- }
- // AM plans use a different endpoint to update plans.
- const data: {
- plan: string | null;
- reservedAttachments: number | null;
- reservedErrors: number | null;
- reservedMonitorSeats: number | null;
- reservedReplays: number | null;
- reservedUptime: number | null;
- reservedProfileDuration?: number | null;
- reservedSpans?: number | null;
- reservedTransactions?: number | null;
- } = {
- plan,
- reservedErrors,
- reservedReplays,
- reservedAttachments,
- reservedMonitorSeats,
- reservedUptime,
- reservedProfileDuration,
- };
- if (reservedSpans) {
- data.reservedSpans = reservedSpans;
- }
- if (reservedTransactions) {
- data.reservedTransactions = reservedTransactions;
- }
- try {
- await api.requestPromise(`/customers/${orgId}/subscription/`, {
- method: 'PUT',
- data,
- });
- addSuccessMessage(
- `Customer account has been updated with ${JSON.stringify(data)}.`
- );
- onConfirm?.(data);
- } catch (error) {
- onConfirm?.({error});
- }
- };
- canSubmit() {
- const {
- activeTier,
- plan,
- reservedErrors,
- reservedTransactions,
- reservedAttachments,
- reservedReplays,
- reservedMonitorSeats,
- reservedUptime,
- reservedSpans,
- reservedProfileDuration,
- am2BillingConfig,
- am3BillingConfig,
- } = this.state;
- if (activeTier === PlanTier.MM2 && plan) {
- return true;
- }
- // TODO(brendan): remove checking profileDuration !== undefined once we launch profile duration
- const profileDurationTier =
- (activeTier === PlanTier.AM2 &&
- am2BillingConfig?.defaultReserved.profileDuration !== undefined) ||
- (activeTier === PlanTier.AM3 &&
- am3BillingConfig?.defaultReserved.profileDuration !== undefined);
- return (
- plan &&
- reservedErrors &&
- reservedReplays &&
- reservedAttachments &&
- reservedMonitorSeats &&
- reservedUptime &&
- (profileDurationTier ? reservedProfileDuration >= 0 : true) &&
- (reservedTransactions || reservedSpans)
- );
- }
- handlePlanChange = (planId: string) => {
- this.setState({plan: planId}, () => {
- this.props.disableConfirmButton(!this.canSubmit());
- });
- };
- handleLimitChange = (
- limit:
- | 'reservedErrors'
- | 'reservedTransactions'
- | 'reservedReplays'
- | 'reservedAttachments'
- | 'reservedMonitorSeats'
- | 'reservedUptime'
- | 'reservedSpans'
- | 'reservedProfileDuration',
- value: number
- ) => {
- this.setState({[limit]: value}, () => {
- this.props.disableConfirmButton(!this.canSubmit());
- });
- };
- renderBody() {
- const {
- plan,
- reservedErrors,
- reservedAttachments,
- reservedReplays,
- reservedTransactions,
- reservedMonitorSeats,
- reservedUptime,
- reservedSpans,
- reservedProfileDuration,
- activeTier,
- loading,
- billingInterval,
- am1BillingConfig,
- am2BillingConfig,
- am3BillingConfig,
- mm2BillingConfig,
- contractInterval,
- } = this.state;
- const {partnerPlanId} = this.props;
- if (loading) {
- return null;
- }
- let planList: BillingConfig['planList'] = [];
- if (activeTier === PlanTier.MM2 && mm2BillingConfig) {
- planList = mm2BillingConfig.planList;
- } else if (activeTier === PlanTier.AM1 && am1BillingConfig) {
- planList = am1BillingConfig.planList;
- } else if (activeTier === PlanTier.AM2 && am2BillingConfig) {
- planList = am2BillingConfig.planList;
- } else if (activeTier === PlanTier.AM3 && am3BillingConfig) {
- planList = am3BillingConfig.planList;
- }
- planList = planList
- .sort((a, b) => a.reservedMinimum - b.reservedMinimum)
- .filter(
- p =>
- p.price &&
- p.contractInterval === contractInterval &&
- p.billingInterval === billingInterval &&
- (p.userSelectable || p.checkoutType === CheckoutType.BUNDLE) &&
- // Plan id on partner sponsored subscriptions is not modifiable so only including
- // the existing plan in the list
- (partnerPlanId === null || partnerPlanId === p.id)
- );
- // Plan for partner sponsored subscriptions is not modifiable so skipping
- // the navigation that will allow modifying billing cycle and plan tier
- const header = partnerPlanId ? null : (
- <React.Fragment>
- <NavTabs>
- <li className={activeTier === PlanTier.AM3 ? 'active' : ''}>
- <a
- data-test-id="am3-tier"
- onClick={() =>
- this.setState({
- activeTier: PlanTier.AM3,
- billingInterval: MONTHLY,
- contractInterval: MONTHLY,
- plan: null,
- })
- }
- >
- AM3
- </a>
- </li>
- <li className={activeTier === PlanTier.AM2 ? 'active' : ''}>
- <a
- data-test-id="am2-tier"
- onClick={() =>
- this.setState({
- activeTier: PlanTier.AM2,
- billingInterval: MONTHLY,
- contractInterval: MONTHLY,
- plan: null,
- })
- }
- >
- AM2
- </a>
- </li>
- <li className={activeTier === PlanTier.AM1 ? 'active' : ''}>
- <a
- data-test-id="am1-tier"
- role="link"
- aria-disabled
- onClick={() =>
- this.setState({
- activeTier: PlanTier.AM1,
- billingInterval: MONTHLY,
- contractInterval: MONTHLY,
- plan: null,
- })
- }
- >
- AM1
- </a>
- </li>
- <li className={activeTier === PlanTier.MM2 ? 'active' : ''}>
- <a
- data-test-id="mm2-tier"
- onClick={() =>
- this.setState({
- activeTier: PlanTier.MM2,
- billingInterval: MONTHLY,
- contractInterval: MONTHLY,
- plan: null,
- })
- }
- >
- MM2
- </a>
- </li>
- </NavTabs>
- <ul className="nav nav-pills">
- <li
- className={classNames({
- active: contractInterval === MONTHLY && billingInterval === MONTHLY,
- })}
- >
- <a
- onClick={() =>
- this.setState({
- billingInterval: MONTHLY,
- contractInterval: MONTHLY,
- plan: null,
- })
- }
- >
- Monthly
- </a>
- </li>
- {activeTier === PlanTier.MM2 && (
- <li
- className={classNames({
- active: contractInterval === ANNUAL && billingInterval === MONTHLY,
- })}
- >
- <a
- onClick={() =>
- this.setState({
- billingInterval: MONTHLY,
- contractInterval: ANNUAL,
- plan: null,
- })
- }
- >
- Annual (Contract)
- </a>
- </li>
- )}
- <li
- className={classNames({
- active: contractInterval === ANNUAL && billingInterval === ANNUAL,
- })}
- >
- <a
- onClick={() =>
- this.setState({
- billingInterval: ANNUAL,
- contractInterval: ANNUAL,
- plan: null,
- })
- }
- >
- Annual (Upfront)
- </a>
- </li>
- </ul>
- </React.Fragment>
- );
- return (
- <Fragment>
- {header}
- <PlanList
- planId={plan}
- reservedErrors={reservedErrors}
- reservedTransactions={reservedTransactions}
- reservedReplays={reservedReplays}
- reservedSpans={reservedSpans}
- reservedAttachments={reservedAttachments}
- reservedMonitorSeats={reservedMonitorSeats}
- reservedUptime={reservedUptime}
- reservedProfileDuration={reservedProfileDuration}
- plans={planList}
- onPlanChange={this.handlePlanChange}
- onLimitChange={this.handleLimitChange}
- />
- </Fragment>
- );
- }
- }
- export default ChangePlanAction;
|