diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.jsx
new file mode 100644
index 0000000000..f6e253f7c1
--- /dev/null
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.jsx
@@ -0,0 +1,67 @@
+import { useParams } from 'react-router-dom'
+
+import {
+ useAccountDetails,
+ useAvailablePlans,
+ usePlanData,
+} from 'services/account'
+import BenefitList from 'shared/plan/BenefitList'
+import ScheduledPlanDetails from 'shared/plan/ScheduledPlanDetails'
+import { useProPlans } from 'shared/utils/billing'
+import { shouldRenderCancelLink } from 'shared/utils/upgradeForm'
+import A from 'ui/A'
+import Icon from 'ui/Icon'
+
+function ProPlanDetails() {
+ const { provider, owner } = useParams()
+ const { data: planData } = usePlanData({ provider, owner })
+ const { data: accountDetails } = useAccountDetails({ provider, owner })
+ const { data: plans } = useAvailablePlans({ provider, owner })
+ const { proPlanMonth, proPlanYear } = useProPlans({ plans })
+
+ const plan = accountDetails?.rootOrganization?.plan ?? accountDetails?.plan
+ const scheduledPhase = accountDetails?.scheduleDetail?.scheduledPhase
+
+ const cancelAtPeriodEnd =
+ accountDetails?.subscriptionDetail?.cancelAtPeriodEnd
+ const trialStatus = planData?.plan?.trialStatus
+
+ return (
+
+
{proPlanYear?.marketingName} plan
+
+
+
+
+
+ ${proPlanYear?.baseUnitPrice}
+ /per user, per month
+
+
+ billed annually or ${proPlanMonth?.baseUnitPrice} for monthly
+ billing
+
+
+ {scheduledPhase && (
+
+ )}
+ {shouldRenderCancelLink(cancelAtPeriodEnd, plan, trialStatus) && (
+
+ Cancel
+
+
+ )}
+
+
+ )
+}
+
+export default ProPlanDetails
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.spec.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.spec.jsx
new file mode 100644
index 0000000000..60aac5fedd
--- /dev/null
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.spec.jsx
@@ -0,0 +1,370 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { render, screen } from '@testing-library/react'
+import { graphql, rest } from 'msw'
+import { setupServer } from 'msw/node'
+import { Suspense } from 'react'
+import { MemoryRouter, Route } from 'react-router-dom'
+
+import { TrialStatuses } from 'services/account'
+import { Plans } from 'shared/utils/billing'
+
+import ProPlanDetails from './ProPlanDetails'
+
+jest.mock('shared/plan/BenefitList', () => () => 'Benefits List')
+jest.mock(
+ 'shared/plan/ScheduledPlanDetails',
+ () => () => 'Scheduled Plan Details'
+)
+
+const proPlanYear = {
+ marketingName: 'Pro',
+ value: 'users-pr-inappy',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ quantity: 10,
+ monthlyUploadLimit: null,
+}
+
+const sentryPlanMonth = {
+ marketingName: 'Sentry Pro',
+ value: 'users-sentrym',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Includes 5 seats',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ trialDays: 14,
+ monthlyUploadLimit: null,
+}
+
+const sentryPlanYear = {
+ marketingName: 'Sentry Pro',
+ value: 'users-sentryy',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Includes 5 seats',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ trialDays: 14,
+}
+
+const allPlansWithoutSentry = [
+ {
+ marketingName: 'Basic',
+ value: 'users-free',
+ billingRate: null,
+ baseUnitPrice: 0,
+ benefits: [
+ 'Up to 5 users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ ],
+ monthlyUploadLimit: 250,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-pr-inappm',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ proPlanYear,
+ {
+ marketingName: 'Pro',
+ value: 'users-enterprisem',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-enterprisey',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+]
+
+const mockPlanData = {
+ baseUnitPrice: 10,
+ benefits: [],
+ billingRate: 'monthly',
+ marketingName: 'Users Basic',
+ monthlyUploadLimit: 250,
+ value: 'users-basic',
+ trialStatus: TrialStatuses.NOT_STARTED,
+ trialStartDate: '',
+ trialEndDate: '',
+ trialTotalDays: 0,
+ pretrialUsersCount: 0,
+ planUserCount: 1,
+}
+
+const server = setupServer()
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, suspense: true } },
+})
+
+const wrapper =
+ (initialEntries = '/plan/gh/codecov/upgrade') =>
+ ({ children }) =>
+ (
+
+
+
+ Loading...}>{children}
+
+
+
+ )
+
+beforeAll(() => {
+ server.listen()
+})
+
+afterEach(() => {
+ queryClient.clear()
+ server.resetHandlers()
+})
+
+afterAll(() => {
+ server.close()
+})
+
+describe('ProPlanDetails', () => {
+ function setup(
+ {
+ isOngoingTrial = false,
+ isSentryPlan = false,
+ hasScheduledPhase = false,
+ hasUserCanceledAtPeriodEnd = false,
+ isProPlan = false,
+ } = {
+ isOngoingTrial: false,
+ isSentryPlan: false,
+ hasScheduledPhase: false,
+ hasUserCanceledAtPeriodEnd: false,
+ isProPlan: false,
+ }
+ ) {
+ server.use(
+ graphql.query('GetPlanData', (req, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.data({
+ owner: {
+ plan: {
+ ...mockPlanData,
+ trialStatus: isOngoingTrial
+ ? TrialStatuses.ONGOING
+ : TrialStatuses.CANNOT_TRIAL,
+ value: isOngoingTrial
+ ? Plans.USERS_TRIAL
+ : isProPlan
+ ? Plans.USERS_PR_INAPPM
+ : Plans.USERS_BASIC,
+ },
+ },
+ })
+ )
+ ),
+ graphql.query('GetAvailablePlans', (req, res, ctx) => {
+ if (isSentryPlan) {
+ return res(
+ ctx.status(200),
+ ctx.data({
+ owner: {
+ availablePlans: [
+ ...allPlansWithoutSentry,
+ sentryPlanMonth,
+ sentryPlanYear,
+ ],
+ },
+ })
+ )
+ } else {
+ return res(
+ ctx.status(200),
+ ctx.data({ owner: { availablePlans: allPlansWithoutSentry } })
+ )
+ }
+ }),
+ rest.get('/internal/gh/codecov/account-details', (req, res, ctx) => {
+ if (isSentryPlan) {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ plan: sentryPlanYear,
+ subscriptionDetail: {
+ cancelAtPeriodEnd: undefined,
+ },
+ activatedUserCount: 10,
+ })
+ )
+ } else {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ plan: proPlanYear,
+ subscriptionDetail: {
+ cancelAtPeriodEnd: hasUserCanceledAtPeriodEnd,
+ },
+ scheduleDetail: {
+ scheduledPhase: hasScheduledPhase
+ ? {
+ quantity: 0,
+ plan: '',
+ startDate: 123456789,
+ }
+ : {},
+ },
+ activatedUserCount: 10,
+ })
+ )
+ }
+ })
+ )
+ }
+
+ describe('when rendered', () => {
+ it('shows pro yearly marketing name', async () => {
+ setup({ isSentryPlan: false })
+ render(, { wrapper: wrapper() })
+
+ const marketingName = await screen.findByRole('heading', {
+ name: /Pro plan/,
+ })
+ expect(marketingName).toBeInTheDocument()
+ })
+
+ it('shows benefits list', async () => {
+ setup({ isSentryPlan: false })
+
+ render(, { wrapper: wrapper() })
+
+ const benefitsList = await screen.findByText(/Benefits List/)
+ expect(benefitsList).toBeInTheDocument()
+ })
+
+ it('shows price', async () => {
+ setup({ isSentryPlan: false })
+
+ render(, { wrapper: wrapper() })
+
+ const price = await screen.findByText(/\$10/)
+ expect(price).toBeInTheDocument()
+ })
+
+ it('shows pricing disclaimer', async () => {
+ setup({ isSentryPlan: false })
+
+ render(, { wrapper: wrapper() })
+
+ const disclaimer = await screen.findByText(
+ /billed annually or \$12 for monthly billing/i
+ )
+ expect(disclaimer).toBeInTheDocument()
+ })
+
+ it('shows schedule phase when there is one', async () => {
+ setup({ isSentryPlan: false, hasScheduledPhase: true })
+
+ render(, { wrapper: wrapper() })
+
+ const scheduleDetails = await screen.findByText(/Scheduled Plan Details/i)
+ expect(scheduleDetails).toBeInTheDocument()
+ })
+
+ it('does not render schedule phase when there is not one', () => {
+ setup({ isSentryPlan: false, hasScheduledPhase: false })
+
+ render(, { wrapper: wrapper() })
+
+ const scheduleDetails = screen.queryByText(/Scheduled Plan Details/i)
+ expect(scheduleDetails).not.toBeInTheDocument()
+ })
+
+ it('shows cancellation link when it is valid', async () => {
+ setup({
+ isSentryPlan: false,
+ hasUserCanceledAtPeriodEnd: false,
+ isOngoingTrial: false,
+ isProPlan: true,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = await screen.findByRole('link', { name: /Cancel/ })
+ expect(link).toBeInTheDocument()
+ expect(link).toHaveAttribute('href', '/plan/gh/codecov/cancel')
+ })
+
+ it('should not render cancellation link when user is ongoing trial', () => {
+ setup({
+ isSentryPlan: false,
+ isOngoingTrial: true,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = screen.queryByRole('link', { name: /Cancel/ })
+ expect(link).not.toBeInTheDocument()
+ })
+
+ it('should not render cancellation link when user has already cancelled', () => {
+ setup({
+ isSentryPlan: false,
+ hasUserCanceledAtPeriodEnd: true,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = screen.queryByRole('link', { name: /Cancel/ })
+ expect(link).not.toBeInTheDocument()
+ })
+
+ it('should not render cancellation link when user is on basic plan', () => {
+ setup({
+ isSentryPlan: false,
+ isOngoingTrial: false,
+ isProPlan: false,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = screen.queryByRole('link', { name: /Cancel/ })
+ expect(link).not.toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/index.js b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/index.js
new file mode 100644
index 0000000000..5568d6f0af
--- /dev/null
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/index.js
@@ -0,0 +1 @@
+export { default } from './ProPlanDetails'
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.jsx
new file mode 100644
index 0000000000..e7c723e274
--- /dev/null
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.jsx
@@ -0,0 +1,60 @@
+import { useParams } from 'react-router-dom'
+
+import sentryCodecov from 'assets/plan/sentry_codecov.svg'
+import {
+ useAccountDetails,
+ useAvailablePlans,
+ usePlanData,
+} from 'services/account'
+import BenefitList from 'shared/plan/BenefitList'
+import { findSentryPlans } from 'shared/utils/billing'
+import { SENTRY_PRICE, shouldRenderCancelLink } from 'shared/utils/upgradeForm'
+import A from 'ui/A'
+import Icon from 'ui/Icon'
+
+function SentryPlanDetails() {
+ const { provider, owner } = useParams()
+ const { data: accountDetails } = useAccountDetails({ provider, owner })
+ const { data: planData } = usePlanData({ provider, owner })
+ const { data: plans } = useAvailablePlans({ provider, owner })
+ const { sentryPlanMonth, sentryPlanYear } = findSentryPlans({ plans })
+
+ const plan = accountDetails?.rootOrganization?.plan ?? accountDetails?.plan
+ const cancelAtPeriodEnd =
+ accountDetails?.subscriptionDetail?.cancelAtPeriodEnd
+ const trialStatus = planData?.plan?.trialStatus
+
+ return (
+
+
+
+ {sentryPlanYear?.marketingName} plan
+
+
+ ${SENTRY_PRICE}
+ /monthly
+
+
+
+ ${sentryPlanMonth?.baseUnitPrice} per user / month if paid monthly
+
+ {/* TODO: Note that there never was schedules shown here like it is in the pro plan details page. This
+ is a bug imo and needs to be here in a future ticket */}
+ {shouldRenderCancelLink(cancelAtPeriodEnd, plan, trialStatus) && (
+
+ Cancel
+
+
+ )}
+
+ )
+}
+export default SentryPlanDetails
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.spec.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.spec.jsx
new file mode 100644
index 0000000000..c46bb393a6
--- /dev/null
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.spec.jsx
@@ -0,0 +1,323 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { render, screen } from '@testing-library/react'
+import { graphql, rest } from 'msw'
+import { setupServer } from 'msw/node'
+import { Suspense } from 'react'
+import { MemoryRouter, Route } from 'react-router-dom'
+
+import { TrialStatuses } from 'services/account'
+import { Plans } from 'shared/utils/billing'
+
+import SentryPlanDetails from './SentryPlanDetails'
+
+jest.mock('shared/plan/BenefitList', () => () => 'Benefits List')
+
+const sentryPlanMonth = {
+ marketingName: 'Sentry Pro',
+ value: 'users-sentrym',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Includes 5 seats',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ trialDays: 14,
+ monthlyUploadLimit: null,
+}
+
+const sentryPlanYear = {
+ marketingName: 'Sentry Pro',
+ value: 'users-sentryy',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Includes 5 seats',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ trialDays: 14,
+}
+
+const allPlans = [
+ {
+ marketingName: 'Basic',
+ value: 'users-free',
+ billingRate: null,
+ baseUnitPrice: 0,
+ benefits: [
+ 'Up to 5 users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ ],
+ monthlyUploadLimit: 250,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-pr-inappm',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-pr-inappy',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ quantity: 10,
+ monthlyUploadLimit: null,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-enterprisem',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-enterprisey',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ sentryPlanMonth,
+ sentryPlanYear,
+]
+
+const mockPlanData = {
+ baseUnitPrice: 10,
+ benefits: [],
+ billingRate: 'monthly',
+ marketingName: 'Users Basic',
+ monthlyUploadLimit: 250,
+ value: 'users-basic',
+ trialStatus: TrialStatuses.NOT_STARTED,
+ trialStartDate: '',
+ trialEndDate: '',
+ trialTotalDays: 0,
+ pretrialUsersCount: 0,
+ planUserCount: 1,
+}
+
+const server = setupServer()
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, suspense: true } },
+})
+
+const wrapper =
+ (initialEntries = '/plan/gh/codecov/upgrade') =>
+ ({ children }) =>
+ (
+
+
+
+ Loading...}>{children}
+
+
+
+ )
+
+beforeAll(() => {
+ server.listen()
+})
+
+afterEach(() => {
+ queryClient.clear()
+ server.resetHandlers()
+})
+
+afterAll(() => {
+ server.close()
+})
+
+describe('SentryPlanDetails', () => {
+ function setup(
+ {
+ isOngoingTrial = false,
+ hasUserCanceledAtPeriodEnd = false,
+ isProPlan = false,
+ } = {
+ isOngoingTrial: false,
+ isSentryPlan: false,
+ hasUserCanceledAtPeriodEnd: false,
+ isProPlan: false,
+ }
+ ) {
+ server.use(
+ graphql.query('GetPlanData', (req, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.data({
+ owner: {
+ plan: {
+ ...mockPlanData,
+ trialStatus: isOngoingTrial
+ ? TrialStatuses.ONGOING
+ : TrialStatuses.CANNOT_TRIAL,
+ value: isOngoingTrial
+ ? Plans.USERS_TRIAL
+ : isProPlan
+ ? Plans.USERS_PR_INAPPM
+ : Plans.USERS_BASIC,
+ },
+ },
+ })
+ )
+ ),
+ graphql.query('GetAvailablePlans', (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.data({
+ owner: {
+ availablePlans: allPlans,
+ },
+ })
+ )
+ }),
+ rest.get('/internal/gh/codecov/account-details', (req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ plan: sentryPlanYear,
+ subscriptionDetail: {
+ cancelAtPeriodEnd: hasUserCanceledAtPeriodEnd,
+ },
+ activatedUserCount: 10,
+ })
+ )
+ })
+ )
+ }
+
+ describe('when rendered', () => {
+ it('renders sentry pro yearly marketing name', async () => {
+ setup()
+ render(, { wrapper: wrapper() })
+
+ const marketingName = await screen.findByRole('heading', {
+ name: /Sentry Pro plan/,
+ })
+ expect(marketingName).toBeInTheDocument()
+ })
+
+ it('renders the sentry image', async () => {
+ setup()
+
+ render(, { wrapper: wrapper() })
+
+ const image = await screen.findByRole('img', {
+ name: 'sentry codecov logos',
+ })
+ expect(image).toBeInTheDocument()
+ })
+
+ it('renders 29 monthly bundle', async () => {
+ setup()
+
+ render(, { wrapper: wrapper() })
+
+ const price = await screen.findByRole('heading', { name: /\$29/i })
+ expect(price).toBeInTheDocument()
+ })
+
+ it('renders benefits list', async () => {
+ setup()
+
+ render(, { wrapper: wrapper() })
+
+ const benefitsList = await screen.findByText(/Benefits List/)
+ expect(benefitsList).toBeInTheDocument()
+ })
+
+ it('renders pricing disclaimer', async () => {
+ setup()
+
+ render(, { wrapper: wrapper() })
+
+ const disclaimer = await screen.findByText(
+ /\$12 per user \/ month if paid monthly/i
+ )
+ expect(disclaimer).toBeInTheDocument()
+ })
+
+ it('renders cancellation link when it is valid', async () => {
+ setup({
+ isSentryPlan: false,
+ hasUserCanceledAtPeriodEnd: false,
+ isOngoingTrial: false,
+ isProPlan: true,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = await screen.findByRole('link', { name: /Cancel/ })
+ expect(link).toBeInTheDocument()
+ expect(link).toHaveAttribute('href', '/plan/gh/codecov/cancel')
+ })
+
+ it('should not render cancellation link when user is ongoing trial', () => {
+ setup({
+ isSentryPlan: false,
+ isOngoingTrial: true,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = screen.queryByRole('link', { name: /Cancel/ })
+ expect(link).not.toBeInTheDocument()
+ })
+
+ it('should not render cancellation link when user has already cancelled', () => {
+ setup({
+ isSentryPlan: false,
+ hasUserCanceledAtPeriodEnd: true,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = screen.queryByRole('link', { name: /Cancel/ })
+ expect(link).not.toBeInTheDocument()
+ })
+
+ it('should not render cancellation link when user is on basic plan', () => {
+ setup({
+ isSentryPlan: false,
+ isOngoingTrial: false,
+ isProPlan: false,
+ })
+
+ render(, { wrapper: wrapper() })
+
+ const link = screen.queryByRole('link', { name: /Cancel/ })
+ expect(link).not.toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/index.js b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/index.js
new file mode 100644
index 0000000000..3843ec7a19
--- /dev/null
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/index.js
@@ -0,0 +1 @@
+export { default } from './SentryPlanDetails'
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.jsx
index 8c0ec6e80c..d828a089a7 100644
--- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.jsx
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.jsx
@@ -1,150 +1,23 @@
-import PropTypes from 'prop-types'
import { useParams } from 'react-router-dom'
-import sentryCodecov from 'assets/plan/sentry_codecov.svg'
-import {
- accountDetailsPropType,
- planPropType,
- usePlanData,
-} from 'services/account'
-import BenefitList from 'shared/plan/BenefitList'
-import ScheduledPlanDetails from 'shared/plan/ScheduledPlanDetails'
+import { useAccountDetails, useAvailablePlans } from 'services/account'
import { canApplySentryUpgrade } from 'shared/utils/billing'
-import { SENTRY_PRICE, shouldRenderCancelLink } from 'shared/utils/upgradeForm'
-import A from 'ui/A'
-import Icon from 'ui/Icon'
-function SentryPlanDetails({
- plan,
- sentryPlanMonth,
- sentryPlanYear,
- cancelAtPeriodEnd,
- trialStatus,
-}) {
- return (
-
-
-
- {sentryPlanYear?.marketingName}
-
-
- ${SENTRY_PRICE}
- /monthly
-
-
-
- ${sentryPlanMonth?.baseUnitPrice} per user / month if paid monthly
-
- {shouldRenderCancelLink(cancelAtPeriodEnd, plan, trialStatus) && (
-
- Cancel
-
-
- )}
-
- )
-}
-
-SentryPlanDetails.propTypes = {
- cancelAtPeriodEnd: PropTypes.bool,
- plan: PropTypes.shape({
- value: PropTypes.string,
- }),
- sentryPlanMonth: planPropType,
- sentryPlanYear: planPropType,
- trialStatus: PropTypes.string,
-}
+import ProPlanDetails from './ProPlanDetails'
+import SentryPlanDetails from './SentryPlanDetails'
-function UpgradeDetails({
- plan,
- plans,
- proPlanMonth,
- proPlanYear,
- sentryPlanMonth,
- sentryPlanYear,
- accountDetails,
- scheduledPhase,
-}) {
+function UpgradeDetails() {
const { provider, owner } = useParams()
- const { data: planData } = usePlanData({ provider, owner })
+ const { data: accountDetails } = useAccountDetails({ provider, owner })
+ const { data: plans } = useAvailablePlans({ provider, owner })
- const cancelAtPeriodEnd =
- accountDetails?.subscriptionDetail?.cancelAtPeriodEnd
- const trialStatus = planData?.plan?.trialStatus
+ const plan = accountDetails?.rootOrganization?.plan ?? accountDetails?.plan
if (canApplySentryUpgrade({ plan, plans })) {
- return (
-
- )
+ return
}
- return (
-
-
{proPlanYear?.marketingName} plan
-
-
-
-
-
- ${proPlanYear?.baseUnitPrice}
- /per user, per month
-
-
- billed annually or ${proPlanMonth?.baseUnitPrice} for monthly
- billing
-
-
- {scheduledPhase && (
-
- )}
- {shouldRenderCancelLink(cancelAtPeriodEnd, plan, trialStatus) && (
-
- Cancel
-
-
- )}
-
-
- )
-}
-
-UpgradeDetails.propTypes = {
- accountDetails: accountDetailsPropType,
- plan: PropTypes.shape({
- value: PropTypes.string,
- }),
- plans: PropTypes.arrayOf(planPropType),
- proPlanMonth: planPropType,
- proPlanYear: planPropType,
- sentryPlanMonth: planPropType,
- sentryPlanYear: planPropType,
- scheduledPhase: PropTypes.shape({
- quantity: PropTypes.number.isRequired,
- plan: PropTypes.string.isRequired,
- startDate: PropTypes.number.isRequired,
- }),
+ return
}
export default UpgradeDetails
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.spec.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.spec.jsx
index 9c6772d676..e65399e707 100644
--- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.spec.jsx
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.spec.jsx
@@ -1,35 +1,17 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import {
- render,
- screen,
- waitForElementToBeRemoved,
-} from '@testing-library/react'
-import { graphql } from 'msw'
+import { render, screen } from '@testing-library/react'
+import { graphql, rest } from 'msw'
import { setupServer } from 'msw/node'
import { Suspense } from 'react'
import { MemoryRouter, Route } from 'react-router-dom'
-import { TrialStatuses } from 'services/account'
-import { Plans } from 'shared/utils/billing'
-
import UpgradeDetails from './UpgradeDetails'
-const proPlanMonth = {
- marketingName: 'Pro Team',
- value: 'users-pr-inappm',
- billingRate: 'monthly',
- baseUnitPrice: 12,
- benefits: [
- 'Configurable # of users',
- 'Unlimited public repositories',
- 'Unlimited private repositories',
- 'Priority Support',
- ],
- quantity: 10,
-}
+jest.mock('./SentryPlanDetails', () => () => 'Sentry Plan Details')
+jest.mock('./ProPlanDetails', () => () => 'Pro Plan Details')
const proPlanYear = {
- marketingName: 'Pro Team',
+ marketingName: 'Pro',
value: 'users-pr-inappy',
billingRate: 'annually',
baseUnitPrice: 10,
@@ -40,10 +22,11 @@ const proPlanYear = {
'Priority Support',
],
quantity: 10,
+ monthlyUploadLimit: null,
}
const sentryPlanMonth = {
- marketingName: 'Sentry Pro Team',
+ marketingName: 'Sentry Pro',
value: 'users-sentrym',
billingRate: 'monthly',
baseUnitPrice: 12,
@@ -54,10 +37,11 @@ const sentryPlanMonth = {
'Priority Support',
],
trialDays: 14,
+ monthlyUploadLimit: null,
}
const sentryPlanYear = {
- marketingName: 'Sentry Pro Team',
+ marketingName: 'Sentry Pro',
value: 'users-sentryy',
billingRate: 'annually',
baseUnitPrice: 10,
@@ -67,49 +51,64 @@ const sentryPlanYear = {
'Unlimited private repositories',
'Priority Support',
],
+ monthlyUploadLimit: null,
trialDays: 14,
}
-const freePlan = {
- marketingName: 'Basic',
- value: 'users-free',
- billingRate: null,
- baseUnitPrice: 0,
- benefits: [
- 'Up to 5 users',
- 'Unlimited public repositories',
- 'Unlimited private repositories',
- ],
-}
-
-const mockPlanData = {
- baseUnitPrice: 10,
- benefits: [],
- billingRate: 'monthly',
- marketingName: 'Users Basic',
- monthlyUploadLimit: 250,
- value: 'users-basic',
- trialStatus: TrialStatuses.NOT_STARTED,
- trialStartDate: '',
- trialEndDate: '',
- trialTotalDays: 0,
- pretrialUsersCount: 0,
- planUserCount: 1,
-}
-
-const trialPlan = {
- marketingName: 'Try Codecov Pro',
- value: 'users-trial',
- billingRate: 'monthly',
- baseUnitPrice: 0,
- benefits: [
- 'Configurable # of users',
- 'Unlimited public repositories',
- 'Unlimited private repositories',
- 'Priority Support',
- ],
- quantity: 10,
-}
+const allPlansWithoutSentry = [
+ {
+ marketingName: 'Basic',
+ value: 'users-free',
+ billingRate: null,
+ baseUnitPrice: 0,
+ benefits: [
+ 'Up to 5 users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ ],
+ monthlyUploadLimit: 250,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-pr-inappm',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ proPlanYear,
+ {
+ marketingName: 'Pro',
+ value: 'users-enterprisem',
+ billingRate: 'monthly',
+ baseUnitPrice: 12,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+ {
+ marketingName: 'Pro',
+ value: 'users-enterprisey',
+ billingRate: 'annually',
+ baseUnitPrice: 10,
+ benefits: [
+ 'Configurable # of users',
+ 'Unlimited public repositories',
+ 'Unlimited private repositories',
+ 'Priority Support',
+ ],
+ monthlyUploadLimit: null,
+ },
+]
const server = setupServer()
const queryClient = new QueryClient({
@@ -143,442 +142,70 @@ afterAll(() => {
})
describe('UpgradeDetails', () => {
- function setup({ isOngoingTrial = false } = { isOngoingTrial: false }) {
+ function setup(
+ { isSentryPlan = false } = {
+ isSentryPlan: false,
+ }
+ ) {
server.use(
- graphql.query('GetPlanData', (req, res, ctx) =>
- res(
- ctx.status(200),
- ctx.data({
- owner: {
- plan: {
- ...mockPlanData,
- trialStatus: isOngoingTrial
- ? TrialStatuses.ONGOING
- : TrialStatuses.CANNOT_TRIAL,
- value: isOngoingTrial ? Plans.USERS_TRIAL : Plans.USERS_BASIC,
+ graphql.query('GetAvailablePlans', (req, res, ctx) => {
+ if (isSentryPlan) {
+ return res(
+ ctx.status(200),
+ ctx.data({
+ owner: {
+ availablePlans: [
+ ...allPlansWithoutSentry,
+ sentryPlanMonth,
+ sentryPlanYear,
+ ],
},
- },
- })
- )
- )
- )
- }
-
- describe('users can apply sentry plan', () => {
- const plan = sentryPlanMonth
- const plans = [plan]
- const accountDetails = {
- activatedUserCount: 5,
- subscriptionDetail: { cancelAtPeriodEnd: false },
- }
-
- it('renders correct image', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const image = await screen.findByRole('img', {
- name: 'sentry codecov logos',
- })
- expect(image).toBeInTheDocument()
- })
-
- it('renders marketing name', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const marketingName = await screen.findByRole('heading', {
- name: /Sentry Pro Team/,
- })
- expect(marketingName).toBeInTheDocument()
- })
-
- it('renders 29 monthly bundle', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const price = await screen.findByRole('heading', { name: /\$29/i })
- expect(price).toBeInTheDocument()
- })
-
- it('renders pricing disclaimer', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const disclaimer = await screen.findByText(
- /\$12 per user \/ month if paid monthly/i
- )
- expect(disclaimer).toBeInTheDocument()
- })
-
- it('renders benefits section', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const benefitOne = await screen.findByText('Includes 5 seats')
- expect(benefitOne).toBeInTheDocument()
-
- const benefitTwo = await screen.findByText(
- 'Unlimited public repositories'
- )
- expect(benefitTwo).toBeInTheDocument()
-
- const benefitThree = await screen.findByText(
- 'Unlimited private repositories'
- )
- expect(benefitThree).toBeInTheDocument()
-
- const benefitFour = await screen.findByText('Priority Support')
- expect(benefitFour).toBeInTheDocument()
- })
-
- it('renders cancellation link', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const link = await screen.findByRole('link', { name: /Cancel/ })
- expect(link).toBeInTheDocument()
- expect(link).toHaveAttribute('href', '/plan/gh/codecov/cancel')
- })
- })
-
- describe('user can not apply sentry plan', () => {
- const plan = proPlanMonth
- const plans = [plan]
- const accountDetails = {
- activatedUserCount: 5,
- subscriptionDetail: { cancelAtPeriodEnd: false },
- }
-
- it('renders marketing name', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const marketingName = await screen.findByRole('heading', {
- name: /Pro Team plan/,
- })
- expect(marketingName).toBeInTheDocument()
- })
-
- it('renders price', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const price = await screen.findByText(/\$10/)
- expect(price).toBeInTheDocument()
- })
-
- it('renders pricing disclaimer', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const disclaimer = await screen.findByText(
- /billed annually or \$12 for monthly billing/i
- )
- expect(disclaimer).toBeInTheDocument()
- })
-
- it('renders benefits section', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const benefitOne = await screen.findByText('Configurable # of users')
- expect(benefitOne).toBeInTheDocument()
-
- const benefitTwo = await screen.findByText(
- 'Unlimited public repositories'
- )
- expect(benefitTwo).toBeInTheDocument()
-
- const benefitThree = await screen.findByText(
- 'Unlimited private repositories'
- )
- expect(benefitThree).toBeInTheDocument()
-
- const benefitFour = await screen.findByText('Priority Support')
- expect(benefitFour).toBeInTheDocument()
- })
-
- it('renders cancellation link', async () => {
- setup()
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const link = await screen.findByRole('link', { name: /Cancel/ })
- expect(link).toBeInTheDocument()
- expect(link).toHaveAttribute('href', '/plan/gh/codecov/cancel')
- })
- })
-
- describe('not rendering cancellation link', () => {
- describe('user is currently on a free plan', () => {
- it('does not render cancel link', async () => {
- setup()
-
- const plan = freePlan
- const plans = [plan]
- const accountDetails = {
- activatedUserCount: 5,
- subscriptionDetail: { cancelAtPeriodEnd: false },
+ })
+ )
+ } else {
+ return res(
+ ctx.status(200),
+ ctx.data({ owner: { availablePlans: allPlansWithoutSentry } })
+ )
}
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const loading = await screen.findByText('Loading...')
- expect(loading).toBeInTheDocument()
-
- await waitForElementToBeRemoved(loading)
-
- const link = screen.queryByRole('link', { name: /Cancel plan/ })
- expect(link).not.toBeInTheDocument()
- })
- })
-
- describe('user is on a trial', () => {
- it('does not render cancel link', async () => {
- setup({ isOngoingTrial: true })
-
- const plan = trialPlan
- const plans = [plan]
- const accountDetails = {
- activatedUserCount: 5,
- subscriptionDetail: { cancelAtPeriodEnd: false },
+ }),
+ rest.get('/internal/gh/codecov/account-details', (req, res, ctx) => {
+ if (isSentryPlan) {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ plan: sentryPlanYear,
+ })
+ )
+ } else {
+ return res(
+ ctx.status(200),
+ ctx.json({
+ plan: proPlanYear,
+ })
+ )
}
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const loading = await screen.findByText('Loading...')
- expect(loading).toBeInTheDocument()
-
- await waitForElementToBeRemoved(loading)
-
- const link = screen.queryByRole('link', { name: /Cancel plan/ })
- expect(link).not.toBeInTheDocument()
})
- })
-
- describe('user has already cancelled plan', () => {
- it('does not render cancel link', async () => {
- setup()
-
- const plan = proPlanMonth
- const plans = [plan]
-
- const accountDetails = {
- activatedUserCount: 5,
- subscriptionDetail: { cancelAtPeriodEnd: true },
- }
-
- render(
- ,
- { wrapper: wrapper() }
- )
-
- const loading = await screen.findByText('Loading...')
- expect(loading).toBeInTheDocument()
+ )
+ }
- await waitForElementToBeRemoved(loading)
+ describe('when user can apply sentry plan', () => {
+ it('renders sentry plan details component', async () => {
+ setup({ isSentryPlan: true })
+ render(, { wrapper: wrapper() })
- const link = screen.queryByRole('link', { name: /Cancel plan/ })
- expect(link).not.toBeInTheDocument()
- })
+ const sentryPlanDetails = await screen.findByText(/Sentry Plan Details/)
+ expect(sentryPlanDetails).toBeInTheDocument()
})
})
- describe('when scheduled phase is valid', () => {
- it('renders scheduled phase', async () => {
- setup()
-
- const plan = proPlanMonth
- const plans = [plan]
- const accountDetails = {
- activatedUserCount: 5,
- subscriptionDetail: {
- cancelAtPeriodEnd: false,
- scheduledPhase: {
- phase: 'pro',
- effectiveDate: '2021-01-01T00:00:00Z',
- startDate: 123456789,
- },
- },
- }
-
- render(
- ,
- { wrapper: wrapper() }
- )
+ describe('user cannot apply sentry plan', () => {
+ it('renders pro plan details component', async () => {
+ setup({ isSentryPlan: false })
+ render(, { wrapper: wrapper() })
- const scheduledPhase = await screen.findByText('Scheduled Details')
- expect(scheduledPhase).toBeInTheDocument()
+ const proPlanDetails = await screen.findByText(/Pro Plan Details/)
+ expect(proPlanDetails).toBeInTheDocument()
})
})
})
diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.jsx
index 2224bc18dc..fe8759b3f2 100644
--- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.jsx
+++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.jsx
@@ -23,7 +23,6 @@ function UpgradePlanPage() {
const { sentryPlanMonth, sentryPlanYear } = findSentryPlans({ plans })
const plan = accountDetails?.rootOrganization?.plan ?? accountDetails?.plan
- const scheduledPhase = accountDetails?.scheduleDetail?.scheduledPhase
useLayoutEffect(() => {
setCrumbs([
@@ -41,16 +40,7 @@ function UpgradePlanPage() {
return (
-
+
MIN_SENTRY_SEATS * baseUnitPrice * 12 - SENTRY_PRICE * 12
-export function shouldRenderCancelLink(accountDetails, plan, trialStatus) {
+export function shouldRenderCancelLink(cancelAtPeriodEnd, plan, trialStatus) {
// cant cancel a free plan
if (isFreePlan(plan?.value)) {
return false
@@ -134,7 +134,7 @@ export function shouldRenderCancelLink(accountDetails, plan, trialStatus) {
}
// plan is already set for cancellation
- if (accountDetails?.subscriptionDetail?.cancelAtPeriodEnd) {
+ if (cancelAtPeriodEnd) {
return false
}
diff --git a/src/shared/utils/upgradeForm.spec.js b/src/shared/utils/upgradeForm.spec.js
index 0b2fc88ad1..c9a8f6b4b4 100644
--- a/src/shared/utils/upgradeForm.spec.js
+++ b/src/shared/utils/upgradeForm.spec.js
@@ -402,7 +402,7 @@ describe('shouldRenderCancelLink', () => {
it('returns true', () => {
// eslint-disable-next-line testing-library/render-result-naming-convention
const value = shouldRenderCancelLink(
- {},
+ false,
{ value: Plans.USERS_PR_INAPPY },
''
)
@@ -413,7 +413,11 @@ describe('shouldRenderCancelLink', () => {
describe('user is on a free plan', () => {
it('returns false', () => {
// eslint-disable-next-line testing-library/render-result-naming-convention
- const value = shouldRenderCancelLink({}, { value: Plans.USERS_BASIC }, '')
+ const value = shouldRenderCancelLink(
+ false,
+ { value: Plans.USERS_BASIC },
+ ''
+ )
expect(value).toBeFalsy()
})
@@ -423,7 +427,7 @@ describe('shouldRenderCancelLink', () => {
it('returns false', () => {
// eslint-disable-next-line testing-library/render-result-naming-convention
const value = shouldRenderCancelLink(
- {},
+ false,
{ value: Plans.USERS_TRIAL },
TrialStatuses.ONGOING
)
@@ -436,7 +440,7 @@ describe('shouldRenderCancelLink', () => {
it('returns false', () => {
// eslint-disable-next-line testing-library/render-result-naming-convention
const value = shouldRenderCancelLink(
- { subscriptionDetail: { cancelAtPeriodEnd: true } },
+ true,
{ value: Plans.USERS_PR_INAPPY },
''
)