123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782 |
- import {Component, Fragment} from 'react';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import type {QueryObserverResult} from '@tanstack/react-query';
- import isEqual from 'lodash/isEqual';
- import moment from 'moment-timezone';
- import type {Client} from 'sentry/api';
- import {Button} from 'sentry/components/button';
- import {Alert} from 'sentry/components/core/alert';
- import ExternalLink from 'sentry/components/links/externalLink';
- import LoadingError from 'sentry/components/loadingError';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import Panel from 'sentry/components/panels/panel';
- import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
- import TextOverflow from 'sentry/components/textOverflow';
- import {t, tct} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
- import type {Organization} from 'sentry/types/organization';
- import type {QueryClient} from 'sentry/utils/queryClient';
- import normalizeUrl from 'sentry/utils/url/normalizeUrl';
- import withApi from 'sentry/utils/withApi';
- import withOrganization from 'sentry/utils/withOrganization';
- import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
- import TextBlock from 'sentry/views/settings/components/text/textBlock';
- import withSubscription from 'getsentry/components/withSubscription';
- import ZendeskLink from 'getsentry/components/zendeskLink';
- import {ANNUAL, MONTHLY} from 'getsentry/constants';
- import {
- type BillingConfig,
- CheckoutType,
- type DataCategories,
- OnDemandBudgetMode,
- type OnDemandBudgets,
- type Plan,
- PlanName,
- PlanTier,
- type PromotionData,
- type Subscription,
- } from 'getsentry/types';
- import {
- hasActiveVCFeature,
- hasPartnerMigrationFeature,
- hasPerformance,
- isAmPlan,
- isBizPlanFamily,
- } from 'getsentry/utils/billing';
- import {getCompletedOrActivePromotion} from 'getsentry/utils/promotions';
- import {showSubscriptionDiscount} from 'getsentry/utils/promotionUtils';
- import {loadStripe} from 'getsentry/utils/stripe';
- import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
- import withPromotions from 'getsentry/utils/withPromotions';
- import CheckoutOverview from 'getsentry/views/amCheckout/checkoutOverview';
- import CheckoutOverviewV2 from 'getsentry/views/amCheckout/checkoutOverviewV2';
- import AddBillingDetails from 'getsentry/views/amCheckout/steps/addBillingDetails';
- import AddDataVolume from 'getsentry/views/amCheckout/steps/addDataVolume';
- import AddPaymentMethod from 'getsentry/views/amCheckout/steps/addPaymentMethod';
- import ContractSelect from 'getsentry/views/amCheckout/steps/contractSelect';
- import OnDemandBudgetsStep from 'getsentry/views/amCheckout/steps/onDemandBudgets';
- import OnDemandSpend from 'getsentry/views/amCheckout/steps/onDemandSpend';
- import PlanSelect from 'getsentry/views/amCheckout/steps/planSelect';
- import ReviewAndConfirm from 'getsentry/views/amCheckout/steps/reviewAndConfirm';
- import SetBudgetAndReserves from 'getsentry/views/amCheckout/steps/setBudgetAndReserves';
- import type {CheckoutFormData} from 'getsentry/views/amCheckout/types';
- import {getBucket} from 'getsentry/views/amCheckout/utils';
- import {
- getTotalBudget,
- hasOnDemandBudgetsFeature,
- parseOnDemandBudgetsFromSubscription,
- } from 'getsentry/views/onDemandBudgets/utils';
- // TODO: push promotion logic to subcomponents
- type Props = {
- api: Client;
- checkoutTier: PlanTier;
- isError: boolean;
- isLoading: boolean;
- onToggleLegacy: (tier: string) => void;
- organization: Organization;
- queryClient: QueryClient;
- subscription: Subscription;
- promotionData?: PromotionData;
- refetch?: () => Promise<QueryObserverResult<PromotionData, unknown>>;
- } & RouteComponentProps<Record<PropertyKey, unknown>, unknown>;
- type State = {
- billingConfig: BillingConfig | null;
- completedSteps: Set<number>;
- currentStep: number;
- error: Error | boolean;
- formData: CheckoutFormData | null;
- loading: boolean;
- };
- class AMCheckout extends Component<Props, State> {
- constructor(props: Props) {
- super(props);
- // TODO(am3): for now, only new customers and migrating partner customers can use the AM3 checkout flow
- if (
- props.checkoutTier === PlanTier.AM3 &&
- !props.subscription.plan.startsWith('am3') &&
- !props.organization.features.includes('partner-billing-migration')
- ) {
- props.onToggleLegacy(props.subscription.planTier);
- }
- let step = 1;
- if (props.location?.hash) {
- const stepMatch = /^#step(\d)$/.exec(props.location.hash);
- if (stepMatch) {
- step = parseInt(stepMatch[1]!, 10);
- if (step < 1 || step > 6) {
- step = 1;
- }
- }
- } else if (
- // skip 'Choose Your Plan' if customer is already on Business plan
- isBizPlanFamily(props.subscription.planDetails) &&
- props.checkoutTier === props.subscription.planTier
- ) {
- step = 2;
- }
- this.initialStep = step;
- this.state = {
- loading: true,
- error: false,
- currentStep: step,
- completedSteps: new Set(),
- formData: null,
- billingConfig: null,
- };
- }
- state: State;
- componentDidMount() {
- const {subscription, organization} = this.props;
- /**
- * Preload Stripe so it's ready when the subscription + cc form becomes
- * available. `loadStripe` ensures Stripe is not loaded multiple times
- */
- loadStripe();
- if (!subscription.canSelfServe) {
- this.handleRedirect();
- } else {
- this.fetchBillingConfig();
- }
- if (organization) {
- trackGetsentryAnalytics('am_checkout.viewed', {organization, subscription});
- }
- }
- componentDidUpdate(prevProps: Props) {
- const {checkoutTier, subscription} = this.props;
- if (checkoutTier === prevProps.checkoutTier) {
- return;
- }
- if (!subscription.canSelfServe) {
- this.handleRedirect();
- } else {
- this.fetchBillingConfig();
- }
- }
- readonly initialStep: number;
- get referrer(): string | undefined {
- const {location} = this.props;
- return location?.query?.referrer;
- }
- /**
- * Managed subscriptions need to go through Sales or Support to make
- * changes to their plan and cannot use the self-serve checkout flow
- */
- handleRedirect() {
- const {organization, router} = this.props;
- return router.push(normalizeUrl(`/settings/${organization.slug}/billing/overview/`));
- }
- async fetchBillingConfig() {
- const {api, organization, checkoutTier} = this.props;
- this.setState({loading: true});
- const endpoint = `/customers/${organization.slug}/billing-config/`;
- try {
- const config = await api.requestPromise(endpoint, {
- method: 'GET',
- data: {tier: checkoutTier},
- });
- const planList = this.getPaidPlans(config);
- const billingConfig = {...config, planList};
- const formData = this.getInitialData(billingConfig);
- this.setState({billingConfig, formData});
- } catch (error) {
- this.setState({error, loading: false});
- if (error.status !== 401 && error.status !== 403) {
- Sentry.captureException(error);
- }
- }
- this.setState({loading: false});
- }
- getPaidPlans(billingConfig: BillingConfig) {
- const paidPlans = billingConfig.planList.filter(
- plan =>
- plan.basePrice &&
- plan.userSelectable &&
- ((plan.billingInterval === MONTHLY && plan.contractInterval === MONTHLY) ||
- (plan.billingInterval === ANNUAL && plan.contractInterval === ANNUAL))
- );
- if (!paidPlans) {
- throw new Error('Cannot get plan options');
- }
- return paidPlans;
- }
- get checkoutSteps() {
- const {organization, subscription, checkoutTier} = this.props;
- const OnDemandStep = hasOnDemandBudgetsFeature(organization, subscription)
- ? OnDemandBudgetsStep
- : OnDemandSpend;
- const preAM3Tiers = [PlanTier.AM1, PlanTier.AM2];
- const notAMTier = !isAmPlan(checkoutTier);
- if (preAM3Tiers.includes(checkoutTier) || notAMTier) {
- // Display for AM1 and AM2 tiers, and non-AM tiers (e.g. L1)
- return [
- PlanSelect,
- AddDataVolume,
- OnDemandStep,
- ContractSelect,
- AddPaymentMethod,
- AddBillingDetails,
- ReviewAndConfirm,
- ];
- }
- // Do not include Payment Method and Billing Details sections for subscriptions billed through partners
- if (subscription.isSelfServePartner) {
- if (hasActiveVCFeature(organization)) {
- // Don't allow VC customers to choose Annual plans
- return [PlanSelect, SetBudgetAndReserves, ReviewAndConfirm];
- }
- return [PlanSelect, SetBudgetAndReserves, ContractSelect, ReviewAndConfirm];
- }
- // Display for AM3 tiers and above
- return [
- PlanSelect,
- SetBudgetAndReserves,
- ContractSelect,
- AddPaymentMethod,
- AddBillingDetails,
- ReviewAndConfirm,
- ];
- }
- get activePlan() {
- const {formData} = this.state;
- const activePlan = formData && this.getPlan(formData.plan);
- if (!activePlan) {
- throw new Error('Cannot get active plan');
- }
- return activePlan;
- }
- getPlan(plan: string) {
- const {billingConfig} = this.state;
- return billingConfig?.planList.find(({id}) => id === plan);
- }
- /**
- * Default to the business plan if:
- * 1. The account has an upsell/upgrade referrer
- * 2. The subscription is free
- * 3. Or, the subscription is on a free trial
- */
- shouldDefaultToBusiness() {
- const {subscription} = this.props;
- const hasUpsell =
- (this.referrer?.startsWith('upgrade') || this.referrer?.startsWith('upsell')) &&
- this.initialStep === 1;
- return hasUpsell || subscription.isFree || subscription.isTrial;
- }
- getBusinessPlan(billingConfig: BillingConfig) {
- const {subscription} = this.props;
- const {planList} = billingConfig;
- return planList.find(({name, contractInterval}) => {
- return (
- name === 'Business' &&
- contractInterval === subscription?.planDetails?.contractInterval
- );
- });
- }
- /**
- * Logic for initial plan:
- * 1. Default to the business plan
- * 2. Then default to the current paid plan
- * 3. Then default to an equivalent paid plan (mm2 Business -> am1 Business)
- * 4. Then default to the server default plan (Team)
- */
- getInitialPlan(billingConfig: BillingConfig) {
- const {subscription, checkoutTier} = this.props;
- const {planList, defaultPlan} = billingConfig;
- const initialPlan = planList.find(({id}) => id === subscription.plan);
- if (this.shouldDefaultToBusiness()) {
- const plan = this.getBusinessPlan(billingConfig);
- if (plan) {
- return plan;
- }
- }
- // Current tier paid plan
- if (initialPlan) {
- return initialPlan;
- }
- // map bundle plans
- if (subscription.planDetails.name === PlanName.BUSINESS_BUNDLE) {
- return planList.find(
- p => p.name === PlanName.BUSINESS && p.contractInterval === 'monthly'
- );
- }
- if (subscription.planDetails.name === PlanName.TEAM_BUNDLE) {
- return planList.find(
- p => p.name === PlanName.TEAM && p.contractInterval === 'monthly'
- );
- }
- // find equivalent current plan for legacy
- const legacyInitialPlan =
- subscription.planTier !== checkoutTier &&
- planList.find(
- ({name, contractInterval}) =>
- name === subscription?.planDetails?.name &&
- contractInterval === subscription?.planDetails?.contractInterval
- );
- return legacyInitialPlan || planList.find(({id}) => id === defaultPlan);
- }
- canComparePrices(initialPlan: Plan) {
- const {subscription} = this.props;
- return (
- // MMx event buckets are priced differently
- hasPerformance(subscription?.planDetails) &&
- subscription.planDetails.name === initialPlan.name &&
- subscription.planDetails.billingInterval === initialPlan.billingInterval
- );
- }
- /**
- * Get the current subscription plan and event volumes.
- * If not available on current tier, use the default plan.
- */
- getInitialData(billingConfig: BillingConfig): CheckoutFormData {
- const {subscription} = this.props;
- const {onDemandMaxSpend, planDetails} = subscription;
- const initialPlan = this.getInitialPlan(billingConfig);
- if (!initialPlan) {
- throw new Error('Cannot get initial plan');
- }
- const canComparePrices = this.canComparePrices(initialPlan);
- // Default to the max event volume per category based on either
- // the current reserved volume or the current reserved price.
- const reserved = Object.fromEntries(
- Object.entries(planDetails.planCategories)
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- .filter(([category, _]) => initialPlan.planCategories[category])
- .map(([category, eventBuckets]) => {
- const currentHistory = subscription.categories[category as DataCategories];
- // When introducing a new category before backfilling, the reserved value from the billing metric
- // history is not available, so we default to 0.
- let events = currentHistory?.reserved || 0;
- if (canComparePrices) {
- const price = getBucket({events, buckets: eventBuckets}).price;
- const eventsByPrice = getBucket({
- price,
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- buckets: initialPlan.planCategories[category],
- }).events;
- events = Math.max(events, eventsByPrice);
- }
- return [category, events];
- })
- );
- const defaultReservedCategories = Object.entries(billingConfig.defaultReserved).map(
- ([k, _]) => k
- );
- // this is the customer's reserved values that overlap with
- // the categories in the new checkout plan
- // e.g. AM2 customers checking out an AM3 plan will have
- // reserved transactions in AM2 but do not need reserved transactions in AM3
- const reservedOverlapping = Object.fromEntries(
- Object.entries(reserved).filter(([k, _]) => defaultReservedCategories.includes(k))
- );
- const data = {
- reserved: {
- ...billingConfig.defaultReserved,
- ...reservedOverlapping,
- },
- ...(onDemandMaxSpend > 0 && {onDemandMaxSpend}),
- onDemandBudget: parseOnDemandBudgetsFromSubscription(subscription),
- };
- return this.getValidData(initialPlan, data);
- }
- getValidData(plan: Plan, data: Omit<CheckoutFormData, 'plan'>): CheckoutFormData {
- const {subscription, organization, checkoutTier} = this.props;
- const {onDemandMaxSpend, onDemandBudget} = data;
- // Verify next plan data volumes before updating form data
- // finds the approximate bucket if event level does not exist
- const nextReserved = Object.fromEntries(
- Object.entries(data.reserved).map(([category, value]) => [
- category,
- getBucket({
- events: value,
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- buckets: plan.planCategories[category],
- shouldMinimize: hasPartnerMigrationFeature(organization),
- }).events,
- ])
- );
- const onDemandSupported = plan.allowOnDemand && subscription.supportsOnDemand;
- // reset ondemand if not allowed or supported
- let newOnDemandMaxSpend = onDemandSupported ? onDemandMaxSpend : 0;
- if (typeof newOnDemandMaxSpend === 'number') {
- newOnDemandMaxSpend = Math.max(newOnDemandMaxSpend, 0);
- }
- let newOnDemandBudget: OnDemandBudgets | undefined = undefined;
- if (
- hasOnDemandBudgetsFeature(organization, subscription) ||
- checkoutTier === PlanTier.AM3
- ) {
- newOnDemandBudget =
- onDemandBudget && onDemandSupported
- ? onDemandBudget
- : {
- budgetMode: OnDemandBudgetMode.SHARED,
- sharedMaxBudget: 0,
- };
- newOnDemandMaxSpend = getTotalBudget(newOnDemandBudget);
- }
- return {
- plan: plan.id,
- onDemandMaxSpend: newOnDemandMaxSpend,
- onDemandBudget: newOnDemandBudget,
- reserved: nextReserved,
- };
- }
- handleUpdate = (updatedData: any) => {
- const {organization, subscription, checkoutTier} = this.props;
- const {formData} = this.state;
- const data = {...formData, ...updatedData};
- const plan = this.getPlan(data.plan) || this.activePlan;
- const validData = this.getValidData(plan, data);
- this.setState({
- formData: validData,
- });
- const analyticsParams = {
- organization,
- subscription,
- plan: plan.id,
- };
- if (this.state.currentStep === 1) {
- trackGetsentryAnalytics('checkout.change_plan', analyticsParams);
- } else if (checkoutTier !== PlanTier.AM3 && this.state.currentStep === 3) {
- trackGetsentryAnalytics('checkout.ondemand_changed', {
- ...analyticsParams,
- cents: validData.onDemandMaxSpend || 0,
- });
- } else if (
- (checkoutTier === PlanTier.AM3 && this.state.currentStep === 3) ||
- (checkoutTier !== PlanTier.AM3 && this.state.currentStep === 4)
- ) {
- trackGetsentryAnalytics('checkout.change_contract', analyticsParams);
- }
- if (!isEqual(validData.reserved, data.reserved)) {
- Sentry.withScope(scope => {
- scope.setExtras({validData, updatedData, previous: formData});
- scope.setLevel('warning' as any);
- Sentry.captureException(new Error('Plan event levels do not match'));
- });
- }
- };
- /**
- * Complete step and all previous steps
- */
- handleCompleteStep = (stepNumber: number) => {
- const {organization, subscription} = this.props;
- const previousSteps = Array.from({length: stepNumber}, (_, idx) => idx + 1);
- trackGetsentryAnalytics('checkout.click_continue', {
- organization,
- subscription,
- step_number: stepNumber,
- plan: this.activePlan.id,
- checkoutType: CheckoutType.STANDARD,
- });
- this.setState(state => ({
- currentStep: state.currentStep + 1,
- completedSteps: new Set([...state.completedSteps, ...previousSteps]),
- }));
- };
- handleEdit = (stepNumber: number) => {
- this.setState({
- currentStep: stepNumber,
- });
- };
- renderSteps() {
- const {organization, onToggleLegacy, subscription, checkoutTier, promotionData} =
- this.props;
- const {currentStep, completedSteps, formData, billingConfig} = this.state;
- const promoClaimed = getCompletedOrActivePromotion(promotionData);
- if (!formData || !billingConfig) {
- return null;
- }
- const promotion = promoClaimed?.promotion;
- const stepProps = {
- formData,
- billingConfig,
- activePlan: this.activePlan,
- onUpdate: this.handleUpdate,
- onCompleteStep: this.handleCompleteStep,
- onEdit: this.handleEdit,
- onToggleLegacy,
- organization,
- subscription,
- checkoutTier,
- promotion,
- };
- return this.checkoutSteps.map((CheckoutStep, idx) => {
- const stepNumber = idx + 1;
- const isActive = currentStep === stepNumber;
- const isCompleted = !isActive && completedSteps.has(stepNumber);
- const prevStepCompleted = completedSteps.has(stepNumber - 1);
- return (
- <CheckoutStep
- {...stepProps}
- key={idx}
- stepNumber={stepNumber}
- isActive={isActive}
- isCompleted={isCompleted}
- prevStepCompleted={prevStepCompleted}
- referrer={this.referrer}
- />
- );
- });
- }
- render() {
- const {subscription, organization, isLoading, promotionData, checkoutTier} =
- this.props;
- const {loading, error, formData, billingConfig} = this.state;
- if (loading || isLoading) {
- return <LoadingIndicator />;
- }
- if (error) {
- return <LoadingError />;
- }
- if (!formData || !billingConfig) {
- return null;
- }
- const promotionClaimed = getCompletedOrActivePromotion(promotionData);
- const promo = promotionClaimed?.promotion;
- const discountInfo = promo?.discountInfo;
- const subscriptionDiscountInfo = showSubscriptionDiscount({
- activePlan: this.activePlan,
- discountInfo,
- });
- const overviewProps = {
- formData,
- billingConfig,
- activePlan: this.activePlan,
- onUpdate: this.handleUpdate,
- organization,
- subscription,
- discountInfo: discountInfo ?? undefined,
- };
- const showAnnualTerms =
- subscription.contractInterval === ANNUAL ||
- this.activePlan.contractInterval === ANNUAL;
- const promotionDisclaimerText =
- promotionData?.activePromotions?.[0]?.promotion.discountInfo.disclaimerText;
- const isOnSponsoredPartnerPlan =
- (subscription.partner?.isActive && subscription.isSponsored) || false;
- return (
- <Fragment>
- <SentryDocumentTitle
- title={t('Change Subscription')}
- orgSlug={organization.slug}
- />
- {isOnSponsoredPartnerPlan && (
- <Alert.Container>
- <Alert type="info" showIcon>
- {t(
- 'Your promotional plan with %s ends on %s.',
- subscription.partner?.partnership.displayName,
- moment(subscription.contractPeriodEnd).format('ll')
- )}
- </Alert>
- </Alert.Container>
- )}
- {promotionDisclaimerText && (
- <Alert.Container>
- <Alert type="info" showIcon>
- {promotionDisclaimerText}
- </Alert>
- </Alert.Container>
- )}
- <SettingsPageHeader
- title="Change Subscription"
- colorSubtitle={subscriptionDiscountInfo}
- data-test-id="change-subscription"
- />
- <CheckoutContainer>
- <div data-test-id="checkout-steps">{this.renderSteps()}</div>
- <SidePanel>
- <OverviewContainer>
- {checkoutTier === PlanTier.AM3 ? (
- <CheckoutOverviewV2 {...overviewProps} />
- ) : (
- <CheckoutOverview {...overviewProps} />
- )}
- <SupportPrompt>
- {t('Have a question?')}
- <TextOverflow>
- {tct('[help:Find an Answer] or [contact:Ask Support]', {
- help: (
- <ExternalLink href="https://sentry.zendesk.com/hc/en-us/categories/17135853065755-Account-Billing" />
- ),
- contact: <ZendeskLink subject="Billing Question" source="checkout" />,
- })}
- </TextOverflow>
- </SupportPrompt>
- </OverviewContainer>
- <DisclaimerText>{discountInfo?.disclaimerText}</DisclaimerText>
- {subscription.canCancel && (
- <CancelSubscription>
- <Button
- to={`/settings/${organization.slug}/billing/cancel/`}
- disabled={subscription.cancelAtPeriodEnd}
- >
- {subscription.cancelAtPeriodEnd
- ? t('Pending Cancellation')
- : t('Cancel Subscription')}
- </Button>
- </CancelSubscription>
- )}
- {showAnnualTerms && (
- <AnnualTerms>
- {tct(
- `Annual subscriptions require a one-year non-cancellable commitment.
- By using Sentry you agree to our [terms: Terms of Service].`,
- {terms: <a href="https://sentry.io/terms/" />}
- )}
- </AnnualTerms>
- )}
- </SidePanel>
- </CheckoutContainer>
- </Fragment>
- );
- }
- }
- const CheckoutContainer = styled('div')`
- display: grid;
- gap: ${space(3)};
- grid-template-columns: 58% auto;
- @media (max-width: ${p => p.theme.breakpoints.large}) {
- grid-template-columns: auto;
- }
- `;
- const SidePanel = styled('div')`
- height: max-content;
- position: sticky;
- top: 70px;
- align-self: start;
- `;
- /**
- * Hide overview at smaller screen sizes
- * but keep cancel subscription button
- */
- const OverviewContainer = styled('div')`
- @media (max-width: ${p => p.theme.breakpoints.large}) {
- display: none;
- }
- `;
- const SupportPrompt = styled(Panel)`
- display: grid;
- grid-template-columns: repeat(2, auto);
- justify-content: space-between;
- gap: ${space(1)};
- padding: ${space(2)};
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.subText};
- align-items: center;
- `;
- const CancelSubscription = styled('div')`
- display: grid;
- justify-items: center;
- margin-bottom: ${space(3)};
- `;
- const DisclaimerText = styled('div')`
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.subText};
- text-align: center;
- margin-bottom: ${space(1)};
- `;
- const AnnualTerms = styled(TextBlock)`
- color: ${p => p.theme.subText};
- font-size: ${p => p.theme.fontSizeMedium};
- `;
- export default withPromotions(withApi(withOrganization(withSubscription(AMCheckout))));
|