CustomDomainVerify.tsx200 lines · main
1import { useParams } from 'common'
2import { AlertCircle, RefreshCw } from 'lucide-react'
3import { useEffect } from 'react'
4import { toast } from 'sonner'
5import { Alert, AlertDescription, AlertTitle, Button, WarningIcon } from 'ui'
6import { Admonition } from 'ui-patterns/admonition'
7
8import { DNSRecord } from './DNSRecord'
9import { DNSTableHeaders } from './DNSTableHeaders'
10import { DocsButton } from '@/components/ui/DocsButton'
11import { InlineLink } from '@/components/ui/InlineLink'
12import Panel from '@/components/ui/Panel'
13import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
14import { useCustomDomainDeleteMutation } from '@/data/custom-domains/custom-domains-delete-mutation'
15import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
16import { useCustomDomainReverifyQuery } from '@/data/custom-domains/custom-domains-reverify-query'
17import { DOCS_URL } from '@/lib/constants'
18
19export const CustomDomainVerify = () => {
20 const { ref: projectRef } = useParams()
21
22 const { data: settings } = useProjectSettingsV2Query({ projectRef })
23
24 const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
25 const customDomain = customDomainData?.customDomain
26 const isSSLCertificateDeploying =
27 customDomain?.ssl.status !== undefined && customDomain.ssl.txt_name === undefined
28
29 const { mutate: deleteCustomDomain, isPending: isDeleting } = useCustomDomainDeleteMutation({
30 onSuccess: () => {
31 toast.success(
32 'Custom domain setup cancelled successfully. It may take a few seconds before your custom domain is fully removed, so you may need to refresh your browser.'
33 )
34 },
35 })
36
37 const {
38 data: reverifyData,
39 refetch: refetchReverify,
40 isFetching: isReverifyLoading,
41 isError: isReverifyError,
42 error: reverifyError,
43 } = useCustomDomainReverifyQuery(
44 { projectRef },
45 {
46 // Poll every 10 seconds if the SSL certificate is being deployed
47 refetchInterval: isSSLCertificateDeploying && !isDeleting ? 10000 : false,
48 }
49 )
50
51 useEffect(() => {
52 if (isReverifyError) {
53 toast.error(reverifyError?.message)
54 }
55 }, [isReverifyError, reverifyError])
56
57 const isNotVerifiedYet = reverifyData?.status === '2_initiated'
58
59 const hasCAAErrors = customDomain?.ssl.validation_errors?.reduce(
60 (acc, error) => acc || error.message.includes('caa_error'),
61 false
62 )
63 const isValidating = (customDomain?.ssl.txt_name ?? '') === ''
64
65 const onReverifyCustomDomain = () => {
66 if (!projectRef) return console.error('Project ref is required')
67 refetchReverify()
68 }
69
70 const onCancelCustomDomain = async () => {
71 if (!projectRef) return console.error('Project ref is required')
72 deleteCustomDomain({ projectRef })
73 }
74
75 return (
76 <>
77 <Panel.Content className="space-y-6">
78 <div>
79 <h4 className="text-foreground mb-2">
80 Configure TXT verification for your custom domain{' '}
81 <code className="text-code-inline">{customDomain?.hostname}</code>
82 </h4>
83 <p className="text-sm text-foreground-light">
84 Set the following TXT record(s) in your DNS provider, then click verify to confirm your
85 control over the domain. Records which have been successfully verified will be removed
86 from this list below.
87 </p>
88 {!isValidating && (
89 <div className="mt-4 mb-2">
90 <Admonition
91 type="note"
92 title={
93 isNotVerifiedYet
94 ? 'Unable to verify records from DNS provider yet.'
95 : 'Please note that it may take up to 24 hours for the DNS records to propagate.'
96 }
97 >
98 <p>
99 You may also visit{' '}
100 <InlineLink href={`https://whatsmydns.net/#TXT/${customDomain?.ssl.txt_name}`}>
101 here
102 </InlineLink>{' '}
103 to check if your DNS has been propagated successfully before clicking verify.
104 </p>
105 {isNotVerifiedYet && (
106 <p className="mt-1">
107 Some registrars will require you to remove the domain name when creating DNS
108 records. As an example, to create a record for{' '}
109 <code className="text-code-inline">foo.app.example.com</code>, you would need to
110 create an entry for <code className="text-code-inline">foo.app</code>.
111 </p>
112 )}
113 </Admonition>
114 </div>
115 )}
116 </div>
117
118 {hasCAAErrors && (
119 <Alert>
120 <WarningIcon />
121 <AlertTitle>Certificate Authority Authentication (CAA) error</AlertTitle>
122 <AlertDescription>
123 Please add a CAA record allowing "digicert.com" to issue certificates for{' '}
124 <code className="text-code-inline">{customDomain?.hostname}</code>. For example:{' '}
125 <code className="text-code-inline">0 issue "digicert.com"</code>
126 </AlertDescription>
127 </Alert>
128 )}
129
130 {customDomain?.ssl.status === 'validation_timed_out' ? (
131 <Alert>
132 <WarningIcon />
133 <AlertTitle>Validation timed out</AlertTitle>
134 <AlertDescription>
135 Please click "Verify" again to retry the validation of the records
136 </AlertDescription>
137 </Alert>
138 ) : (
139 <div className="space-y-2">
140 <DNSTableHeaders display={customDomain?.ssl.txt_name ?? ''} />
141
142 {customDomain?.verification_errors?.includes(
143 'custom hostname does not CNAME to this zone.'
144 ) && (
145 <DNSRecord
146 type="CNAME"
147 name={customDomain?.hostname}
148 value={settings?.app_config?.endpoint ?? 'Loading...'}
149 />
150 )}
151
152 {!isValidating && customDomain?.ssl.status === 'pending_validation' && (
153 <DNSRecord
154 type="TXT"
155 name={customDomain?.ssl.txt_name ?? 'Loading...'}
156 value={customDomain?.ssl.txt_value ?? 'Loading...'}
157 />
158 )}
159
160 {customDomain?.ssl.status === 'pending_deployment' && (
161 <div className="flex items-center justify-center space-x-2 py-8">
162 <AlertCircle size={16} strokeWidth={1.5} />
163 <p className="text-sm text-foreground-light">
164 SSL certificate is being deployed. Please wait a few minutes and try again.
165 </p>
166 </div>
167 )}
168 </div>
169 )}
170 </Panel.Content>
171
172 <div className="border-t border-muted" />
173
174 <Panel.Content>
175 <div className="flex items-center justify-between">
176 <DocsButton href={`${DOCS_URL}/guides/platform/custom-domains`} />
177 <div className="flex items-center space-x-2">
178 <Button
179 type="default"
180 onClick={onCancelCustomDomain}
181 loading={isDeleting}
182 className="self-end"
183 >
184 Cancel
185 </Button>
186 <Button
187 icon={<RefreshCw />}
188 onClick={onReverifyCustomDomain}
189 loading={!isValidating && isReverifyLoading}
190 disabled={isDeleting || isReverifyLoading || isValidating}
191 className="self-end"
192 >
193 Verify
194 </Button>
195 </div>
196 </div>
197 </Panel.Content>
198 </>
199 )
200}