mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Update subscription change page with new design that includes team tier
This commit is contained in:
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
|
||||
@@ -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 },
|
||||
|
||||
Reference in new issue
Block a user