@@ -328,7 +330,11 @@ function LogoutObstacle ({ onClose }) {
removeLocalWallets()
- await signOut({ callbackUrl: '/' })
+ // Next Auth redirects to the imposed NEXT_AUTH_URL
+ // so we need to do manual redirects for custom domains.
+ onClose()
+ await signOut({ callbackUrl: '/', redirect: !domain })
+ domain && router.push('/')
}}
>
logout
diff --git a/components/nav/index.js b/components/nav/index.js
index beacbd6fc..892d7d683 100644
--- a/components/nav/index.js
+++ b/components/nav/index.js
@@ -3,16 +3,23 @@ import DesktopHeader from './desktop/header'
import MobileHeader from './mobile/header'
import StickyBar from './sticky-bar'
import { PriceCarouselProvider } from './price-carousel'
+import { useDomain } from '../territory-domains'
export default function Navigation ({ sub }) {
const router = useRouter()
+ const { domain } = useDomain()
const path = router.asPath.split('?')[0]
const props = {
prefix: sub ? `/~${sub}` : '',
path,
pathname: router.pathname,
- topNavKey: path.split('/')[sub ? 2 : 1] ?? '',
- dropNavKey: path.split('/').slice(sub ? 2 : 1).join('/'),
+ topNavKey: domain
+ // on custom domains, the nav key is in the first path segment
+ ? path.split('/')[1] ?? ''
+ : path.split('/')[sub ? 2 : 1] ?? '',
+ dropNavKey: domain
+ ? path.split('/').slice(1).join('/')
+ : path.split('/').slice(sub ? 2 : 1).join('/'),
sub
}
diff --git a/components/territory-domains.js b/components/territory-domains.js
new file mode 100644
index 000000000..ef827381d
--- /dev/null
+++ b/components/territory-domains.js
@@ -0,0 +1,207 @@
+import { Badge } from 'react-bootstrap'
+import { Form, Input, SubmitButton, CopyButton } from './form'
+import { useMutation, useQuery } from '@apollo/client'
+import { customDomainSchema } from '@/lib/validate'
+import { useToast } from '@/components/toast'
+import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
+import { GET_DOMAIN, SET_DOMAIN } from '@/fragments/domains'
+import { useEffect, createContext, useContext, useState } from 'react'
+import Moon from '@/svgs/moon-fill.svg'
+import ClipboardLine from '@/svgs/clipboard-line.svg'
+import RefreshLine from '@/svgs/refresh-line.svg'
+import styles from './item.module.css'
+
+// Domain context for custom domains
+const DomainContext = createContext({
+ domain: {
+ domainName: null,
+ subName: null
+ }
+})
+
+export const DomainProvider = ({ domain: ssrDomain, children }) => {
+ const [domain, setDomain] = useState(ssrDomain || null)
+
+ // maintain the custom domain state across re-renders
+ useEffect(() => {
+ if (ssrDomain && !domain) {
+ setDomain(ssrDomain)
+ }
+ }, [ssrDomain])
+
+ return (
+
+ {/* TODO: Placeholder for Branding */}
+ {children}
+
+ )
+}
+
+export const useDomain = () => useContext(DomainContext)
+
+const getStatusBadge = (type, status) => {
+ switch (status) {
+ case 'VERIFIED':
+ return
{type} verified
+ default:
+ return
{type} pending
+ }
+}
+
+const DomainLabel = ({ domain, polling }) => {
+ const { status, records } = domain || {}
+
+ return (
+
+
custom domain
+ {domain && (
+
+ {status !== 'HOLD'
+ ? (
+ <>
+ {getStatusBadge('CNAME', records?.CNAME?.status)}
+ {getStatusBadge('SSL', records?.SSL?.status)}
+ >
+ )
+ : (
+ <>
+ HOLD
+
+
+
+ >
+ )}
+ {polling && }
+
+ )}
+
+ )
+}
+
+const DomainGuidelines = ({ domain }) => {
+ const { records } = domain || {}
+
+ const dnsRecord = ({ record }) => {
+ return (
+
+
+
+ host
+
+ }
+ />
+
+ {record?.recordName}
+
+
+
+ value
+
+ }
+ />
+
+ {record?.recordValue}
+
+
+ )
+ }
+
+ return (
+
+ {records?.CNAME?.status === 'PENDING' && (
+
+
Step 1: Verify your domain
+
Add the following DNS record to verify ownership of your domain:
+
CNAME
+ {dnsRecord({ record: records?.CNAME })}
+
+ )}
+ {records?.SSL?.status === 'PENDING' && (
+
+
Step 2: Prepare your domain for SSL
+
We issued an SSL certificate for your domain. To validate it, add the following CNAME record:
+
CNAME
+ {dnsRecord({ record: records?.SSL })}
+
+ )}
+
+ )
+}
+
+export default function CustomDomainForm ({ sub }) {
+ const [setDomain] = useMutation(SET_DOMAIN)
+
+ // Get the custom domain and poll for changes
+ const { data, refetch } = useQuery(GET_DOMAIN, SSR
+ ? {}
+ : {
+ variables: { subName: sub.name },
+ pollInterval: NORMAL_POLL_INTERVAL,
+ nextFetchPolicy: 'cache-and-network',
+ onCompleted: ({ domain }) => {
+ if (domain?.status !== 'PENDING') {
+ return { pollInterval: 0 }
+ }
+ }
+ })
+ const toaster = useToast()
+
+ const { domainName, status } = data?.domain || {}
+ const polling = status === 'PENDING'
+
+ // Update the custom domain
+ const onSubmit = async ({ domainName }) => {
+ try {
+ await setDomain({
+ variables: {
+ subName: sub.name,
+ domainName
+ }
+ })
+ refetch()
+ if (domainName) {
+ toaster.success('started domain verification')
+ } else {
+ toaster.success('domain removed successfully')
+ }
+ } catch (error) {
+ toaster.danger(error.message)
+ }
+ }
+
+ return (
+ <>
+
+ {data?.domain && data?.domain?.status !== 'ACTIVE' && (
+
+ )}
+ >
+ )
+}
diff --git a/components/territory-form.js b/components/territory-form.js
index 0983002c8..6221909c5 100644
--- a/components/territory-form.js
+++ b/components/territory-form.js
@@ -5,7 +5,7 @@ import FeeButton, { FeeButtonProvider } from './fee-button'
import { gql, useApolloClient, useLazyQuery } from '@apollo/client'
import { useCallback, useMemo, useState } from 'react'
import { useRouter } from 'next/router'
-import { MAX_TERRITORY_DESC_LENGTH, POST_TYPES, TERRITORY_BILLING_OPTIONS, TERRITORY_PERIOD_COST } from '@/lib/constants'
+import { MAX_TERRITORY_DESC_LENGTH, POST_TYPES, SN_ADMIN_IDS, TERRITORY_BILLING_OPTIONS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { territorySchema } from '@/lib/validate'
import { useMe } from './me'
import Info from './info'
@@ -14,11 +14,14 @@ import { purchasedType } from '@/lib/territory'
import { SUB } from '@/fragments/subs'
import { usePaidMutation } from './use-paid-mutation'
import { UNARCHIVE_TERRITORY, UPSERT_SUB } from '@/fragments/paidAction'
+import TerritoryDomains, { useDomain } from './territory-domains'
+import Link from 'next/link'
export default function TerritoryForm ({ sub }) {
const router = useRouter()
const client = useApolloClient()
const { me } = useMe()
+ const { domain } = useDomain()
const [upsertSub] = usePaidMutation(UPSERT_SUB)
const [unarchiveTerritory] = usePaidMutation(UNARCHIVE_TERRITORY)
@@ -286,6 +289,17 @@ export default function TerritoryForm ({ sub }) {
/>
+ {SN_ADMIN_IDS.includes(Number(me.id)) &&
+ <>
+ {sub && !domain &&
+