/* 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//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) && }{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), })), }; } }; }