| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- 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 briven 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
|