Files
Alaister YoungandAlaister Young caceeb429f [MUL-1336] fix(studio): show HA project costs as free during Alpha (#49383)
HA (Multigres) projects are free during Alpha, but the project creation
form still presented the forced large compute as a real charge. The
footer now shows **$0/m** for HA projects, with the usual compute price
struck through + a "Free during Alpha" note in the cost-breakdown
tooltip and the compute size dropdown. Follows the pattern from #49249.

Addresses
[MUL-1336](https://linear.app/supabase/issue/MUL-1336/bug-when-creating-new-projects).

<img width="688" height="167" alt="Screenshot 2026-08-21 at 5 27 39 PM"
src="https://github.com/user-attachments/assets/804b9243-77ae-4961-9083-5e1290734d3a"
/>
<img width="503" height="202" alt="Screenshot 2026-08-21 at 5 27 32 PM"
src="https://github.com/user-attachments/assets/f217edd4-2204-4e5e-87f8-f54974e3c6fa"
/>

**Changed:**
- `ProjectCreationFooter`: "Additional costs" shows `$0/m` when HA is
on; the tooltip gains a "High availability projects are free during
Alpha for up to 2 projects." sentence; the New-project row's price
renders struck through with a "Free during Alpha" sub-line; the HA
project's compute is excluded from "Total Monthly Compute Costs"
(clamped at 0 so credits can't produce a negative total — a no-op for
non-HA since spend already floors above zero)
- `ComputeSizeSelector`: the per-option `$X/hour (~$Y/month)` line
renders struck through with "Free during Alpha" beneath it when HA is on
(both tagged `data-field="instance-details"` so the collapsed trigger
keeps hiding them)
- `ProjectCreationForm`: threads the watched `highAvailability` value
into the footer

The "Confirm compute costs" modal was already suppressed for HA by the
existing `!values.highAvailability` guard — no change needed there.

## To test

- On a paid org, open New Project and toggle High Availability on: the
footer should read `$0/m` (brand green, no strikethrough), its ⓘ tooltip
should show the HA sentence and the New row's `$110` struck through with
"Free during Alpha", and the Total should exclude the $110; the compute
dropdown's Large option should show its price struck through with "Free
during Alpha"
- Toggle HA off (and on/off a few times): all cost displays should
revert exactly to normal — green real price, no strikethrough, no "Free
during Alpha" anywhere outside the HA toggle's own description
- With HA off and compute size Medium, submit: the "Confirm compute
costs" modal should still appear as before (Cancel works)
- Collapsed compute-size trigger should never show a price line or "Free
during Alpha" in either state

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## New Features

- High-availability project options now show compute pricing as free
during Alpha.
- Standard compute prices are displayed with a strikethrough alongside
the Alpha-free notice.
- Project cost summaries accurately show no additional compute charge
for high-availability selections.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-08-24 16:10:14 +08:00

241 lines
9.6 KiB
TypeScript

import { useFlag } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { UseFormReturn } from 'react-hook-form'
import {
Badge,
Button,
cn,
PopoverSeparator,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { CreateProjectForm } from './ProjectCreation.schema'
import { instanceLabel, monthlyInstancePrice } from './ProjectCreation.utils'
import { InlineLink } from '@/components/ui/InlineLink'
import { OrgProject } from '@/data/projects/org-projects-infinite-query'
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { DOCS_URL } from '@/lib/constants'
/** `close` = close the popup (Vercel interstitial); `studio` = navigate into Studio. */
export type ProjectCreationCancelAction = 'studio' | 'close' | 'hidden'
interface ProjectCreationFooterProps {
form: UseFormReturn<CreateProjectForm>
canCreateProject: boolean
instanceSize?: string
highAvailability?: boolean
organizationProjects: OrgProject[]
isCreatingNewProject: boolean
isSuccessNewProject: boolean
cancelAction?: ProjectCreationCancelAction
}
export const ProjectCreationFooter = ({
form,
canCreateProject,
instanceSize,
highAvailability = false,
organizationProjects,
isCreatingNewProject,
isSuccessNewProject,
cancelAction = 'studio',
}: ProjectCreationFooterProps) => {
const router = useRouter()
const { data: currentOrg } = useSelectedOrganizationQuery()
const isFreePlan = currentOrg?.plan?.id === 'free'
const { lastVisitedOrganization } = useLastVisitedOrganization()
const [showCloseWindowHint, setShowCloseWindowHint] = useState(false)
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
const additionalMonthlySpend = isFreePlan
? 0
: monthlyInstancePrice(instanceSize) - availableComputeCredits
const showAdditionalCosts =
!isFreePlan && !projectCreationDisabled && canCreateProject && additionalMonthlySpend > 0
// High availability is free during Alpha, so the new project costs $0 and is excluded from
// the totals below — the tooltip table still shows the usual compute price struck through.
const displayedAdditionalMonthlySpend = highAvailability ? 0 : additionalMonthlySpend
const newProjectComputeCosts = highAvailability ? 0 : monthlyInstancePrice(instanceSize)
// [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now
// Clamped since a free HA project can leave the compute credits larger than the costs
const monthlyComputeCosts = Math.max(
0,
// current project costs
organizationProjects.reduce((prev, acc) => {
const primaryDatabase = acc.databases.find((db) => db.identifier === acc.ref)
const cost = !!primaryDatabase ? monthlyInstancePrice(primaryDatabase.infra_compute_size) : 0
return prev + cost
}, 0) +
// selected compute size
newProjectComputeCosts -
// compute credits
10
)
const onCancel = () => {
if (cancelAction === 'close') {
window.close()
return
}
if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
else router.push('/organizations')
}
useEffect(() => {
// Browsers only allow closing windows that were opened by script (i.e. have a live `opener`).
// Detect this upfront so we don't need to attempt-and-check after the user clicks cancel.
if (cancelAction === 'close' && !window.opener) setShowCloseWindowHint(true)
}, [cancelAction])
return (
<div key="panel-footer" className="grid grid-cols-12 w-full gap-4 items-center">
{showAdditionalCosts && (
<div className="col-span-4">
<div className="flex justify-between text-sm">
<span>Additional costs</span>
<div className="text-brand flex gap-1 items-center font-mono font-medium">
<span role="status">${displayedAdditionalMonthlySpend}/m</span>
<InfoTooltip side="top" className="max-w-[450px] p-0">
<div className="p-4 text-sm text-foreground-light space-y-1">
<p>
Each project includes a dedicated Postgres instance running on its own server.
You are charged for the{' '}
<InlineLink href={`${DOCS_URL}/guides/platform/billing-on-supabase`}>
Compute resource
</InlineLink>{' '}
of that server, independent of your database usage.
</p>
{highAvailability && (
<p>High availability projects are free during Alpha for up to 2 projects.</p>
)}
{monthlyComputeCosts > 0 && (
<p>Compute costs are applied on top of your subscription plan costs.</p>
)}
</div>
<Table className="mt-2">
<TableHeader className="[&_th]:h-7">
<TableRow className="py-2">
<TableHead className="w-[170px]">Project</TableHead>
<TableHead>Compute Size</TableHead>
<TableHead className="text-right">Monthly Costs</TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_td]:py-2">
{organizationProjects.map((project) => {
const primaryDb = project.databases.find(
(db) => db.identifier === project.ref
)
return (
<TableRow key={project.ref} className="text-foreground-light">
<TableCell className="w-[170px] truncate">{project.name}</TableCell>
<TableCell className="text-center">
{instanceLabel(primaryDb?.infra_compute_size)}
</TableCell>
<TableCell className="text-right">
${monthlyInstancePrice(primaryDb?.infra_compute_size)}
</TableCell>
</TableRow>
)
})}
<TableRow>
<TableCell className="w-[170px] flex gap-2">
<span className="truncate">
{form.getValues('projectName') || 'New project'}
</span>
<Badge variant="success">New</Badge>
</TableCell>
<TableCell className="text-center">{instanceLabel(instanceSize)}</TableCell>
<TableCell className="text-right">
<span className={cn(highAvailability && 'line-through')}>
${monthlyInstancePrice(instanceSize)}
</span>
{highAvailability && (
<p className="text-xs text-foreground-lighter">Free during Alpha</p>
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
<PopoverSeparator />
<Table>
<TableHeader className="[&_th]:h-7">
<TableRow>
<TableHead colSpan={2}>Compute Credits</TableHead>
<TableHead colSpan={1} className="text-right">
-$10
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_td]:py-2">
<TableRow className="text-foreground">
<TableCell colSpan={2}>
Total Monthly Compute Costs
{/**
* API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
* Will be adjusted in the future [kevin]
*/}
{organizationProjects.length > 0 && (
<p className="text-xs text-foreground-lighter">Excluding Read replicas</p>
)}
</TableCell>
<TableCell colSpan={1} className="text-right">
${monthlyComputeCosts}
</TableCell>
</TableRow>
</TableBody>
</Table>
</InfoTooltip>
</div>
</div>
</div>
)}
<div
className={cn(
showAdditionalCosts ? 'col-span-8' : 'col-span-12',
'flex items-center gap-x-2 ml-auto'
)}
>
{cancelAction === 'hidden' ? null : showCloseWindowHint ? (
<p role="status" aria-live="polite" className="text-xs text-foreground-muted mr-3">
Close window to cancel
</p>
) : (
<Button
type="button"
variant="default"
disabled={isCreatingNewProject || isSuccessNewProject}
onClick={onCancel}
>
Cancel
</Button>
)}
<Button
type="submit"
loading={isCreatingNewProject || isSuccessNewProject}
disabled={!canCreateProject}
>
Create new project
</Button>
</div>
</div>
)
}