diff --git a/www/src/components/account/billing/BillingManagePlan.tsx b/www/src/components/account/billing/BillingManagePlan.tsx
index 0acc1a613..430e6cffa 100644
--- a/www/src/components/account/billing/BillingManagePlan.tsx
+++ b/www/src/components/account/billing/BillingManagePlan.tsx
@@ -1,31 +1,134 @@
-import { Div } from 'honorable'
-import { useContext } from 'react'
+import { Button, Flex } from '@pluralsh/design-system'
-import SubscriptionContext from '../../../contexts/SubscriptionContext'
+import { useCallback, useContext, useState } from 'react'
-import BillingPreview from './BillingPreview'
-import BillingPricingCards from './BillingPricingCards'
+import { useSearchParams } from 'react-router-dom'
+
+import styled, { useTheme } from 'styled-components'
+
+import SubscriptionContext from 'contexts/SubscriptionContext'
+
+import BillingPricingCards, { ContactUs } from './BillingPricingCards'
import BillingPricingTable from './BillingPricingTable'
import ConfirmPayment from './ConfirmPayment'
-function BillingManagePlan() {
- const { isEnterprisePlan } = useContext(SubscriptionContext)
+import BillingDowngradeModal from './BillingDowngradeModal'
+import BillingStartTrialModal from './BillingStartTrialModal'
+import BillingUpgradeToProfessionalModal from './BillingUpgradeToProfessionalModal'
+
+export default function BillingManagePlan() {
+ const theme = useTheme()
+ const [searchParams, setSearchParams] = useSearchParams()
+
+ const [downgradeModalOpen, setDowngradeModalOpen] = useState(false)
+
+ const upgradeToProfessionalModalOpen =
+ typeof searchParams.get('upgrade') === 'string'
+ const setUpgradeToProfessionalModalOpen = useCallback(
+ (isOpen) => {
+ setSearchParams((sp) => {
+ if (isOpen) {
+ sp.set('upgrade', '1')
+ } else {
+ sp.delete('upgrade')
+ }
+
+ return sp
+ })
+ },
+ [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 (
- <>
+
- {!isEnterprisePlan && }
-
-
-
-
-
-
- >
+ setUpgradeToProfessionalModalOpen(true)}
+ onCancel={() => setDowngradeModalOpen(true)}
+ />
+ setUpgradeToProfessionalModalOpen(true)}
+ onCancel={() => setDowngradeModalOpen(true)}
+ />
+ {/* Modals */}
+ setUpgradeToProfessionalModalOpen(false)}
+ />
+ setDowngradeModalOpen(false)}
+ />
+ setOpenTrialModal(false)}
+ />
+
)
}
-export default BillingManagePlan
+export function ProPlanCTA({
+ onUpgrade,
+ onCancel,
+}: {
+ onUpgrade: () => void
+ onCancel: () => void
+}) {
+ const { isProPlan, isEnterprisePlan } = useContext(SubscriptionContext)
+
+ return isProPlan ? (
+
+ Cancel plan
+
+ ) : isEnterprisePlan ? (
+
+ You have an Enterprise plan
+
+ ) : (
+
+ Upgrade
+
+ )
+}
+
+export function EnterprisePlanCTA() {
+ const { isProPlan } = useContext(SubscriptionContext)
+
+ return isProPlan ? :
+}
+
+const ActionBtnSC = styled(Button)({
+ width: '100%',
+})
diff --git a/www/src/components/account/billing/BillingPricingCard.tsx b/www/src/components/account/billing/BillingPricingCard.tsx
index 56951efd5..cc4190399 100644
--- a/www/src/components/account/billing/BillingPricingCard.tsx
+++ b/www/src/components/account/billing/BillingPricingCard.tsx
@@ -67,7 +67,7 @@ function BillingPricingCard({
diff --git a/www/src/components/account/billing/BillingPricingCards.tsx b/www/src/components/account/billing/BillingPricingCards.tsx
index 0d01ebdea..230b5dc15 100644
--- a/www/src/components/account/billing/BillingPricingCards.tsx
+++ b/www/src/components/account/billing/BillingPricingCards.tsx
@@ -1,119 +1,65 @@
-import { Button } from '@pluralsh/design-system'
+import { Button, Card } from '@pluralsh/design-system'
import { Flex } from 'honorable'
-import { useCallback, useContext, useState } from 'react'
-import { useSearchParams } from 'react-router-dom'
+import { useContext } from 'react'
-import SubscriptionContext from '../../../contexts/SubscriptionContext'
+import styled from 'styled-components'
+
+import { ButtonProps } from '@pluralsh/design-system/dist/components/Button'
-import BillingDowngradeModal from './BillingDowngradeModal'
+import SubscriptionContext from '../../../contexts/SubscriptionContext'
import BillingPricingCard from './BillingPricingCard'
-import BillingStartTrialModal from './BillingStartTrialModal'
-import BillingUpgradeToProfessionalModal from './BillingUpgradeToProfessionalModal'
+import { EnterprisePlanCTA } from './BillingManagePlan'
-function ContactUs({ primary }: { primary?: boolean }) {
+export function ContactUs({ ...props }: ButtonProps) {
return (
)
}
-function CurrentPlanButton() {
- return (
-
- )
-}
-
-function BillingPricingCards() {
- const [searchParams, setSearchParams] = useSearchParams()
+function BillingPricingCards({
+ onCancel,
+ onUpgrade,
+}: {
+ onCancel: () => void
+ onUpgrade: () => void
+}) {
const { isProPlan, isEnterprisePlan } = useContext(SubscriptionContext)
- const [downgradeModalOpen, setDowngradeModalOpen] = useState(false)
-
- const upgradeToProfessionalModalOpen =
- typeof searchParams.get('upgrade') === 'string'
- const setUpgradeToProfessionalModalOpen = useCallback(
- (isOpen) => {
- setSearchParams((sp) => {
- if (isOpen) {
- sp.set('upgrade', '1')
- } else {
- sp.delete('upgrade')
- }
-
- return sp
- })
- },
- [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 (
- <>
-
+
+
+
- Free
-
-
- >
- }
+ title="Pro Plan"
+ subtitle="Cost based on # of clusters"
items={[
{
- label: 'Free forever',
- checked: true,
- },
- {
- label: 'Unlimited open-source apps',
- checked: true,
- },
- {
- label: 'Up to 2 users',
+ label: '30 day free trial',
checked: true,
},
{
- label: '1 cluster',
+ label: 'Up to 10 clusters',
checked: true,
},
{
- label: 'OAuth integration',
+ label: 'Plural cloud hosting',
checked: true,
},
{
- label: 'Community support',
+ label: '24 hour, 99.9% SLA uptime',
checked: true,
},
]}
@@ -122,79 +68,95 @@ function BillingPricingCards() {
) : isEnterprisePlan ? (
-
+
) : (
-
+
)
}
/>
- Tailored
-
-
- >
- }
+ title="Enterprise"
+ subtitle="Custom"
items={[
{
- label: 'Everything in Pro plan',
+ label: 'Pro plan perks',
checked: false,
},
{
- label: '4 hour SLA',
+ label: 'Unlimited clusters',
checked: true,
},
{
- label: 'Dedicated SRE',
+ label: 'Flexible hosting options',
checked: true,
},
{
- label: 'SSO',
+ label: '1 hour SLA',
checked: true,
},
{
- label: 'Commercial license',
+ label: 'Customized training',
checked: true,
},
{
- label: 'Cost optimization',
+ label: 'Dedicated success team',
checked: true,
},
]}
- callToAction={
- isEnterprisePlan ? (
-
- ) : isProPlan ? (
-
- ) : (
-
- )
- }
+ callToAction={}
/>
- setUpgradeToProfessionalModalOpen(false)}
- />
- setDowngradeModalOpen(false)}
- />
- setOpenTrialModal(false)}
- />
- >
+
+ )
+}
+
+function CurrentPlanCard({ onCancel }: { onCancel: () => void }) {
+ const { subscription, isProPlan } = useContext(SubscriptionContext)
+
+ return (
+
+
+ You are currently on the{' '}
+ {subscription?.plan?.name ?? 'Free'} Plan
+
+ {isProPlan && (
+
+ )}
+
)
}
+const CurrentPlanCardSC = styled(Card)(({ theme }) => ({
+ ...theme.partials.text.body1Bold,
+ padding: theme.spacing.medium,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ '& em': {
+ fontStyle: 'normal',
+ color: theme.colors['text-primary-accent'],
+ },
+}))
export default BillingPricingCards
diff --git a/www/src/components/account/billing/BillingPricingTable.tsx b/www/src/components/account/billing/BillingPricingTable.tsx
index 70a6a9a79..7a6f33934 100644
--- a/www/src/components/account/billing/BillingPricingTable.tsx
+++ b/www/src/components/account/billing/BillingPricingTable.tsx
@@ -1,285 +1,171 @@
import { CheckIcon, CloseIcon } from '@pluralsh/design-system'
-import { Div } from 'honorable'
+import styled from 'styled-components'
-const columnStyles = {
- position: 'relative',
- boxShadow: '-12px 0 12px 0px rgb(14 16 21 / 20%)',
- '&:first-child': {
- boxShadow: 'none',
- '> div': {
- display: 'flex',
- justifyContent: 'flex-end',
- textAlign: 'right',
+import { ReactNode } from 'react'
+
+import { EnterprisePlanCTA, ProPlanCTA } from './BillingManagePlan'
+
+export default function BillingPricingTable({
+ onUpgrade,
+ onCancel,
+}: {
+ onUpgrade: () => void
+ onCancel: () => void
+}) {
+ const plans = getPlans({
+ proPlanCTA: (
+
+ ),
+ enterprisePlanCTA: ,
+ })
+
+ return (
+
+
+
+ {plans.map((plan) => (
+
+ ))}
+
+
+
+
+ {plans.map((plan) => (
+ {plan.name}
+ ))}
+
+
+
+ {rows.map((rowLabel) => (
+
+ {rowLabel !== 'action' && rowLabel}
+ {plans.map((plan) => (
+ {plan.values[rowLabel]}
+ ))}
+
+ ))}
+
+
+ )
+}
+
+const CheckIconSC = styled(CheckIcon).attrs(() => ({
+ color: 'icon-success',
+}))``
+
+const TableSC = styled.table(({ theme }) => ({
+ tableLayout: 'fixed',
+ width: '100%',
+ background: theme.colors['fill-one'],
+ borderSpacing: 0,
+ borderRadius: theme.borderRadiuses.medium,
+ border: theme.borders.default,
+ '& th, & td': {
+ borderBottom: theme.borders.default,
+ borderRight: theme.borders.default,
+ // targets last cell in each row
+ '&:last-child': {
+ borderRight: 'none',
},
},
- '> div': {
- display: 'flex',
- alignItems: 'center',
- padding: '8px 16px',
- height: 52,
- borderTop: '1px solid border-fill-two',
- },
- '> div:nth-child(even)': {
- backgroundColor: 'fill-two',
+ // targets each cell in last row
+ '& tr:last-child td': {
+ borderBottom: 'none',
},
- '> div:nth-child(odd)': {
- backgroundColor: 'fill-one',
- },
- '> div:first-child': {
- height: 96,
+}))
+
+const TableBodySC = styled.tbody(({ theme }) => ({
+ '& tr:nth-child(odd)': {
+ backgroundColor: theme.colors['fill-one-selected'],
},
-}
+}))
-const firstColumnCellProps = {
- body2: true,
- borderLeft: '1px solid border-fill-two',
-}
+const TableHeaderCellSC = styled.th(({ theme }) => ({
+ ...theme.partials.text.subtitle2,
+ padding: `${theme.spacing.xlarge}px ${theme.spacing.large}px`,
+ textAlign: 'left',
+}))
-const lastColumnCellProps = {
- borderRight: '1px solid border-fill-two',
-}
+const TableCellSC = styled.td(({ theme }) => ({
+ ...theme.partials.text.body2,
+ padding: `${theme.spacing.medium}px ${theme.spacing.large}px`,
+ color: theme.colors['text-xlight'],
+ whiteSpace: 'pre-wrap',
+}))
-function BillingPricingTable() {
- return (
-
-
-
-
- Open-Source Apps
-
-
Clusters
-
Users
-
Services
-
Roles
-
Groups
-
Service accounts
-
Continuous deployment
-
Discord Forum
-
Community support
-
- Private Slack Connect to Plural engineers
-
-
Dedicated support engineer
-
Onboarding
-
Emergency Hotfixes
-
SLAs
-
Coverage
-
Authentication
-
VPN
-
Audit logs
-
SOC 2
-
GDPR
-
Compliance reports
-
Training
-
Developer support
-
Commercial license
-
Cost optimization
-
- Invoices
-
-
-
-
- Open-source
-
-
Unlimited
-
Free
-
Up to 2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Best effort
-
Google OAuth + OIDC
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Custom
-
-
- Unlimited
-
-
- Custom
-
-
- Unlimited
-
-
- Unlimited
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
4 hours
-
- Extended
-
-
- SSO + Google OAuth + OIDC
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Available
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
+const rows = [
+ 'Clusters',
+ 'Hosting',
+ 'SLA',
+ 'SLA Response',
+ 'Communication',
+ 'Success Team',
+ 'Customized Training',
+ 'Multi-cluster Deployment',
+ 'CD Pipelines',
+ 'IaC Management',
+ 'PR Automations',
+ 'Git Integrations',
+ 'Policy Management',
+ 'GDPR Compliant',
+ 'SOC 2 Compliant',
+ 'action', // not shown but used for bottom button
+]
-export default BillingPricingTable
+const getPlans = ({
+ proPlanCTA,
+ enterprisePlanCTA,
+}: {
+ proPlanCTA: ReactNode
+ enterprisePlanCTA: ReactNode
+}) => [
+ {
+ name: 'Pro Plan',
+ values: {
+ Clusters: 'Up to 10',
+ Hosting: 'Plural shared infrastructure',
+ SLA: '99.9% uptime',
+ 'SLA Response': '1 business day response',
+ Communication: 'Email support',
+ 'Success Team': ,
+ 'Customized Training': ,
+ 'Multi-cluster Deployment': ,
+ 'CD Pipelines': ,
+ 'IaC Management': ,
+ 'PR Automations': ,
+ 'Git Integrations': ,
+ 'Policy Management': ,
+ 'GDPR Compliant': ,
+ 'SOC 2 Compliant': ,
+ action: proPlanCTA,
+ },
+ },
+ {
+ name: 'Enterprise',
+ values: {
+ Clusters: 'Unlimited',
+ Hosting: 'Shared, dedicated, or on-prem infrastructure',
+ SLA: '99.9% uptime',
+ 'SLA Response': '1 hour guaranteed response',
+ Communication: 'Dedicated Slack or Teams\nOn-demand Calls',
+ 'Success Team': ,
+ 'Customized Training': ,
+ 'Multi-cluster Deployment': ,
+ 'CD Pipelines': ,
+ 'IaC Management': ,
+ 'PR Automations': ,
+ 'Git Integrations': ,
+ 'Policy Management': ,
+ 'GDPR Compliant': ,
+ 'SOC 2 Compliant': ,
+ action: enterprisePlanCTA,
+ },
+ },
+]
diff --git a/www/src/components/account/billing/PaymentForm.tsx b/www/src/components/account/billing/PaymentForm.tsx
index 8b2c5f29e..9f4040afb 100644
--- a/www/src/components/account/billing/PaymentForm.tsx
+++ b/www/src/components/account/billing/PaymentForm.tsx
@@ -182,6 +182,7 @@ function PaymentFormInner() {
{formVariant === PaymentFormVariant.Upgrade && (
, '$variant'>) {
minWidth="175px"
left={sidebarWidth + 8}
border="1px solid border"
- // Fix incorrect borders due to mixed element types
- {...{
- '&, &>*:first-of-type:not(:first-child) > div': {
- borderTop: theme.borders['fill-two'],
- },
- }}
onClick={() => setIsMenuOpened(false)}
>