Files
supabase/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx
T
Kevin Grüneberg e1bd23d66f fix: do not translate USD prices (#35466)
When translation tools go over prices, they wrongfully convert USD into other currencies without changing the price. $25 becomes Rp25 (Rupiah).

By using `translate='no'` we avoid the translation of pricing info. There are some tooltips left that are not covered yet, but this should be the majority already.

Kept the docs Pricing component as simple as possible, just wrapping in a span to avoid translation, not modifying any formatting.
2025-05-06 12:03:35 +08:00

252 lines
9.3 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
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 { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useFlag } from 'hooks/ui/useFlag'
import { formatCurrency } from 'lib/helpers'
import { useAddonsPagePanel } from 'state/addons-page'
import {
Alert,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Radio,
SidePanel,
cn,
} from 'ui'
import { ExternalLink, AlertCircle } from 'lucide-react'
const CustomDomainSidePanel = () => {
const { ref: projectRef } = useParams()
const organization = useSelectedOrganization()
const customDomainsDisabledDueToQuota = useFlag('customDomainsDisabledDueToQuota')
const [selectedOption, setSelectedOption] = useState<string>('cd_none')
const canUpdateCustomDomain = useCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const { panel, closePanel } = useAddonsPagePanel()
const visible = panel === 'customDomain'
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({
onSuccess: () => {
toast.success(`Successfully enabled custom domain`)
closePanel()
},
onError: (error) => {
toast.error(`Unable to enable custom domain: ${error.message}`)
},
})
const { mutate: removeAddon, isLoading: 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 isFreePlan = organization?.plan?.id === 'free'
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}
disabled={
isFreePlan ||
isLoading ||
!hasChanges ||
isSubmitting ||
!canUpdateCustomDomain ||
// Allow disabling, but do not allow opting in
(subscriptionCDOption === undefined && customDomainsDisabledDueToQuota)
}
tooltip={
isFreePlan
? 'Unable to enable custom domain on a Free Plan'
: !canUpdateCustomDomain
? 'You do not have permission to update custom domain'
: undefined
}
header={
<div className="flex items-center justify-between">
<h4>Custom domains</h4>
<Button asChild type="default" icon={<ExternalLink strokeWidth={1.5} />}>
<Link
href="https://supabase.com/docs/guides/platform/custom-domains"
target="_blank"
rel="noreferrer"
>
About custom domains
</Link>
</Button>
</div>
}
>
<SidePanel.Content>
<div className="py-6 space-y-4">
{subscriptionCDOption === undefined &&
selectedCustomDomain !== undefined &&
customDomainsDisabledDueToQuota && (
<Alert_Shadcn_ variant="default" className="mb-2">
<AlertCircle className="h-4 w-4" />
<AlertTitle_Shadcn_>
Adding new custom domains temporarily disabled
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ 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_Shadcn_>
</Alert_Shadcn_>
)}
<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{' '}
<Link href={`/project/${projectRef}/settings/general`} className="text-brand">
General Settings
</Link>{' '}
page after enabling the add-on.
</p>
<div className={cn('!mt-8 pb-4', isFreePlan && 'opacity-75')}>
<Radio.Group
type="large-cards"
size="tiny"
id="custom-domain"
onChange={(event: any) => setSelectedOption(event.target.value)}
>
<Radio
name="custom-domain"
checked={selectedOption === 'cd_none'}
className="col-span-4 !p-0"
label="No custom domain"
value="cd_none"
>
<div className="w-full group">
<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-[1px]"> / month</p>
</div>
</div>
</div>
</Radio>
{availableOptions.map((option) => (
<Radio
className="col-span-4 !p-0"
name="custom-domain"
key={option.identifier}
disabled={isFreePlan}
checked={selectedOption === option.identifier}
label={option.name}
value={option.identifier}
>
<div className="w-full group">
<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-[1px]"> / month</p>
</div>
</div>
</div>
</Radio>
))}
</Radio.Group>
</div>
{hasChanges && selectedOption !== 'cd_none' && (
<p className="text-sm text-foreground-light">
There are no immediate charges. The addon is billed at the end of your billing cycle
based on your usage and prorated to the hour.
</p>
)}
{isFreePlan && (
<Alert
withIcon
variant="info"
title="Custom domains are unavailable on the Free Plan"
actions={
<Button asChild type="default">
<Link
href={`/org/${organization?.slug}/billing?panel=subscriptionPlan&source=customDomainSidePanel`}
>
View available plans
</Link>
</Button>
}
>
Upgrade your plan to add a custom domain to your project
</Alert>
)}
</div>
</SidePanel.Content>
</SidePanel>
)
}
export default CustomDomainSidePanel