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 ( +
+ sentry codecov logos +

+ {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 ( -
- sentry codecov logos -

- {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 }, '' )