Update subscription change page with new design that includes team tier

This commit is contained in:
Joshen Lim committed 2023-01-17 11:57:53 +08:00
1 parent 132022225f
commit 1d66832113
6 files changed
+204 -204

No files matched your search

@@ -2,8 +2,9 @@ import Link from 'next/link'
import { FC } from 'react'
import { Transition } from '@headlessui/react'
import Plans from './Plans/Plans'
import PlanCard from './Plans/PlanCard'
import { formatTierOptions } from './PlanSelection.utils'
import { Button, IconExternalLink } from 'ui'
interface Props {
visible: boolean
@@ -16,7 +17,10 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
const formattedTiers = formatTierOptions(tiers)
// [Kevin] TODO Remove after team plan is generally available
const gridCols = formattedTiers.length === 3 ? 'gap-8 xl:grid-cols-3' : 'gap-4 xl:grid-cols-4'
const gridCols =
formattedTiers.length === 3
? 'gap-8 lg:grid-cols-3'
: 'gap-4 lg:grid-cols-3 xl:grid-cols-billingWithTeam'
return (
<Transition
@@ -28,15 +32,27 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
<div>
<h4 className="text-lg mb-8">Change your project's subscription</h4>
<div className={`grid py-8 grid-cols-2 ${gridCols}`}>
<Plans plans={formattedTiers} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
{formattedTiers.map((plan) => {
return (
<PlanCard
key={plan.id}
plan={plan}
currentPlan={currentPlan}
onSelectPlan={() => onSelectPlan(plan)}
/>
)
})}
</div>
<div className="flex justify-center items-center mt-4">
<Link href="https://supabase.com/pricing">
<a
target="_blank"
className="text-sm underline text-scale-1100 hover:text-scale-1200 transition"
>
See detailed comparisons across plans
<a target="_blank">
<Button
type="link"
icon={<IconExternalLink size={14} strokeWidth={1.5} />}
className="text-sm text-scale-1000 hover:text-scale-1100 hover:bg-scale-400"
>
See detailed comparisons across plans
</Button>
</a>
</Link>
</div>
@@ -17,8 +17,8 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const getButtonType = (plan: any, currentPlan: any) => {
if (['Enterprise', 'Free tier'].includes(plan.name)) {
// Enterprise/Free are always default
if (['Free tier'].includes(plan.name)) {
// Free is always default
return 'default'
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE) {
// If the current plan is free, other plans are primary
@@ -90,7 +90,7 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
return (
<Link href="https://supabase.com/contact/enterprise">
<a>
<Button disabled={disabled} type={type} block size="medium">
<Button disabled={disabled} type={type} block>
{ctaText}
</Button>
</a>
@@ -100,20 +100,33 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
return (
<div className="flex flex-col items-center justify-center space-y-2">
<Button
disabled={disabled || projectUpdateDisabled}
type={type}
onClick={() => onSelectPlan(plan)}
block
size="medium"
>
{ctaText}
</Button>
{!disabled && projectUpdateDisabled && (
<p className="text-sm text-scale-1100">
Subscription changes are currently disabled, our engineers are working on a fix
</p>
)}
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger className="w-full">
<Button
block
type={type}
disabled={disabled || projectUpdateDisabled}
onClick={() => onSelectPlan(plan)}
>
{ctaText}
</Button>
</Tooltip.Trigger>
{!disabled && projectUpdateDisabled && (
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 w-[260px] flex items-center justify-center',
].join(' ')}
>
<span className="text-xs text-scale-1200 text-center">
Subscription changes are currently disabled, our engineers are working on a fix
</span>
</div>
</Tooltip.Content>
)}
</Tooltip.Root>
</div>
)
}
@@ -0,0 +1,131 @@
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { FC } from 'react'
import { IconCheck } from 'ui'
import { useFlag } from 'hooks'
import PlanCTAButton from './PlanCTAButton'
import { PRICING_META } from './Plans.Constants'
interface Props {
plan: any
currentPlan: any
onSelectPlan: () => void
}
const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
const teamTierEnabled = useFlag('teamTier')
const planMeta = PRICING_META[plan.id]
const isEnterprise = plan.id === 'Enterprise'
const isCurrentPlan =
plan.id === currentPlan?.prod_id ||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) ||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG)
return (
<div
className={[
'px-6 py-6 border rounded flex flex-col justify-between',
`${isEnterprise ? 'bg-scale-200' : 'bg-scale-300'}`,
`${isEnterprise && teamTierEnabled ? 'lg:col-span-3 xl:col-auto' : ''}`,
].join(' ')}
>
<div
className={[
'space-y-8',
`${isEnterprise ? 'flex flex-col xl:space-x-0 xl:flex-col' : ''}`,
`${isEnterprise && teamTierEnabled ? 'lg:flex-row lg:space-x-24' : ''}`,
].join(' ')}
>
<div
className={[
'space-y-4',
`${isEnterprise ? 'xl:w-auto' : ''}`,
`${isEnterprise && teamTierEnabled ? 'xl:w-auto' : ''}`,
].join(' ')}
>
<div className="flex items-center space-x-2">
<p
className={[
'text-brand-900 text-sm',
`${isEnterprise ? 'lg:text-base xl:text-sm font-bold font-mono uppercase' : ''}`,
].join(' ')}
>
{planMeta.name}
</p>
{isCurrentPlan ? (
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
Current plan
</div>
) : planMeta.new ? (
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">New</div>
) : (
<></>
)}
</div>
<p className="text-sm text-scale-1100">{planMeta.description}</p>
<div
className={[
'space-y-2',
`${
isEnterprise
? 'pb-6 border-b lg:pt-6 lg:border-b-0 lg:border-t xl:pt-0 xl:pb-6 xl:border-t-0 xl:border-b'
: 'border-b border-t py-8'
}`,
].join(' ')}
>
{planMeta.priceMonthly !== undefined ? (
<>
<p className="text-sm text-scale-1000">Starting from</p>
<div className="space-y-1">
<p className="text-[3.4rem] leading-none">${planMeta.priceMonthly}</p>
<p className="text-sm text-scale-1000">{planMeta.priceUnit}</p>
</div>
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5 w-fit">
{planMeta.warning}
</div>
</>
) : (
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
)}
</div>
</div>
<div className={`${isEnterprise ? 'lg:space-y-0 lg:!mt-0 xl:space-y-4' : 'space-y-4'}`}>
<p className="text-xs text-scale-1000">{planMeta.preface}</p>
<ul
role="list"
className={[
`${isEnterprise ? 'xl:gap-0 xl:columns-1' : ''}`,
`${isEnterprise && teamTierEnabled ? 'lg:gap-16 lg:columns-2' : ''}`,
].join(' ')}
>
{planMeta.features.map((feature) => (
<li key={feature} className="flex py-2">
<div className="w-[12px]">
<IconCheck
className="h-3 w-3 text-brand-900 translate-y-[2.5px]"
aria-hidden="true"
strokeWidth={3}
/>
</div>
<p className="ml-3 text-xs text-scale-1100">{feature}</p>
</li>
))}
</ul>
</div>
</div>
{!isEnterprise && (
<div className="space-y-4 !mt-20">
<div className="space-y-2">
<p className="text-sm text-scale-1100">{planMeta.additional}</p>
<p className="text-xs text-scale-1000">{planMeta.scale}</p>
</div>
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
</div>
)}
</div>
)
}
export default PlanCard
@@ -9,10 +9,11 @@ import { STRIPE_PRODUCT_IDS } from 'lib/constants'
export const PRICING_META = {
[STRIPE_PRODUCT_IDS.FREE]: {
name: 'Free',
href: '#',
priceMonthly: 0,
warning: 'Limit of 2 free projects',
description: 'Perfect for passion projects & simple websites.',
priceMonthly: 0,
priceUnit: 'per month per project',
warning: 'Limit of 2 free projects',
preface: 'Get started with:',
features: [
'Up to 500MB database & 1GB file storage',
'Up to 2GB bandwidth',
@@ -25,17 +26,15 @@ export const PRICING_META = {
'2 million Realtime messages',
'Community support',
],
additional: 'Free projects are paused after 1 week of inactivity.',
cta: 'Get Started',
scale: 'Free projects are paused after 1 week of inactivity.',
},
[STRIPE_PRODUCT_IDS.PRO]: {
name: 'Pro',
href: '#',
from: true,
priceMonthly: 25,
warning: '+ any additional usage',
description: 'For production applications with the option to scale.',
priceMonthly: 25,
priceUnit: 'per month per project',
warning: '+ any additional usage',
preface: 'Everything in the Free plan, plus:',
features: [
'8GB database & 100GB file storage',
'50GB bandwidth',
@@ -50,19 +49,17 @@ export const PRICING_META = {
'No project pausing',
'Email support',
],
scale: 'Additional fees apply for usage and storage beyond the limits above.',
shutdown: '',
preface: 'Everything in the base plan, plus',
additional: 'Need more? Turn off your spend cap to Pay As You Grow ',
cta: 'Get Started',
scale: 'Additional fees apply for usage and storage beyond the limits above.',
},
[STRIPE_PRODUCT_IDS.TEAM]: {
new: true,
name: 'Team',
href: '#',
from: true,
priceMonthly: 599,
warning: '+ usage',
description: 'For collaborative teams with different permissions and access.',
priceMonthly: 599,
priceUnit: 'per month per organization',
warning: '+ usage',
preface: 'The following features will apply to all projects within the organization:',
features: [
'Usage-based pricing',
'100,000 monthly active users included',
@@ -75,10 +72,6 @@ export const PRICING_META = {
'14 day backups',
'28 day log retention',
],
scale: '',
shutdown: '',
additional: '',
cta: 'Get Started',
},
Enterprise: {
name: 'Enterprise',
@@ -94,8 +87,5 @@ export const PRICING_META = {
`On-premise support`,
`24×7×365 premium enterprise support`,
],
scale: '',
shutdown: '',
cta: 'Contact Us',
},
}
@@ -1,154 +0,0 @@
import { FC } from 'react'
import { IconCheck } from 'ui'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { StripeProduct } from 'components/interfaces/Billing'
import PlanCTAButton from './PlanCTAButton'
import { PRICING_META } from './Plans.Constants'
const AnimatedGradientBackground = () => (
<div
className={[
'absolute top-0 left-0 h-full w-full animate-sway',
'bg-gradient-to-b from-transparent via-transparent to-green-900',
].join(' ')}
/>
)
const CurrentSubscriptionBanner = () => (
<div
className={[
'absolute -top-7 left-0 h-8 w-full',
'flex items-center justify-center',
'bg-brand-400 text-brand-900',
'rounded-t-md border border-t border-l border-r border-b-0',
].join(' ')}
>
<p className="text-xs">Current plan</p>
</div>
)
interface Props {
plans: any[]
currentPlan?: StripeProduct
onSelectPlan: (plan: any) => void
}
/**
* JSX below for the mapped plans is also very similar to www/pages/pricing
* TO DO: move to use shared components for these
*
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
*/
const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
return (
<>
{/* Existing available plans */}
{plans.map((plan) => {
const isCurrentPlan =
plan.id === currentPlan?.prod_id ||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) ||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG)
return (
<div key={PRICING_META[plan.id].name} className="relative h-full">
{plan.id === STRIPE_PRODUCT_IDS.PRO && (
<div className="absolute left-0 bottom-0 h-full w-full overflow-hidden opacity-50">
<AnimatedGradientBackground />
</div>
)}
<div className="flex h-full flex-col overflow-hidden rounded border">
{isCurrentPlan && <CurrentSubscriptionBanner />}
<div className="bg-white px-6 py-6 dark:bg-scale-300">
<span
className="text-cd inline-flex rounded-full text-base font-normal tracking-wide text-scale-1200"
id="tier-standard"
>
{PRICING_META[plan.id].name}
</span>
<div className="mt-2 flex items-baseline">
<div className="flex space-x-2">
{PRICING_META[plan.id].priceMonthly !== undefined ? (
<>
<div>
{PRICING_META[plan.id].from && (
<span className="text-sm font-medium text-scale-1200 mr-1">From</span>
)}
<span className="text-2xl">${PRICING_META[plan.id].priceMonthly}</span>
<span className="text-xl font-medium text-scale-900">/mo</span>
</div>
</>
) : (
<span className="text-2xl">Contact Us</span>
)}
<div className="flex h-8 -mt-[2px]">
{PRICING_META[plan.id].warning && (
<div className="mt-2 rounded-md bg-brand-300 bg-opacity-30 px-2 py-1 text-xs text-brand-1000">
{PRICING_META[plan.id].warning}
</div>
)}
</div>
</div>
</div>
<div className="py-4">
<PlanCTAButton
plan={plan}
currentPlan={currentPlan}
onSelectPlan={onSelectPlan}
/>
</div>
<p className="text-sm text-scale-1100">{PRICING_META[plan.id].description}</p>
</div>
<div
className={[
'flex-1 flex-col justify-between',
'space-y-6 border-t dark:border-scale-400',
'bg-scale-100 dark:bg-scale-300',
'hidden h-full px-6 py-6 lg:flex',
].join(' ')}
>
<div className="space-y-6">
{PRICING_META[plan.id].preface && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].preface}</p>
)}
<ul role="list" className="divide-y dark:divide-scale-400">
{PRICING_META[plan.id].features.map((feature) => (
<li key={feature} className="flex py-2">
<div className="w-[12px]">
<IconCheck
className="h-3 w-3 text-brand-900 translate-y-[2.5px]"
aria-hidden="true"
strokeWidth={3}
/>
</div>
<p className="mb-0 ml-3 text-xs text-scale-1100 dark:text-scale-1200">
{feature}
</p>
</li>
))}
</ul>
</div>
<div className="flex flex-col gap-6">
<div className="space-y-2">
{PRICING_META[plan.id].additional && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].additional}</p>
)}
{PRICING_META[plan.id].scale && (
<p className="text-xs text-scale-900">{PRICING_META[plan.id].scale}</p>
)}
{PRICING_META[plan.id].shutdown && (
<p className="text-xs text-scale-900">{PRICING_META[plan.id].shutdown}</p>
)}
</div>
</div>
</div>
</div>
</div>
)
})}
</>
)
}
export default Plans
+4
View File
@@ -1,3 +1,4 @@
const { repeat } = require('lodash')
const ui = require('./../packages/config/ui.config.js')
const blueGray = {
@@ -120,6 +121,9 @@ module.exports = ui({
fontSize: {
grid: '13px',
},
gridTemplateColumns: {
billingWithTeam: 'repeat(3, minmax(0, 1fr)) 0.9fr',
},
colors: {
// gray: { ...gray },
// green: { ...green },