diff --git a/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx b/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx index c2c52459e0e..3c54921b215 100644 --- a/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx +++ b/studio/components/interfaces/Billing/PlanSelection/PlanSelection.tsx @@ -2,8 +2,9 @@ import Link from 'next/link' import { FC } from 'react' import { Transition } from '@headlessui/react' -import Plans from './Plans/Plans' +import PlanCard from './Plans/PlanCard' import { formatTierOptions } from './PlanSelection.utils' +import { Button, IconExternalLink } from 'ui' interface Props { visible: boolean @@ -16,7 +17,10 @@ const PlanSelection: FC = ({ visible, tiers, currentPlan, onSelectPlan }) const formattedTiers = formatTierOptions(tiers) // [Kevin] TODO Remove after team plan is generally available - const gridCols = formattedTiers.length === 3 ? 'gap-8 xl:grid-cols-3' : 'gap-4 xl:grid-cols-4' + const gridCols = + formattedTiers.length === 3 + ? 'gap-8 lg:grid-cols-3' + : 'gap-4 lg:grid-cols-3 xl:grid-cols-billingWithTeam' return ( = ({ visible, tiers, currentPlan, onSelectPlan })

Change your project's subscription

- + {formattedTiers.map((plan) => { + return ( + onSelectPlan(plan)} + /> + ) + })}
- - See detailed comparisons across plans + +
diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx index 05dbf005a53..ca1595eaf6c 100644 --- a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCTAButton.tsx @@ -17,8 +17,8 @@ const PlanCTAButton: FC = ({ plan, currentPlan, onSelectPlan }) => { const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') const getButtonType = (plan: any, currentPlan: any) => { - if (['Enterprise', 'Free tier'].includes(plan.name)) { - // Enterprise/Free are always default + if (['Free tier'].includes(plan.name)) { + // Free is always default return 'default' } else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE) { // If the current plan is free, other plans are primary @@ -90,7 +90,7 @@ const PlanCTAButton: FC = ({ plan, currentPlan, onSelectPlan }) => { return ( - @@ -100,20 +100,33 @@ const PlanCTAButton: FC = ({ plan, currentPlan, onSelectPlan }) => { return (
- - {!disabled && projectUpdateDisabled && ( -

- Subscription changes are currently disabled, our engineers are working on a fix -

- )} + + + + + {!disabled && projectUpdateDisabled && ( + + +
+ + Subscription changes are currently disabled, our engineers are working on a fix + +
+
+ )} +
) } diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx new file mode 100644 index 00000000000..e9ebd88adec --- /dev/null +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/PlanCard.tsx @@ -0,0 +1,131 @@ +import { STRIPE_PRODUCT_IDS } from 'lib/constants' +import { FC } from 'react' +import { IconCheck } from 'ui' + +import { useFlag } from 'hooks' +import PlanCTAButton from './PlanCTAButton' +import { PRICING_META } from './Plans.Constants' + +interface Props { + plan: any + currentPlan: any + onSelectPlan: () => void +} + +const PlanCard: FC = ({ plan, currentPlan, onSelectPlan }) => { + const teamTierEnabled = useFlag('teamTier') + + const planMeta = PRICING_META[plan.id] + const isEnterprise = plan.id === 'Enterprise' + const isCurrentPlan = + plan.id === currentPlan?.prod_id || + (plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) || + (plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG) + + return ( +
+
+
+
+

+ {planMeta.name} +

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

{planMeta.description}

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

Starting from

+
+

${planMeta.priceMonthly}

+

{planMeta.priceUnit}

+
+
+ {planMeta.warning} +
+ + ) : ( + + )} +
+
+
+

{planMeta.preface}

+
    + {planMeta.features.map((feature) => ( +
  • +
    +
    +

    {feature}

    +
  • + ))} +
