mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 07:39:07 +03:00
Refactor the Subscription modal to match the design.
This commit is contained in:
1 parent
b36c9f86b3
commit
27bf41b891
2 files changed
+204
-149
No files matched your search
+203
-147
@@ -1,6 +1,6 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { isArray } from 'lodash'
|
||||
import { ChevronRight, ExternalLink } from 'lucide-react'
|
||||
import { Box, ChevronRight, ExternalLink, Pause } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
@@ -10,7 +10,6 @@ import { billingPartnerLabel } from 'components/interfaces/Billing/Subscription/
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
|
||||
import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/organization-billing-subscription-preview'
|
||||
@@ -25,24 +24,13 @@ import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { formatCurrency } from 'lib/helpers'
|
||||
import { pickFeatures, pickFooter, plans as subscriptionsPlans } from 'shared-data/plans'
|
||||
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconCheck,
|
||||
IconInfo,
|
||||
Modal,
|
||||
SidePanel,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { Alert_Shadcn_, Badge, Button, IconCheck, Modal, SidePanel, WarningIcon, cn } from 'ui'
|
||||
import DowngradeModal from './DowngradeModal'
|
||||
import EnterpriseCard from './EnterpriseCard'
|
||||
import ExitSurveyModal from './ExitSurveyModal'
|
||||
import MembersExceedLimitModal from './MembersExceedLimitModal'
|
||||
import PaymentMethodSelection from './PaymentMethodSelection'
|
||||
import UpgradeSurveyModal from './UpgradeModal'
|
||||
import PartnerIcon from 'components/ui/PartnerIcon'
|
||||
|
||||
const PlanUpdateSidePanel = () => {
|
||||
const router = useRouter()
|
||||
@@ -320,14 +308,14 @@ const PlanUpdateSidePanel = () => {
|
||||
<Modal
|
||||
loading={isUpdating}
|
||||
alignFooter="right"
|
||||
size="xlarge"
|
||||
visible={selectedTier !== undefined && selectedTier !== 'tier_free'}
|
||||
size="xxlarge"
|
||||
onCancel={() => setSelectedTier(undefined)}
|
||||
onConfirm={onUpdateSubscription}
|
||||
dialogOverlayProps={{ className: 'pointer-events-none' }}
|
||||
header={`Confirm ${planMeta?.change_type === 'downgrade' ? 'downgrade' : 'upgrade'} to ${subscriptionPlanMeta?.name}`}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div>
|
||||
{subscriptionPreviewError && (
|
||||
<AlertError
|
||||
error={subscriptionPreviewError}
|
||||
@@ -335,7 +323,7 @@ const PlanUpdateSidePanel = () => {
|
||||
/>
|
||||
)}
|
||||
{subscriptionPreviewIsLoading && (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 px-4">
|
||||
<span className="text-sm">Estimating monthly costs...</span>
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
@@ -345,167 +333,235 @@ const PlanUpdateSidePanel = () => {
|
||||
{subscriptionPreviewInitialized && (
|
||||
<div>
|
||||
<Table
|
||||
className="mt-2"
|
||||
containerClassName="!bg-transparent !border-collapse"
|
||||
className="mt-2 !bg-transparent !border-collapse"
|
||||
borderless={true}
|
||||
head={[
|
||||
<Table.th key="header-item">Item</Table.th>,
|
||||
<Table.th key="header-count" className="text-right pr-4">
|
||||
<Table.th
|
||||
key="header-item"
|
||||
className="!text-foreground-muted uppercase sans !bg-transparent"
|
||||
>
|
||||
Item
|
||||
</Table.th>,
|
||||
<Table.th
|
||||
key="header-count"
|
||||
className="text-right pr-4 !text-foreground-muted uppercase sans !bg-transparent"
|
||||
>
|
||||
Usage
|
||||
</Table.th>,
|
||||
<Table.th key="header-unit-price">Unit Price</Table.th>,
|
||||
<Table.th key="header-price" className="text-right">
|
||||
<Table.th
|
||||
key="header-unit-price"
|
||||
className="!text-foreground-muted uppercase sans !bg-transparent"
|
||||
>
|
||||
Unit Price
|
||||
</Table.th>,
|
||||
<Table.th
|
||||
key="header-price"
|
||||
className="text-right !text-foreground-muted uppercase sans !bg-transparent"
|
||||
>
|
||||
Cost
|
||||
</Table.th>,
|
||||
]}
|
||||
body={
|
||||
<>
|
||||
{subscriptionPreview.breakdown.map((item) => (
|
||||
<>
|
||||
<Table.tr key={item.description}>
|
||||
<Table.td>
|
||||
{item.breakdown && item.breakdown.length > 0 && (
|
||||
<Button
|
||||
type="text"
|
||||
className="!pl-0 !pr-1"
|
||||
icon={
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'transition',
|
||||
usageFeesExpanded.includes(item.description) && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
usageFeesExpanded.includes(item.description)
|
||||
? collapseUsageFee(item.description)
|
||||
: expandUsageFee(item.description)
|
||||
}
|
||||
<Table.tr key={1} className="!bg-transparent">
|
||||
<Table.td className="!border-dotted !border-0 !border-b-2">
|
||||
<div className="flex items-center gap-2 pl-8">
|
||||
<Badge variant="brand">PRO</Badge>
|
||||
<span>Organization plan</span>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="text-right pr-4 tabular-nums !border-dotted !border-0 !border-b-2"></Table.td>
|
||||
<Table.td className="text-foreground-muted !border-dotted !border-0 !border-b-2">
|
||||
{formatCurrency(25)} per month
|
||||
</Table.td>
|
||||
<Table.td className="text-right !border-dotted !border-0 !border-b-2">
|
||||
<Badge className="flex gap-1 justify-between text-base">
|
||||
<span className="line-through !text-foreground-lighter">
|
||||
{formatCurrency(0)}
|
||||
</span>
|
||||
<span>{formatCurrency(25)}</span>
|
||||
</Badge>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
<Table.tr key={2} className="!bg-transparent">
|
||||
<Table.td>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Button
|
||||
type="text"
|
||||
className="!px-1"
|
||||
icon={
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'transition',
|
||||
usageFeesExpanded.includes('example item description') &&
|
||||
'rotate-90'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{item.description ?? 'Unknown'}{' '}
|
||||
{item.description.endsWith('Compute') && ` (Hours)`}
|
||||
</Table.td>
|
||||
<Table.td className="text-right pr-4 tabular-nums">
|
||||
{item.quantity?.toLocaleString()}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{item.unit_price_desc
|
||||
? item.unit_price_desc
|
||||
: item.unit_price === 0
|
||||
? 'FREE'
|
||||
: item.unit_price
|
||||
? `${formatCurrency(item.unit_price)}`
|
||||
: ''}
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
{formatCurrency(item.total_price)}
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
}
|
||||
onClick={() =>
|
||||
usageFeesExpanded.includes('example item description')
|
||||
? collapseUsageFee('example item description')
|
||||
: expandUsageFee('example item description')
|
||||
}
|
||||
/>
|
||||
<Button className="!px-1.5" type="default" icon={<Box />}></Button>
|
||||
<span className="text-foreground-lighter">Project</span>
|
||||
<span className="text-foreground">Project name</span>
|
||||
<Badge className="flex gap-1 justify-between sans uppercase">
|
||||
<span className="line-through text-foreground-lighter">Nano</span>
|
||||
<span className="text-foreground">Micro</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td></Table.td>
|
||||
<Table.td className="text-foreground-muted">
|
||||
{formatCurrency(10)} per month
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<Badge className="flex gap-1 justify-between text-base">
|
||||
<span className="line-through !text-foreground-lighter">
|
||||
{formatCurrency(0)}
|
||||
</span>
|
||||
<span>{formatCurrency(10)}</span>
|
||||
</Badge>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
<Table.tr key={3} className="!bg-surface-200">
|
||||
<Table.td colSpan={4} className="w-full !py-3 !px-6 !text-foreground-muted">
|
||||
Any un-paused projects will be charged for compute hours
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
<Table.tr key={4} className="!bg-transparent">
|
||||
<Table.td>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Button
|
||||
type="text"
|
||||
className="!px-1"
|
||||
icon={
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'transition',
|
||||
usageFeesExpanded.includes('example item description') &&
|
||||
'rotate-90'
|
||||
)}
|
||||
/>
|
||||
}
|
||||
onClick={() =>
|
||||
usageFeesExpanded.includes('example item description')
|
||||
? collapseUsageFee('example item description')
|
||||
: expandUsageFee('example item description')
|
||||
}
|
||||
/>
|
||||
<Button className="!px-1.5" type="default" icon={<Box />}></Button>
|
||||
<span className="text-foreground-lighter">Project</span>
|
||||
<span className="text-foreground">Project name</span>
|
||||
<Badge className="flex gap-1 justify-between sans uppercase text-foreground-lighter">
|
||||
<Pause size="14" />
|
||||
<span>Paused</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td></Table.td>
|
||||
<Table.td className="text-foreground-muted">
|
||||
{formatCurrency(10)} per month
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<Badge className="flex gap-1 justify-between text-base">
|
||||
<span className="line-through !text-foreground-lighter">
|
||||
{formatCurrency(0)}
|
||||
</span>
|
||||
<span className="">{formatCurrency(10)}</span>
|
||||
</Badge>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
|
||||
{usageFeesExpanded.includes(item.description) &&
|
||||
item.breakdown &&
|
||||
item.breakdown.length > 0 &&
|
||||
item.breakdown.map((project) => (
|
||||
<Table.tr key={project.project_ref}>
|
||||
<Table.td className="!pl-12">{project.project_name}</Table.td>
|
||||
<Table.td className="text-right pr-4 tabular-nums">
|
||||
{project.usage}
|
||||
</Table.td>
|
||||
<Table.td />
|
||||
<Table.td />
|
||||
</Table.tr>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
|
||||
<Table.tr>
|
||||
<Table.td className="font-medium">
|
||||
Monthly Costs (excluding over-usage and credits)
|
||||
<Table.tr className="!bg-transparent">
|
||||
<Table.td className="flex flex-col gap-2">
|
||||
<span className="sans !text-foreground-lighter uppercase pl-8">
|
||||
Credits
|
||||
</span>
|
||||
<span className="text-foreground pl-8">Up to $10 Compute credits</span>
|
||||
</Table.td>
|
||||
<Table.td />
|
||||
<Table.td />
|
||||
<Table.td className="text-right font-medium">
|
||||
{formatCurrency(
|
||||
Math.round(
|
||||
subscriptionPreview.breakdown.reduce(
|
||||
(prev, cur) => prev + cur.total_price,
|
||||
0
|
||||
)
|
||||
) ?? 0
|
||||
)}
|
||||
<Table.td className="text-right align-bottom !text-base">
|
||||
<span className="pr-3">{formatCurrency(-10)}</span>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
<Table.tr className="!bg-transparent">
|
||||
<Table.td className="!border-none" />
|
||||
<Table.td className="!border-none" />
|
||||
<Table.td className="!border-dotted !border-0 !border-b-2 !pt-8">
|
||||
Costs before upgrade
|
||||
</Table.td>
|
||||
<Table.td className="text-right !text-base !border-dotted !border-0 !border-b-2 !pt-8">
|
||||
<span className="pr-3">{formatCurrency(0)}</span>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
<Table.tr className="!bg-transparent h-1" />
|
||||
<Table.tr className="!bg-transparent ">
|
||||
<Table.td className="!border-none" />
|
||||
<Table.td className="!border-none" />
|
||||
<Table.td className="!border-dotted !border-0 !border-t-2 !pb-8">
|
||||
<div className="flex flex-col ">
|
||||
<span className="text-foreground text-lg font-thin">
|
||||
New monthly cost
|
||||
</span>
|
||||
<span>(Excluding over-usage and credits)</span>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="text-right !border-dotted !border-0 !border-t-2 !pb-8 align-top">
|
||||
<span className="pr-3 font-thin text-lg text-foreground">
|
||||
{formatCurrency(35)}
|
||||
</span>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</>
|
||||
}
|
||||
></Table>
|
||||
|
||||
<InformationBox
|
||||
className="mt-4"
|
||||
title="Usage-billing for Compute"
|
||||
icon={<IconInfo />}
|
||||
defaultVisibility={true}
|
||||
hideCollapse={true}
|
||||
description={
|
||||
<div>
|
||||
<p className="text-sm mt-2">
|
||||
Each project is a dedicated server and database. Paid plans come with $10 of
|
||||
Compute Credits to cover one project on the default Micro Compute size or
|
||||
parts of any compute addon. Additional unpaused projects on paid plans will
|
||||
incur compute usage costs starting at $10 per month, billed hourly.
|
||||
</p>
|
||||
|
||||
{subscription?.plan?.id === 'free' && (
|
||||
<p className="text-sm mt-2">
|
||||
{subscription?.plan?.id === 'free' && (
|
||||
<Alert_Shadcn_ className="py-6 pl-14 pr-8 rounded-none border-l-0 border-r-0">
|
||||
<div className="flex flex-row gap-4 items-center">
|
||||
<WarningIcon className="w-6 h-6 p-1 bg-foreground" />
|
||||
<div className="flex-1">
|
||||
<p className="text-foreground">
|
||||
Mixing paid and non-paid projects in a single organization is not possible.
|
||||
If you want projects to be on the Free Plan, use self-serve project
|
||||
transfers.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-x-3 mt-2">
|
||||
<Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/org-based-billing"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How billing works
|
||||
</Link>
|
||||
</Button>
|
||||
{subscription?.plan?.id === 'free' && (
|
||||
<Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/project-transfer"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Project transfers
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<p className="text-foreground-lighter">
|
||||
To keep projects on a free plan, move them to another organization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/platform/project-transfer"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Project transfers docs
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal.Content>
|
||||
</div>
|
||||
|
||||
<Modal.Content>
|
||||
{!billingViaPartner ? (
|
||||
<div className="py-4 space-y-2">
|
||||
<p className="text-sm">
|
||||
Upon clicking confirm, your monthly invoice will be adjusted and your credit card
|
||||
<div className="pl-8 pr-2 py-4 space-y-4">
|
||||
<PaymentMethodSelection
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
onSelectPaymentMethod={setSelectedPaymentMethod}
|
||||
/>
|
||||
<p className="text-sm pl-1 text-foreground-light">
|
||||
When upgrading to Pro, the monthly invoice will be adjusted and your credit card
|
||||
will be charged immediately. Changing the plan resets your billing cycle and may
|
||||
result in a prorated charge for previous usage.
|
||||
result in a prorated charge for the previous usage.
|
||||
</p>
|
||||
|
||||
<div className="!mt-4">
|
||||
<PaymentMethodSelection
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
onSelectPaymentMethod={setSelectedPaymentMethod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 space-y-2">
|
||||
|
||||
@@ -7,14 +7,13 @@ import { Button, ButtonVariantProps } from '../Button/Button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
} from '../shadcn/ui/dialog'
|
||||
|
||||
export interface ModalProps extends React.ComponentProps<typeof DialogContent> {
|
||||
|
||||
Reference in new issue
Block a user