import { useState } from "react"; 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, authWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { Button } from "@executor-js/react/components/button"; import { Badge } from "@executor-js/react/components/badge"; import { Input } from "@executor-js/react/components/input"; import { Label } from "@executor-js/react/components/label"; import { CopyButton } from "@executor-js/react/components/copy-button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, } from "@executor-js/react/components/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@executor-js/react/components/dropdown-menu"; import { orgMembersAtom } from "@executor-js/react/api/account-atoms"; import { OrgPage as SharedOrgPage } from "@executor-js/react/pages/org"; import { orgDomainsAtom, getDomainVerificationLink, deleteDomain } from "../../web/org-atoms"; import { deleteOrganization, useAuth } from "../../web/auth"; // --------------------------------------------------------------------------- // 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={ } dangerZoneSection={} />
); } // Destructive org teardown, admin-only. Hidden entirely for non-admins (the // backend enforces admin + name-confirmation regardless). Deleting the org // removes the workspace and all of its data for every member, cancels billing, // and logs the caller out. function DangerZoneSection() { const auth = useAuth(); const membersResult = useAtomValue(orgMembersAtom); const doDelete = useAtomSet(deleteOrganization, { mode: "promiseExit" }); const [open, setOpen] = useState(false); const [confirmText, setConfirmText] = useState(""); const [deleting, setDeleting] = useState(false); const organizationName = auth.status === "authenticated" ? auth.organization?.name : undefined; // Only admins may delete. Derive the caller's role from the members list // (already loaded for this page); render nothing while it loads or for // members, so a delete control never flashes for someone who can't use it. const isAdmin = AsyncResult.match(membersResult, { onInitial: () => false, onFailure: () => false, onSuccess: ({ value }) => value.members.some((m) => m.isCurrentUser && m.status === "active" && m.role === "admin"), }); if (!isAdmin || !organizationName) return null; const confirmed = confirmText.trim() === organizationName.trim(); const handleDelete = async () => { if (!confirmed || deleting) return; setDeleting(true); const exit = await doDelete({ payload: { confirmName: confirmText.trim() }, reactivityKeys: authWriteKeys, }); trackEvent("org_deleted", { success: Exit.isSuccess(exit) }); if (Exit.isSuccess(exit)) { // The org (and the caller's session) are gone. A full navigation resets // all app state and lets the auth gate rehydrate to another membership or // the create-org screen. toast.success(`Deleted ${organizationName}`); window.location.href = "/"; return; } setDeleting(false); toast.error("Failed to delete organization"); }; return (

Delete organization

Permanently delete this organization and all of its data for every member. This cannot be undone.

{ if (deleting) return; if (!v) setConfirmText(""); setOpen(v); }} > Delete organization This permanently deletes{" "} {organizationName}, including every integration, connection, credential, and policy, for all members. Billing is canceled and everyone loses access immediately. This cannot be undone.
setConfirmText((e.target as HTMLInputElement).value)} onKeyDown={(e) => { if (e.key === "Enter") handleDelete(); }} className="h-9 text-sm" />
); } 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.

)}
); }