Skip to content

Commit

Permalink
feat: Paid/Free plan seats limit banner (#2830)
Browse files Browse the repository at this point in the history
* feat: Add activation banner for trial eligible owners

* pull out interface + spec stuff

* Update to reflect paid plan activation banner

* Refactor CircleCI repo onboarding into one file (#2806)

* Refactor Other CI repo onboarding into one file (#2807)

* Update repo onboarding title position and page alignment (#2818)

* sec: 390 - Add validation for potential XSS vuln (#2797)

* add tests, and validation for provider

* add back supportServiceless param

* ref: 1548 Part 1: Convert all Header files to TS (#2821)

* ref all header files to TS

* remove prop types and rebase

* fix: Remove repository from GUT settings page header (#2823)

Small tweak removing `repository` from the GUT settings page.

* Install radix-ui react radio group (#2825)

* Update repo onboarding steps with new Card component (#2819)

GH codecov/engineering-team#1665

* feat: Add hasSeatsLeft to plan query

* Update to reflect SeatsLimitReachedBanner

* feat: paid plan activation banner

* update with from FreePlanSeatsLimitBanner

* value duplicate

* feat: paid plan seats limit banner

* just one more small tweak

* update name to FreePlanSeatsLimitBanner

* remove queryclient call

* update tests

* fix padding

* Update to have a const for plan value

---------

Co-authored-by: Spencer Murray <[email protected]>
Co-authored-by: ajay-sentry <[email protected]>
Co-authored-by: nicholas-codecov <[email protected]>
  • Loading branch information
4 people committed May 13, 2024
1 parent a50e79d commit b1a3527
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { MemoryRouter, Route } from 'react-router-dom'
import ActivationBanner from './ActivationBanner'

jest.mock('./TrialEligibleBanner', () => () => 'TrialEligibleBanner')
jest.mock('./FreePlanSeatsLimitBanner', () => () => 'FreePlanSeatsLimitBanner')
jest.mock('./PaidPlanSeatsLimitBanner', () => () => 'PaidPlanSeatsLimitBanner')

const queryClient = new QueryClient()

Expand Down Expand Up @@ -51,7 +53,8 @@ describe('ActivationBanner', () => {
function setup(
privateRepos = true,
trialStatus = 'NOT_STARTED',
value = 'users-basic'
value = 'users-basic',
hasSeatsLeft = true
) {
server.use(
graphql.query('GetPlanData', (req, res, ctx) => {
Expand All @@ -64,6 +67,7 @@ describe('ActivationBanner', () => {
...mockTrialData,
trialStatus,
value,
hasSeatsLeft,
},
pretrialPlan: {
baseUnitPrice: 10,
Expand All @@ -89,12 +93,32 @@ describe('ActivationBanner', () => {
})

it('does not render trial eligible banner if user is not eligible to trial', async () => {
setup(false)
setup(false, 'ONGOING', 'users-basic', true)
const { container } = render(<ActivationBanner />, { wrapper })

await waitFor(() => queryClient.isFetching)
await waitFor(() => !queryClient.isFetching)

expect(container).toBeEmptyDOMElement()
})

it('renders seats limit reached banner if user has no seats left and on free plan', async () => {
setup(true, 'ONGOING', 'users-basic', false)
render(<ActivationBanner />, { wrapper })

const FreePlanSeatsLimitBanner = await screen.findByText(
/FreePlanSeatsLimitBanner/
)
expect(FreePlanSeatsLimitBanner).toBeInTheDocument()
})

it('renders seats limit reached banner if user has no seats left and on paid plan', async () => {
setup(true, 'ONGOING', 'users-inappy', false)
render(<ActivationBanner />, { wrapper })

const PaidPlanSeatsLimitBanner = await screen.findByText(
/PaidPlanSeatsLimitBanner/
)
expect(PaidPlanSeatsLimitBanner).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useParams } from 'react-router-dom'

import { TrialStatuses, usePlanData } from 'services/account'
import { isBasicPlan } from 'shared/utils/billing'
import { isBasicPlan, isFreePlan } from 'shared/utils/billing'

import FreePlanSeatsLimitBanner from './FreePlanSeatsLimitBanner'
import PaidPlanSeatsLimitBanner from './PaidPlanSeatsLimitBanner'
import TrialEligibleBanner from './TrialEligibleBanner'

interface URLParams {
Expand All @@ -21,16 +23,22 @@ function ActivationBanner() {
isBasicPlan(planData?.plan?.value) &&
planData?.hasPrivateRepos &&
isNewTrial
const seatsLimitReached = !planData?.plan?.hasSeatsLeft
const isFreePlanValue = isFreePlan(planData?.plan?.value)

if (!isTrialEligible) {
return null
if (isTrialEligible) {
return <TrialEligibleBanner />
}

return (
<div className="mt-4">
<TrialEligibleBanner />
</div>
)
if (seatsLimitReached && isFreePlanValue) {
return <FreePlanSeatsLimitBanner />
}

if (seatsLimitReached && !isFreePlanValue) {
return <PaidPlanSeatsLimitBanner />
}

return null
}

export default ActivationBanner
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route } from 'react-router-dom'

import FreePlanSeatsLimitBanner from './FreePlanSeatsLimitBanner'

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<MemoryRouter initialEntries={['/gh/codecov/gazebo/new']}>
<Route path="/:provider/:owner/:repo/new">{children}</Route>
</MemoryRouter>
)

describe('FreePlanSeatsLimitBanner', () => {
it('renders the banner with correct content', () => {
render(<FreePlanSeatsLimitBanner />, { wrapper })

const bannerHeading = screen.getByRole('heading', {
name: /All Seats Taken/,
})
expect(bannerHeading).toBeInTheDocument()

const description = screen.getByText(
/Your organization is on the Developer free plan/i
)
expect(description).toBeInTheDocument()
})

it('renders correct links', () => {
render(<FreePlanSeatsLimitBanner />, { wrapper })

const upgradeLink = screen.getByRole('link', { name: /Upgrade/ })
expect(upgradeLink).toBeInTheDocument()
expect(upgradeLink).toHaveAttribute('href', '/plan/gh/codecov/upgrade')

const manageMembersLink = screen.getByRole('link', {
name: /manage members/,
})
expect(manageMembersLink).toBeInTheDocument()
expect(manageMembersLink).toHaveAttribute('href', '/members/gh/codecov')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import A from 'ui/A'
import Banner from 'ui/Banner'
import BannerContent from 'ui/Banner/BannerContent'
import BannerHeading from 'ui/Banner/BannerHeading'
import Button from 'ui/Button'

function FreePlanSeatsLimitBanner() {
return (
<Banner variant="plain">
<BannerContent>
<BannerHeading>
<h2 className="font-semibold">&#8505; All Seats Taken</h2>
</BannerHeading>
<div className="flex justify-between">
<p className="pr-2">
Your organization is on the Developer free plan, limited to one
seat, which is currently occupied. You can add any amount of seats
by upgrading for more flexibility.{' '}
<A
to={{ pageName: 'membersTab' }}
hook="manage-members-paid-plan"
isExternal={false}
>
manage members
</A>
</p>
<div className="flex items-start justify-end">
<Button
hook="trial-eligible-banner-start-trial"
to={{
pageName: 'upgradeOrgPlan',
}}
disabled={false}
variant="primary"
>
Upgrade
</Button>
</div>
</div>
</BannerContent>
</Banner>
)
}

export default FreePlanSeatsLimitBanner
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './FreePlanSeatsLimitBanner'
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route } from 'react-router-dom'

import PaidPlanSeatsLimitBanner from './PaidPlanSeatsLimitBanner'

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<MemoryRouter initialEntries={['/gh/codecov/gazebo/new']}>
<Route path="/:provider/:owner/:repo/new">{children}</Route>
</MemoryRouter>
)

describe('PaidPlanSeatsLimitBanner', () => {
it('renders the banner with correct content', () => {
render(<PaidPlanSeatsLimitBanner />, { wrapper })

const bannerHeading = screen.getByRole('heading', {
name: /Seats Limit Reached/,
})
expect(bannerHeading).toBeInTheDocument()

const description = screen.getByText(
/Your organization has utilized all available seats on this plan./i
)
expect(description).toBeInTheDocument()
})

it('renders correct links', () => {
render(<PaidPlanSeatsLimitBanner />, { wrapper })

const upgradeLink = screen.getByRole('link', { name: /Upgrade/ })
expect(upgradeLink).toBeInTheDocument()
expect(upgradeLink).toHaveAttribute('href', '/plan/gh/codecov/upgrade')

const manageMembersLink = screen.getByRole('link', {
name: /manage members/,
})
expect(manageMembersLink).toBeInTheDocument()
expect(manageMembersLink).toHaveAttribute('href', '/members/gh/codecov')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import A from 'ui/A'
import Banner from 'ui/Banner'
import BannerContent from 'ui/Banner/BannerContent'
import BannerHeading from 'ui/Banner/BannerHeading'
import Button from 'ui/Button'

function PaidPlanSeatsLimitBanner() {
return (
<Banner variant="plain">
<BannerContent>
<BannerHeading>
<h2 className="font-semibold">&#8505; Seats Limit Reached</h2>
</BannerHeading>
<div className="flex justify-between">
<p className="pr-2">
Your organization has utilized all available seats on this plan. To
add more members, please increase your seat count.{' '}
<A
to={{ pageName: 'membersTab' }}
hook="manage-members-paid-plan"
isExternal={false}
>
manage members
</A>
</p>
<div className="flex items-start justify-end">
<Button
hook="trial-eligible-banner-start-trial"
to={{
pageName: 'upgradeOrgPlan',
}}
disabled={false}
variant="primary"
>
Upgrade
</Button>
</div>
</div>
</BannerContent>
</Banner>
)
}

export default PaidPlanSeatsLimitBanner
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './PaidPlanSeatsLimitBanner'

0 comments on commit b1a3527

Please sign in to comment.