chore: rework spend cap copy

Co-Authored-By: Francesco Sansalvadore <f.sansalvadore@gmail.com>
Co-Authored-By: Greg Richardson <greg@supabase.io>
This commit is contained in:
committed 2023-03-22 07:50:23 +01:00
1 parent 79f8436394
commit c1eca93c9d
8 files changed
+258 -114

No files matched your search

+2 -1
View File
@@ -449,7 +449,8 @@ export default function IndexPage() {
<p className="mt-3 prose lg:max-w-lg">
The Pro tier has a usage quota included and a spend cap turned on by default. If you
need to go beyond the inclusive limits, simply switch off your spend cap to pay for
additional usage.
additional usage and scale seamlessly. Note that your project will run into
restrictions if you have the spend cap enabled and exhaust your quota.
</p>
</div>
<div>
+1 -1
View File
@@ -9,7 +9,7 @@ interface Props extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'size'> {
disabled?: boolean
layout?: 'horizontal' | 'vertical' | 'flex'
error?: string
descriptionText?: string
descriptionText?: string | React.ReactNode
label?: string | React.ReactNode
afterLabel?: string
beforeLabel?: string
@@ -120,6 +120,11 @@ const PaymentSummaryPanel: FC<Props> = ({
const isChangingCustomDomains =
currentAddons.customDomains?.id !== selectedAddons.customDomains?.id
const togglingOnSpendCap =
currentPlan.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PAYG &&
selectedPlan &&
selectedPlan.metadata.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
// If it's enterprise we only only changing of add-ons
const hasChangesToPlan = isEnterprise
? isChangingComputeSize || isChangingPITRDuration
@@ -316,6 +321,16 @@ const PaymentSummaryPanel: FC<Props> = ({
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
)}
{togglingOnSpendCap && (
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Enabling spend cap"
description="Exceeding your plan's quota will result in service restrictions. With the spend cap disabled, you'll be charged for usage beyond the quota."
/>
)}
</div>
</div>
)}
@@ -14,7 +14,8 @@ 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 = currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM || useFlag('teamTier')
const teamTierEnabled =
currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM || useFlag('teamTier')
const planMeta = PRICING_META[plan.id]
const isEnterprise = plan.id === 'Enterprise'
@@ -118,8 +119,29 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
{!isEnterprise && (
<div className="space-y-4 !mt-20">
<div className="space-y-2">
<p className="text-sm text-scale-1100">{planMeta.additional}</p>
<p className="text-xs text-scale-1000">{planMeta.scale}</p>
{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>
)}
{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} />
@@ -49,8 +49,6 @@ export const PRICING_META = {
'No project pausing',
'Email support',
],
additional: 'Need more? Turn off your spend cap to Pay As You Grow ',
scale: 'Additional fees apply for usage and storage beyond the limits above.',
},
[STRIPE_PRODUCT_IDS.TEAM]: {
new: true,
@@ -1,8 +1,7 @@
import { FC, useEffect, useRef, useState } from 'react'
import { Transition } from '@headlessui/react'
import { useRouter } from 'next/router'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconHelpCircle, Toggle, Modal } from 'ui'
import { IconHelpCircle, Toggle } from 'ui'
import { useStore } from 'hooks'
import { post, patch } from 'lib/common/fetch'
@@ -30,6 +29,7 @@ import {
import BackButton from 'components/ui/BackButton'
import SupportPlan from './AddOns/SupportPlan'
import HCaptcha from '@hcaptcha/react-hcaptcha'
import SpendCapModal from './SpendCapModal'
// Do not allow compute size changes for af-south-1
@@ -263,7 +263,7 @@ const ProUpgrade: FC<Props> = ({
<div className="flex items-center justify-between gap-16 px-6 py-4 border rounded border-panel-border-light border-panel-border-dark bg-panel-body-light drop-shadow-sm dark:bg-panel-body-dark">
<div>
<div className="flex items-center space-x-2">
<p>Enable spend cap</p>
<p>Spend cap</p>
<IconHelpCircle
size={16}
strokeWidth={1.5}
@@ -272,7 +272,10 @@ const ProUpgrade: FC<Props> = ({
/>
</div>
<p className="text-sm text-scale-1100">
If enabled, additional resources will not be charged on a per-usage basis
By default, Pro projects have a spend cap enabled to control costs and prevent
exceeding limits. This restricts usage upon exhausting the quota. To scale
without restrictions, disable the spend cap and be charged for overusage beyond
the quota.
</p>
</div>
<Toggle
@@ -364,103 +367,11 @@ const ProUpgrade: FC<Props> = ({
/>
{/* Spend caps helper modal */}
<Modal
hideFooter
<SpendCapModal
visible={showSpendCapHelperModal}
size="large"
header="Enabling spend cap"
onCancel={() => setShowSpendCapHelperModal(false)}
>
<div className="py-4 space-y-4">
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">
A spend cap allows you to restrict your project's resource usage within the limits
of the Pro tier. Disabling the spend cap will then remove those limits and any
additional resources consumed beyond the Pro tier limits will be charged on a
per-usage basis
</p>
<p className="text-sm">
The table below shows an overview of which resources are chargeable, and how they
are charged:
</p>
{/* Maybe instead of a table, show something more interactive like a spend cap playground */}
{/* Maybe ideate this in Figma first but this is good enough for now */}
<div className="border rounded border-scale-600 bg-scale-500">
<div className="flex items-center px-4 pt-2 pb-1">
<p className="w-[50%] text-sm text-scale-1100">Item</p>
<p className="w-[25%] text-sm text-scale-1100">Limit</p>
<p className="w-[25%] text-sm text-scale-1100">Rate</p>
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Database size</p>
<p className="w-[25%] text-sm">8GB</p>
<p className="w-[25%] text-sm">$0.125/GB</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Data transfer</p>
<p className="w-[25%] text-sm">50GB</p>
<p className="w-[25%] text-sm">$0.09/GB</p>
</div>
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
<div className="flex w-[50%] items-center space-x-2">
<p className="text-sm">Auth MAUs</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="transition opacity-50 cursor-pointer hover:opacity-100"
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
Monthly Active Users: A user that has made an API request in the last
month
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
</div>
<p className="w-[25%] text-sm">100,000</p>
<p className="w-[25%] text-sm">$0.00325/user</p>
</div>
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Storage size</p>
<p className="w-[25%] text-sm">100GB</p>
<p className="w-[25%] text-sm">$0.021/GB</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Data Transfer</p>
<p className="w-[25%] text-sm">50GB</p>
<p className="w-[25%] text-sm">$0.09/GB</p>
</div>
</div>
</div>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="flex items-center gap-2">
<Button block type="primary" onClick={() => setShowSpendCapHelperModal(false)}>
Understood
</Button>
</div>
</Modal.Content>
</div>
</Modal>
onHide={() => setShowSpendCapHelperModal(false)}
/>
</>
)
}
@@ -0,0 +1,149 @@
import { FC } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconHelpCircle, Modal } from 'ui'
import Link from 'next/link'
interface Props {
visible: boolean
onHide: () => void
}
const SpendCapModal: FC<Props> = ({ visible, onHide }) => {
return (
<Modal hideFooter visible={visible} size="xlarge" header="Spend cap" onCancel={() => onHide()}>
<div className="py-4 space-y-4">
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">
Enabling the spend cap sets a limit on your usage to stay within your plan's quota,
which controls costs but can limit service. Disabling the spend cap removes these
limits, but any extra usage beyond the plan's limit will be charged per usage.
</p>
<p className="text-sm">
Take a look at the following table to see which resources are chargeable and how they
are charged:
</p>
{/* Maybe instead of a table, show something more interactive like a spend cap playground */}
{/* Maybe ideate this in Figma first but this is good enough for now */}
<div className="border rounded border-scale-600 bg-scale-500">
<div className="flex items-center px-4 pt-2 pb-1">
<p className="w-[50%] text-sm text-scale-1100">Item</p>
<p className="w-[25%] text-sm text-scale-1100">Limit</p>
<p className="w-[25%] text-sm text-scale-1100">Rate</p>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Database size</p>
<p className="w-[25%] text-sm">8GB</p>
<p className="w-[25%] text-sm">$0.125/GB</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Database egress</p>
<p className="w-[25%] text-sm">50GB</p>
<p className="w-[25%] text-sm">$0.09/GB</p>
</div>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<div className="flex w-[50%] items-center space-x-2">
<p className="text-sm">Auth MAUs</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="transition opacity-50 cursor-pointer hover:opacity-100"
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
Monthly Active Users: A user that has made an API request in the last
month
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
</div>
<p className="w-[25%] text-sm">100,000</p>
<p className="w-[25%] text-sm">$0.00325/user</p>
</div>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Storage size</p>
<p className="w-[25%] text-sm">100GB</p>
<p className="w-[25%] text-sm">$0.021/GB</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Storage egress</p>
<p className="w-[25%] text-sm">200GB</p>
<p className="w-[25%] text-sm">$0.09/GB</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Asset transformations</p>
<p className="w-[25%] text-sm">100 origin images</p>
<p className="w-[25%] text-sm">$5 per 1000 origin images</p>
</div>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Realtime concurrent peak connections</p>
<p className="w-[25%] text-sm">500</p>
<p className="w-[25%] text-sm">$10/1000</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Realtime messages</p>
<p className="w-[25%] text-sm">5 Million</p>
<p className="w-[25%] text-sm">$2.50/Million</p>
</div>
</div>
<div className="py-2">
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Function invocations</p>
<p className="w-[25%] text-sm">2 Million</p>
<p className="w-[25%] text-sm">$2 per Million</p>
</div>
<div className="flex items-center px-4 py-1">
<p className="w-[50%] text-sm">Function count</p>
<p className="w-[25%] text-sm">100 functions</p>
<p className="w-[25%] text-sm">$10 per 100 functions</p>
</div>
</div>
</div>
<p>
See{' '}
<Link href="https://supabase.com/pricing" passHref>
<a className="text-brand-900" target="_blank">
<span>pricing page</span>
</a>
</Link>{' '}
for a full overview.
</p>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="flex items-center gap-2">
<Button block type="primary" onClick={() => onHide()}>
Understood
</Button>
</div>
</Modal.Content>
</div>
</Modal>
)
}
export default SpendCapModal
+56 -8
View File
@@ -4,7 +4,7 @@ import { debounce, isUndefined, values } from 'lodash'
import { toJS } from 'mobx'
import { observer } from 'mobx-react-lite'
import generator from 'generate-password'
import { Button, Listbox, IconUsers, Input, Alert } from 'ui'
import { Button, Listbox, IconUsers, Input, Alert, IconHelpCircle, Toggle } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { NextPageWithLayout } from 'types'
@@ -33,6 +33,7 @@ import {
NotOrganizationOwnerWarning,
EmptyPaymentMethodWarning,
} from 'components/interfaces/Organization/NewProject'
import SpendCapModal from 'components/interfaces/Billing/SpendCapModal'
const Wizard: NextPageWithLayout = () => {
const router = useRouter()
@@ -55,6 +56,10 @@ const Wizard: NextPageWithLayout = () => {
const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
const [paymentMethods, setPaymentMethods] = useState<any[] | undefined>(undefined)
const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false)
const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(true)
const organizations = values(toJS(app.organizations.list()))
const currentOrg = organizations.find((o: any) => o.slug === slug)
const stripeCustomerId = currentOrg?.stripe_customer_id
@@ -150,13 +155,16 @@ const Wizard: NextPageWithLayout = () => {
const onClickNext = async () => {
setNewProjectLoading(true)
const dbTier = dbPricingTierKey === 'PRO' && !isSpendCapEnabled ? 'PAYG' : dbPricingTierKey
const data: Record<string, any> = {
cloud_provider: PROVIDERS.AWS.id, // hardcoded for DB instances to be under AWS
org_id: currentOrg?.id,
name: projectName,
db_pass: dbPass,
db_region: dbRegion,
db_pricing_tier_id: (PRICING_TIER_PRODUCT_IDS as any)[dbPricingTierKey],
db_pricing_tier_id: (PRICING_TIER_PRODUCT_IDS as any)[dbTier],
kps_enabled: kpsEnabled,
}
if (postgresVersion) {
@@ -243,7 +251,7 @@ const Wizard: NextPageWithLayout = () => {
<>
<Panel.Content
className={[
'Form section-block--body has-inputs-centered space-y-4 border-t border-b',
'space-y-4 border-t border-b',
'border-panel-border-interior-light dark:border-panel-border-interior-dark',
].join(' ')}
>
@@ -274,7 +282,7 @@ const Wizard: NextPageWithLayout = () => {
<>
<Panel.Content
className={[
'Form section-block--body has-inputs-centered border-t border-b',
'border-t border-b',
'border-panel-border-interior-light dark:border-panel-border-interior-dark',
].join(' ')}
>
@@ -293,7 +301,7 @@ const Wizard: NextPageWithLayout = () => {
{showCustomVersionInput && (
<Panel.Content
className={[
'Form section-block--body has-inputs-centered border-t border-b',
'border-t border-b',
'border-panel-border-interior-light dark:border-panel-border-interior-dark',
].join(' ')}
>
@@ -316,7 +324,7 @@ const Wizard: NextPageWithLayout = () => {
</Panel.Content>
)}
<Panel.Content className="border-b Form section-block--body has-inputs-centered border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Panel.Content className="border-b border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Input
id="password"
copy={dbPass.length > 0}
@@ -338,7 +346,7 @@ const Wizard: NextPageWithLayout = () => {
/>
</Panel.Content>
<Panel.Content className="border-b Form section-block--body has-inputs-centered border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Panel.Content className="border-b border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Listbox
layout="horizontal"
label="Region"
@@ -372,7 +380,7 @@ const Wizard: NextPageWithLayout = () => {
)}
{isAdmin && (
<Panel.Content className="Form section-block--body has-inputs-centered ">
<Panel.Content>
<Listbox
label="Pricing Plan"
layout="horizontal"
@@ -421,6 +429,46 @@ const Wizard: NextPageWithLayout = () => {
)}
</Panel.Content>
)}
{!isSelectFreeTier && (
<>
<Panel.Content className="border-b border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Toggle
id="project-name"
layout="horizontal"
label={
<div className="flex space-x-4">
<span>Spend Cap</span>
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="transition opacity-50 cursor-pointer hover:opacity-100"
onClick={() => setShowSpendCapHelperModal(true)}
/>
</div>
}
placeholder="Project name"
checked={isSpendCapEnabled}
onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)}
descriptionText={
<div>
<p>
By default, Pro projects have spend caps to control costs. When enabled,
usage is limited to the plan's quota, with restrictions when limits are
exceeded. To scale beyond Pro limits without restrictions, disable the
spend cap and pay for over-usage beyond the quota.
</p>
</div>
}
/>
</Panel.Content>
<SpendCapModal
visible={showSpendCapHelperModal}
onHide={() => setShowSpendCapHelperModal(false)}
/>
</>
)}
</>
)}
</>