+
+
+ {!isEnterprise && ( +
+
+

{planMeta.additional}

+

{planMeta.scale}

+
+ + +
+ )} +
+ ) +} + +export default PlanCard diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts index 1e8b6f8e918..29bf1fe3ff6 100644 --- a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts +++ b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts @@ -9,10 +9,11 @@ import { STRIPE_PRODUCT_IDS } from 'lib/constants' export const PRICING_META = { [STRIPE_PRODUCT_IDS.FREE]: { name: 'Free', - href: '#', - priceMonthly: 0, - warning: 'Limit of 2 free projects', description: 'Perfect for passion projects & simple websites.', + priceMonthly: 0, + priceUnit: 'per month per project', + warning: 'Limit of 2 free projects', + preface: 'Get started with:', features: [ 'Up to 500MB database & 1GB file storage', 'Up to 2GB bandwidth', @@ -25,17 +26,15 @@ export const PRICING_META = { '2 million Realtime messages', 'Community support', ], - additional: 'Free projects are paused after 1 week of inactivity.', - - cta: 'Get Started', + scale: 'Free projects are paused after 1 week of inactivity.', }, [STRIPE_PRODUCT_IDS.PRO]: { name: 'Pro', - href: '#', - from: true, - priceMonthly: 25, - warning: '+ any additional usage', description: 'For production applications with the option to scale.', + priceMonthly: 25, + priceUnit: 'per month per project', + warning: '+ any additional usage', + preface: 'Everything in the Free plan, plus:', features: [ '8GB database & 100GB file storage', '50GB bandwidth', @@ -50,19 +49,17 @@ export const PRICING_META = { 'No project pausing', 'Email support', ], - scale: 'Additional fees apply for usage and storage beyond the limits above.', - shutdown: '', - preface: 'Everything in the base plan, plus', additional: 'Need more? Turn off your spend cap to Pay As You Grow ', - cta: 'Get Started', + scale: 'Additional fees apply for usage and storage beyond the limits above.', }, [STRIPE_PRODUCT_IDS.TEAM]: { + new: true, name: 'Team', - href: '#', - from: true, - priceMonthly: 599, - warning: '+ usage', description: 'For collaborative teams with different permissions and access.', + priceMonthly: 599, + priceUnit: 'per month per organization', + warning: '+ usage', + preface: 'The following features will apply to all projects within the organization:', features: [ 'Usage-based pricing', '100,000 monthly active users included', @@ -75,10 +72,6 @@ export const PRICING_META = { '14 day backups', '28 day log retention', ], - scale: '', - shutdown: '', - additional: '', - cta: 'Get Started', }, Enterprise: { name: 'Enterprise', @@ -94,8 +87,5 @@ export const PRICING_META = { `On-premise support`, `24×7×365 premium enterprise support`, ], - scale: '', - shutdown: '', - cta: 'Contact Us', }, } diff --git a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.tsx b/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.tsx deleted file mode 100644 index a08e0b5b14c..00000000000 --- a/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { FC } from 'react' -import { IconCheck } from 'ui' - -import { STRIPE_PRODUCT_IDS } from 'lib/constants' -import { StripeProduct } from 'components/interfaces/Billing' -import PlanCTAButton from './PlanCTAButton' -import { PRICING_META } from './Plans.Constants' - -const AnimatedGradientBackground = () => ( -
-) - -const CurrentSubscriptionBanner = () => ( -
-

Current plan

-
-) - -interface Props { - plans: any[] - currentPlan?: StripeProduct - onSelectPlan: (plan: any) => void -} - -/** - * JSX below for the mapped plans is also very similar to www/pages/pricing - * TO DO: move to use shared components for these - * - * https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx - */ - -const Plans: FC = ({ plans, currentPlan, onSelectPlan }) => { - return ( - <> - {/* Existing available plans */} - {plans.map((plan) => { - const isCurrentPlan = - plan.id === currentPlan?.prod_id || - (plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) || - (plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG) - - return ( -
- {plan.id === STRIPE_PRODUCT_IDS.PRO && ( -
- -
- )} -
- {isCurrentPlan && } -
- - {PRICING_META[plan.id].name} - -
-
- {PRICING_META[plan.id].priceMonthly !== undefined ? ( - <> -
- {PRICING_META[plan.id].from && ( - From - )} - ${PRICING_META[plan.id].priceMonthly} - /mo -
- - ) : ( - Contact Us - )} -
- {PRICING_META[plan.id].warning && ( -
- {PRICING_META[plan.id].warning} -
- )} -
-
-
-
- -
-

{PRICING_META[plan.id].description}

-
-
-
- {PRICING_META[plan.id].preface && ( -

{PRICING_META[plan.id].preface}

- )} -
    - {PRICING_META[plan.id].features.map((feature) => ( -
  • -
    -
    -

    - {feature} -

    -
  • - ))} -
-
-
-
- {PRICING_META[plan.id].additional && ( -

{PRICING_META[plan.id].additional}

- )} - {PRICING_META[plan.id].scale && ( -

{PRICING_META[plan.id].scale}

- )} - {PRICING_META[plan.id].shutdown && ( -

{PRICING_META[plan.id].shutdown}

- )} -
-
-
-
-
- ) - })} - - ) -} - -export default Plans diff --git a/studio/tailwind.config.js b/studio/tailwind.config.js index a8cab68a3f1..21148b6013e 100644 --- a/studio/tailwind.config.js +++ b/studio/tailwind.config.js @@ -1,3 +1,4 @@ +const { repeat } = require('lodash') const ui = require('./../packages/config/ui.config.js') const blueGray = { @@ -120,6 +121,9 @@ module.exports = ui({ fontSize: { grid: '13px', }, + gridTemplateColumns: { + billingWithTeam: 'repeat(3, minmax(0, 1fr)) 0.9fr', + }, colors: { // gray: { ...gray }, // green: { ...green },