"use client" // "Delete my account": the person types the account's e-mail address and confirms, from a session // whose sign-in is recent (the server checks both; this form only says so first). The answer is the // deletion's state: completed, or erasing while a module's service is unavailable -- in both cases the // person is signed out everywhere, and the app's browser modules clear what they kept (`onDeleted`). import { useTransport } from "@conusai-kit/ui" import { type FormEvent, type ReactElement, useId, useState } from "react" import type { DeletionView } from "../contract/procedures.js" export interface AccountDeletionLabels { readonly heading: string readonly body: string /** The services whose data is erased, already named; empty when there are none besides the app. */ readonly services: string /** Set when the person is the last owner of an organization others belong to: the form is closed. */ readonly lastOwner?: string /** Set when an organization would be left without members. */ readonly onlyMember?: string readonly reauth: string readonly reauthLink: string readonly notInteractive: string readonly confirmLabel: string readonly confirmHint: string readonly submit: string readonly busy: string readonly completed: string /** With `{modules}` still in it. */ readonly erasing: string readonly home: string /** The app's `app.failed` message, with `{detail}` still in it. */ readonly failed: string readonly sectionNames: Readonly> } export interface AccountDeletionProps { readonly labels: AccountDeletionLabels /** Whether the session may delete now: a person, signed in recently, owning no organization alone. */ readonly interactive: boolean readonly recentSignIn: boolean /** Where a person signs in again and comes back to this page. */ readonly reauthHref: string /** A deletion already under way, when the page was rendered. */ readonly deletion: DeletionView | null /** Runs once the deletion answered, before the result is shown: the app ends its browser modules' context. */ readonly onDeleted?: (deletion: DeletionView) => Promise | unknown } type State = | { readonly kind: "idle" | "busy" } | { readonly kind: "deleted"; readonly deletion: DeletionView } | { readonly kind: "failed"; readonly detail: string; readonly code: string } const codeOf = (error: unknown): string => { const problem = (error as { problem?: { code?: unknown } } | null)?.problem return typeof problem?.code === "string" ? problem.code : "unknown" } export function AccountDeletionPanel(props: AccountDeletionProps): ReactElement { const { labels } = props const { call, describe } = useTransport() const headingId = useId() const inputId = useId() const hintId = useId() const [state, setState] = useState( props.deletion ? { kind: "deleted", deletion: props.deletion } : { kind: "idle" }, ) const submit = async (event: FormEvent): Promise => { event.preventDefault() const confirm = String(new FormData(event.currentTarget).get("confirm") ?? "").trim() if (confirm.length === 0) return setState({ kind: "busy" }) try { const deletion = (await call("account.deletion.request", { confirm })) as DeletionView // The session is revoked now: what the browser modules kept for it goes too (contract ยง12). await Promise.resolve(props.onDeleted?.(deletion)).catch(() => undefined) setState({ kind: "deleted", deletion }) } catch (error) { setState({ kind: "failed", detail: describe(error), code: codeOf(error) }) } } const reauth = (

{labels.reauth} {labels.reauthLink}

) if (state.kind === "deleted") { const pending = state.deletion.pending.map((key) => labels.sectionNames[key] ?? key).join(", ") return (

{labels.heading}

{state.deletion.state === "completed" ? labels.completed : labels.erasing.replace("{modules}", pending)}

{labels.home}
) } const blocked = !props.interactive || labels.lastOwner !== undefined return (

{labels.heading}

{labels.body}

{labels.services ?

{labels.services}

: null} {labels.onlyMember ?

{labels.onlyMember}

: null} {labels.lastOwner ?

{labels.lastOwner}

: null} {!props.interactive ?

{labels.notInteractive}

: null} {props.interactive && !props.recentSignIn ? reauth : null} {blocked ? null : (

{labels.confirmHint}

)} {state.kind === "failed" && state.code === "reauthentication_required" ? reauth : null} {state.kind === "failed" && state.code !== "reauthentication_required" ? (

{labels.failed.replace("{detail}", state.detail)}

) : null}
) }