import { createFileRoute, Link } from "@tanstack/react-router"; import { Exit } from "effect"; import { useAtomValue, useAtomSet } from "@effect/atom-react"; import { trackEvent } from "@executor-js/react/api/analytics"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { useCustomer } from "autumn-js/react"; import { toast } from "sonner"; import { orgDomainWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { Button } from "@executor-js/react/components/button"; import { Badge } from "@executor-js/react/components/badge"; import { CopyButton } from "@executor-js/react/components/copy-button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@executor-js/react/components/dropdown-menu"; import { OrgPage as SharedOrgPage } from "@executor-js/react/pages/org"; import { orgDomainsAtom, getDomainVerificationLink, deleteDomain } from "../../web/org-atoms"; // --------------------------------------------------------------------------- // Cloud organization page. The members / roles / invite / org-name surface is // the SHARED `@executor-js/react` OrgPage over the provider-neutral // `/account/*` atoms — identical to self-host. Cloud composes its WorkOS-only // extras AROUND that page: // - a seat/billing banner (Autumn member-limit upsell) // - the WorkOS domain-verification section (over the surviving cloud-local // `/org/domains` endpoints) // These are cloud additions, not a fork of the shared page. // --------------------------------------------------------------------------- export const Route = createFileRoute("/{-$orgSlug}/org")({ component: OrgPage, }); type DomainData = { id: string; domain: string; state: string; verificationToken?: string; verificationPrefix?: string; }; function OrgPage() { return (
{/* Shared members / roles / invite / org-name surface. */} } upgradeAction={ } />
); } function DomainsSection() { const domainsResult = useAtomValue(orgDomainsAtom); const doDeleteDomain = useAtomSet(deleteDomain, { mode: "promiseExit" }); const doGetVerificationLink = useAtomSet(getDomainVerificationLink, { mode: "promiseExit", }); const { check, isLoading: customerLoading } = useCustomer(); const canUseDomains = customerLoading ? false : check({ featureId: "domain-verification" }).allowed; const handleDeleteDomain = async (domainId: string, domain: string) => { const exit = await doDeleteDomain({ params: { domainId }, reactivityKeys: orgDomainWriteKeys, }); trackEvent("org_domain_removed", { success: Exit.isSuccess(exit) }); toast[Exit.isSuccess(exit) ? "success" : "error"]( Exit.isSuccess(exit) ? `Removed ${domain}` : "Failed to remove domain", ); }; const handleAddDomain = async () => { const exit = await doGetVerificationLink({ reactivityKeys: orgDomainWriteKeys, }); trackEvent("org_domain_added", { success: Exit.isSuccess(exit) }); if (Exit.isSuccess(exit)) { window.open(exit.value.link, "_blank"); } else { toast.error("Failed to generate verification link"); } }; return (

Domains

Verify a domain to let anyone with a matching email join automatically.

{!canUseDomains && (

Join by domain is available on the Team plan.

)} {AsyncResult.match(domainsResult, { onInitial: () => (
{[1, 2].map((i) => (
))}
), onFailure: () => (

Failed to load domains

), onSuccess: ({ value }) => { if (value.domains.length === 0) { if (!canUseDomains) return null; return (

No domains yet. Add your company domain so members can join without an invite.

); } return (
{value.domains.map((d: DomainData) => ( handleDeleteDomain(d.id, d.domain)} /> ))}
); }, })}
); } function DomainCard({ domain: d, onDelete }: { domain: DomainData; onDelete: () => void }) { const isVerified = d.state === "verified"; const isPending = d.state === "pending"; const recordValue = d.verificationPrefix ? `${d.verificationPrefix}=${d.verificationToken}` : (d.verificationToken ?? ""); const copyPromptValue = `Add a DNS TXT record for domain verification:\n\nDomain: ${d.domain}\nRecord name: @\nRecord value: ${recordValue}\n\nPlease add this TXT record to my DNS configuration.`; return (

{d.domain}

{isVerified ? "Verified" : isPending ? "Pending" : "Failed"}
Remove domain
{!isVerified && d.verificationToken && (

Add this TXT record to your DNS provider to verify ownership.

Type

Name

Value

TXT

@

{recordValue}

DNS changes can take up to 72 hours to propagate, but usually complete within a few minutes.

)}
); }