Skip to content

Commit

Permalink
feat(ui): add trial plan support (#1146)
Browse files Browse the repository at this point in the history
  • Loading branch information
floreks authored Jul 10, 2023
1 parent 29c0be0 commit 3c6200c
Show file tree
Hide file tree
Showing 20 changed files with 403 additions and 25 deletions.
2 changes: 1 addition & 1 deletion www/src/components/account/Domains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ function DomainsInner({ q, setDomainSelected }: any) {
</Box>
</Table>
) : (
<Span>You do not have any domains set yet.</Span>
<Span padding="medium">You do not have any domains set yet.</Span>
)}
</Flex>
)
Expand Down
3 changes: 3 additions & 0 deletions www/src/components/account/Groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import SubscriptionContext from '../../contexts/SubscriptionContext'

import { Confirm } from '../utils/Confirm'

import BillingTrialBanner from './billing/BillingTrialBanner'

import { ViewGroup } from './Group'
import { CreateGroup } from './CreateGroup'
import { EditGroupAttributes, EditGroupMembers } from './EditGroup'
Expand Down Expand Up @@ -171,6 +173,7 @@ export function Groups() {
<CreateGroup q={q} />
</PageTitle>
<BillingLegacyUserBanner feature="groups" />
<BillingTrialBanner />
{isAvailable ? (
<List>
<Header
Expand Down
2 changes: 1 addition & 1 deletion www/src/components/account/Role.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function GeneralAttributes({
setBindings,
}: any) {
const [repositories, setRepositories] = useState(
attributes.repositories.join(', ')
attributes.repositories?.join(', ')
)

return (
Expand Down
3 changes: 3 additions & 0 deletions www/src/components/account/Roles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import SubscriptionContext from '../../contexts/SubscriptionContext'

import { Confirm } from '../utils/Confirm'

import BillingTrialBanner from './billing/BillingTrialBanner'

import { DELETE_ROLE, ROLES_Q } from './queries'
import { hasRbac } from './utils'
import { Info } from './Info'
Expand Down Expand Up @@ -175,6 +177,7 @@ export function Roles() {
<CreateRole q={q} />
</PageTitle>
<BillingLegacyUserBanner feature="roles" />
<BillingTrialBanner />
{isAvailable ? (
<List>
<Header
Expand Down
3 changes: 3 additions & 0 deletions www/src/components/account/ServiceAccounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import LoadingIndicator from '../utils/LoadingIndicator'

import SubscriptionContext from '../../contexts/SubscriptionContext'

import BillingTrialBanner from './billing/BillingTrialBanner'

import { USERS_Q } from './queries'
import { CreateServiceAccount } from './CreateServiceAccount'
import { ServiceAccount } from './User'
Expand Down Expand Up @@ -129,6 +131,7 @@ export function ServiceAccounts() {
<CreateServiceAccount q={q} />
</PageTitle>
<BillingLegacyUserBanner feature="service accounts" />
<BillingTrialBanner />
{isAvailable ? (
<List>
<Header
Expand Down
39 changes: 35 additions & 4 deletions www/src/components/account/billing/BillingPricingCards.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Button } from '@pluralsh/design-system'
import { Flex } from 'honorable'
import { useCallback, useContext, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Flex } from 'honorable'
import { Button } from '@pluralsh/design-system'

import PlatformPlansContext from '../../../contexts/PlatformPlansContext'
import SubscriptionContext from '../../../contexts/SubscriptionContext'

import BillingPricingCard from './BillingPricingCard'
import BillingDowngradeModal from './BillingDowngradeModal'

import BillingPricingCard from './BillingPricingCard'
import BillingStartTrialModal from './BillingStartTrialModal'
import BillingUpgradeToProfessionalModal from './BillingUpgradeToProfessionalModal'

function ContactUs({ primary }: { primary?: boolean }) {
Expand Down Expand Up @@ -42,7 +44,8 @@ function BillingPricingCards() {
const [searchParams, setSearchParams] = useSearchParams()
const { clusterMonthlyPricing, userMonthlyPricing } =
useContext(PlatformPlansContext)
const { isProPlan, isEnterprisePlan } = useContext(SubscriptionContext)
const { isProPlan, isEnterprisePlan, isTrialAvailable } =
useContext(SubscriptionContext)

const [downgradeModalOpen, setDowngradeModalOpen] = useState(false)

Expand All @@ -63,6 +66,22 @@ function BillingPricingCards() {
[setSearchParams]
)

const trialModalOpen = typeof searchParams.get('trial') === 'string'
const setOpenTrialModal = useCallback(
(isOpen) => {
setSearchParams((params) => {
if (isOpen) {
params.set('trial', '1')
} else {
params.delete('trial')
}

return params
})
},
[setSearchParams]
)

return (
<>
<Flex gap="medium">
Expand Down Expand Up @@ -159,6 +178,14 @@ function BillingPricingCards() {
<CurrentPlanButton />
) : isEnterprisePlan ? (
<ContactUs />
) : isTrialAvailable ? (
<Button
primary
width="100%"
onClick={() => setOpenTrialModal(true)}
>
Start free trial
</Button>
) : (
<Button
primary
Expand Down Expand Up @@ -224,6 +251,10 @@ function BillingPricingCards() {
open={downgradeModalOpen}
onClose={() => setDowngradeModalOpen(false)}
/>
<BillingStartTrialModal
open={trialModalOpen}
onClose={() => setOpenTrialModal(false)}
/>
</>
)
}
Expand Down
98 changes: 98 additions & 0 deletions www/src/components/account/billing/BillingStartTrialModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Button, Modal } from '@pluralsh/design-system'
import styled from 'styled-components'

import { useBeginTrialMutation } from '../../../generated/graphql'
import { GqlError } from '../../utils/Alert'

type BillingStartTrialModalProps = {
open: boolean
onClose: () => void
}

const ErrorWrapper = styled.div(({ theme }) => ({
paddingBottom: theme.spacing.large,
}))

const Header = styled.div(({ theme }) => ({
...theme.partials.text.body1,
fontWeight: '600',
}))

const Description = styled.div(({ theme }) => ({
...theme.partials.text.body2,
color: theme.colors['text-light'],
marginTop: theme.spacing.medium,

'& > ul': {
paddingLeft: theme.spacing.large,
},
}))

const ButtonGroup = styled.div(({ theme }) => ({
display: 'flex',
gap: theme.spacing.medium,
justifyContent: 'flex-end',
marginTop: theme.spacing.xlarge,
}))

function BillingStartTrialModal({
open,
onClose,
}: BillingStartTrialModalProps) {
const [beginTrial, { loading, error }] = useBeginTrialMutation({
onCompleted: onClose,
})

return (
<Modal
BackdropProps={{ zIndex: 20 }}
open={open}
onClose={onClose}
style={{ padding: 0 }}
size="large"
header="Free Trial Confirmation"
>
{error && (
<ErrorWrapper>
<GqlError
error={error}
header="Could not start free trial"
/>
</ErrorWrapper>
)}

<Header>Experience Plural Professional free for 30 days.</Header>
<Description>
Try out full feature access to Plural Professional risk free for 30
days. Features include:
<ul>
<li>Multi-cluster management</li>
<li>Cluster promotions</li>
<li>Advanced user management with groups and roles</li>
<li>Service accounts</li>
<li>VPNs</li>
</ul>
</Description>

<ButtonGroup>
<Button
secondary
alignSelf="flex-end"
onClick={() => onClose()}
>
Cancel
</Button>
<Button
primary
loading={loading}
onClick={beginTrial}
alignSelf="flex-end"
>
Start free trial
</Button>
</ButtonGroup>
</Modal>
)
}

export default BillingStartTrialModal
11 changes: 8 additions & 3 deletions www/src/components/account/billing/BillingSubscriptionChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import { Link } from 'react-router-dom'
import SubscriptionContext from '../../../contexts/SubscriptionContext'

function BillingSubscriptionChip() {
const { isProPlan, isEnterprisePlan } = useContext(SubscriptionContext)
const { isProPlan, isEnterprisePlan, isTrialPlan } =
useContext(SubscriptionContext)

return (
<Link
to="/account/billing"
style={{ textDecoration: 'none' }}
>
<Chip
severity={isEnterprisePlan || isProPlan ? 'info' : 'neutral'}
severity={
isTrialPlan || isEnterprisePlan || isProPlan ? 'info' : 'neutral'
}
fillLevel={2}
height={32}
>
{isEnterprisePlan
{isTrialPlan
? 'Free trial'
: isEnterprisePlan
? 'Custom'
: isProPlan
? 'Professional'
Expand Down
22 changes: 22 additions & 0 deletions www/src/components/account/billing/BillingSubscriptionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ function BillingSubscriptionProvider({
const isGrandfathered =
isLegacyUser && moment().isBefore(moment(grandfatheredUntil))
const isDelinquent = moment().isSameOrAfter(moment(account?.delinquentAt))
const isTrialPlan = plan?.name === 'Pro Trial'
const trialUntil = subscription?.trialUntil
// Trial is only available if it hasn't been used, it's not the current plan, and user is on a free plan
const isTrialAvailable =
account?.trialed !== true && !isTrialPlan && !isPaidPlan
// When number of days for trial to expire is lower, it will be marked as expiring soon
const trialExpiringSoonDays = 7 // one week

// Marking grandfathering as expired only for a month after expiry date.
// Afterwards expiry banners will not be visible and UI will be the same as for open-source users.
Expand All @@ -95,6 +102,15 @@ function BillingSubscriptionProvider({
moment(grandfatheredUntil).add(1, 'M')
)

const isTrialExpired = moment().isBetween(
moment(trialUntil),
moment(trialUntil).add(1, 'M')
)

const daysUntilTrialExpires = moment(trialUntil).diff(moment(), 'days')
const isTrialExpiringSoon = daysUntilTrialExpires <= trialExpiringSoonDays
const isTrialed = account?.trialed === true && isTrialExpired

return {
subscription,
billingAddress,
Expand All @@ -106,6 +122,12 @@ function BillingSubscriptionProvider({
isGrandfathered,
isGrandfatheringExpired,
isDelinquent,
isTrialPlan,
isTrialExpired,
isTrialAvailable,
isTrialExpiringSoon,
isTrialed,
daysUntilTrialExpires,
account,
availableFeatures,
paymentMethods,
Expand Down
65 changes: 65 additions & 0 deletions www/src/components/account/billing/BillingTrialBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Callout } from '@pluralsh/design-system'
import { useContext, useMemo } from 'react'
import { Link } from 'react-router-dom'
import styled from 'styled-components'

import SubscriptionContext from '../../../contexts/SubscriptionContext'

const Wrap = styled.div(({ theme }) => ({
marginBottom: theme.spacing.large,
}))

const Message = styled.p(({ theme }) => ({
...theme.partials.text.body2,
color: theme.colors['text-xlight'],
}))

const MessageLink = styled.a(({ theme }) => ({
...theme.partials.text.inlineLink,
}))

function BillingTrialBanner() {
const {
isTrialExpired,
isPaidPlan,
isTrialExpiringSoon,
isTrialed,
isTrialPlan,
} = useContext(SubscriptionContext)
const shouldDisplayBanner = useMemo(
() => (isTrialPlan && isTrialExpiringSoon) || (isTrialed && !isPaidPlan),
[isPaidPlan, isTrialExpiringSoon, isTrialPlan, isTrialed]
)
const message = useMemo(
() =>
isTrialExpired ? 'Free trial expired. ' : 'Free trial expiring soon. ',
[isTrialExpired]
)

if (!shouldDisplayBanner) return null

return (
<Wrap>
<Callout
severity={isTrialExpiringSoon || isTrialExpired ? 'danger' : 'warning'}
title={message}
closed={false}
>
<Message>
Your free trial {isTrialExpired ? 'has expired' : 'is expiring soon'}.
Upgrade to Plural Professional to&nbsp;
{isTrialExpired ? 'reactivate' : 'maintain'} full feature
access.&nbsp;
<MessageLink
as={Link}
to="/account/billing?upgrade=true"
>
Upgrade now.
</MessageLink>
</Message>
</Callout>
</Wrap>
)
}

export default BillingTrialBanner
Loading

0 comments on commit 3c6200c

Please sign in to comment.