Files
supabase/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx
Gildas Garcia 0713a1efc1 chore: remove shadcn suffix for Input, Textarea, Alert and Collapsible (#45867)
## Problem

Now that we migrated old components to their new shadcn alternatives, we
don't need the `_Shadcn_` suffix anymore.

## Solution

Remove it

<img width="659" height="609" alt="image"
src="https://github.com/user-attachments/assets/2d7271a9-066a-4dcc-92fe-729b106d2c2f"
/>
2026-05-15 14:55:37 +02:00

234 lines
9.0 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import { AlertCircle } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import {
Alert,
AlertDescription,
AlertTitle,
cn,
RadioGroupCard,
RadioGroupCardItem,
SidePanel,
} from 'ui'
import { TaxDisclaimer } from '@/components/interfaces/Billing/TaxDisclaimer'
import { DocsButton } from '@/components/ui/DocsButton'
import { InlineLink } from '@/components/ui/InlineLink'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
import { useProjectAddonRemoveMutation } from '@/data/subscriptions/project-addon-remove-mutation'
import { useProjectAddonUpdateMutation } from '@/data/subscriptions/project-addon-update-mutation'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import type { AddonVariantId } from '@/data/subscriptions/types'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { DOCS_URL } from '@/lib/constants'
import { formatCurrency } from '@/lib/helpers'
import { useAddonsPagePanel } from '@/state/addons-page'
const CustomDomainSidePanel = () => {
const { ref: projectRef } = useParams()
const customDomainsDisabledDueToQuota = useFlag('customDomainsDisabledDueToQuota')
const [selectedOption, setSelectedOption] = useState<string>('cd_none')
const { can: canUpdateCustomDomain } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const { panel, closePanel } = useAddonsPagePanel()
const visible = panel === 'customDomain'
const { data: addons, isPending: isLoading } = useProjectAddonsQuery({ projectRef })
const { mutate: updateAddon, isPending: isUpdating } = useProjectAddonUpdateMutation({
onSuccess: () => {
toast.success(`Successfully enabled custom domain`)
closePanel()
},
onError: (error) => {
toast.error(`Unable to enable custom domain: ${error.message}`)
},
})
const { mutate: removeAddon, isPending: isRemoving } = useProjectAddonRemoveMutation({
onSuccess: () => {
toast.success(`Successfully disabled custom domain`)
closePanel()
},
onError: (error) => {
toast.error(`Unable to disable custom domain: ${error.message}`)
},
})
const isSubmitting = isUpdating || isRemoving
const subscriptionCDOption = (addons?.selected_addons ?? []).find(
(addon) => addon.type === 'custom_domain'
)
const availableOptions =
(addons?.available_addons ?? []).find((addon) => addon.type === 'custom_domain')?.variants ?? []
const { hasAccess: hasAccessToCustomDomain, isLoading: isLoadingEntitlement } =
useCheckEntitlements('custom_domain')
const hasChanges = selectedOption !== (subscriptionCDOption?.variant.identifier ?? 'cd_none')
const selectedCustomDomain = availableOptions.find(
(option) => option.identifier === selectedOption
)
useEffect(() => {
if (visible) {
if (subscriptionCDOption !== undefined) {
setSelectedOption(subscriptionCDOption.variant.identifier)
} else {
setSelectedOption('cd_none')
}
}
}, [visible, isLoading])
const onConfirm = async () => {
if (!projectRef) return console.error('Project ref is required')
if (selectedOption === 'cd_none' && subscriptionCDOption !== undefined) {
removeAddon({ projectRef, variant: subscriptionCDOption.variant.identifier })
} else {
updateAddon({ projectRef, type: 'custom_domain', variant: selectedOption as AddonVariantId })
}
}
return (
<SidePanel
size="large"
visible={visible}
onCancel={closePanel}
onConfirm={onConfirm}
loading={isLoading || isSubmitting || isLoadingEntitlement}
disabled={
!hasAccessToCustomDomain ||
isLoadingEntitlement ||
isLoading ||
!hasChanges ||
isSubmitting ||
!canUpdateCustomDomain ||
// Allow disabling, but do not allow opting in
(subscriptionCDOption === undefined && customDomainsDisabledDueToQuota)
}
tooltip={
!hasAccessToCustomDomain
? 'Unable to enable custom domain on a Free Plan'
: !canUpdateCustomDomain
? 'You do not have permission to update custom domain'
: undefined
}
header={
<div className="flex w-full items-center justify-between">
<h4>Custom domains</h4>
<DocsButton href={`${DOCS_URL}/guides/platform/custom-domains`} />
</div>
}
>
<SidePanel.Content>
<div className="py-6 space-y-4">
{subscriptionCDOption === undefined &&
selectedCustomDomain !== undefined &&
customDomainsDisabledDueToQuota && (
<Alert variant="default" className="mb-2">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Adding new custom domains temporarily disabled</AlertTitle>
<AlertDescription className="flex flex-col gap-3">
We are working with our upstream DNS provider before we are able to sign up new
custom domains. Please check back in a few hours.
</AlertDescription>
</Alert>
)}
<p className="text-sm">
Custom domains allow you to present a branded experience to your users. You may set up
your custom domain in the{' '}
<InlineLink href={`/project/${projectRef}/settings/general#custom-domains`}>
General Settings
</InlineLink>{' '}
page after enabling the add-on.
</p>
<div className={cn('mt-8! pb-4', !hasAccessToCustomDomain && 'opacity-75')}>
<RadioGroupCard
id="custom-domain"
className="flex flex-wrap gap-3"
value={selectedOption}
onValueChange={(value) => setSelectedOption(value)}
>
<RadioGroupCardItem
value="cd_none"
id="cd_none"
label={
<div className="w-full group text-left">
<div className="border-b border-default px-4 py-2 group-hover:border-control">
<p className="text-sm">No custom domain</p>
</div>
<div className="px-4 py-2">
<p className="text-foreground-light">
Use the default supabase domain for your API
</p>
<div className="flex items-center space-x-1 mt-2">
<p className="text-foreground text-sm" translate="no">
$0
</p>
<p className="text-foreground-light translate-y-px"> / month</p>
</div>
</div>
</div>
}
showIndicator={false}
/>
{availableOptions.map((option) => (
<RadioGroupCardItem
key={option.identifier}
value={option.identifier}
id={option.identifier}
label={
<div className="w-full group text-left">
<div className="border-b border-default px-4 py-2 group-hover:border-control">
<p className="text-sm">{option.name}</p>
</div>
<div className="px-4 py-2">
<p className="text-foreground-light">
Present a branded experience to your users
</p>
<div className="flex items-center space-x-1 mt-2">
<p className="text-foreground text-sm" translate="no">
{formatCurrency(option.price)}
</p>
<p className="text-foreground-light translate-y-px"> / month</p>
</div>
</div>
</div>
}
showIndicator={false}
/>
))}
</RadioGroupCard>
<TaxDisclaimer className="mt-3" />
</div>
{hasChanges && selectedOption !== 'cd_none' && (
<p className="text-sm text-foreground-light">
There are no immediate charges. The add-on is billed at the end of your billing cycle
based on your usage and prorated to the hour.
</p>
)}
{!hasAccessToCustomDomain && (
<UpgradeToPro
addon="customDomain"
source="customDomainSidePanel"
featureProposition="enable custom domains"
primaryText="Custom domains are a Pro Plan add-on"
secondaryText="Enable the add-on to serve your project on your own domain name."
/>
)}
</div>
</SidePanel.Content>
</SidePanel>
)
}
export default CustomDomainSidePanel