diff --git a/static/gsApp/components/partnerPlanEndingBanner.tsx b/static/gsApp/components/partnerPlanEndingBanner.tsx index 073dbe98d3bf74..945e81276a1f6e 100644 --- a/static/gsApp/components/partnerPlanEndingBanner.tsx +++ b/static/gsApp/components/partnerPlanEndingBanner.tsx @@ -3,6 +3,7 @@ import PartnerPlanEndingBackground from 'getsentry-images/partnership/plan-endin import {Tag} from 'sentry/components/core/badge/tag'; import {LinkButton} from 'sentry/components/core/button/linkButton'; +import {Flex} from 'sentry/components/core/layout'; import {IconClock} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -52,7 +53,14 @@ function PartnerPlanEndingBanner({ : 'Business'; return ( - +
@@ -80,20 +88,10 @@ function PartnerPlanEndingBanner({
-
+ ); } -const PartnerPlanEndingBannerWrapper = styled('div')` - border: 1px solid ${p => p.theme.border}; - border-radius: ${p => p.theme.borderRadius}; - background: ${p => p.theme.background}; - margin-bottom: ${space(2)}; - display: flex; - justify-content: space-between; - align-items: center; -`; - const PartnerPlanEndingText = styled('div')` padding: ${space(2)}; display: flex; diff --git a/static/gsApp/views/subscriptionPage/headerCards/headerCards.tsx b/static/gsApp/views/subscriptionPage/headerCards/headerCards.tsx index b745d9dde13702..6031c796d585c3 100644 --- a/static/gsApp/views/subscriptionPage/headerCards/headerCards.tsx +++ b/static/gsApp/views/subscriptionPage/headerCards/headerCards.tsx @@ -1,13 +1,16 @@ -import {css} from '@emotion/react'; -import styled from '@emotion/styled'; +import moment from 'moment-timezone'; +import {Flex, Grid} from 'sentry/components/core/layout'; +import {Text} from 'sentry/components/core/text'; import ErrorBoundary from 'sentry/components/errorBoundary'; -import Panel from 'sentry/components/panels/panel'; -import {space} from 'sentry/styles/space'; +import {IconGrid, IconUpgrade} from 'sentry/icons'; +import {t, tct} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; +import {toTitleCase} from 'sentry/utils/string/toTitleCase'; import type {Subscription} from 'getsentry/types'; -import {hasNewBillingUI} from 'getsentry/utils/billing'; +import {getPlanIcon, getProductIcon, hasNewBillingUI} from 'getsentry/utils/billing'; +import SubscriptionHeaderCard from 'getsentry/views/subscriptionPage/headerCards/subscriptionHeaderCard'; import SeerAutomationAlert from 'getsentry/views/subscriptionPage/seerAutomationAlert'; import {SubscriptionCard} from './subscriptionCard'; @@ -19,28 +22,90 @@ interface HeaderCardsProps { } export function HeaderCards({organization, subscription}: HeaderCardsProps) { + const isNewBillingUI = hasNewBillingUI(organization); + + const cards = [ + , + ].filter(card => card !== null); + return ( - - - - + {isNewBillingUI ? ( + + {cards} + + ) : ( + + + + + )} ); } -// TODO(checkout v3): update this with the real layout -const HeaderCardWrapper = styled(Panel)<{hasNewCheckout: boolean}>` - display: grid; - margin-bottom: ${space(2)}; - - ${p => - !p.hasNewCheckout && - css` - @media (min-width: ${p.theme.breakpoints.lg}) { - grid-template-columns: auto minmax(0, 600px); - gap: ${space(2)}; - } - `} -`; +function PlanCard({ + subscription, + organization, +}: { + organization: Organization; + subscription: Subscription; +}) { + const button = subscription.canSelfServe + ? subscription.isFree + ? { + ariaLabel: t('Upgrade plan'), + label: t('Upgrade plan'), + linkTo: `/settings/${organization.slug}/billing/checkout/?referrer=upgrade_plan`, + icon: , + priority: 'primary' as const, + } + : { + ariaLabel: t('Edit plan'), + label: t('Edit plan'), + linkTo: `/settings/${organization.slug}/billing/checkout/?referrer=edit_plan`, + icon: , + priority: 'default' as const, + } + : undefined; + return ( + } + subtitle={tct('[startDate] - [endDate]', { + startDate: moment(subscription.contractPeriodStart).format('MMM D, YYYY'), + endDate: moment(subscription.contractPeriodEnd).format('MMM D, YYYY'), + })} + sections={[ + + {getPlanIcon(subscription.planDetails)} + {t('%s Plan', subscription.planDetails.name)} + , + ...Object.values(subscription.addOns ?? {}) + .filter(addOn => addOn.enabled) + .map(addOn => ( + + {getProductIcon(addOn.apiName)} + + {toTitleCase(addOn.productName, {allowInnerUpperCase: true})} + + + )), + ]} + button={button} + /> + ); +} diff --git a/static/gsApp/views/subscriptionPage/headerCards/subscriptionCard.tsx b/static/gsApp/views/subscriptionPage/headerCards/subscriptionCard.tsx index eb04a90a95117f..092be263feb107 100644 --- a/static/gsApp/views/subscriptionPage/headerCards/subscriptionCard.tsx +++ b/static/gsApp/views/subscriptionPage/headerCards/subscriptionCard.tsx @@ -7,21 +7,14 @@ import moment from 'moment-timezone'; import {Tag} from 'sentry/components/core/badge/tag'; import {LinkButton} from 'sentry/components/core/button/linkButton'; -import {IconEdit, IconGrid} from 'sentry/icons'; -import {t, tct} from 'sentry/locale'; +import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import {ANNUAL} from 'getsentry/constants'; import type {Subscription} from 'getsentry/types'; -import { - hasNewBillingUI, - isDeveloperPlan, - isEnterprise, - isTeamPlan, -} from 'getsentry/utils/billing'; +import {isDeveloperPlan, isEnterprise, isTeamPlan} from 'getsentry/utils/billing'; import formatCurrency from 'getsentry/utils/formatCurrency'; -import SubscriptionHeaderCard from 'getsentry/views/subscriptionPage/headerCards/subscriptionHeaderCard'; import {shouldSeeSpendVisibility} from 'getsentry/views/subscriptionPage/utils'; interface SubscriptionCardProps { @@ -88,31 +81,6 @@ export function SubscriptionCard({subscription, organization}: SubscriptionCardP const hasBillingPerms = organization.access?.includes('org:billing'); - const hasNewCheckout = hasNewBillingUI(organization); - - if (hasNewCheckout) { - // TODO(checkout v3): update this with the real layout, this is just a placeholder for knip - return ( - } - subtitle={tct('[startDate] - [endDate]', { - startDate: moment(subscription.contractPeriodStart).format('MMM D, YYYY'), - endDate: moment(subscription.contractPeriodEnd).format('MMM D, YYYY'), - })} - sections={[ - {t('%s Plan', subscription.planDetails?.name)}, - ]} - button={{ - ariaLabel: t('Edit plan'), - label: t('Edit plan'), - linkTo: `/settings/${organization.slug}/billing/checkout/?referrer=edit_plan`, - icon: , - }} - /> - ); - } - return ( diff --git a/static/gsApp/views/subscriptionPage/headerCards/subscriptionHeaderCard.tsx b/static/gsApp/views/subscriptionPage/headerCards/subscriptionHeaderCard.tsx index d21b1d7cb3e1bd..c2b3e9df46e067 100644 --- a/static/gsApp/views/subscriptionPage/headerCards/subscriptionHeaderCard.tsx +++ b/static/gsApp/views/subscriptionPage/headerCards/subscriptionHeaderCard.tsx @@ -10,6 +10,7 @@ import type {SVGIconProps} from 'sentry/icons/svgIcon'; interface ButtonInfo { ariaLabel: string; label: React.ReactNode; + priority: 'primary' | 'default'; icon?: React.ReactNode; linkTo?: string; onClick?: () => void; @@ -42,9 +43,7 @@ function SubscriptionHeaderCard({ {isValidElement(button.icon) ? cloneElement(button.icon, {size: 'sm'} as SVGIconProps) : null} - - {button.label} - + {button.label} ); }; @@ -68,7 +67,7 @@ function SubscriptionHeaderCard({ )} {subtitle && {subtitle}} - + {sections.map((section, index) => { const isLast = index === sections.length - 1; return ( @@ -81,11 +80,19 @@ function SubscriptionHeaderCard({ {button && (button.linkTo ? ( - + {getButtonContent()} ) : ( - ))} diff --git a/static/gsApp/views/subscriptionPage/managedNote.tsx b/static/gsApp/views/subscriptionPage/managedNote.tsx index 94b6669b981196..2b60cc340e90cc 100644 --- a/static/gsApp/views/subscriptionPage/managedNote.tsx +++ b/static/gsApp/views/subscriptionPage/managedNote.tsx @@ -1,4 +1,4 @@ -import Panel from 'sentry/components/panels/panel'; +import {Container} from 'sentry/components/core/layout'; import PanelBody from 'sentry/components/panels/panelBody'; import {tct} from 'sentry/locale'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; @@ -83,7 +83,12 @@ function ManagedNote({subscription}: Props) { (subscription.customPrice !== null && subscription.customPrice > 0); return ( - + {isSalesAccount @@ -96,7 +101,7 @@ function ManagedNote({subscription}: Props) { DEFAULT_MESSAGE)} - + ); } diff --git a/static/gsApp/views/subscriptionPage/pendingChanges.tsx b/static/gsApp/views/subscriptionPage/pendingChanges.tsx index 1b451b3632f2c8..93713fcb19f767 100644 --- a/static/gsApp/views/subscriptionPage/pendingChanges.tsx +++ b/static/gsApp/views/subscriptionPage/pendingChanges.tsx @@ -397,26 +397,24 @@ class PendingChanges extends Component { } return ( - - - - {Object.entries(changes).map(([effectiveDate, items]) => ( -
- {tct('The following changes will take effect on [date]:', { - date: {moment(effectiveDate).format('ll')}, - })} - - {items.map((item, itemIdx) => ( -
  • - {item} -
  • - ))} -
    -
    - ))} -
    -
    -
    + + + {Object.entries(changes).map(([effectiveDate, items]) => ( +
    + {tct('The following changes will take effect on [date]:', { + date: {moment(effectiveDate).format('ll')}, + })} + + {items.map((item, itemIdx) => ( +
  • + {item} +
  • + ))} +
    +
    + ))} +
    +
    ); } } diff --git a/static/gsApp/views/subscriptionPage/subscriptionHeader.spec.tsx b/static/gsApp/views/subscriptionPage/subscriptionHeader.spec.tsx index 55000bba30f145..10eb5440c5bcec 100644 --- a/static/gsApp/views/subscriptionPage/subscriptionHeader.spec.tsx +++ b/static/gsApp/views/subscriptionPage/subscriptionHeader.spec.tsx @@ -2,7 +2,10 @@ import moment from 'moment-timezone'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig'; -import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; +import { + SubscriptionFixture, + SubscriptionWithSeerFixture, +} from 'getsentry-test/fixtures/subscription'; import {render, screen, within} from 'sentry-test/reactTestingLibrary'; import {PendingChangesFixture} from 'getsentry/__fixtures__/pendingChanges'; @@ -40,6 +43,59 @@ describe('SubscriptionHeader', () => { }); }); + it('renders for free plan with new billing UI', () => { + const organization = OrganizationFixture({ + features: ['subscriptions-v3'], + access: ['org:billing'], + }); + const subscription = SubscriptionFixture({organization, plan: 'am3_f'}); + render( + + ); + expect(screen.getByText('Developer Plan')).toBeInTheDocument(); + expect(screen.queryByText('Seer')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Edit plan'})).not.toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Upgrade plan'})).toBeInTheDocument(); + }); + + it('renders for paid plan and add-ons with new billing UI', () => { + const organization = OrganizationFixture({ + features: ['subscriptions-v3'], + access: ['org:billing'], + }); + const subscription = SubscriptionWithSeerFixture({ + organization, + plan: 'am3_business', + isFree: false, + }); + render( + + ); + expect(screen.getByText('Business Plan')).toBeInTheDocument(); + expect(screen.getByText('Seer')).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Edit plan'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Upgrade plan'})).not.toBeInTheDocument(); + }); + + it('renders for managed plan with new billing UI', () => { + const organization = OrganizationFixture({ + features: ['subscriptions-v3'], + access: ['org:billing'], + }); + const subscription = SubscriptionFixture({ + organization, + plan: 'am3_business_ent_auf', + isFree: false, + canSelfServe: false, + }); + render( + + ); + expect(screen.getByText('Enterprise (Business) Plan')).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Edit plan'})).not.toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Upgrade plan'})).not.toBeInTheDocument(); + }); + it('does not render editable sections for YY partnership', async () => { const organization = OrganizationFixture({ features: ['usage-log'], diff --git a/static/gsApp/views/subscriptionPage/subscriptionHeader.tsx b/static/gsApp/views/subscriptionPage/subscriptionHeader.tsx index e71a40b63b2c77..658a56a6bbd8f0 100644 --- a/static/gsApp/views/subscriptionPage/subscriptionHeader.tsx +++ b/static/gsApp/views/subscriptionPage/subscriptionHeader.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import {Button} from 'sentry/components/core/button'; import {LinkButton} from 'sentry/components/core/button/linkButton'; +import {Flex} from 'sentry/components/core/layout'; import {TabList, Tabs} from 'sentry/components/core/tabs'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {IconCodecov} from 'sentry/icons'; @@ -179,7 +180,7 @@ const TabsContainer = styled('div')` */ function BodyWithBillingPerms({organization, subscription}: any) { return ( - + {subscription.pendingChanges ? ( ) : null} @@ -192,7 +193,7 @@ function BodyWithBillingPerms({organization, subscription}: any) { )} - +
    ); } diff --git a/static/gsApp/views/subscriptionPage/trialAlert.tsx b/static/gsApp/views/subscriptionPage/trialAlert.tsx index c0db8f84502d33..361c26c58432e0 100644 --- a/static/gsApp/views/subscriptionPage/trialAlert.tsx +++ b/static/gsApp/views/subscriptionPage/trialAlert.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import {Button} from 'sentry/components/core/button'; -import Panel from 'sentry/components/panels/panel'; +import {Container} from 'sentry/components/core/layout'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; @@ -41,7 +41,12 @@ function TrialAlert({organization, subscription}: Props) { : 'business plan'; return ( - + @@ -67,7 +72,7 @@ function TrialAlert({organization, subscription}: Props) { )} - + ); }