Ctrl K
ComunidadGuías…

frontend/src/skins/withAccounts.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* eslint-disable */
// @ts-nocheck
/**
 * Accounts, as a mixin over a generated skin component.
 *
 * The four skins render accounts in four idioms but drive one flow. This holds
 * the flow — state, requests, bindings — and each skin supplies only what is
 * genuinely theme-specific: where its nav items live, what shape they are, and
 * which CSS variables to paint with.
 *
 * It subclasses rather than edits: `src/skins/<id>/SkinApp.tsx` is regenerated
 * from the design source, so a subclass is the only thing that survives.
 * `renderVals()` is extended, never replaced.
 */
import { mergeNavItem } from "./navInject";
import { lazy, Suspense } from "react";
import { toast } from "sonner";
import {
    accountApi, describeKey, defaultKeyLabel, downloadRecoveryCode, initialAccountState,
    MIN_PASSWORD_LENGTH, ACCOUNT_COPY, EMAIL_RESET,
} from "./accountLogic";
import { CLERK_BLOCKED_MESSAGE, clerkInstance, clerkLoadFailed, isClerkDocument, isClerkEnabled, subscribeClerkInstance, whenClerkReady } from "@/lib/clerk/instance";
import { documentPath } from "@/lib/documentLocation";
import { AccountSecurityError, ACCOUNT_CLEANUP_PENDING, securityErrorMessage } from "@/lib/clerk/securityActions";
import { captureAccountReturn, consumeAccountReturn, clearAccountReturn } from "@/lib/account-return";
import { SSO_RETURN } from "@/lib/clerk/accountApi";

const AccountReverificationBridge = lazy(() => import("@/components/account/AccountReverificationBridge"));

const ACCOUNT_PATH = /^\/account(?:\/(?:keys|settings|sign-in|sign-up))?\/?$/;
const ACCOUNT_HINT_KEY = "privatools.account-present";
function accountHint() {
    try { return localStorage.getItem(ACCOUNT_HINT_KEY) === "1"; } catch { return false; }
}
function rememberAccountHint(present) {
    try { if (present) localStorage.setItem(ACCOUNT_HINT_KEY, "1"); else localStorage.removeItem(ACCOUNT_HINT_KEY); } catch { /* Presentation never gates account access. */ }
}
type AccountLocation = { pathname: string; hash?: string; search?: string };

