Refactor the Subscription modal to match the design.

This commit is contained in:
Ivan Vasilov committed 2024-08-26 17:41:52 +02:00
1 parent b36c9f86b3
commit 27bf41b891
2 files changed
+204 -149

No files matched your search

@@ -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">
+1 -2
View File
@@ -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> {