From e3df2f453d9cc9ab22ccd18d35c2610cc1488a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Mon, 5 Jun 2023 12:51:18 +0200 Subject: [PATCH] feat: add team plan --- .../Pricing/ComputePricingModal.tsx | 8 +- apps/www/components/Pricing/PricingIcons.tsx | 4 +- .../components/Pricing/PricingTableRow.tsx | 36 +- apps/www/next.config.mjs | 2 +- apps/www/package.json | 1 + apps/www/pages/pricing/CHANGELOG.txt | 99 ---- apps/www/pages/pricing/index.tsx | 437 +++++++++--------- package-lock.json | 2 + .../shared-data}/Pricing.json | 156 ++++--- packages/shared-data/index.ts | 4 +- packages/shared-data/package.json | 1 + packages/shared-data/plans.ts | 117 +++++ packages/shared-data/tsconfig.json | 5 + .../interfaces/Billing/Billing.constants.tsx | 17 - .../Billing/ExitSurvey/ExitSurvey.tsx | 2 +- .../Billing/PlanSelection/PlanSelection.tsx | 38 +- .../PlanSelection/Plans/PlanCTAButton.tsx | 40 +- .../Billing/PlanSelection/Plans/PlanCard.tsx | 109 ++--- .../Plans/PlanCardEnterprise.tsx | 64 +++ .../PlanSelection/Plans/Plans.Constants.ts | 62 ++- .../interfaces/Billing/TeamUpgrade.tsx | 6 +- .../interfaces/BillingV2/Billing.constants.ts | 17 + .../Subscription/Tier/EnterpriseCard.tsx | 68 ++- .../Subscription/Tier/ExitSurveyModal.tsx | 2 +- .../Subscription/Tier/Tier.constants.ts | 94 ---- .../Subscription/Tier/TierUpdateSidePanel.tsx | 55 +-- .../DeleteProjectButton.tsx | 2 +- .../Settings/Logs/UpgradePrompt.tsx | 14 +- studio/next.config.js | 2 +- .../[ref]/settings/billing/update/index.tsx | 12 +- 30 files changed, 744 insertions(+), 732 deletions(-) delete mode 100644 apps/www/pages/pricing/CHANGELOG.txt rename {apps/www/data => packages/shared-data}/Pricing.json (81%) create mode 100644 packages/shared-data/plans.ts create mode 100644 packages/shared-data/tsconfig.json create mode 100644 studio/components/interfaces/Billing/PlanSelection/Plans/PlanCardEnterprise.tsx delete mode 100644 studio/components/interfaces/BillingV2/Subscription/Tier/Tier.constants.ts diff --git a/apps/www/components/Pricing/ComputePricingModal.tsx b/apps/www/components/Pricing/ComputePricingModal.tsx index c7bbf0e164f..eb76e04349b 100644 --- a/apps/www/components/Pricing/ComputePricingModal.tsx +++ b/apps/www/components/Pricing/ComputePricingModal.tsx @@ -88,9 +88,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo {column.key === 'dedicated' ? ( column.value ? ( - + ) : ( - + ) ) : ( column.value @@ -124,9 +124,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo > {column.key === 'dedicated' ? ( column.value ? ( - + ) : ( - + ) ) : ( column.value diff --git a/apps/www/components/Pricing/PricingIcons.tsx b/apps/www/components/Pricing/PricingIcons.tsx index 5dd4c804921..223a92a59df 100644 --- a/apps/www/components/Pricing/PricingIcons.tsx +++ b/apps/www/components/Pricing/PricingIcons.tsx @@ -12,7 +12,7 @@ export const Check = () => ( export const IconPricingIncludedCheck = (props: any) => ( - Included in {props.tier} + Included in {props.plan} ) @@ -32,7 +32,7 @@ export const IconPricingMinus = (props: any) => ( clipRule="evenodd" /> - Not included in {props.tier} + Not included in {props.plan} ) diff --git a/apps/www/components/Pricing/PricingTableRow.tsx b/apps/www/components/Pricing/PricingTableRow.tsx index 135b21f3b5b..af3f8d6280c 100644 --- a/apps/www/components/Pricing/PricingTableRow.tsx +++ b/apps/www/components/Pricing/PricingTableRow.tsx @@ -48,35 +48,35 @@ export const PricingTableRowDesktop = (props: any) => { )} - {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 ( - {typeof tierValue === 'boolean' && tierValue === true ? ( - - ) : typeof tierValue === 'boolean' && tierValue === false ? ( + {typeof planValue === 'boolean' && planValue === true ? ( + + ) : typeof planValue === 'boolean' && planValue === false ? (
- +
) : ( - {feat.tooltips?.[tierName] && ( + {feat.tooltips?.[planName] && ( )} - {tierValue} + {planValue} )} @@ -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) => { - {typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === true ? ( + {typeof feat.plans[plan] === 'boolean' && feat.plans[plan] === true ? (
- +
- ) : typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === false ? ( + ) : typeof feat.plans[plan] === 'boolean' && feat.plans[plan] === false ? (
- +
) : ( - {feat.tiers[tier]} + {feat.plans[plan]} )} diff --git a/apps/www/next.config.mjs b/apps/www/next.config.mjs index 0f5255b97bf..cbd15868625 100644 --- a/apps/www/next.config.mjs +++ b/apps/www/next.config.mjs @@ -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) } diff --git a/apps/www/package.json b/apps/www/package.json index 96fcc2d8c2f..d77f1589f52 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -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", diff --git a/apps/www/pages/pricing/CHANGELOG.txt b/apps/www/pages/pricing/CHANGELOG.txt deleted file mode 100644 index 62db4f4b1ec..00000000000 --- a/apps/www/pages/pricing/CHANGELOG.txt +++ /dev/null @@ -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. - - diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx index 0f0f2e3cbc2..5d6089d50e5 100644 --- a/apps/www/pages/pricing/index.tsx +++ b/apps/www/pages/pricing/index.tsx @@ -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 (

- {tier} + {plan}

{from && From} @@ -242,7 +159,7 @@ export default function IndexPage() {
-
+
{/*
*/}
- {tiers.map((tier) => ( + {plansExceptEnterprise.map((plan) => (
- {tier.name === 'Pro' && ( + {plan.name === 'Pro' && (

Most Popular

)}
@@ -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} - {tier.nameBadge && ( - - {tier.nameBadge} + {plan.nameBadge && ( + + {plan.nameBadge} )}
-

- {tier.description} +

+ {plan.description}

-

- {tier.priceLabel} -

-

- {tier.name !== 'Enterprise' && '$'} - {tier.priceMonthly} -

- {tier.costUnit && ( -

{tier.costUnit}

+ {plan.priceLabel && ( +

+ {plan.priceLabel} +

)} - {tier.warning && ( +
+

+ ${plan.priceMonthly} +

+

+ {plan.costUnit} +

+
+ + {plan.warning && (

- - {tier.warning} + + {plan.warning}

)} @@ -342,18 +260,18 @@ export default function IndexPage() {
- {tier.preface && ( -

{tier.preface}

+ {plan.preface && ( +

{plan.preface}

)} {/*

Included with plan:

*/}
    - {tier.features.map((feature) => ( + {plan.features.map((feature) => (
+ +
+
+
+
+

+ {planEnterprise.name} +

+

+ {planEnterprise.description} +

+ + + + +
+
+
+
    + {planEnterprise.features.map((feature) => ( +
  • +
  • + ))} +
+ + + + +
+
+
+
-
- -
+
@@ -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)} > Free Pro + Team Enterprise
- {activeMobileTier === 'Free' && ( + {activeMobilePlan === 'Free' && ( <> )} - {activeMobileTier === 'Pro' && ( + {activeMobilePlan === 'Pro' && ( <> )} - {activeMobileTier === 'Enterprise' && ( + {activeMobilePlan === 'Team' && ( <> + + + + + + + + + + )} + + {activeMobilePlan === 'Enterprise' && ( + <> + @@ -712,14 +724,14 @@ export default function IndexPage() { >
- {tiers.map((tier) => ( + {plans.map((plan) => (

- {tier.name} + {plan.name}

- {tiers.map((tier) => ( + {plans.map((plan) => ( -

{tier.description}

+

{plan.description}

- + - {tiers.map((tier) => ( - + {plans.map((plan) => ( +
<> - {tier.name !== 'Enterprise' && '$'} - {tier.priceMonthly} + {plan.name !== 'Enterprise' && '$'} + {plan.priceMonthly} - {tier.name !== 'Enterprise' && ( -

per project per month

+ {['Pro', 'Free'].includes(plan.name) && ( +

per month

+ )} + {['Team'].includes(plan.name) && ( +

per month

)} - {tier.warning && ( + {plan.warning && (

- - {tier.warning} + + {plan.warning}

)} -
- +
+ @@ -875,7 +890,7 @@ export default function IndexPage() { - + + + + diff --git a/package-lock.json b/package-lock.json index 33105e96400..05c0a4385aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/apps/www/data/Pricing.json b/packages/shared-data/Pricing.json similarity index 81% rename from apps/www/data/Pricing.json rename to packages/shared-data/Pricing.json index ed7b1fe6b56..b24b43f883f 100644 --- a/apps/www/data/Pricing.json +++ b/packages/shared-data/Pricing.json @@ -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 } } diff --git a/packages/shared-data/index.ts b/packages/shared-data/index.ts index 37d9a59f2a2..c55fe15a7b7 100644 --- a/packages/shared-data/index.ts +++ b/packages/shared-data/index.ts @@ -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 } diff --git a/packages/shared-data/package.json b/packages/shared-data/package.json index 3a2165ffed7..9dd0d49e5be 100644 --- a/packages/shared-data/package.json +++ b/packages/shared-data/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "description": "Data shared between Supabase apps", "main": "./index.ts", + "types": "./index.ts", "scripts": {}, "author": "", "license": "MIT" diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts new file mode 100644 index 00000000000..495abdbda6d --- /dev/null +++ b/packages/shared-data/plans.ts @@ -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', + }, +] diff --git a/packages/shared-data/tsconfig.json b/packages/shared-data/tsconfig.json new file mode 100644 index 00000000000..cd6c94d6e8b --- /dev/null +++ b/packages/shared-data/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/react-library.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/studio/components/interfaces/Billing/Billing.constants.tsx b/studio/components/interfaces/Billing/Billing.constants.tsx index 7661fa41f61..5b55dbb1ba8 100644 --- a/studio/components/interfaces/Billing/Billing.constants.tsx +++ b/studio/components/interfaces/Billing/Billing.constants.tsx @@ -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', diff --git a/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx b/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx index 306b05142f0..b488e3011b7 100644 --- a/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx +++ b/studio/components/interfaces/Billing/ExitSurvey/ExitSurvey.tsx @@ -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 diff --git a/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx b/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx index be18a54c621..a063a545588 100644 --- a/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx +++ b/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx @@ -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 = ({ 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 ( = ({ visible, tiers, currentPlan, onSelectPlan }) >

Change your project's subscription

-
- {formattedTiers.map((plan) => { - return ( - onSelectPlan(plan)} - /> - ) - })} +
+ {formattedTiers + .filter((it) => it.name !== 'Enterprise') + .map((plan) => { + return ( + onSelectPlan(plan)} + /> + ) + })} +
+
+ onSelectPlan(enterprisePlan)} + />
diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx index faa1ba68311..af2991c9fbb 100644 --- a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx @@ -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 ( + + + + + + ) + } + return (
diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx index 8804f44446b..74102beab90 100644 --- a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx @@ -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 = ({ 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 = ({ plan, currentPlan, onSelectPlan }) => { return (
-
-
+
+
-

- {planMeta.name} -

+

{planMeta.name}

{isCurrentPlan ? (
Current plan @@ -66,16 +37,7 @@ const PlanCard: FC = ({ plan, currentPlan, onSelectPlan }) => { )}

{planMeta.description}

-
+
{planMeta.priceMonthly !== undefined ? ( <>

Starting from

@@ -92,15 +54,9 @@ const PlanCard: FC = ({ plan, currentPlan, onSelectPlan }) => { )}
-
+

{planMeta.preface}

-
    +
      {planMeta.features.map((feature) => (
    • @@ -116,35 +72,34 @@ const PlanCard: FC = ({ plan, currentPlan, onSelectPlan }) => {
- {!isEnterprise && ( -
-
-

{planMeta.scale}

- {planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO && ( -
-

- Need more? Turn off your spend cap to Pay As You Grow -

-

- Additional fees apply for spend beyond the limits above -

{' '} -
- )} +
+
+

{planMeta.scale}

- {planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && ( -
-

Spend cap is currently disabled

-

- You will be charged for usage beyond the plan limits -

{' '} -
- )} -
+ {planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO && ( +
+

+ Need more? Turn off your spend cap to Pay As You Grow +

+

+ Additional fees apply for spend beyond the limits above +

{' '} +
+ )} - + {planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && ( +
+

Spend cap is currently disabled

+

+ You will be charged for usage beyond the plan limits +

{' '} +
+ )}
- )} + + +
) } diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCardEnterprise.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCardEnterprise.tsx new file mode 100644 index 00000000000..a586e9b6d27 --- /dev/null +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCardEnterprise.tsx @@ -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 = ({ 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 ( +
+
+
+
+
+

{planMeta.name}

+ {isCurrentPlan ? ( +
+ Current plan +
+ ) : planMeta.new ? ( +
New
+ ) : ( + <> + )} +
+

{planMeta.description}

+
+ +
+
+
+

{planMeta.preface}

+
    + {planMeta.features.map((feature) => ( +
  • +
  • + ))} +
+
+
+
+
+ ) +} + +export default PlanCardEnterprise diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts index 2bc41b3cdee..5099f80d369 100644 --- a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts @@ -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`, ], }, } diff --git a/studio/components/interfaces/Billing/TeamUpgrade.tsx b/studio/components/interfaces/Billing/TeamUpgrade.tsx index e5902fc7963..f0e83f6bb5f 100644 --- a/studio/components/interfaces/Billing/TeamUpgrade.tsx +++ b/studio/components/interfaces/Billing/TeamUpgrade.tsx @@ -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(null) const captchaRef = useRef(null) diff --git a/studio/components/interfaces/BillingV2/Billing.constants.ts b/studio/components/interfaces/BillingV2/Billing.constants.ts index 0b7203cbf01..519a1ca2afc 100644 --- a/studio/components/interfaces/BillingV2/Billing.constants.ts +++ b/studio/components/interfaces/BillingV2/Billing.constants.ts @@ -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', +] diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/EnterpriseCard.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/EnterpriseCard.tsx index f3ef23e63be..af9608f0dd0 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/EnterpriseCard.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/Tier/EnterpriseCard.tsx @@ -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 (
-
+

{plan.name}

{isCurrentPlan ? (
Current plan
- ) : plan.new ? ( -
New
- ) : ( - <> - )} + ) : plan.nameBadge ? ( +
+ {plan.nameBadge} +
+ ) : null}
-

{plan.preface}

+

{plan.description}

- - - - - + + +
- {!isTeamTierEnabled &&
} +
+
    + {plan.features.map((feature) => ( +
  • +
  • + ))} +
-
    - {plan.features.map((feature) => ( -
  • -
    -
    -

    {feature}

    -
  • - ))} -
+ + + +
) } diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx index eae5482a505..27346959ee4 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/Tier/ExitSurveyModal.tsx @@ -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 diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/Tier.constants.ts b/studio/components/interfaces/BillingV2/Subscription/Tier/Tier.constants.ts deleted file mode 100644 index 20ad87687bc..00000000000 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/Tier.constants.ts +++ /dev/null @@ -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', -] diff --git a/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx b/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx index 9f7321bf02a..4e69db4e9fc 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/Tier/TierUpdateSidePanel.tsx @@ -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 = () => { >
- {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 ( - - ) + return } 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 = () => {
Current plan
- ) : plan.new ? ( + ) : plan.nameBadge ? (
- New + {plan.nameBadge}
) : ( <> @@ -169,32 +157,33 @@ const TierUpdateSidePanel = () => { ) : (

${price}

)} -

per month

+

{tierMeta?.costUnit}

-
-
- Usage-based plan +
+
+ {tierMeta?.warning}
{isCurrentPlan ? ( - ) : ( + ) : plan.id !== 'tier_team' ? ( + ) : ( + + + + + )}
diff --git a/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx b/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx index 7a80c2d4b9f..b1fe6d9553f 100644 --- a/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx +++ b/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectButton.tsx @@ -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' diff --git a/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx b/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx index 59fde57debf..25a45b84b75 100644 --- a/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx +++ b/studio/components/interfaces/Settings/Logs/UpgradePrompt.tsx @@ -17,10 +17,6 @@ const UpgradePrompt: React.FC = ({ 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 ( = ({ show, setShowUpgradePrompt, subscripti

Pro

{TIER_QUERY_LIMITS.PRO.text}

- {teamTierEnabled && ( -
-

Team

-

{TIER_QUERY_LIMITS.TEAM.text}

-
- )} +
+

Team

+

{TIER_QUERY_LIMITS.TEAM.text}

+

Enterprise

{TIER_QUERY_LIMITS.ENTERPRISE.text}

diff --git a/studio/next.config.js b/studio/next.config.js index 517f1b8e1cf..5305c2d290e 100644 --- a/studio/next.config.js +++ b/studio/next.config.js @@ -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') diff --git a/studio/pages/project/[ref]/settings/billing/update/index.tsx b/studio/pages/project/[ref]/settings/billing/update/index.tsx index 024c2d06407..b1da19e8d66 100644 --- a/studio/pages/project/[ref]/settings/billing/update/index.tsx +++ b/studio/pages/project/[ref]/settings/billing/update/index.tsx @@ -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 (
@@ -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 ( <> -
+