feat: add team plan

This commit is contained in:
Kevin Grüneberg committed 2023-06-08 10:59:56 +02:00
1 parent 121c28b111
commit e3df2f453d
30 files changed
+744 -732

No files matched your search

@@ -88,9 +88,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
<td key={column.key} className="p-3">
{column.key === 'dedicated' ? (
column.value ? (
<IconPricingIncludedCheck tier="Pro plan" />
<IconPricingIncludedCheck plan="Pro plan" />
) : (
<IconPricingMinus tier="Free plan" />
<IconPricingMinus plan="Free plan" />
)
) : (
column.value
@@ -124,9 +124,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
>
{column.key === 'dedicated' ? (
column.value ? (
<IconPricingIncludedCheck tier="Pro plan" />
<IconPricingIncludedCheck plan="Pro plan" />
) : (
<IconPricingMinus tier="Free plan" />
<IconPricingMinus plan="Free plan" />
)
) : (
column.value
+2 -2
View File
@@ -12,7 +12,7 @@ export const Check = () => (
export const IconPricingIncludedCheck = (props: any) => (
<span className="mx-auto">
<Check />
<span className="sr-only">Included in {props.tier}</span>
<span className="sr-only">Included in {props.plan}</span>
</span>
)
@@ -32,7 +32,7 @@ export const IconPricingMinus = (props: any) => (
clipRule="evenodd"
/>
</svg>
<span className="sr-only">Not included in {props.tier}</span>
<span className="sr-only">Not included in {props.plan}</span>
</>
)
+18 -18
View File
@@ -48,35 +48,35 @@ export const PricingTableRowDesktop = (props: any) => {
)}
</th>
{Object.entries(feat.tiers).map((entry: any, i) => {
const tierName = entry[0]
const tierValue = entry[1]
{Object.entries(feat.plans).map((entry: any, i) => {
const planName = entry[0]
const planValue = entry[1]
return (
<td
key={i}
className={[
`px-6 tier-${tierName}`,
typeof tierValue === 'boolean' ? 'text-center' : '',
`px-6 tier-${planName}`,
typeof planValue === 'boolean' ? 'text-center' : '',
].join(' ')}
>
{typeof tierValue === 'boolean' && tierValue === true ? (
<IconPricingIncludedCheck tier={tierValue} />
) : typeof tierValue === 'boolean' && tierValue === false ? (
{typeof planValue === 'boolean' && planValue === true ? (
<IconPricingIncludedCheck plan={planValue} />
) : typeof planValue === 'boolean' && planValue === false ? (
<div className="text-scale-900">
<IconPricingMinus tier={tierValue} />
<IconPricingMinus plan={planValue} />
</div>
) : (
<span className="text-scale-1200 text-xs dark:text-white flex items-center gap-3">
{feat.tooltips?.[tierName] && (
{feat.tooltips?.[planName] && (
<span
className="shrink-0 hover:text-scale-300 cursor-pointer transition-colors"
data-tip={feat.tooltips[tierName]}
data-tip={feat.tooltips[planName]}
>
<IconPricingInfo />
</span>
)}
{tierValue}
{planValue}
</span>
)}
</td>
@@ -96,7 +96,7 @@ export const PricingTableRowDesktop = (props: any) => {
export const PricingTableRowMobile = (props: any) => {
const category = props.category
const tier = props.tier
const plan = props.plan
return (
<>
@@ -127,16 +127,16 @@ export const PricingTableRowMobile = (props: any) => {
</span>
</th>
<td className="py-3 pr-4 text-right">
{typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === true ? (
{typeof feat.plans[plan] === 'boolean' && feat.plans[plan] === true ? (
<div className="inline-block">
<IconPricingIncludedCheck tier={tier} />
<IconPricingIncludedCheck plan={plan} />
</div>
) : typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === false ? (
) : typeof feat.plans[plan] === 'boolean' && feat.plans[plan] === false ? (
<div className="inline-block">
<IconPricingMinus tier={tier} />
<IconPricingMinus plan={plan} />
</div>
) : (
<span className="text-scale-1200 block text-sm">{feat.tiers[tier]}</span>
<span className="text-scale-1200 block text-sm">{feat.plans[plan]}</span>
)}
</td>
</tr>
+1 -1
View File
@@ -90,6 +90,6 @@ const nextConfig = {
// next.config.js.
export default () => {
const plugins = [withMDX, withBundleAnalyzer, withTM(['ui', 'common'])]
const plugins = [withMDX, withBundleAnalyzer, withTM(['ui', 'common', 'shared-data'])]
return plugins.reduce((acc, next) => next(acc), nextConfig)
}
+1
View File
@@ -55,6 +55,7 @@
"remark": "^13.0.0",
"remark-gfm": "^3.0.1",
"remark-html": "^15.0.1",
"shared-data": "*",
"swiper": "^6.5.0",
"swr": "^0.5.4",
"typescript": "^4.0.3",
-99
View File
@@ -1,99 +0,0 @@
Pricing Changelog
### 18 Jan 2023
- Clarified over-usage of Realtime messages per month
- On the Pro plan, $2.50 per million after included 5 million messages
### 09 Jan 2023
----
Database Compute Add ons
- Clarified iops values to the latest instance types
### 02 Nov 2022
----
Database
- guidance on backups > 1TB in size
- Removed "Realtime functionality" as it is redundant with new realtime section
Auth
- added “advanced security features”
- MAUs changed to unlimited for Enterprise
Realtime
— Added new realtime section
- Concurrent Peak Connections
- Max Messages Per Month
- Max Message Size
Edge Functions
- Runtime hours removed
- Number of functions, usage pricing added after limit on Pro
- Invocations, usage pricing added after limit on Pro
Support
- Security features moved to own section
Platform Security and Compliance
- New section made
- added Access roles
- added Custom Domains
- added Vanity URLs added
Database Compute Add ons
- New label “Optimized Compute”
### 16 June 2022
----
Auth
- New item “MAUs (Monthly Active Users)”
Security & Compliance
- New item: “Security questionnaire help” for enterprise
- New item - “Metrics Endpoint” for enterprise and pro plan users
Database Compute Add ons
- New item: “4XL”
- New item: “8XL”
- New item: “12XL”
- New item: “16XL”
Database
- New item: “Point in time recovery” for enterprise
Clarifications
- General - Clearer language that we charge on a per project per month
- Storage - Uploads for Pro, changed from 3GB → 5GB in title card
- Storage - file upload size - clarified that it can be changed in dashboard
- All products - “Transfer” renamed to “Egress”
- Database compute add-ons - Number of connections more accurate
- Storage - Free plan egress per month clarified
### 31 March 2022
----
Edge Functions pricing announced
### 29 March 2022
----
— Enterprise pricing plan announced
— Pay as you go plan now the same as the Pro plan
27 August 2021
----
- Changes "Dedicated support" to "Designated support".
- Dedicated = one individual solely focused on an account.
- Designated = individual who may be helping across various accounts, but still a single point of contact. May be dedicated or non-dedicated.
28 July 2021
----
- Clarified pricing on the "Optimized database instances" to include the starting price of $50/month.
### 29 March 2021
----
Released our Beta pricing.
+231 -206
View File
@@ -8,21 +8,25 @@ import ReactMarkdown from 'react-markdown'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
import { PricingTableRowDesktop, PricingTableRowMobile } from '~/components/Pricing/PricingTableRow'
import pricing from '~/data/Pricing.json'
import pricing from 'shared-data/Pricing.json'
import pricingFaq from '~/data/PricingFAQ.json'
import { useTheme } from 'common/Providers'
import ComputePricingModal from '~/components/Pricing/ComputePricingModal'
import { plans } from 'shared-data/plans'
export default function IndexPage() {
const router = useRouter()
const { basePath, asPath } = useRouter()
const { isDarkMode } = useTheme()
const [showComputeModal, setShowComputeModal] = useState(false)
const [activeMobileTier, setActiveMobileTier] = useState('Free')
const [activeMobilePlan, setActiveMobilePlan] = useState('Free')
const meta_title = 'Pricing & fees | Supabase'
const meta_description =
'Explore Supabase fees and pricing information. Find our competitive pricing tiers, with no hidden pricing. We have a generous free plan for those getting started, and Pay As You Go for those scaling up.'
'Explore Supabase fees and pricing information. Find our competitive pricing plans, with no hidden pricing. We have a generous free plan for those getting started, and Pay As You Go for those scaling up.'
const plansExceptEnterprise = plans.filter((it) => it.name !== 'Enterprise')
const planEnterprise = plans.find((it) => it.name === 'Enterprise')!
// Ability to scroll into pricing sections like storage
useEffect(() => {
@@ -45,93 +49,6 @@ export default function IndexPage() {
}
}, [asPath])
/**
* @mildtomato same plan metadata is also in /studio dashboard
* would be good if this constant was shared across apps
*
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
*/
const tiers = [
{
name: 'Free',
nameBadge: '',
costUnit: 'per month per project',
href: 'https://supabase.com/dashboard/new/new-project',
priceLabel: 'Starting from',
priceMonthly: 0,
warning: 'Limit of 2 free projects',
description: 'Perfect for passion projects & simple websites.',
preface: 'Get started with:',
features: [
'Social OAuth providers',
'Up to 500MB database & 1GB file storage',
'Up to 2GB bandwidth',
'Up to 50MB file uploads',
'50,000 monthly active users',
'Up to 500K Edge Function invocations',
'200 concurrent Realtime connections',
'2 million Realtime messages',
'1-day log retention',
'Community support',
],
scale: 'Free projects are paused after 1 week of inactivity.',
cta: 'Get Started',
},
{
name: 'Pro',
nameBadge: '',
costUnit: 'per month per project',
href: 'https://supabase.com/dashboard/new/new-project',
from: true,
priceLabel: 'Starting from',
warning: '+ usage',
priceMonthly: 25,
description: 'For production applications with the option to scale.',
features: [
'No project pausing',
'8GB database & 100GB file storage',
'50GB bandwidth',
'5GB file uploads',
'100,000 monthly active users',
'2M Edge Function invocations',
'500 concurrent Realtime connections',
'5 million Realtime messages',
'7-day log retention',
'Email support',
'Daily backups',
],
scale: 'Additional fees apply for usage and storage beyond the limits above.',
shutdown: '',
preface: 'Everything in the Free plan, plus:',
additional: '',
cta: 'Get Started',
},
{
name: 'Enterprise',
href: 'https://forms.supabase.com/enterprise',
description: 'For large-scale applications managing serious workloads.',
features: [
`Designated Support manager & SLAs`,
`Enterprise OAuth providers`,
`SSO/ SAML`,
`SOC2`,
`Custom contracts & invoicing`,
`On-premise support`,
`24×7×365 premium enterprise support`,
`Custom Security questionnaires`,
`Private Slack channel`,
`Uptime SLA`,
],
priceLabel: '',
priceMonthly: 'Contact us',
preface: 'These apply to all projects within the organization:',
scale: '',
shutdown: '',
cta: 'Contact Us',
},
]
const addons = [
{
name: 'Optimized Compute',
@@ -174,21 +91,21 @@ export default function IndexPage() {
description,
priceDescription,
price,
tier,
plan,
showDollarSign = true,
from = false,
}: {
description: string
priceDescription: string
price: string
tier: string
plan: string
showDollarSign?: boolean
from?: boolean
}) => {
return (
<div className="mt-8 px-4 mobile-header">
<h2 className="gradient-text-brand-500 dark:gradient-text-brand-100 text-3xl font-medium uppercase font-mono">
{tier}
{plan}
</h2>
<div className="flex items-baseline gap-2">
{from && <span className="text-scale-1200 text-base">From</span>}
@@ -242,7 +159,7 @@ export default function IndexPage() {
</div>
</div>
<div className="mx-auto flex max-w-7xl flex-col">
<div className="mx-auto max-w-7xl flex flex-col">
{/* <div className="absolute inset-0 shadow-sm bg-scale-200 h-3/5" /> */}
<div
@@ -252,28 +169,28 @@ export default function IndexPage() {
"
>
<div className="mx-auto max-w-md grid lg:max-w-6xl lg:grid-cols-3 gap-24 lg:gap-5">
{tiers.map((tier) => (
{plansExceptEnterprise.map((plan) => (
<div
key={`row-${tier.name}`}
key={`row-${plan.name}`}
className={[
tier.name === 'Pro'
plan.name === 'Pro'
? 'bg-brand-1100 dark:bg-brand-900 border px-0.5 -mt-8 rounded-[6px]'
: '',
].join(' ')}
>
{tier.name === 'Pro' && (
{plan.name === 'Pro' && (
<p className="text-xs text-center py-2 text-white">Most Popular</p>
)}
<div
key={tier.name}
key={plan.name}
className={[
'flex flex-col overflow-hidden',
tier.name === 'Pro' ? '' : 'border h-full rounded-[4px]',
plan.name === 'Pro' ? '' : 'border h-full rounded-[4px]',
].join(' ')}
>
<div
className={`dark:bg-scale-300 bg-white px-8 pt-6 rounded-tr-[4px] rounded-tl-[4px] ${
tier.name === 'Pro' ? 'rounded-tr-[4px] rounded-tl-[4px]' : ''
plan.name === 'Pro' ? 'rounded-tr-[4px] rounded-tl-[4px]' : ''
}`}
>
<div className="mb-2 flex items-center gap-2">
@@ -282,55 +199,56 @@ export default function IndexPage() {
className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-normal
uppercase flex items-center gap-4 font-mono"
>
{tier.name}
{plan.name}
</h3>
{tier.nameBadge && (
<span className="bg-scale-300 text-scale-900 dark:bg-scale-400 dark:text-scale-1100 rounded-md bg-opacity-10 py-0.5 px-2 text-xs [background-image: none]">
{tier.nameBadge}
{plan.nameBadge && (
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
{plan.nameBadge}
</span>
)}
</div>
</div>
<p className="text-scale-1100 my-4 h-[55px] text-sm border-b dark:border-scale-500 pb-4 lg:pr-20">
{tier.description}
<p className="text-scale-1100 my-4 text-sm border-b dark:border-scale-500 pb-4 2xl:pr-4">
{plan.description}
</p>
<div
className="
text-scale-1200 flex items-baseline
text-5xl
font-normal
lg:text-4xl
xl:text-4xl
border-b
dark:border-scale-500
pt-4
pb-8
min-h-[175px]
"
className={`
text-scale-1200 flex items-baseline
text-5xl
font-normal
lg:text-4xl
xl:text-4xl
border-b
dark:border-scale-500
min-h-[175px] ${plan.priceLabel ? 'pt-6' : 'pt-10'}`}
>
<div className="flex flex-col gap-1">
<div className="flex items-end gap-2">
<div>
<p className="text-scale-900 ml-1 text-xs font-normal">
{tier.priceLabel}
</p>
<p
className={`mt-2 gradient-text-scale-500 dark:gradient-text-scale-100 pb-1 ${
tier.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
}`}
>
{tier.name !== 'Enterprise' && '$'}
{tier.priceMonthly}
</p>
{tier.costUnit && (
<p className="text-scale-900 mt-0.5 text-xs">{tier.costUnit}</p>
{plan.priceLabel && (
<p className="text-scale-900 ml-1 text-xs font-normal">
{plan.priceLabel}
</p>
)}
{tier.warning && (
<div className="flex items-end">
<p
className={`mt-2 gradient-text-scale-500 dark:gradient-text-scale-100 pb-1 ${
plan.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
}`}
>
${plan.priceMonthly}
</p>
<p className="text-scale-900 mb-1.5 ml-1 text-xs">
{plan.costUnit}
</p>
</div>
{plan.warning && (
<p className="-mt-2">
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-xs ">
{tier.warning}
<span className="bg-scale-200 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
{plan.warning}
</span>
</p>
)}
@@ -342,18 +260,18 @@ export default function IndexPage() {
<div
className={[
`dark:border-scale-400 dark:bg-scale-300 flex h-full rounded-bl-[4px] rounded-br-[4px] flex-1 flex-col bg-white px-8 py-6`,
tier.name === 'Pro' ? 'mb-0.5 rounded-bl-[4px] rounded-br-[4px]' : '',
plan.name === 'Pro' ? 'mb-0.5 rounded-bl-[4px] rounded-br-[4px]' : '',
].join(' ')}
>
{tier.preface && (
<p className="text-scale-1100 text-xs mt-2 mb-4">{tier.preface}</p>
{plan.preface && (
<p className="text-scale-1100 text-xs mt-2 mb-4">{plan.preface}</p>
)}
{/* <p className="text-scale-900 text-sm">Included with plan:</p> */}
<ul role="list" className="text-xs text-scale-1000">
{tier.features.map((feature) => (
{plan.features.map((feature) => (
<li key={feature} className="flex items-center py-2 first:mt-0">
<IconCheck
className="text-brand-900 h-4 w-4 "
className="text-brand-900 h-4 w-4"
aria-hidden="true"
strokeWidth={3}
/>
@@ -365,15 +283,13 @@ export default function IndexPage() {
<div className="flex flex-col gap-6 mt-auto prose">
<div className="space-y-2 mt-12">
{tier.additional && <p className="text-sm">{tier.additional}</p>}
{tier.scale && <p className="text-xs">{tier.scale}</p>}
{tier.shutdown && (
<p className="text-scale-1000 text-xs">{tier.shutdown}</p>
{plan.footer && (
<p className="text-xs whitespace-pre-wrap">{plan.footer}</p>
)}
</div>
<a href={tier.href}>
<a href={plan.href}>
<Button block size="small">
{tier.cta}
{plan.cta}
</Button>
</a>
</div>
@@ -384,6 +300,51 @@ export default function IndexPage() {
</div>
</div>
</div>
<div className="mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-md lg:max-w-6xl grid grid-cols-1 lg:grid-cols-3 mx-auto border bg-scale-100 rounded-md p-8 lg:px-0 mt-8 gap-x-5">
<div className="flex flex-col justify-center lg:px-8">
<div>
<h3
className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-normal
uppercase flex items-center gap-4 font-mono"
>
{planEnterprise.name}
</h3>
<p className="text-scale-1100 my-4 text-sm pb-2 2xl:pr-4">
{planEnterprise.description}
</p>
<a href={planEnterprise.href} className="hidden lg:block">
<Button type="default" size="small">
{planEnterprise.cta}
</Button>
</a>
</div>
</div>
<div className="flex flex-col justify-center lg:col-span-2">
<ul role="list" className="text-xs text-scale-1000 lg:grid lg:grid-cols-2 lg:gap-x-5">
{planEnterprise.features.map((feature) => (
<li key={feature} className="flex items-center py-2 first:mt-0">
<IconCheck
className="text-brand-900 h-4 w-4 "
aria-hidden="true"
strokeWidth={3}
/>
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
</li>
))}
</ul>
<a href={planEnterprise.href} className="visible lg:hidden mt-8">
<Button block type="default" size="small">
{planEnterprise.cta}
</Button>
</a>
</div>
</div>
</div>
<div className="text-center mt-24">
<a href="#compare-plans">
<Button size="tiny" type="default">
@@ -486,12 +447,10 @@ export default function IndexPage() {
</p>
</div>
<div>
<div className="">
<img
className="w-full"
src={`${basePath}/images/pricing/spend-cap${isDarkMode ? '' : '-light'}.png`}
/>
</div>
<img
className="w-full"
src={`${basePath}/images/pricing/spend-cap${isDarkMode ? '' : '-light'}.png`}
/>
</div>
</div>
</div>
@@ -519,79 +478,80 @@ export default function IndexPage() {
id="change-plan"
name="Change plan"
layout="vertical"
value={activeMobileTier}
value={activeMobilePlan}
className="min-w-[120px] bg-slate-400 text-red-500"
onChange={(e) => setActiveMobileTier(e.target.value)}
onChange={(e) => setActiveMobilePlan(e.target.value)}
>
<Select.Option value="Free">Free</Select.Option>
<Select.Option value="Pro">Pro</Select.Option>
<Select.Option value="Team">Team</Select.Option>
<Select.Option value="Enterprise">Enterprise</Select.Option>
</Select>
</div>
</div>
{activeMobileTier === 'Free' && (
{activeMobilePlan === 'Free' && (
<>
<MobileHeader
tier="Free"
plan="Free"
price={'0'}
priceDescription={'/mo'}
description={'Perfect for hobby projects and experiments'}
/>
<PricingTableRowMobile
category={pricing.database}
tier={'free'}
plan={'free'}
icon={Solutions['database'].icon}
sectionId="database"
/>
<PricingTableRowMobile
category={pricing.auth}
tier={'free'}
plan={'free'}
icon={Solutions['authentication'].icon}
sectionId="auth"
/>
<PricingTableRowMobile
category={pricing.storage}
tier={'free'}
plan={'free'}
icon={Solutions['storage'].icon}
sectionId="storage"
/>
<PricingTableRowMobile
category={pricing.realtime}
tier={'free'}
plan={'free'}
icon={Solutions['realtime'].icon}
sectionId="realtime"
/>
<PricingTableRowMobile
category={pricing['edge-functions']}
tier={'free'}
plan={'free'}
icon={Solutions['edge-functions'].icon}
sectionId="edge-functions"
/>
<PricingTableRowMobile
category={pricing.dashboard}
tier={'free'}
plan={'free'}
icon={pricing.dashboard.icon}
sectionId="dashboard"
/>
<PricingTableRowMobile
category={pricing.security}
tier={'free'}
plan={'free'}
icon={pricing.security.icon}
sectionId="security"
/>
<PricingTableRowMobile
category={pricing.support}
tier={'free'}
plan={'free'}
icon={pricing.support.icon}
sectionId="support"
/>
</>
)}
{activeMobileTier === 'Pro' && (
{activeMobilePlan === 'Pro' && (
<>
<MobileHeader
tier="Pro"
plan="Pro"
from={false}
price={'25'}
priceDescription={'/mo + additional use'}
@@ -599,51 +559,103 @@ export default function IndexPage() {
/>
<PricingTableRowMobile
category={pricing.database}
tier={'pro'}
plan={'pro'}
icon={Solutions['database'].icon}
/>
<PricingTableRowMobile
category={pricing.auth}
tier={'pro'}
plan={'pro'}
icon={Solutions['authentication'].icon}
/>
<PricingTableRowMobile
category={pricing.storage}
tier={'pro'}
plan={'pro'}
icon={Solutions['storage'].icon}
/>
<PricingTableRowMobile
category={pricing.realtime}
tier={'pro'}
plan={'pro'}
icon={Solutions['realtime'].icon}
/>
<PricingTableRowMobile
category={pricing['edge-functions']}
tier={'pro'}
plan={'pro'}
icon={Solutions['edge-functions'].icon}
/>
<PricingTableRowMobile
category={pricing.dashboard}
tier={'pro'}
plan={'pro'}
icon={pricing.dashboard.icon}
/>
<PricingTableRowMobile
category={pricing.security}
tier={'pro'}
plan={'pro'}
icon={pricing.security.icon}
/>
<PricingTableRowMobile
category={pricing.support}
tier={'pro'}
plan={'pro'}
icon={pricing.support.icon}
/>
</>
)}
{activeMobileTier === 'Enterprise' && (
{activeMobilePlan === 'Team' && (
<>
<MobileHeader
tier="Enterprise"
plan="Team"
from={false}
price={'599'}
priceDescription={'/mo + additional use'}
description={'Collaborate with different permissions and access patterns'}
/>
<PricingTableRowMobile
category={pricing.database}
plan={'team'}
icon={Solutions['database'].icon}
/>
<PricingTableRowMobile
category={pricing.auth}
plan={'team'}
icon={Solutions['authentication'].icon}
/>
<PricingTableRowMobile
category={pricing.storage}
plan={'team'}
icon={Solutions['storage'].icon}
/>
<PricingTableRowMobile
category={pricing.realtime}
plan={'team'}
icon={Solutions['realtime'].icon}
/>
<PricingTableRowMobile
category={pricing['edge-functions']}
plan={'team'}
icon={Solutions['edge-functions'].icon}
/>
<PricingTableRowMobile
category={pricing.dashboard}
plan={'team'}
icon={pricing.dashboard.icon}
/>
<PricingTableRowMobile
category={pricing.security}
plan={'team'}
icon={pricing.security.icon}
/>
<PricingTableRowMobile
category={pricing.support}
plan={'team'}
icon={pricing.support.icon}
/>
</>
)}
{activeMobilePlan === 'Enterprise' && (
<>
<MobileHeader
plan="Enterprise"
price={'Contact us for a quote'}
priceDescription={''}
description={
@@ -653,42 +665,42 @@ export default function IndexPage() {
/>
<PricingTableRowMobile
category={pricing.database}
tier={'enterprise'}
plan={'enterprise'}
icon={Solutions['database'].icon}
/>
<PricingTableRowMobile
category={pricing.auth}
tier={'enterprise'}
plan={'enterprise'}
icon={Solutions['authentication'].icon}
/>
<PricingTableRowMobile
category={pricing.storage}
tier={'enterprise'}
plan={'enterprise'}
icon={Solutions['storage'].icon}
/>
<PricingTableRowMobile
category={pricing.realtime}
tier={'enterprise'}
plan={'enterprise'}
icon={Solutions['realtime'].icon}
/>
<PricingTableRowMobile
category={pricing['edge-functions']}
tier={'enterprise'}
plan={'enterprise'}
icon={Solutions['edge-functions'].icon}
/>
<PricingTableRowMobile
category={pricing.dashboard}
tier={'enterprise'}
plan={'enterprise'}
icon={pricing.dashboard.icon}
/>
<PricingTableRowMobile
category={pricing.security}
tier={'enterprise'}
plan={'enterprise'}
icon={pricing.security.icon}
/>
<PricingTableRowMobile
category={pricing.support}
tier={'enterprise'}
plan={'enterprise'}
icon={pricing.support.icon}
/>
</>
@@ -712,14 +724,14 @@ export default function IndexPage() {
></div>
</th>
{tiers.map((tier) => (
{plans.map((plan) => (
<th
className="text-scale-1200 w-1/4 px-6 pr-2 pt-2 pb-2 text-left text-sm font-normal"
scope="col"
key={tier.name}
key={plan.name}
>
<h3 className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-mono font-normal uppercase flex items-center gap-4">
{tier.name}
{plan.name}
</h3>
<div
className="h-0.25 absolute bottom-0 left-0 w-full"
@@ -735,13 +747,13 @@ export default function IndexPage() {
scope="col"
></th>
{tiers.map((tier) => (
{plans.map((plan) => (
<th
className="text-scale-1200 w-1/4 px-6 pt-2 pb-2 text-left text-sm font-normal"
scope="col"
key={`th-${tier.name}`}
key={`th-${plan.name}`}
>
<p className="p text-sm border-b border-scale-700 pb-4">{tier.description}</p>
<p className="p text-sm border-b border-scale-700 pb-4">{plan.description}</p>
<div
className="h-0.25 absolute bottom-0 left-0 w-full"
style={{ height: '1px' }}
@@ -750,47 +762,50 @@ export default function IndexPage() {
))}
</tr>
<tbody className="border-scale-700 dark:border-scale-400 divide-scale-700 dark:divide-scale-400 divide-y">
<tr className="">
<tr>
<th
className="text-scale-900 px-6 py-8 text-left align-top text-sm font-medium dark:text-white"
scope="row"
></th>
{tiers.map((tier) => (
<td className="h-full px-6 py-2 align-top" key={`price-${tier.name}`}>
{plans.map((plan) => (
<td className="h-full px-6 py-2 align-top" key={`price-${plan.name}`}>
<div className="relative table h-full w-full">
<div className="flex flex-col justify-between h-full">
<>
<span
className={`text-scale-1200 ${
tier.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
plan.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
}`}
>
{tier.name !== 'Enterprise' && '$'}
{tier.priceMonthly}
{plan.name !== 'Enterprise' && '$'}
{plan.priceMonthly}
</span>
{tier.name !== 'Enterprise' && (
<p className="p text-xs mt-1">per project per month</p>
{['Pro', 'Free'].includes(plan.name) && (
<p className="p text-xs mt-1">per month</p>
)}
{['Team'].includes(plan.name) && (
<p className="p text-xs mt-1">per month</p>
)}
</>
{tier.warning && (
{plan.warning && (
<p className="-mt-2">
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-xs ">
{tier.warning}
<span className="bg-scale-100 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
{plan.warning}
</span>
</p>
)}
<div className={tier.name === 'Enterprise' ? 'mt-auto' : 'mt-8'}>
<Link href={tier.href} as={tier.href}>
<div className={plan.name === 'Enterprise' ? 'mt-auto' : 'mt-8'}>
<Link href={plan.href} as={plan.href}>
<a>
<Button
size="tiny"
type={tier.name === 'Enterprise' ? 'default' : 'primary'}
type={plan.name === 'Enterprise' ? 'default' : 'primary'}
block
>
{tier.cta}
{plan.cta}
</Button>
</a>
</Link>
@@ -875,7 +890,7 @@ export default function IndexPage() {
</td>
<td className="px-6 pt-5">
<Link href="https://forms.supabase.com/enterprise">
<Link href="https://forms.supabase.com/team">
<a>
<Button size="tiny" type="primary" block>
Contact us
@@ -883,6 +898,16 @@ export default function IndexPage() {
</a>
</Link>
</td>
<td className="px-6 pt-5">
<Link href="https://forms.supabase.com/enterprise">
<a>
<Button size="tiny" type="default" block>
Contact us
</Button>
</a>
</Link>
</td>
</tr>
</tfoot>
</table>
+2
View File
@@ -764,6 +764,7 @@
"remark": "^13.0.0",
"remark-gfm": "^3.0.1",
"remark-html": "^15.0.1",
"shared-data": "*",
"swiper": "^6.5.0",
"swr": "^0.5.4",
"typescript": "^4.0.3",
@@ -82836,6 +82837,7 @@
"remark": "^13.0.0",
"remark-gfm": "^3.0.1",
"remark-html": "^15.0.1",
"shared-data": "*",
"swiper": "^6.5.0",
"swr": "^0.5.4",
"tailwindcss": "^3.1.8",
@@ -8,17 +8,19 @@
"tooltips": {
"main": "A Postgres database with no restrictions? You get it. No pseudo limited users, you are the postgres root user. No caveats."
},
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Unlimited API requests",
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
@@ -27,9 +29,10 @@
"tooltips": {
"main": "Billing is based on the average database size in GB throughout the billing period."
},
"tiers": {
"plans": {
"free": "500 MB",
"pro": "8 GB, then $0.125 per GB",
"team": "8 GB, then $0.125 per GB",
"enterprise": "Unlimited"
}
},
@@ -37,11 +40,13 @@
"title": "Automatic backups",
"tooltips": {
"main": "Backups are entire copies of your database that can be restored in the future.",
"pro": "7 days of backup (if > 1TB, contact for Enterprise pricing)"
"pro": "7 days of backup (if > 1TB, contact for Enterprise pricing)",
"team": "14 days of backup (if > 1TB, contact for Enterprise pricing)"
},
"tiers": {
"plans": {
"free": false,
"pro": "7 days",
"team": "14 days",
"enterprise": "Custom"
}
},
@@ -49,11 +54,13 @@
"title": "Point in time recovery",
"tooltips": {
"main": "PITR cannot be applied retroactively, projects can only be rolled back to the point from which PITR has been applied.",
"pro": "$100 per 7 days. If > 28 day rollback period, contact enterprise"
"pro": "$100 per 7 days. If > 28 day rollback period, contact enterprise",
"team": "$100 per 7 days. If > 28 day rollback period, contact enterprise"
},
"tiers": {
"plans": {
"free": false,
"pro": "$100 per 7 days",
"team": "$100 per 7 days",
"enterprise": true
}
},
@@ -62,9 +69,10 @@
"tooltips": {
"main": "Projects that have no activity or API requests will be paused. They can be reactivated via the dashboard"
},
"tiers": {
"plans": {
"free": "After 1 inactive week",
"pro": "Never",
"team": "Never",
"enterprise": "Never"
}
},
@@ -73,9 +81,10 @@
"tooltips": {
"main": "Billing is based on the total sum of outgoing traffic of your database in GB throughout your billing period. Only download operations are counted towards the limit"
},
"tiers": {
"plans": {
"free": "2GB",
"pro": "50 GB, then $0.09 per GB",
"team": "50 GB, then $0.09 per GB",
"enterprise": "Unlimited"
}
}
@@ -88,9 +97,10 @@
{
"title": "Total Users",
"tooltips": { "main": "The maximum number of users your project can have" },
"tiers": {
"plans": {
"free": "Unlimited",
"pro": "Unlimited",
"team": "Unlimited",
"enterprise": "Unlimited"
}
},
@@ -99,74 +109,83 @@
"tooltips": {
"main": "Distinct users requesting your API throughout the billing period."
},
"tiers": {
"plans": {
"free": "50,000",
"pro": "100,000, then $0.00325 per MAU",
"team": "100,000, then $0.00325 per MAU",
"enterprise": "Unlimited"
}
},
{
"title": "Social OAuth providers",
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Custom SMTP server",
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Remove Supabase branding from emails",
"tiers": {
"plans": {
"free": false,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Enterprise OAuth providers",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": true
}
},
{
"title": "Audit trails",
"tiers": {
"plans": {
"free": "1 hour",
"pro": "7 days",
"team": "28 days",
"enterprise": true
}
},
{
"title": "Supabase Auth emails",
"tooltips": { "main": "Rate limits do not apply to Custom SMTP" },
"tiers": {
"plans": {
"free": "30 / hour",
"pro": "100 / hour",
"team": "100 / hour",
"enterprise": "Contact Us"
}
},
{
"title": "Single Sign-On (SAML 2.0)",
"tiers": {
"plans": {
"free": false,
"pro": "50 free, then $0.015 per MAU",
"team": "50 free, then $0.015 per MAU",
"enterprise": "Contact Us"
}
},
{
"title": "Advanced security features",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": "Contact Us"
}
}
@@ -182,9 +201,10 @@
"tooltips": {
"main": "The sum of all objects' size in your storage buckets."
},
"tiers": {
"plans": {
"free": "1 GB",
"pro": "100 GB, then $0.021 per GB",
"team": "100 GB, then $0.021 per GB",
"enterprise": "Unlimited"
}
},
@@ -193,26 +213,29 @@
"tooltips": {
"main": "Total sum of outgoing traffic (egress) from your storage buckets, only download operations are counted."
},
"tiers": {
"plans": {
"free": "2 GB",
"pro": "200 GB, then $0.09 per GB",
"team": "200 GB, then $0.09 per GB",
"enterprise": "Unlimited"
}
},
{
"title": "Custom access controls",
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Max file upload size",
"tooltips": { "main": "You can change the upload size in the dashboard" },
"tiers": {
"plans": {
"free": "50MB",
"pro": "5GB",
"team": "5GB",
"enterprise": "Unlimited"
}
},
@@ -221,9 +244,11 @@
"tooltips": {
"main": "Distinct count of all images that were transformed in the billing period, ignoring any transformations."
},
"tiers": {
"plans": {
"free": false,
"pro": "100 origin images, then $5 per 1000 origin images"
"pro": "100 origin images, then $5 per 1000 origin images",
"team": "100 origin images, then $5 per 1000 origin images",
"enterprise": "Unlimited"
}
}
]
@@ -238,17 +263,19 @@
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period."
},
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period.",
"tiers": {
"plans": {
"free": "500K/month",
"pro": "2 Million, then $2 per 1 Million",
"team": "2 Million, then $2 per 1 Million",
"enterprise": "Unlimited"
}
},
{
"title": "Script size",
"tiers": {
"plans": {
"free": "2 MB",
"pro": "10 MB",
"team": "10 MB",
"enterprise": "Unlimited"
}
},
@@ -257,9 +284,10 @@
"tooltips": {
"main": "Billing is based on the maximum amount of functions at any point in time throughout your billing period."
},
"tiers": {
"plans": {
"free": "10",
"pro": "100, then $10 per additional 100",
"team": "100, then $10 per additional 100",
"enterprise": "Unlimited"
}
}
@@ -271,9 +299,10 @@
"features": [
{
"title": "Postgres Changes",
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
@@ -282,9 +311,10 @@
"tooltips": {
"main": "We only charge for successful connections - not connection attempts!"
},
"tiers": {
"plans": {
"free": "200",
"pro": "500, then $10 per 1000",
"team": "500, then $10 per 1000",
"enterprise": "Unlimited concurrent connections and volume discount"
}
},
@@ -293,17 +323,19 @@
"tooltips": {
"main": "Billing is based on the total sum of Realtime messages throughout your billing period."
},
"tiers": {
"plans": {
"free": "2 Million",
"pro": "5 Million, then $2.50 per Million",
"team": "5 Million, then $2.50 per Million",
"enterprise": "Volume discounts on messages"
}
},
{
"title": "Max Message Size",
"tiers": {
"plans": {
"free": "250 KB",
"pro": "3 MB",
"team": "3 MB",
"enterprise": "Custom"
}
}
@@ -315,25 +347,28 @@
"features": [
{
"title": "Team members",
"tiers": {
"plans": {
"free": "Unlimited",
"pro": "Unlimited",
"team": "Unlimited",
"enterprise": "Unlimited"
}
},
{
"title": "Access controls",
"tiers": {
"plans": {
"free": "Coming soon",
"pro": "Coming soon",
"team": true,
"enterprise": true
}
},
{
"title": "Audit trails",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": true,
"enterprise": true
}
}
@@ -345,73 +380,82 @@
"features": [
{
"title": "On Premises / BYO cloud",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": true
}
},
{
"title": "Log retention (API & Database)",
"tiers": {
"plans": {
"free": "1 day",
"pro": "7 days",
"team": "28 days",
"enterprise": "90 days"
}
},
{
"title": "Log drain",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": "Coming soon",
"enterprise": "Coming soon"
}
},
{
"title": "Metrics endpoint",
"tiers": {
"plans": {
"free": false,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "SOC2",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": true,
"enterprise": true
}
},
{
"title": "SSO",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": "Contact Us",
"enterprise": "Contact Us"
}
},
{
"title": "99.9% SLA",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": true
}
},
{
"title": "Access Roles",
"tiers": {
"plans": {
"free": "Owner, Developer",
"pro": "Owner, Developer",
"team": "Additional owner(s), admin, read-only, billing admin, custom",
"enterprise": "Additional owner(s), admin, read-only, billing admin, custom"
}
},
{
"title": "Vanity URLs",
"tiers": {
"plans": {
"free": false,
"pro": true,
"team": true,
"enterprise": true
}
},
@@ -420,9 +464,10 @@
"tooltips": {
"enterprise": "Volume discounts available."
},
"tiers": {
"plans": {
"free": false,
"pro": "$10 per domain per month per project add on",
"team": "$10 per domain per month per project add on",
"enterprise": "1, additional $10/domain/month"
}
}
@@ -434,57 +479,64 @@
"features": [
{
"title": "Community support",
"tiers": {
"plans": {
"free": true,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Email support",
"tiers": {
"plans": {
"free": false,
"pro": true,
"team": true,
"enterprise": true
}
},
{
"title": "Email support SLA",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": true,
"enterprise": true
}
},
{
"title": "Designated support",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": true
}
},
{
"title": "On Boarding Support",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": true
}
},
{
"title": "Designated customer success engineer ",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": false,
"enterprise": true
}
},
{
"title": "Security Questionnaire Help",
"tiers": {
"plans": {
"free": false,
"pro": false,
"team": true,
"enterprise": true
}
}
+3 -1
View File
@@ -1,4 +1,6 @@
import tweets from './tweets'
import logConstants from './logConstants'
import { plans, PricingInformation } from './plans'
export { tweets, logConstants }
export { tweets, logConstants, plans }
export type { PricingInformation }
+1
View File
@@ -3,6 +3,7 @@
"version": "0.0.0",
"description": "Data shared between Supabase apps",
"main": "./index.ts",
"types": "./index.ts",
"scripts": {},
"author": "",
"license": "MIT"
+117
View File
@@ -0,0 +1,117 @@
export interface PricingInformation {
id: string
name: string
nameBadge?: string
costUnit?: string
href: string
priceLabel?: string
priceMonthly: number | string
warning?: string
description: string
preface: string
features: string[]
footer?: string
cta: string
}
export const plans: PricingInformation[] = [
{
id: 'tier_free',
name: 'Free',
nameBadge: '',
costUnit: '/ month',
href: 'https://supabase.com/dashboard/new/new-project',
priceLabel: '',
priceMonthly: 0,
warning: 'Limit of 2 free projects',
description: 'Perfect for passion projects & simple websites.',
preface: 'Get started with:',
features: [
'Unlimited API requests',
'Social OAuth providers',
'Up to 500MB database space',
'Up to 1GB file storage',
'Up to 2GB bandwidth',
'Up to 50MB file uploads',
'Up to 50,000 monthly active users',
'Up to 500K Edge Function invocations',
'Up to 200 concurrent Realtime connections',
'Up to 2 million Realtime messages',
'1-day log retention',
'Community support',
],
footer: 'Free projects are paused after 1 week of inactivity.',
cta: 'Get Started',
},
{
id: 'tier_pro',
name: 'Pro',
nameBadge: '',
costUnit: '/ month',
href: 'https://supabase.com/dashboard/new/new-project',
priceLabel: 'From',
warning: 'Usage based plan',
priceMonthly: 25,
description: 'For production applications with the option to scale.',
features: [
'No project pausing',
'Daily backups stored for 7 days',
'8GB database space included',
'100GB file storage included',
'50GB bandwidth included',
'5GB file uploads included',
'100,000 monthly active users included',
'2M Edge Function invocations included',
'500 concurrent Realtime connections included',
'5 million Realtime messages included',
'7-day log retention',
'Email support',
],
footer:
'Your cost control settings determine if you allow over-usage.',
preface: 'Everything in the Free plan, plus:',
cta: 'Get Started',
},
{
id: 'tier_team',
name: 'Team',
nameBadge: 'New',
costUnit: '/ month',
href: 'https://forms.supabase.com/team',
priceLabel: 'From',
warning: 'Usage based plan',
priceMonthly: 599,
description: 'Collaborate with different permissions and access patterns.',
features: [
'Additional Organization member roles',
'Daily backups stored for 14 days',
'Standardised Security Questionnaire',
'SOC2',
'SSO for Supabase Dashboard',
'Priority email support & SLAs',
'28-day log retention',
],
footer: 'Additional fees apply for usage beyond included usage.',
preface: 'Everything in the Pro plan, plus:',
cta: 'Contact Us',
},
{
id: 'tier_enterprise',
name: 'Enterprise',
href: 'https://forms.supabase.com/enterprise',
description: 'For large-scale applications managing serious workloads.',
features: [
`Designated Support manager & SLAs`,
`Enterprise OAuth providers`,
`SSO/SAML`,
`On-premise support`,
`24×7×365 premium enterprise support`,
`Private Slack channel`,
],
priceLabel: '',
priceMonthly: 'Contact us',
preface: '',
footer: '',
cta: 'Contact Us',
},
]
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "tsconfig/react-library.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}
@@ -1,22 +1,5 @@
import { IconArchive, IconCode, IconDatabase, IconKey, IconZap } from 'ui'
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
export const USAGE_BASED_PRODUCTS = [
{
title: 'Database',
@@ -8,11 +8,11 @@ import { Button, Form, Input, Modal, IconArrowLeft, Alert } from 'ui'
import { useFlag, useStore } from 'hooks'
import { post, patch } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { CANCELLATION_REASONS } from '../Billing.constants'
import { UpdateSuccess } from '../'
import { SubscriptionPreview } from '../Billing.types'
import { StripeSubscription } from 'components/interfaces/Billing'
import HCaptcha from '@hcaptcha/react-hcaptcha'
import { CANCELLATION_REASONS } from 'components/interfaces/BillingV2/Billing.constants'
interface ExitSurveyProps {
freeTier: any
@@ -5,6 +5,7 @@ import { Transition } from '@headlessui/react'
import PlanCard from './Plans/PlanCard'
import { formatTierOptions } from './PlanSelection.utils'
import { Button, IconExternalLink } from 'ui'
import PlanCardEnterprise from './Plans/PlanCardEnterprise'
interface Props {
visible: boolean
@@ -16,11 +17,7 @@ interface Props {
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 lg:grid-cols-3'
: 'gap-4 lg:grid-cols-3 xl:grid-cols-billingWithTeam'
const enterprisePlan = formattedTiers.find((it) => it.name === 'Enterprise')!
return (
<Transition
@@ -31,17 +28,26 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
>
<div>
<h4 className="text-lg">Change your project's subscription</h4>
<div className={`grid py-8 grid-cols-2 ${gridCols}`}>
{formattedTiers.map((plan) => {
return (
<PlanCard
key={plan.id}
plan={plan}
currentPlan={currentPlan}
onSelectPlan={() => onSelectPlan(plan)}
/>
)
})}
<div className={`grid py-8 grid-cols-1 gap-8 md:grid-cols-3`}>
{formattedTiers
.filter((it) => it.name !== 'Enterprise')
.map((plan) => {
return (
<PlanCard
key={plan.id}
plan={plan}
currentPlan={currentPlan}
onSelectPlan={() => onSelectPlan(plan)}
/>
)
})}
</div>
<div>
<PlanCardEnterprise
plan={enterprisePlan}
currentPlan={currentPlan}
onSelectPlan={() => onSelectPlan(enterprisePlan)}
/>
</div>
<div className="flex justify-center items-center mt-4">
<Link href="https://supabase.com/pricing">
@@ -20,13 +20,23 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
const isTeamTier = currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const getButtonType = (plan: any, currentPlan: any) => {
if (
currentPlan.supabase_prod_id !== PRICING_TIER_PRODUCT_IDS.ENTERPRISE &&
plan.name === 'Enterprise'
) {
return 'default'
}
if (['Free tier', 'Free plan'].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
return 'primary'
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM && !['Team tier', 'Team plan'].includes(plan.name)) {
} else if (
currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM &&
!['Team tier', 'Team plan'].includes(plan.name)
) {
// Non-free plans are default (pro), when team plan is selected
return 'default'
}
@@ -60,18 +70,10 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
}
}
if (plan.id === STRIPE_PRODUCT_IDS.TEAM) {
if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM) {
return 'Edit plan configuration'
} else if (
[STRIPE_PRODUCT_IDS.FREE, STRIPE_PRODUCT_IDS.PRO, STRIPE_PRODUCT_IDS.PAYG].includes(
currentPlan.prod_id
)
) {
return 'Upgrade to Team'
} else {
return 'Contact sales'
}
if (plan.id === STRIPE_PRODUCT_IDS.TEAM && currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM) {
return 'Edit plan configuration'
} else {
return 'Contact sales'
}
}
@@ -103,6 +105,18 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
)
}
if (isTeamTier && plan.name === 'Team plan') {
return (
<Link href="https://forms.supabase.com/team">
<a>
<Button disabled={disabled} type={type} block>
{ctaText}
</Button>
</a>
</Link>
)
}
return (
<div className="flex flex-col items-center justify-center space-y-2">
<Tooltip.Root delayDuration={0}>
@@ -1,8 +1,7 @@
import { PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
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'
@@ -13,12 +12,7 @@ interface Props {
}
const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
const teamTierEnabled =
useFlag('teamTier') || currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
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) ||
@@ -26,35 +20,12 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
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(' ')}
className={['px-6 py-6 border rounded flex flex-col justify-between bg-scale-300'].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={['space-y-8'].join(' ')}>
<div className={['space-y-4'].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>
<p className={['text-brand-900 text-sm'].join(' ')}>{planMeta.name}</p>
{isCurrentPlan ? (
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
Current plan
@@ -66,16 +37,7 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
)}
</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(' ')}
>
<div className={['space-y-2', 'border-b border-t py-8'].join(' ')}>
{planMeta.priceMonthly !== undefined ? (
<>
<p className="text-sm text-scale-1000">Starting from</p>
@@ -92,15 +54,9 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
)}
</div>
</div>
<div className={`${isEnterprise ? 'lg:space-y-0 lg:!mt-0 xl:space-y-4' : 'space-y-4'}`}>
<div className="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(' ')}
>
<ul role="list">
{planMeta.features.map((feature) => (
<li key={feature} className="flex py-2">
<div className="w-[12px]">
@@ -116,35 +72,34 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
</ul>
</div>
</div>
{!isEnterprise && (
<div className="space-y-4 !mt-20">
<div className="space-y-2">
<p className="text-xs text-scale-1000">{planMeta.scale}</p>
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO && (
<div>
<p className="text-sm text-scale-1100">
Need more? Turn off your spend cap to Pay As You Grow
</p>
<p className="text-xs text-scale-1000">
Additional fees apply for spend beyond the limits above
</p>{' '}
</div>
)}
<div className="space-y-4 !mt-20">
<div className="space-y-2">
<p className="text-xs text-scale-1000">{planMeta.scale}</p>
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && (
<div>
<p className="text-sm text-scale-1100">Spend cap is currently disabled</p>
<p className="text-xs text-scale-1000">
You will be charged for usage beyond the plan limits
</p>{' '}
</div>
)}
</div>
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO && (
<div>
<p className="text-sm text-scale-1100">
Need more? Turn off your spend cap to Pay As You Grow
</p>
<p className="text-xs text-scale-1000">
Additional fees apply for spend beyond the limits above
</p>{' '}
</div>
)}
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && (
<div>
<p className="text-sm text-scale-1100">Spend cap is currently disabled</p>
<p className="text-xs text-scale-1000">
You will be charged for usage beyond the plan limits
</p>{' '}
</div>
)}
</div>
)}
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
</div>
</div>
)
}
@@ -0,0 +1,64 @@
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { FC } from 'react'
import { IconCheck } from 'ui'
import PlanCTAButton from './PlanCTAButton'
import { PRICING_META } from './Plans.Constants'
interface Props {
plan: any
currentPlan: any
onSelectPlan: () => void
}
const PlanCardEnterprise: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
const planMeta = PRICING_META[plan.id]
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-col bg-scale-200 flex">
<div className="space-y-8">
<div className=" grid grid-cols-3 space-x-8">
<div className="space-y-4 pr-14">
<div className="flex items-center ">
<p className="text-brand-900 text-sm">{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 border-t pb-6 lg:pt-6 xl:pt-0 xl:pb-6">
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
</div>
</div>
<div className="space-y-4 lg:col-span-2">
<p className="text-xs text-scale-1000">{planMeta.preface}</p>
<ul role="list" className="text-xs text-scale-1000 md:grid md:grid-cols-2 md:gap-x-10">
{planMeta.features.map((feature) => (
<li key={feature} className="flex items-center py-2 first:mt-0">
<IconCheck
className="text-brand-900 h-4 w-4"
aria-hidden="true"
strokeWidth={3}
/>
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
)
}
export default PlanCardEnterprise
@@ -1,11 +1,7 @@
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
/**
* @mildtomato same plan metadata is also in /www website
* would be good if this constant was shared across apps
*
* https://github.com/supabase/supabase/blob/master/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
*/
// This file will get deleted soon and superseded by components in BillingV2/*
export const PRICING_META = {
[STRIPE_PRODUCT_IDS.FREE]: {
id: 'tier_free',
@@ -13,18 +9,20 @@ export const PRICING_META = {
name: 'Free',
description: 'Perfect for passion projects & simple websites.',
priceMonthly: 0,
priceUnit: 'per month per project',
priceUnit: '/ month',
warning: 'Limit of 2 free projects',
preface: 'Get started with:',
features: [
'Unlimited API requests',
'Social OAuth providers',
'Up to 500MB database & 1GB file storage',
'Up to 500MB database space',
'Up to 1GB file storage',
'Up to 2GB bandwidth',
'Up to 50MB file uploads',
'50,000 monthly active users',
'Up to 50,000 monthly active users',
'Up to 500K Edge Function invocations',
'200 concurrent Realtime connections',
'2 million Realtime messages',
'Up to 200 concurrent Realtime connections',
'Up to 2 million Realtime messages',
'1-day log retention',
'Community support',
],
@@ -36,21 +34,22 @@ export const PRICING_META = {
name: 'Pro',
description: 'For production applications with the option to scale.',
priceMonthly: 25,
priceUnit: 'per month per project',
warning: '+ any additional usage',
priceUnit: '/ month',
warning: 'Usage based plan',
preface: 'Everything in the Free plan, plus:',
features: [
'No project pausing',
'8GB database & 100GB file storage',
'50GB bandwidth',
'5GB file uploads',
'100,000 monthly active users',
'2M Edge Function invocations',
'500 concurrent Realtime connections',
'5 million Realtime messages',
'Daily backups stored for 7 days',
'8GB database space included',
'100GB file storage included',
'50GB bandwidth included',
'5GB file uploads included',
'100,000 monthly active users included',
'2M Edge Function invocations included',
'500 concurrent Realtime connections included',
'5 million Realtime messages included',
'7-day log retention',
'Email support',
'Daily backups',
],
},
[STRIPE_PRODUCT_IDS.TEAM]: {
@@ -59,20 +58,17 @@ export const PRICING_META = {
name: 'Team',
description: 'Collaborate with different permissions and access patterns.',
priceMonthly: 599,
priceUnit: 'per month per organization',
warning: '+ usage',
preface: 'The following features will apply to all projects within the organization:',
priceUnit: '/ month',
warning: 'Usage based plan',
preface: 'Everything in the Pro plan, plus:',
features: [
'Usage-based pricing',
'100,000 monthly active users included',
'Organization member roles (ABAC)',
'Additional Organization member roles',
'Daily backups stored for 14 days',
'Standardised Security Questionnaire',
'SOC2',
'SSO for Supabase Dashboard',
'Priority email support & SLAs',
'1 XS compute instance',
'14 day backups',
'28 day log retention',
'28-day log retention',
],
},
Enterprise: {
@@ -85,14 +81,10 @@ export const PRICING_META = {
features: [
`Designated Support manager & SLAs`,
`Enterprise OAuth providers`,
`SSO/ SAML`,
`SOC2`,
`Custom contracts & invoicing`,
`SSO/SAML`,
`On-premise support`,
`24×7×365 premium enterprise support`,
`Custom Security questionnaires`,
`Private Slack channel`,
`Uptime SLA`,
],
},
}
@@ -48,10 +48,8 @@ const TeamUpgrade = ({
const { app, ui } = useStore()
const router = useRouter()
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
const userIsOnTeamTier =
currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
// Team tier is enabled when the user is already on the team tier (manually assigned by us)
const teamTierEnabled = currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const captchaRef = useRef<HCaptcha>(null)
@@ -1 +1,18 @@
export const USAGE_APPROACHING_THRESHOLD = 0.8
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
@@ -1,66 +1,60 @@
import clsx from 'clsx'
import { Button, IconCheck } from 'ui'
import { PricingInformation } from './Tier.constants'
import Link from 'next/link'
import { PricingInformation } from 'shared-data'
export interface EnterpriseCardProps {
plan: PricingInformation
isCurrentPlan: boolean
isTeamTierEnabled: boolean
}
const EnterpriseCard = ({ plan, isCurrentPlan, isTeamTierEnabled }: EnterpriseCardProps) => {
const EnterpriseCard = ({ plan, isCurrentPlan }: EnterpriseCardProps) => {
return (
<div
key={plan.id}
className={clsx(
'flex border rounded-md bg-scale-200',
isTeamTierEnabled
? 'px-12 py-8 col-span-12 justify-between'
: 'px-6 py-6 col-span-4 flex-col'
'grid grid-cols-1 md:grid-cols-3 border rounded-md bg-scale-200',
'py-4 col-span-12 justify-between gap-x-8'
)}
>
<div>
<div className="flex flex-col justify-center px-4">
<div className="flex items-center space-x-2">
<p className={clsx('text-brand-900 text-sm uppercase')}>{plan.name}</p>
{isCurrentPlan ? (
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
Current plan
</div>
) : plan.new ? (
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">New</div>
) : (
<></>
)}
) : plan.nameBadge ? (
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">
{plan.nameBadge}
</div>
) : null}
</div>
<p className="text-sm mt-2 mb-4">{plan.preface}</p>
<p className="text-sm mt-2 mb-4">{plan.description}</p>
<Link href="https://supabase.com/contact/enterprise">
<a target="blank" rel="noreferrer">
<Button block={!isTeamTierEnabled} type="default">
Contact Sales Team
</Button>
</a>
</Link>
<a href={plan.href} className="hidden md:block" target="_blank">
<Button block type="default" size="tiny">
{plan.cta}
</Button>
</a>
</div>
{!isTeamTierEnabled && <div className="border-t my-6" />}
<div className="flex flex-col justify-center col-span-2 px-4 md:px-0">
<ul role="list" className="text-xs text-scale-1000 md:grid md:grid-cols-2 md:gap-x-10">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center py-2 first:mt-0">
<IconCheck className="text-brand-900 h-4 w-4" aria-hidden="true" strokeWidth={3} />
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
</li>
))}
</ul>
<ul role="list" className={clsx(isTeamTierEnabled && 'pr-12')}>
{plan.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>
<a href={plan.href} className="visible md:hidden mt-8" target="_blank">
<Button block type="default" size="tiny">
{plan.cta}
</Button>
</a>
</div>
</div>
)
}
@@ -10,8 +10,8 @@ import { includes, without } from 'lodash'
import { useRouter } from 'next/router'
import { useReducer, useRef, useState } from 'react'
import { Button, Input, Modal } from 'ui'
import { CANCELLATION_REASONS } from './Tier.constants'
import ProjectUpdateDisabledTooltip from '../../ProjectUpdateDisabledTooltip'
import { CANCELLATION_REASONS } from '../../Billing.constants'
export interface ExitSurveyModalProps {
visible: boolean
@@ -1,94 +0,0 @@
export interface PricingInformation {
id: string
new: boolean
name: string
preface?: string
features: string[]
footer?: string
}
// This only contains the meta information of plans like description and features, prices are from the API
// Also ideally shift this to common folder (e.g packages/data)
export const SUBSCRIPTION_PLANS: PricingInformation[] = [
{
id: 'tier_free',
new: false,
name: 'Free',
preface: undefined,
features: [
'Social OAuth providers',
'Up to 500MB database & 1GB file storage',
'Up to 2GB bandwidth',
'Up to 50MB file uploads',
'50,000 monthly active users',
'Up to 500K Edge Function invocations',
'200 concurrent Realtime connections',
'2 million Realtime messages',
'1-day log retention',
'Community support',
],
footer: undefined,
},
{
id: 'tier_pro',
new: false,
name: 'Pro',
preface: undefined,
features: [
'No project pausing',
'8GB database & 100GB file storage',
'50GB bandwidth',
'5GB file uploads',
'100,000 monthly active users',
'2M Edge Function invocations',
'500 concurrent Realtime connections',
'5 million Realtime messages',
'7-day log retention',
'Email support',
'Daily backups',
],
footer:
'Your cost control settings determine if you wish to pay for anything more than the above.',
},
{
id: 'tier_team',
new: true,
name: 'Team',
preface: 'Everything in Pro, plus:',
features: [
'Organization member roles (ABAC)',
'Standardised Security Questionnaire',
'SOC2',
'SSO for Supabase Dashboard',
'Priority email support & SLAs',
'14 day backups',
'28 day log retention',
],
footer: undefined,
},
{
id: 'tier_enterprise',
new: false,
name: 'Enterprise',
preface: 'For large-scale applications managing serious workloads',
features: [`Designated Support manager & SLAs`, `SSO/ SAML`, `Custom contracts & invoicing`],
footer: undefined,
},
]
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Bug issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
]
@@ -5,9 +5,7 @@ import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-l
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useProjectPlansQuery } from 'data/subscriptions/project-plans-query'
import { useProjectSubscriptionUpdateMutation } from 'data/subscriptions/project-subscription-update-mutation'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useFlag, useStore } from 'hooks'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { useStore } from 'hooks'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
@@ -16,7 +14,7 @@ import EnterpriseCard from './EnterpriseCard'
import ExitSurveyModal from './ExitSurveyModal'
import MembersExceedLimitModal from './MembersExceedLimitModal'
import PaymentMethodSelection from './PaymentMethodSelection'
import { SUBSCRIPTION_PLANS } from './Tier.constants'
import { plans as subscriptionsPlans } from 'shared-data/plans'
const TierUpdateSidePanel = () => {
const { ui } = useStore()
@@ -36,16 +34,13 @@ const TierUpdateSidePanel = () => {
const { data: plans, isLoading: isLoadingPlans } = useProjectPlansQuery({ projectRef })
const { data: addons } = useProjectAddonsQuery({ projectRef })
const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug })
const { data: subscription, isLoading } = useProjectSubscriptionV2Query({ projectRef })
const { mutateAsync: updateSubscriptionTier } = useProjectSubscriptionUpdateMutation()
const availablePlans = plans ?? []
const subscriptionAddons = addons?.selected_addons ?? []
const userIsOnTeamTier = subscription?.plan?.id === 'team'
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
const selectedTierMeta = SUBSCRIPTION_PLANS.find((tier) => tier.id === selectedTier)
const selectedTierMeta = subscriptionsPlans.find((tier) => tier.id === selectedTier)
const selectedPlanMeta = availablePlans.find(
(plan) => plan.id === selectedTier?.split('tier_')[1]
)
@@ -118,22 +113,15 @@ const TierUpdateSidePanel = () => {
>
<SidePanel.Content>
<div className="py-6 grid grid-cols-12 gap-3">
{SUBSCRIPTION_PLANS.map((plan) => {
{subscriptionsPlans.map((plan) => {
const planMeta = availablePlans.find((p) => p.id === plan.id.split('tier_')[1])
const tierMeta = subscriptionsPlans.find((it) => it.id === plan.id)
const price = planMeta?.price ?? 0
const isDowngradeOption = planMeta?.change_type === 'downgrade'
const isCurrentPlan = planMeta?.is_current ?? false
if (plan.id === 'tier_team' && !teamTierEnabled) return null
if (plan.id === 'tier_enterprise') {
return (
<EnterpriseCard
key={plan.id}
plan={plan}
isCurrentPlan={isCurrentPlan}
isTeamTierEnabled={teamTierEnabled}
/>
)
return <EnterpriseCard key={plan.id} plan={plan} isCurrentPlan={isCurrentPlan} />
}
return (
@@ -141,7 +129,7 @@ const TierUpdateSidePanel = () => {
key={plan.id}
className={clsx(
'border rounded-md px-4 py-4 flex flex-col items-start justify-between',
teamTierEnabled && plan.id === 'tier_enterprise' ? 'col-span-12' : 'col-span-4',
plan.id === 'tier_enterprise' ? 'col-span-12' : 'col-span-12 md:col-span-4',
plan.id === 'tier_enterprise' ? 'bg-scale-200' : 'bg-scale-300'
)}
>
@@ -152,9 +140,9 @@ const TierUpdateSidePanel = () => {
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
Current plan
</div>
) : plan.new ? (
) : plan.nameBadge ? (
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">
New
{plan.nameBadge}
</div>
) : (
<></>
@@ -169,32 +157,33 @@ const TierUpdateSidePanel = () => {
) : (
<p className="text-scale-1200 text-lg">${price}</p>
)}
<p className="text-scale-1000 text-sm">per month</p>
<p className="text-scale-1000 text-sm">{tierMeta?.costUnit}</p>
</div>
<div
className={clsx(
'flex mt-1 mb-4',
plan.id !== PRICING_TIER_PRODUCT_IDS.TEAM && 'opacity-0'
)}
>
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">
Usage-based plan
<div className={clsx('flex mt-1 mb-4', !tierMeta?.warning && 'opacity-0')}>
<div className="bg-scale-200 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
{tierMeta?.warning}
</div>
</div>
{isCurrentPlan ? (
<Button block disabled type="default">
Current plan
</Button>
) : (
) : plan.id !== 'tier_team' ? (
<Button
block
type={isDowngradeOption ? 'default' : 'primary'}
loading={isLoading}
disabled={isLoading}
onClick={() => setSelectedTier(plan.id as any)}
>
{isDowngradeOption ? 'Downgrade' : 'Upgrade'} to {plan.name}
</Button>
) : (
<Link href={plan.href} passHref className="hidden md:block">
<a target="_blank">
<Button block type="primary">
Contact Us
</Button>
</a>
</Link>
)}
<div className="border-t my-6" />
@@ -8,7 +8,7 @@ import { useStore, checkPermissions } from 'hooks'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { delete_, post } from 'lib/common/fetch'
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
import { CANCELLATION_REASONS } from 'components/interfaces/BillingV2/Billing.constants'
interface Props {
type?: 'danger' | 'default'
@@ -17,10 +17,6 @@ const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt, subscripti
const router = useRouter()
const { ref } = router.query
// Team plan is enabled when the flag is turned on OR the user is already on the team plan (manually assigned by us)
const userIsOnTeamTier = subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
return (
<Modal
hideFooter
@@ -51,12 +47,10 @@ const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt, subscripti
<p className="w-[40%] text-sm">Pro</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
</div>
{teamTierEnabled && (
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Team</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.TEAM.text}</p>
</div>
)}
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Team</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.TEAM.text}</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[40%] text-sm">Enterprise</p>
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
+1 -1
View File
@@ -4,7 +4,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
})
// this is required to use shared packages in the packages directory
const withTM = require('next-transpile-modules')(['ui', 'common'])
const withTM = require('next-transpile-modules')(['ui', 'common', 'shared-data'])
// Required for nextjs standalone build
const path = require('path')
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { Button, Modal } from 'ui'
import { useFlag, useStore } from 'hooks'
import { useStore } from 'hooks'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { get } from 'lib/common/fetch'
import { API_URL, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
@@ -103,10 +103,6 @@ const BillingUpdate: NextPageWithLayout = () => {
}
}
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
const userIsOnTeamTier = subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
if (isLoadingProducts || isEnterprise) {
return (
<div className="flex items-center justify-center w-full h-full">
@@ -115,13 +111,11 @@ const BillingUpdate: NextPageWithLayout = () => {
)
}
const productTiers = (products?.tiers ?? []).filter(
(tier) => teamTierEnabled || tier.id !== STRIPE_PRODUCT_IDS.TEAM
)
const productTiers = (products?.tiers ?? [])
return (
<>
<div className={`mx-auto my-10 ${teamTierEnabled ? 'max-w-[90vw]' : 'max-w-[80vw]'}`}>
<div className={`mx-auto my-10 max-w-[90vw] xl:max-w-[80vw]`}>
<PlanSelection
visible={!selectedPlan || (selectedPlan && showConfirmDowngrade)}
tiers={productTiers}