mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import clsx from 'clsx'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
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 { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
|
|
import Telemetry from 'lib/telemetry'
|
|
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
|
import {
|
|
Alert,
|
|
AlertDescription_Shadcn_,
|
|
Alert_Shadcn_,
|
|
Button,
|
|
IconAlertTriangle,
|
|
IconExternalLink,
|
|
Radio,
|
|
SidePanel,
|
|
} from 'ui'
|
|
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
|
|
|
const CustomDomainSidePanel = () => {
|
|
const { ui } = useStore()
|
|
const router = useRouter()
|
|
const { ref: projectRef } = useParams()
|
|
const organization = useSelectedOrganization()
|
|
|
|
const [selectedOption, setSelectedOption] = useState<string>('cd_none')
|
|
|
|
const canUpdateCustomDomain = useCheckPermissions(
|
|
PermissionAction.BILLING_WRITE,
|
|
'stripe.subscriptions'
|
|
)
|
|
|
|
const snap = useSubscriptionPageStateSnapshot()
|
|
const visible = snap.panelKey === 'customDomain'
|
|
const onClose = () => snap.setPanelKey(undefined)
|
|
|
|
const { data: addons, isLoading } = useProjectAddonsQuery({ projectRef })
|
|
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
|
const { mutate: updateAddon, isLoading: isUpdating } = useProjectAddonUpdateMutation({
|
|
onSuccess: () => {
|
|
ui.setNotification({
|
|
category: 'success',
|
|
message: `Successfully enabled custom domain`,
|
|
})
|
|
onClose()
|
|
},
|
|
onError: (error) => {
|
|
ui.setNotification({
|
|
error,
|
|
category: 'error',
|
|
message: `Unable to enable custom domain: ${error.message}`,
|
|
})
|
|
},
|
|
})
|
|
const { mutate: removeAddon, isLoading: isRemoving } = useProjectAddonRemoveMutation({
|
|
onSuccess: () => {
|
|
ui.setNotification({
|
|
category: 'success',
|
|
message: `Successfully disabled custom domain`,
|
|
})
|
|
onClose()
|
|
},
|
|
onError: (error) => {
|
|
ui.setNotification({
|
|
error,
|
|
category: 'error',
|
|
message: `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 = subscription?.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')
|
|
}
|
|
Telemetry.sendActivity(
|
|
{
|
|
activity: 'Side Panel Viewed',
|
|
source: 'Dashboard',
|
|
data: {
|
|
title: 'Custom domains',
|
|
section: 'Add ons',
|
|
},
|
|
projectRef,
|
|
},
|
|
router
|
|
)
|
|
}
|
|
}, [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 })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<SidePanel
|
|
size="large"
|
|
visible={visible}
|
|
onCancel={onClose}
|
|
onConfirm={onConfirm}
|
|
loading={isLoading || isSubmitting}
|
|
disabled={isFreePlan || isLoading || !hasChanges || isSubmitting || !canUpdateCustomDomain}
|
|
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={<IconExternalLink 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">
|
|
<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={clsx('!mt-8 pb-4', isFreePlan && 'opacity-75')}>
|
|
<Radio.Group
|
|
type="large-cards"
|
|
size="tiny"
|
|
id="custom-domain"
|
|
onChange={(event: any) => {
|
|
setSelectedOption(event.target.value)
|
|
Telemetry.sendActivity(
|
|
{
|
|
activity: 'Option Selected',
|
|
source: 'Dashboard',
|
|
data: {
|
|
title: 'Custom domains',
|
|
section: 'Add ons',
|
|
option: event.target.label,
|
|
},
|
|
projectRef,
|
|
},
|
|
router
|
|
)
|
|
}}
|
|
>
|
|
<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-scale-500 px-4 py-2 group-hover:border-scale-600">
|
|
<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">$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-scale-500 px-4 py-2 group-hover:border-scale-600">
|
|
<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">${option.price}</p>
|
|
<p className="text-foreground-light translate-y-[1px]"> / month</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Radio>
|
|
))}
|
|
</Radio.Group>
|
|
</div>
|
|
|
|
{hasChanges && (
|
|
<>
|
|
{selectedOption === 'cd_none' ||
|
|
(selectedCustomDomain?.price ?? 0) < (subscriptionCDOption?.variant.price ?? 0) ? (
|
|
<p className="text-sm text-foreground-light">
|
|
Upon clicking confirm, the amount of that's unused during the current billing
|
|
cycle will be returned as credits that can be used for subsequent billing cycles
|
|
</p>
|
|
) : (
|
|
<p className="text-sm text-foreground-light">
|
|
Upon clicking confirm, the amount of{' '}
|
|
<span className="text-foreground">
|
|
${selectedCustomDomain?.price.toLocaleString()}
|
|
</span>{' '}
|
|
will be added to your monthly invoice. You're immediately charged for the
|
|
remaining days of your billing cycle. The addon is prepaid per month and in case
|
|
of a downgrade, you get credits for the remaining time.
|
|
</p>
|
|
)}
|
|
|
|
{subscription?.billing_via_partner &&
|
|
subscription.scheduled_plan_change?.target_plan !== undefined && (
|
|
<Alert_Shadcn_ variant={'warning'} className="mb-2">
|
|
<IconAlertTriangle className="h-4 w-4" />
|
|
<AlertDescription_Shadcn_>
|
|
You have a scheduled subscription change that will be canceled if you change
|
|
your custom domain add on.
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{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`}>
|
|
View available plans
|
|
</Link>
|
|
</Button>
|
|
}
|
|
>
|
|
Upgrade your plan to add a custom domain to your project
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
</SidePanel.Content>
|
|
</SidePanel>
|
|
)
|
|
}
|
|
|
|
export default CustomDomainSidePanel
|