/** Shared with the application shell; hashes still require a canonical path navigation. */
export function accountPathForLocation(current: AccountLocation | null = typeof location === "undefined" ? null : location) {
    if (!current) return null;
    const hashPath = (current.hash || "").replace(/^#/, "").split("?")[0];
    const path = ACCOUNT_PATH.test(hashPath) ? hashPath : current.pathname;
    return ACCOUNT_PATH.test(path || "") ? path.replace(/\/+$/, "") : null;
}

export function accountModeForLocation(current: AccountLocation | null = typeof location === "undefined" ? null : location) {
    const path = accountPathForLocation(current);
    if (path === "/account/sign-up") return "signup";
    if (path === "/account/sign-in") return "signin";
    if (path === "/account") {
        const mode = new URLSearchParams(current?.search || "").get("mode");
        if (mode === "signup" || mode === "recover") return mode;
    }
    return "signin";
}

/**
 * @param Base    the generated component
 * @param config  theme specifics:
 *   route        hash or path this surface answers on
 *   isActive()   whether that route is current
 *   navKey       binding name holding the nav list ("navSys", "navMain", …)
 *   navItem()    builds one nav item in this theme's shape
 *   injectNav()  optional — for themes whose nav is grouped rather than flat
 *   palette      CSS variables this theme paints accounts with
 */
export function withAccounts(Base, config) {
    return class WithAccounts extends Base {
        _setAccountSecurityRunner = runner => { this._accountSecurityRunner = runner; };

        render() {
            return <>{isClerkEnabled() && isClerkDocument(documentPath) && <Suspense fallback={null}><AccountReverificationBridge onReady={this._setAccountSecurityRunner} /></Suspense>}{super.render()}</>;
        }

        constructor(props) {
            super(props);
            this.state = { ...this.state, acct: { ...initialAccountState, mode: accountModeForLocation(), accountHint: accountHint(), resolved: false } };
            this._accountRouteIdentity = accountPathForLocation();
            if (this._accountRouteIdentity) captureAccountReturn(location.search);
            this._onAcctNav = () => {
                this._ensureAccountPath(); this._syncAccountMode(); this.forceUpdate();
                if (accountPathForLocation()) captureAccountReturn(location.search);
                const userId = this.state.acct.user?.id;
                if (userId && this._isAccountActive() && this._keysLoadedUser !== userId) this._loadKeys(userId);
            };
            this._onAccountHint = event => { if (event.key === ACCOUNT_HINT_KEY || event.key === null) this._setAcct({ accountHint: accountHint() }); };
        }

        componentDidMount() {
            this._accountUnmounted = false;
            if (super.componentDidMount) super.componentDidMount();
            window.addEventListener("hashchange", this._onAcctNav);
            window.addEventListener("popstate", this._onAcctNav);
            window.addEventListener("privatools:clerk-blocked", this._onClerkBlocked);
            window.addEventListener("storage", this._onAccountHint);
            if (EMAIL_RESET) this._unsubscribeClerk = subscribeClerkInstance(this._syncClerkAccount);
            this._ensureAccountPath();
            // The block usually happens before this route is even opened, so
            // check the flag as well as listening for the event.
            if (clerkLoadFailed()) this._onClerkBlocked();
            this._bootAccount();
        }

        componentWillUnmount() {
            this._accountUnmounted = true;
            this._clerkSyncId = (this._clerkSyncId || 0) + 1;
            this._keysRequestId = (this._keysRequestId || 0) + 1;
            if (super.componentWillUnmount) super.componentWillUnmount();
            window.removeEventListener("hashchange", this._onAcctNav);
            window.removeEventListener("popstate", this._onAcctNav);
            window.removeEventListener("privatools:clerk-blocked", this._onClerkBlocked);
            window.removeEventListener("storage", this._onAccountHint);
            this._unsubscribeClerk?.();
        }

        /**
         * Put the account view on the /account *path*, never a hash of some
         * other page.
         *
         * The CSP that lets clerk-js load is scoped per path, and a hash never
         * reaches the server — so `/#/account` and `/tool/x#/account` are served
         * the homepage's and the tool's policies, which do not name Clerk, and
         * sign-in dies with "Failed to load Clerk JS". Only a real navigation
         * gets the right headers, so any hash that lands here is turned into
         * one. `replace`, not `assign`: the broken URL should not sit in the
         * back button.
         */
        /**
         * Settle who is signed in, once Clerk can actually answer.
         *
         * Two things had to move here. Clerk is parked from an effect, so
         * calling `me()` straight out of componentDidMount could beat it and
         * report a signed-in visitor as signed out, permanently. And a visitor
         * coming back from GitHub arrives with the handshake half-finished:
         * it completes only if this page asks it to.
         */
        _bootAccount = async () => {
            if (EMAIL_RESET) {
                const clerk = await whenClerkReady();
                if (this._accountUnmounted) return;
                if (!clerk) {
                    if (clerkLoadFailed()) this._onClerkBlocked();
                    this._setAcct({ resolved: true });
                    return;
                }
                // Only on the leg back from the provider, which signInWithSocial
                // marks explicitly. Clerk's own resources cannot be used to
                // detect this: an untouched signIn already reads
                // "needs_identifier", so a truthiness check runs the callback
                // on every ordinary visit to the page.
                const returning = new URLSearchParams(location.search).has(SSO_RETURN);
                // `session` settles before `user` does, and running the
                // callback against a finished flow is what bounces the visitor
                // to the hosted portal. Check both.
                const alreadyIn = Boolean(clerk.user || clerk.session);
                // And the verification must still be live. A page that has seen
                // an earlier attempt keeps a spent one around — status
                // "expired", strategy still oauth_github — and handing that to
                // handleRedirectCallback fails, on which Clerk navigates to its
                // hosted portal. So a stale marker in a bookmarked URL was
                // enough to throw someone off the site entirely.
                const LIVE = ["transferable", "verified", "unverified"];
                const live = (v) => Boolean(v && LIVE.includes(v.status));
                const pending = live(clerk.client?.signIn?.firstFactorVerification)
                    || live(clerk.client?.signUp?.verifications?.externalAccount);
                if (returning && !alreadyIn && pending && !this._socialCompleting) {
                    this._socialCompleting = true;
                    try {
                        await accountApi.completeSocialRedirect();
                        // Clerk leaves its bookkeeping in the address bar.
                        history.replaceState(null, "", location.pathname + location.hash);
                    } catch (err) {
                        this._setAcct({ busy: false, error: err.message });
                    } finally { this._socialCompleting = false; }
                }
                return this._syncClerkAccount(clerkInstance() || clerk);
            }
            accountApi.me()
                .then(({ user }) => { this._setAcct({ user, resolved: true }); this._resumeAccountReturn(user); if (this._isAccountActive()) this._loadKeys(user.id); })
                .catch(() => { this._setAcct({ resolved: true }); });
        };

        _syncClerkAccount = async clerk => {
            if (this._accountUnmounted || !clerk?.loaded) return;
            if (!clerk.user) {
                if (clerk.session) return; // User can settle just after its session.
                this._clerkSignature = "";
                this._clerkSyncId = (this._clerkSyncId || 0) + 1;
                rememberAccountHint(false);
                if (this.state.acct.user) {
                    this._keysRequestId = (this._keysRequestId || 0) + 1;
                    this._keysLoadedUser = null;
                    this._setAcct({ ...initialAccountState, accountHint: false, resolved: true });
                } else this._setAcct({ accountHint: false, resolved: true });
                return;
            }
            const source = clerk.user;
            const signature = JSON.stringify([source.id, source.primaryEmailAddress?.emailAddress, source.username, source.passwordEnabled]);
            // Token refreshes emit too. Avoid rerendering the whole tool app or
            // refetching API keys when the account itself has not changed.
            if (signature === this._clerkSignature) return;
            this._clerkSignature = signature;
            const syncId = this._clerkSyncId = (this._clerkSyncId || 0) + 1;
            try {
                const { user } = await accountApi.me();
                if (this._accountUnmounted || syncId !== this._clerkSyncId) return;
                const changedUser = this.state.acct.user?.id !== user.id;
                rememberAccountHint(true);
                if (changedUser) {
                    this._keysRequestId = (this._keysRequestId || 0) + 1;
                    this._keysLoadedUser = null;
                }
                this._setAcct({ user, accountHint: true, blocked: false, resolved: true, ...(changedUser ? { keys: [], freshKey: "", freshKeyCopied: false } : {}) });
                this._resumeAccountReturn(user);
                if (changedUser && this._isAccountActive()) this._loadKeys(user.id);
            } catch { if (syncId === this._clerkSyncId) { this._clerkSignature = ""; this._setAcct({ resolved: true }); } }
        };

        _resumeAccountReturn = (user, recoveryCode = "") => {
            if (!user || recoveryCode || !this._isAccountActive()) return;
            const target = consumeAccountReturn();
            if (!target) return;
            // Both destinations are in the existing account document's CSP scope.
            history.replaceState(null, "", target);
            window.dispatchEvent(new PopStateEvent("popstate"));
        };

        _ensureAccountPath = () => {
            if (typeof location === "undefined") return;
            const hashPath = (location.hash || "").replace(/^#/, "").split("?")[0];
            if (!ACCOUNT_PATH.test(hashPath)) return;
            const target = hashPath.replace(/\/+$/, "");
            if (location.pathname.replace(/\/+$/, "") === target) return;
            location.replace(target);
        };

        _isAccountActive = () => Boolean(config.isActive?.() || accountPathForLocation());

        _syncAccountMode = () => {
            const path = accountPathForLocation();
            if (path === this._accountRouteIdentity) return;
            this._accountRouteIdentity = path;
            if (!path) return;
            this._setAcct({ mode: accountModeForLocation(), error: "", needsEmailCode: false, signInVerification: "", verificationDestination: "", emailCode: "", resetEmailSent: false, password: "", recoveryInput: "" });
        };

        _acctChooseMode = (mode) => {
            const target = mode === "signup" ? "/account/sign-up" : "/account/sign-in";
            this._setAcct({ mode, error: "", needsEmailCode: false, signInVerification: "", verificationDestination: "", emailCode: "", resetEmailSent: false, password: "", recoveryInput: "" });
            // All canonical account subpaths share the account CSP. A visitor
            // outside that family still needs a full navigation for its headers.
            if (!ACCOUNT_PATH.test(location.pathname || "")) { location.assign(target); return; }
            history.pushState(null, "", target);
            this._accountRouteIdentity = target;
            window.dispatchEvent(new PopStateEvent("popstate"));
        };

        _onClerkBlocked = () => {
            this._setAcct({ busy: false, blocked: true, resolved: true, error: CLERK_BLOCKED_MESSAGE });
        };

        _setAcct(patch) {
            if (this._accountUnmounted) return;
            this.setState((s) => ({ acct: { ...s.acct, ...(typeof patch === "function" ? patch(s.acct) : patch) } }));
        }

        _loadKeys(userId = this.state.acct.user?.id) {
            if (this._keysLoadPromise && this._keysLoadUser === userId && this._keysLoadId === this._keysRequestId) return this._keysLoadPromise;
            const requestId = this._keysRequestId = (this._keysRequestId || 0) + 1;
            this._keysLoadUser = userId; this._keysLoadId = requestId;
            this._setAcct({ keysLoading: true });
            const request = accountApi.listKeys()
                .then(({ keys }) => { if (requestId === this._keysRequestId) { this._keysLoadedUser = userId; this._setAcct({ keys, keysLoading: false, error: "" }); } })
                .catch(() => {
                    if (requestId === this._keysRequestId) this._setAcct({ keysLoading: false, error: "Your API keys could not be loaded. Check your connection and retry." });
                }).finally(() => { if (this._keysLoadPromise === request) this._keysLoadPromise = null; });
            this._keysLoadPromise = request;
            return request;
        }

        _acctSubmit = (event) => {
            if (event && event.preventDefault) event.preventDefault();
            const { mode, email, password, username } = this.state.acct;
            if (this.state.acct.busy) return;
            // One form, three modes. Recover takes a different endpoint and a
            // different field, so it branches here rather than duplicating the
            // whole form in each design's markup.
            if (mode === "recover") return this._acctRecover(null);
            this._setAcct({ busy: true, error: "" });
            const request = mode === "signup"
                ? accountApi.register(email, password, username || undefined)
                : accountApi.login(email, password);
            request
                .then((res) => {
                    if (res.status === "needs_sign_in_code") {
                        this._setAcct({ busy: false, password: "", signInVerification: res.verification, verificationDestination: res.destination, emailCode: "" });
                        return;
                    }
                    // Clerk can stop half way and email a code. Local auth never
                    // did, so `user` would be null here and the form would
                    // silently reappear as though nothing had happened.
                    if (res.status === "needs_email_code") {
                        // Verification now has its own form, so the password
                        // can leave application state as soon as Clerk accepts it.
                        this._setAcct({
                            busy: false, error: "", password: "",
                            needsEmailCode: true, emailCode: "",
                        });
                        return;
                    }
                    // Signup answers with the recovery code, and it is answered
                    // exactly once. There is no reset email, so dropping it here
                    // — which is what this used to do — left the account with no
                    // way back in at all. Clerk sends "" and the panel that
                    // renders it is already conditional, so it just does not show.
                    this._setAcct({
                        user: res.user, busy: false, password: "", error: "",
                        recoveryCode: res.recovery_code ?? "", recoverySaved: false,
                    });
                    this._resumeAccountReturn(res.user, res.recovery_code);
                    this._loadKeys(res.user.id);
                })
                .catch((err) => this._setAcct({ busy: false, error: err.message }));
        };

        _acctPasskey = () => {
            if (this.state.acct.busy) return;
            this._setAcct({ busy: true, error: "" });
            accountApi.loginWithPasskey().then(res => {
                if (res.status === "needs_sign_in_code") {
                    this._setAcct({ busy: false, password: "", signInVerification: res.verification, verificationDestination: res.destination, emailCode: "" });
                    return;
                }
                this._setAcct({ user: res.user, busy: false, password: "", error: "" });
                this._resumeAccountReturn(res.user, res.recovery_code);
                this._loadKeys(res.user.id);
            }).catch(err => this._setAcct({ busy: false, error: err.message }));
        };

        _acctVerifySignIn = event => {
            event?.preventDefault();
            const code = this.state.acct.emailCode.trim();
            if (!code || this.state.acct.busy) return;
            this._setAcct({ busy: true, error: "" });
            accountApi.verifySignIn(code).then(res => {
                if (!res.user) throw new Error("Verification is not complete. Try a new code.");
                this._setAcct({ user: res.user, busy: false, signInVerification: "", emailCode: "", password: "", error: "" });
                this._resumeAccountReturn(res.user, res.recovery_code);
                this._loadKeys(res.user.id);
            }).catch(err => this._setAcct({ busy: false, error: err.message }));
        };

        _acctResendCode = () => {
            if (this.state.acct.busy) return;
            this._setAcct({ busy: true, error: "" });
            const request = this.state.acct.signInVerification ? accountApi.resendSignInCode() : accountApi.resendSignUpCode();
            request.then(() => this._setAcct({ busy: false, emailCode: "" }))
                .catch(err => this._setAcct({ busy: false, error: err.message }));
        };

        /** Finish a Clerk sign-up with the code it emailed. */
        _acctVerifyEmail = (event) => {
            if (event && event.preventDefault) event.preventDefault();
            const code = (this.state.acct.emailCode || "").trim();
            if (!code || this.state.acct.busy) return;
            this._setAcct({ busy: true, error: "" });
            accountApi.verifyEmailCode(code)
                .then(({ user }) => {
                    this._setAcct({
                        user, busy: false, error: "", needsEmailCode: false,
                        emailCode: "", recoveryCode: "", recoverySaved: false,
                        password: "",
                    });
                    this._resumeAccountReturn(user);
                    this._loadKeys(user.id);
                })
                .catch((err) => this._setAcct({ busy: false, error: err.message }));
        };

        /**
         * Reset a password. Two very different flows behind one submit:
         *
         * - Local auth: one call with the recovery code issued at signup.
         * - Clerk: stage one emails a code, stage two redeems it. Clerk has no
         *   `recover` — calling it would throw — so the branch is not cosmetic.
         *   `finishPasswordReset` activates the session, so success here IS a
         *   sign-in, not a return to the form.
         */
        _acctRecover = (event) => {
            if (event && event.preventDefault) event.preventDefault();
            const { email, recoveryInput, password, resetEmailSent } = this.state.acct;
            if (this.state.acct.busy) return;
            this._setAcct({ busy: true, error: "" });
            if (EMAIL_RESET && !resetEmailSent) {
                accountApi.startPasswordReset(email)
                    .then(() => this._setAcct({ busy: false, resetEmailSent: true, error: "" }))
                    .catch((err) => this._setAcct({ busy: false, error: err.message }));
                return;
            }
            if (EMAIL_RESET) {
                accountApi.finishPasswordReset(recoveryInput, password)
                    .then((res) => {
                        if (res.status === "needs_sign_in_code") {
                            this._setAcct({
                                busy: false, password: "", recoveryInput: "", error: "",
                                mode: "signin", resetEmailSent: false, emailCode: "",
                                signInVerification: res.verification, verificationDestination: res.destination,
                            });
                            return;
                        }
                        this._setAcct({
                            busy: false, password: "", recoveryInput: "", error: "",
                            mode: "signin", resetEmailSent: false, user: res.user,
                        });
                        this._resumeAccountReturn(res.user, res.recovery_code);
                        this._loadKeys(res.user.id);
                    })
                    .catch((err) => this._setAcct({ busy: false, error: err.message }));
                return;
            }
            accountApi.recover(email, recoveryInput, password)
                .then(({ recovery_code }) => this._setAcct({
                    busy: false, password: "", recoveryInput: "", error: "",
                    mode: "signin", recoveryCode: recovery_code, recoverySaved: false,
                    user: null, keys: [], freshKey: "", freshKeyCopied: false,
                }))
                .catch((err) => this._setAcct({ busy: false, error: err.message }));
        };

        _acctCopyRecovery = () => {
            const code = this.state.acct.recoveryCode;
            if (!code || !navigator.clipboard) return;
            navigator.clipboard.writeText(code)
                .then(() => this._setAcct({ recoverySaved: true }))
                .catch(() => { /* the code is on screen either way */ });
        };

        /** Dismiss the panel. Only reachable once the code has been shown. */
        _acctAckRecovery = () => { this._setAcct({ recoveryCode: "", recoverySaved: true }); this._resumeAccountReturn(this.state.acct.user); };

        _acctDownloadRecovery = () => {
            const { recoveryCode, user } = this.state.acct;
            if (!recoveryCode) return;
            downloadRecoveryCode(recoveryCode, user ? user.email : "");
            this._setAcct({ recoverySaved: true });
        };

        /** Open and close the "replace my code" form. */
        _acctToggleRotate = () => this._setAcct({
            rotating: !this.state.acct.rotating, rotatePassword: "", error: "",
        });
        _acctSetRotatePassword = (e) => this._setAcct({ rotatePassword: e.target.value, error: "" });

        /** Mint a fresh code for someone already signed in. */
        _acctRotate = (event) => {
            if (event && event.preventDefault) event.preventDefault();
            const { rotatePassword } = this.state.acct;
            this._setAcct({ busy: true, error: "" });
            accountApi.rotateRecovery(rotatePassword)
                .then(({ recovery_code }) => this._setAcct({
                    busy: false, rotating: false, rotatePassword: "", error: "",
                    recoveryCode: recovery_code, recoverySaved: false,
                }))
                .catch((err) => this._setAcct({ busy: false, error: err.message }));
        };

        _acctNewKey = () => {
            if (this._keyRequest || this.state.acct.busy || this.state.acct.freshKey) return;
            this._keyRequest = true;
            this._keysRequestId = (this._keysRequestId || 0) + 1;
            this._setAcct({ busy: true, keysLoading: false, error: "" });
            accountApi.createKey(defaultKeyLabel(this.state.acct.keys))
                .then(({ key, record }) => this._setAcct(a => ({ freshKey: key, freshKeyCopied: false, keys: [record, ...a.keys] })))
                .catch((err) => this._setAcct({ error: err.message }))
                .finally(() => { this._keyRequest = false; this._setAcct({ busy: false }); });
        };

        _acctCopyKey = () => {
            const key = this.state.acct.freshKey;
            if (!key) return;
            if (!navigator.clipboard) { this._setAcct({ error: "Copy is unavailable. Select the key and copy it manually." }); return; }
            navigator.clipboard.writeText(key)
                .then(() => this._setAcct({ freshKeyCopied: true, error: "" }))
                .catch(() => this._setAcct({ error: "Copy is unavailable. Select the key and copy it manually." }));
        };

        _acctAckKey = () => this._setAcct({ freshKey: "", freshKeyCopied: false });

        _acctRevoke = (keyId) => {
            if (this._keyRequest || this.state.acct.busy || this.state.acct.keys.find(k => k.key_id === keyId)?.revoked) return;
            this._keyRequest = true;
            this._keysRequestId = (this._keysRequestId || 0) + 1;
            this._setAcct({ busy: true, keysLoading: false, error: "" });
            accountApi.revokeKey(keyId)
                .then(() => this._setAcct(a => ({ keys: a.keys.map(k => k.key_id === keyId ? { ...k, revoked: true } : k) })))
                .catch((err) => this._setAcct({ error: err.message }))
                .finally(() => { this._keyRequest = false; this._setAcct({ busy: false }); });
        };

        // Social sign-in. accountApi.signInWithSocial redirects the tab to the
        // provider when Clerk is configured, and rejects with a plain message
        // on the local-auth build — where SOCIAL_SIGN_IN is empty and no skin
        // renders the buttons in the first place.
        _acctSocial = (provider) => {
            this._setAcct({ busy: true, error: "" });
            accountApi.signInWithSocial(provider)
                .catch((err) => this._setAcct({ busy: false, error: err.message }));
        };

        _acctSignOut = () => {
            if (this.state.acct.busy) return;
            this._setAcct({ busy: true, error: "" });
            accountApi.logout()
                .then(() => {
                    rememberAccountHint(false);
                    clearAccountReturn();
                    this._keysRequestId = (this._keysRequestId || 0) + 1;
                    this._setAcct({ ...initialAccountState, accountHint: false, resolved: true });
                })
                .catch(() => this._setAcct({ busy: false, error: "You could not be signed out. Check your connection and try again." }));
        };

        _acctDelete = (confirmed = false) => {
            if (this.state.acct.busy || this._deleteRequest || !this.state.acct.user) return;
            if (confirmed !== true && !this.state.acct.confirmingDelete) {
                this._setAcct({ confirmingDelete: true });
                return;
            }
            const accountId = this.state.acct.user.id;
            const request = {}; this._deleteRequest = request;
            const isCurrent = () => !this._accountUnmounted && this._deleteRequest === request && this.state.acct.user?.id === accountId;
            this._setAcct({ busy: true, error: "" });
            const run = EMAIL_RESET ? this._accountSecurityRunner : action => action();
            const perform = async () => {
                if (!run) throw new AccountSecurityError("verification_incomplete");
                return run(() => {
                    if (!isCurrent()) throw new AccountSecurityError("account_changed");
                    return accountApi.deleteAccount(accountId);
                });
            };
            perform()
                .then(result => {
                    // Clerk may have already published the expected signed-out state.
                    if (this._accountUnmounted || (this.state.acct.user && this.state.acct.user.id !== accountId)) return;
                    rememberAccountHint(false);
                    clearAccountReturn();
                    this._keysRequestId = (this._keysRequestId || 0) + 1;
                    this._setAcct({ ...initialAccountState, accountHint: false, resolved: true });
                    if (result.cleanupPending) toast.warning(ACCOUNT_CLEANUP_PENDING, { duration: 15_000 });
                    else toast.success("Your account has been deleted.");
                })
                .catch(err => { if (isCurrent()) this._setAcct({ error: securityErrorMessage(err, "Your account could not be deleted. Complete the identity check and try again."), confirmingDelete: false }); })
                .finally(() => {
                    if (this._deleteRequest === request) {
                        this._deleteRequest = null;
                        if (!this._accountUnmounted && (!this.state.acct.user || this.state.acct.user.id === accountId)) this._setAcct({ busy: false });
                    }
                });
        };

        _acctGo = (e) => {
            if (e && e.preventDefault) e.preventDefault();
            config.navigate();
        };

        /**
         * The design's router does not know this route and would title it 404.
         *
         * The three designs name this differently — Aurora and Structured have
         * `titleFor(route, param)` returning a string, Carbon has
         * `setTitle(route, sub)` writing document.title itself — so both are
         * covered rather than assuming one shape.
         */
        _acctTitle() {
            const path = accountPathForLocation();
            const title = path === "/account/settings" ? "Account settings" : path === "/account/keys" && this.state.acct.user ? "API keys" : this.state.acct.user ? "Account" : this.state.acct.mode === "signup" ? "Create an account" : this.state.acct.mode === "recover" ? "Reset your password" : "Sign in";
            return title + " — PrivaTools";
        }

        titleFor(route, param) {
            if (this._isAccountActive()) return this._acctTitle();
            return super.titleFor ? super.titleFor(route, param) : document.title;
        }

        setTitle(route, sub) {
            if (this._isAccountActive()) {
                try { document.title = this._acctTitle(); } catch (e) { /* SSR */ }
                return;
            }
            if (super.setTitle) super.setTitle(route, sub);
        }

        renderVals() {
            const v = super.renderVals();
            const a = this.state.acct;
            const active = this._isAccountActive();
            const signedIn = Boolean(a.user);
            const p = config.palette;

            const item = config.navItem({
                label: signedIn || a.accountHint ? "Account" : "Sign in",
                icon: signedIn ? "account_circle" : "login",
                onClick: this._acctGo,
                active,
            });

            const nav = config.injectNav
                ? config.injectNav(v, item)
                : { [config.navKey]: mergeNavItem(v[config.navKey] ?? [], item) };

            return {
                ...v,
                ...nav,
                // Whatever the theme's own router resolved for this unknown
                // path has to be switched off, or its page renders underneath
                // ours. Usually that is the 404 — but Structured's route table
                // treats "/" as a prefix pattern, so every unmatched path
                // resolves to `home` instead.
                ...(active
                    ? Object.fromEntries((config.suppressFlags ?? ["is404"]).map((f) => [f, false]))
                    : {}),

                isAccount: active,
                isAccountSettings: active && accountPathForLocation() === "/account/settings",
                isAccountKeys: active && accountPathForLocation() === "/account/keys",
                acctUser: a.user,
                acctNavigationSignedIn: signedIn || a.accountHint,
                acctSettingsHref: "/account/settings",
                acctKeysHref: "/account/keys",
                acctTitle: signedIn ? "Account" : (
                    a.mode === "signup" ? "Create an account"
                        : a.mode === "recover" ? "Reset your password" : "Sign in"),
                acctLede: signedIn
                    ? "Manage the API keys issued to this account."
                    : "Only needed for the developer API. Every tool works without one.",

                acctSignedOut: !signedIn,
                acctSignedIn: signedIn,

                acctEmail: a.email,
                acctPassword: a.password,
                acctSetEmail: (e) => this._setAcct({ email: e.target.value, error: "" }),
                acctSetPassword: (e) => this._setAcct({ password: e.target.value, error: "" }),
                acctSubmit: a.needsEmailCode ? this._acctVerifyEmail : this._acctSubmit,
                acctBusy: a.busy,
                acctBusyOpacity: a.busy ? ".6" : "1",
                acctSubmitLabel: a.busy ? "Working…" : (
                    a.needsEmailCode ? "Verify email"
                        : a.mode === "signup" ? "Create account"
                            : a.mode === "recover" ? "Reset password" : "Sign in"),
                acctPasswordLabel: a.mode === "recover" ? "New password" : "Password",
                acctPwAutocomplete: a.mode === "signin" ? "current-password" : "new-password",
                acctHintD: (a.mode === "signup" && !a.needsEmailCode) ? "block" : "none",
                acctPasswordHint: `At least ${MIN_PASSWORD_LENGTH} characters. Length is what makes a password strong.`,
                acctCopyStorage: ACCOUNT_COPY.storage,
                acctError: a.error,
                acctErrD: a.error ? "block" : "none",

                acctShowSignIn: () => this._acctChooseMode("signin"),
                acctShowSignUp: () => this._acctChooseMode("signup"),
                acctSignInBd: a.mode === "signin" ? p.accent : p.line,
                acctSignInBg: a.mode === "signin" ? p.accentSoft : "transparent",
                acctSignInFg: a.mode === "signin" ? p.accent : p.dim,
                acctSignUpBd: a.mode === "signup" ? p.accent : p.line,
                acctSignUpBg: a.mode === "signup" ? p.accentSoft : "transparent",
                acctSignUpFg: a.mode === "signup" ? p.accent : p.dim,

                acctEmailShown: a.user ? a.user.email : "",
                acctSignOut: this._acctSignOut,
                acctDelete: this._acctDelete,
                acctDeleteLabel: a.confirmingDelete ? "Press again to delete for good" : "Delete account",

                // The code is shown once, and the panel deliberately stands in
                // front of everything else until it is acknowledged — a code
                // scrolled past is an account that will eventually be lost.
                acctRecoveryCode: a.recoveryCode,
                acctRecoveryD: a.recoveryCode ? "block" : "none",
                acctBodyD: a.recoveryCode ? "none" : "block",
                acctCopyRecovery: this._acctCopyRecovery,
                acctCopyLabel: a.recoverySaved ? "Copied" : "Copy",
                acctAckRecovery: this._acctAckRecovery,
                acctDownloadRecovery: this._acctDownloadRecovery,

                // Replacing a mislaid code, from inside a signed-in session.
                acctRotating: a.rotating,
                acctRotateFormD: a.rotating ? "block" : "none",
                acctRotateOpenD: a.rotating ? "none" : "block",
                acctToggleRotate: this._acctToggleRotate,
                acctRotatePassword: a.rotatePassword,
                acctSetRotatePassword: this._acctSetRotatePassword,
                acctRotateSubmit: this._acctRotate,
                acctRotateLabel: a.busy ? "Working…" : "Generate",
                acctRecoveryNudgeD: a.recoverySaved ? "none" : "block",

                // The email-code step reuses the recovery-code input rather than
                // adding a field. The skins' markup comes from their design
                // sources and cannot grow one, but this input is already the
                // right shape — a short code, one-time-code autocomplete, in
                // the same form — so it is retargeted instead of duplicated.
                acctRecoverD: (a.mode === "recover" || a.needsEmailCode) ? "block" : "none",
                acctCredsD: a.mode === "recover" ? "none" : "block",
                acctCodeLabel: a.needsEmailCode ? "Emailed code" : "Recovery code",
                acctRecoveryInput: a.needsEmailCode ? a.emailCode : a.recoveryInput,
                acctSetRecoveryInput: (e) => this._setAcct(
                    a.needsEmailCode
                        ? { emailCode: e.target.value, error: "" }
                        : { recoveryInput: e.target.value, error: "" },
                ),
                acctRecoverSubmit: this._acctRecover,
                acctShowRecover: () => this._setAcct({ mode: "recover", error: "", password: "", recoveryInput: "", emailCode: "", resetEmailSent: false }),
                acctRecoverLabel: a.busy ? "Working…" : "Reset password",
                acctVerifyEmail: this._acctVerifyEmail,

                acctNewKey: this._acctNewKey,
                acctNewKeyValue: a.freshKey,
                acctNewKeyD: a.freshKey ? "block" : "none",
                acctNoKeysD: a.keys.length === 0 ? "block" : "none",
                acctKeys: a.keys.map((k) => ({
                    label: k.label,
                    labelColor: k.revoked ? p.faint : p.text,
                    meta: describeKey(k),
                    revoked: k.revoked,
                    revokeD: k.revoked ? "none" : "inline-flex",
                    revoke: () => this._acctRevoke(k.key_id),
                })),
            };
        }
    };
}