import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button, ConfirmDialog, DataTableView, StatGrid, Tabs, usePaneHeaderTabs, type DataTableCell, type DataTableColumn, type StatItem, } from "../../../components"; import { useAppSelector, usePaneAppConfig } from "../../../state/app/context"; import { useChartQueries, useFxRatesMap } from "../../../market-data/hooks"; import { buildPortfolioFinancialsMap } from "../../../market-data/portfolio-financials"; import { useLiveTickerFinancialsMap, useSampledValue } from "../../../state/hooks/live-ticker-financials"; import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map"; import { blendHex, colors } from "../../../theme/colors"; import type { PaneProps } from "../../../types/plugin"; import { Box, ScrollBox, Text, Textarea, TextAttributes, useRendererHost, useUiCapabilities, type BoxRenderable, type ScrollBoxRenderable, type TextareaRenderable, } from "../../../ui"; import { useDialog, type AlertContext, type PromptContext } from "../../../ui/dialog"; import type { SelectControl } from "../../../components/ui/select-button"; import { apiClient, type AccountProfile, type CloudPricing } from "../../../api-client"; import { chatController } from "../chat/controller"; import { SignInWall } from "../cloud/auth-actions"; import { TeamsAccountTab } from "../cloud/team/acm-tab"; import { afterLayout, revealInScrollBox } from "../cloud/reveal-in-scroll-box"; import { AccountTextField, CheckboxRow, FieldRow, PublicAnalyticsGroup, accountFieldLabelWidth, } from "./form-components"; import { NO_PORTFOLIO_VALUE, buildPublishedProfileAnalyticsPreview, buildProfileAnalyticsPreview, buildPortfolioChoices, computeCumulativeReturn, countPortfolioHoldings, emptyToNull, formatCloudMonthlyPrice, formatPlan, formatTrialEnd, formatTrialOffer, getPortfolioPositionTickers, portfolioOptionIds, profileToDraft, type AccountDraft, type AccountFieldKey, type PlanPriceDisplay, } from "./model"; import { PasswordChangeDialog } from "./password-dialog"; import { useAccountManagementFooter } from "./footer"; import { useAccountManagementKeyboard } from "./keyboard"; import { buildTrackedCurrencies } from "../analytics/sector-model"; import { buildBenchmarkReturnSeries, buildPortfolioChartTargets, buildPortfolioReturnSeries, } from "../analytics/pane-model"; import { computeDatedBeta } from "../analytics/metrics"; import { useCloudSyncStatus } from "../../../sync/react"; import { cloudSyncController } from "../../../sync/controller"; import { setSyncedProfileAnalytics } from "../../../sync/profile-analytics"; import { t, tf } from "../../../i18n"; import { useAppLanguage } from "../../../i18n/react"; import { consumeRequestedAccountManagementTab, subscribeRequestedAccountManagementTab, type AccountManagementTab, } from "./navigation"; import { openCloudUpgrade } from "../shared/cloud-upgrade"; import { resolvePlanAccess } from "../shared/plan-access"; import { usePluginPaneState } from "../../runtime"; type AccountBusy = "profile" | "password" | "alerts" | "billing" | "delete" | null; const ACCOUNT_TAB_DEFS: Array<{ label: string; value: AccountManagementTab }> = [ { label: "Profile", value: "profile" }, { label: "Emails", value: "emails" }, { label: "Pro", value: "pro" }, { label: "Teams", value: "teams" }, { label: "Advanced", value: "advanced" }, ]; const ACCOUNT_TAB_FIELD_ORDER: Record = { profile: [ "profilePublic", "acceptUnknownDms", "username", "name", "company", "title", "publicEmail", "xAccount", "bio", "sharedPortfolioId", ], emails: [ "chatEmailNotificationsEnabled", "weeklyRoundupEnabled", "positionAlertsEnabled", "emailAlertsOffAction", ], pro: ["upgradeAction"], // The Teams tab owns its own keyboard handling (a list, not form fields). teams: [], advanced: ["passwordAction", "deleteAccountAction"], }; const PROFILE_PREVIEW_SAMPLE_MS = 10_000; const PLAN_COMPARISON_ROWS = [ { capability: "US equities", free: "15m delay", pro: "Real-time", proTone: "positive" }, { capability: "Options data", free: "15m delay", pro: "Real-time", proTone: "positive" }, { capability: "News", free: "12h delay", pro: "Real-time wire", proTone: "positive" }, { capability: "AI command bar", free: "Included", pro: "Included", proTone: "neutral" }, { capability: "Cloud sync", free: "Included", pro: "Included", proTone: "neutral" }, { capability: "X data", free: "No", pro: "Included", proTone: "positive" }, { capability: "AI Screener", free: "No", pro: "Soon", proTone: "muted" }, ] as const; type PlanColumn = DataTableColumn & { id: "capability" | "free" | "pro" }; type PlanRow = typeof PLAN_COMPARISON_ROWS[number]; /** Free against Pro, one row per capability, with the Pro column tinted. */ function PlanComparison({ width, activePlan, }: { width: number; activePlan: "free" | "pro"; }) { const valueWidth = Math.max(10, Math.min(16, Math.floor((width - 16) / 2))); const columns = useMemo(() => [ { id: "capability", label: t("Capability"), width: 14, align: "left", flexGrow: 1 }, { id: "free", label: t("Free"), width: valueWidth, align: "left" }, { id: "pro", label: t("Pro"), width: valueWidth, align: "left" }, ], [valueWidth]); const proCellBg = blendHex(colors.panel, colors.selected, activePlan === "pro" ? 0.42 : 0.26); const renderCell = useCallback((row: PlanRow, column: PlanColumn): DataTableCell => { if (column.id === "capability") return { text: t(row.capability), color: colors.textDim }; if (column.id === "free") return { text: t(row.free), color: activePlan === "free" ? colors.text : colors.textDim }; return { text: t(row.pro), backgroundColor: proCellBg, color: row.proTone === "positive" ? colors.positive : row.proTone === "muted" ? colors.textMuted : colors.text, attributes: row.proTone === "positive" ? TextAttributes.BOLD : 0, }; }, [activePlan, proCellBg]); const height = PLAN_COMPARISON_ROWS.length + 1; return ( columns={columns} items={[...PLAN_COMPARISON_ROWS]} selection={{ kind: "none" }} rootWidth={width} rootHeight={height} virtualize={false} sortColumnId={null} sortDirection="asc" getItemKey={(row) => row.capability} renderCell={renderCell} emptyStateTitle="" /> ); } /** * Reloads the profile whenever the account identity *or* its entitlement changes, * so a completed checkout shows the new plan as soon as the session refreshes. */ function buildAccountSessionMarker(): string { const snapshot = chatController.getSnapshot(); const user = apiClient.getCurrentUser(); return [ apiClient.getSessionToken() ?? "", snapshot.user?.id ?? "", snapshot.user?.username ?? "", user?.effectivePlan ?? user?.plan ?? "", user?.trialEndsAt ?? "", ].join(":"); } export function AccountManagementPane({ focused, width, height }: PaneProps) { const language = useAppLanguage(); const dialog = useDialog(); const renderer = useRendererHost(); const { nativePaneChrome } = useUiCapabilities(); const config = usePaneAppConfig(); const portfolios = config.portfolios; const baseCurrency = config.baseCurrency; const tickers = useAppSelector((state) => state.tickers); const cachedFinancials = useAppSelector((state) => state.financials); const cachedExchangeRates = useAppSelector((state) => state.exchangeRates); const [sessionMarker, setSessionMarker] = useState(() => buildAccountSessionMarker()); const [hasSession, setHasSession] = useState(() => apiClient.isSignedIn()); const [profile, setProfile] = useState(null); const [pricing, setPricing] = useState(null); const [draft, setDraft] = useState(() => profileToDraft(null)); const [activeTab, setActiveTab] = usePluginPaneState("activeTab", "profile"); const [activeField, setActiveField] = useState( () => ACCOUNT_TAB_FIELD_ORDER[activeTab][0] ?? "username", ); const [message, setMessage] = useState<{ tone: "info" | "success" | "error"; text: string } | null>(null); const [busy, setBusy] = useState(null); // A tab requested before the pane mounted wins over the restored one, but only once. useEffect(() => { const requested = consumeRequestedAccountManagementTab(); if (!requested) return; setActiveTab(requested); setActiveField(ACCOUNT_TAB_FIELD_ORDER[requested][0] ?? "username"); }, []); useEffect(() => subscribeRequestedAccountManagementTab((tab) => { setActiveTab(tab); setActiveField(ACCOUNT_TAB_FIELD_ORDER[tab][0] ?? "username"); }), []); const syncStatus = useCloudSyncStatus(); const bioRef = useRef(null); const portfolioSelectRef = useRef(null); const refreshedSyncRevisionRef = useRef(null); const draftRef = useRef(draft); draftRef.current = draft; const formWidth = Math.max(24, Math.min(70, width - 2)); const contentWidth = formWidth; const twoColumns = formWidth >= 60; const fieldWidth = twoColumns ? Math.max(22, Math.floor((formWidth - 3) / 2)) : Math.max(18, Math.min(46, formWidth - 2)); const formLabelWidth = accountFieldLabelWidth(formWidth); const bodyHeight = Math.max(5, height); const fieldOrder = ACCOUNT_TAB_FIELD_ORDER[activeTab]; const accountTabs = ACCOUNT_TAB_DEFS.map((tab) => ({ ...tab, label: t(tab.label) })); const planAccess = useMemo(() => resolvePlanAccess(profile), [profile]); const planPrice = useMemo(() => formatCloudMonthlyPrice(pricing), [language, pricing]); const trialOffer = useMemo(() => formatTrialOffer(pricing), [language, pricing]); const planStatusLabel = useMemo(() => { const trialEnd = planAccess.isTrialActive ? formatTrialEnd(planAccess.trialEndsAt) : null; return trialEnd ? tf("Pro trial — ends {date}", { date: trialEnd }) : formatPlan(profile?.plan); }, [language, planAccess.isTrialActive, planAccess.trialEndsAt, profile?.plan]); const planStats = useMemo(() => { const priceDetail = planPrice.anchor ? [planPrice.note, tf("list {price}", { price: planPrice.anchor })].filter(Boolean).join(", ") : planPrice.note ?? undefined; return [ { id: "plan", label: "Plan", value: planStatusLabel, tone: planAccess.hasProAccess ? "positive" : "neutral", detail: profile?.email ?? undefined, }, { id: "price", label: "Price", value: planPrice.price, detail: priceDetail }, ...(planAccess.hasProAccess ? [] : [{ id: "trial", label: "Trial", value: trialOffer }]), ]; }, [planAccess.hasProAccess, planPrice, planStatusLabel, profile?.email, trialOffer]); const portfolioHoldingCounts = useMemo(() => countPortfolioHoldings(tickers), [tickers]); const portfolioChoices = useMemo( () => buildPortfolioChoices(portfolios, portfolioHoldingCounts), [language, portfolioHoldingCounts, portfolios], ); const selectedAnalyticsPortfolio = useMemo( () => portfolios.find((portfolio) => portfolio.id === draft.sharedPortfolioId) ?? null, [draft.sharedPortfolioId, portfolios], ); // Only the Profile tab renders the shared-portfolio preview, so the quote, // FX, and chart requests below stay off on every other tab. const portfolioTickers = useMemo( () => activeTab === "profile" && draft.sharedPortfolioId ? getPortfolioPositionTickers(tickers, draft.sharedPortfolioId) : [], [activeTab, draft.sharedPortfolioId, tickers], ); const instrumentOptions = useMemo(() => ({ portfolioId: draft.sharedPortfolioId || undefined, }), [draft.sharedPortfolioId]); // The positions stream in the background (about once a second each) and // merge with the portfolio pane's own subscriptions. const marketFinancials = useLiveTickerFinancialsMap(portfolioTickers, { surface: "portfolio", visible: false, weight: 10, instrumentOptions, }); const liveFinancials = useMemo( () => buildPortfolioFinancialsMap(portfolioTickers, cachedFinancials, marketFinancials, instrumentOptions), [portfolioTickers, cachedFinancials, marketFinancials, instrumentOptions], ); // The preview is a 1Y return and a beta: position values only weight daily // returns, so price ticks need not recompute them. const financials = useSampledValue( liveFinancials, PROFILE_PREVIEW_SAMPLE_MS, `${draft.sharedPortfolioId ?? ""}\u001f${[...liveFinancials.keys()].join(",")}`, ); const trackedCurrencies = useMemo( () => buildTrackedCurrencies(portfolioTickers, financials, baseCurrency), [baseCurrency, financials, portfolioTickers], ); const fetchedExchangeRates = useFxRatesMap(trackedCurrencies); const effectiveExchangeRates = selectEffectiveExchangeRates(fetchedExchangeRates, cachedExchangeRates); const chartTargets = useMemo( () => buildPortfolioChartTargets(portfolioTickers, instrumentOptions), [portfolioTickers, instrumentOptions], ); const chartRequests = useMemo( () => chartTargets.flatMap((target) => target.request ? [target.request] : []), [chartTargets], ); const chartEntries = useChartQueries(chartRequests); const spyRequest = useMemo( () => ({ instrument: { symbol: "SPY", exchange: "" }, bufferRange: "1Y" as const, granularity: "range" as const, }), [], ); const spyChartRequests = useMemo( () => portfolioTickers.length > 0 ? [spyRequest] : [], [portfolioTickers.length, spyRequest], ); const spyChartEntries = useChartQueries(spyChartRequests); const columnContext = useMemo(() => ({ activeTab: draft.sharedPortfolioId || undefined, baseCurrency, exchangeRates: effectiveExchangeRates, now: Date.now(), }), [baseCurrency, draft.sharedPortfolioId, effectiveExchangeRates]); const portfolioReturnSeries = useMemo( () => buildPortfolioReturnSeries({ chartTargets, chartEntries, financials, columnContext, }).returns, [chartEntries, chartTargets, columnContext, financials], ); const spyReturnSeries = useMemo( () => buildBenchmarkReturnSeries(spyRequest, spyChartEntries), [spyChartEntries, spyRequest], ); const oneYearReturn = useMemo( () => portfolioReturnSeries ? computeCumulativeReturn(portfolioReturnSeries) : null, [portfolioReturnSeries], ); const beta = useMemo( () => (portfolioReturnSeries ? computeDatedBeta(portfolioReturnSeries, spyReturnSeries.returns) : null), [portfolioReturnSeries, spyReturnSeries], ); const localAnalyticsPreview = useMemo( () => buildProfileAnalyticsPreview({ beta, portfolio: selectedAnalyticsPortfolio, portfolioTickers, selectedPortfolioId: draft.sharedPortfolioId, oneYearReturn, }), [ beta, draft.sharedPortfolioId, language, portfolioTickers, selectedAnalyticsPortfolio, oneYearReturn, ], ); useEffect(() => { const portfolioId = selectedAnalyticsPortfolio?.id; // Off the Profile tab the preview is computed from no holdings, so publishing // it would overwrite real analytics with blanks. if (!portfolioId || activeTab !== "profile") return; const changed = setSyncedProfileAnalytics(portfolioId, localAnalyticsPreview.publicAnalytics); if (changed) cloudSyncController.schedulePush("profile-analytics"); }, [ activeTab, localAnalyticsPreview.publicAnalytics?.oneYearReturn, localAnalyticsPreview.publicAnalytics?.spyBeta, selectedAnalyticsPortfolio?.id, ]); const publicAnalyticsPreview = useMemo( () => buildPublishedProfileAnalyticsPreview({ analytics: profile?.portfolioAnalytics ?? null, draftProfilePublic: draft.profilePublic, portfolio: selectedAnalyticsPortfolio, profileLoaded: !!profile, savedProfilePublic: profile?.profilePublic === true, savedSharedPortfolioId: profile?.sharedPortfolioId ?? "", selectedPortfolioId: draft.sharedPortfolioId, syncing: syncStatus.phase === "syncing", }), [ draft.profilePublic, draft.sharedPortfolioId, language, profile, selectedAnalyticsPortfolio, syncStatus.phase, ], ); const profileAnalyticsDetail = useMemo(() => { if (!draft.sharedPortfolioId) return t("No public portfolio analytics"); if (!profile) return t("Loading published metrics"); if ( draft.profilePublic !== profile.profilePublic || draft.sharedPortfolioId !== (profile.sharedPortfolioId ?? "") ) { return t("Save profile to update published metrics"); } if (profile.profilePublic !== true) return t("Public profile is off"); if (syncStatus.phase === "syncing") return t("Syncing published metrics"); if (!profile.portfolioAnalytics) return t("Waiting for published metrics"); return t("Published public metrics"); }, [ draft.profilePublic, draft.sharedPortfolioId, language, profile, syncStatus.phase, ]); useEffect(() => { const unsubscribe = chatController.subscribe((snapshot) => { setHasSession(apiClient.isSignedIn() || snapshot.hasSavedSession); setSessionMarker(buildAccountSessionMarker()); }); void chatController.refreshSession().catch(() => {}); return unsubscribe; }, []); const loadProfile = useCallback(async () => { if (!apiClient.isSignedIn()) { setProfile(null); setDraft(profileToDraft(null)); return; } setMessage({ tone: "info", text: t("Loading account profile...") }); try { const nextProfile = await apiClient.getAccountProfile(); setProfile(nextProfile); setDraft(profileToDraft(nextProfile)); setMessage(null); } catch (error) { setMessage({ tone: "error", text: error instanceof Error ? error.message : t("Failed to load account profile."), }); } }, []); useEffect(() => { void loadProfile(); }, [loadProfile, sessionMarker]); useEffect(() => { let cancelled = false; void apiClient.getCloudPricing() .then((nextPricing) => { if (!cancelled) setPricing(nextPricing); }) // Pricing is decoration: the comparison table falls back to list prices. .catch(() => {}); return () => { cancelled = true; }; }, []); useEffect(() => { if (!hasSession || !apiClient.isSignedIn()) return; if (syncStatus.phase !== "synced" || syncStatus.revision == null) return; if (refreshedSyncRevisionRef.current === syncStatus.revision) return; refreshedSyncRevisionRef.current = syncStatus.revision; let cancelled = false; void (async () => { const nextProfile = await apiClient.getAccountProfile().catch(() => null); if (cancelled || !nextProfile) return; setProfile(nextProfile); await chatController.refreshSession().catch(() => {}); })(); return () => { cancelled = true; }; }, [hasSession, syncStatus.phase, syncStatus.revision]); useEffect(() => { if (!fieldOrder.includes(activeField)) { setActiveField(fieldOrder[0] ?? "username"); } }, [activeField, fieldOrder]); const setDraftValue = useCallback((key: K, value: AccountDraft[K]) => { setDraft((current) => ({ ...current, [key]: value })); }, []); const selectTab = useCallback((tab: string) => { const nextTab = tab as AccountManagementTab; setActiveTab(nextTab); setActiveField(ACCOUNT_TAB_FIELD_ORDER[nextTab][0] ?? "username"); }, []); // Signed out, every tab is the same sign-in wall, so the strip stays away. // h/l and the arrows switch tabs; the portfolio picker keeps them while it is // the active field, and text fields keep them for the cursor. const tabsInHeader = usePaneHeaderTabs(hasSession || apiClient.isSignedIn() ? { tabs: accountTabs, activeValue: activeTab, onSelect: selectTab, focused, } : null); // The strip and the gap the column puts under it. const tabRows = tabsInHeader ? 0 : 2; const openPasswordDialog = useCallback(() => { if (busy) return; setActiveField("passwordAction"); void dialog.alert({ closeOnClickOutside: false, content: (context: AlertContext) => ( { setBusy("password"); setMessage({ tone: "info", text: t("Changing password...") }); try { await apiClient.changePassword(currentPassword, newPassword); setMessage({ tone: "success", text: t("Password changed.") }); } catch (error) { setMessage({ tone: "error", text: error instanceof Error ? error.message : t("Failed to change password."), }); throw error; } finally { setBusy(null); } }} /> ), }).catch(() => {}); }, [busy, dialog]); const openPortfolioPicker = useCallback(async () => { setActiveField("sharedPortfolioId"); portfolioSelectRef.current?.open(); }, []); // A field the keyboard moves to can sit below the fold in a short pane. // Pointer moves (a hover over the stats row, a click) leave the scroll alone. const scrollRef = useRef(null); const revealActiveFieldRef = useRef(false); const cycleField = useCallback((delta: number) => { setActiveField((current) => { const index = fieldOrder.indexOf(current); const nextIndex = Math.max(0, Math.min(fieldOrder.length - 1, index + delta)); const next = fieldOrder[nextIndex] ?? "username"; if (next !== current) revealActiveFieldRef.current = true; return next; }); }, [fieldOrder]); const fieldNodes = useRef(new Map()); const fieldNodeRef = useMemo(() => { const refs = new Map void>(); return (key: AccountFieldKey) => { let ref = refs.get(key); if (!ref) { ref = (node: BoxRenderable | null) => { if (node) fieldNodes.current.set(key, node); else fieldNodes.current.delete(key); }; refs.set(key, ref); } return ref; }; }, []); useEffect(() => { if (!revealActiveFieldRef.current) return; revealActiveFieldRef.current = false; return afterLayout(() => revealInScrollBox(scrollRef.current, fieldNodes.current.get(activeField) ?? null)); }, [activeField]); const cyclePortfolio = useCallback((delta: number) => { const optionIds = portfolioOptionIds(portfolios); const currentValue = draftRef.current.sharedPortfolioId || NO_PORTFOLIO_VALUE; const currentIndex = Math.max(0, optionIds.indexOf(currentValue)); const nextIndex = (currentIndex + delta + optionIds.length) % optionIds.length; const nextValue = optionIds[nextIndex] ?? NO_PORTFOLIO_VALUE; setDraftValue("sharedPortfolioId", nextValue === NO_PORTFOLIO_VALUE ? "" : nextValue); }, [portfolios, setDraftValue]); const saveProfile = useCallback(async () => { const current = draftRef.current; const bio = bioRef.current?.editBuffer.getText() ?? current.bio; if (!current.username.trim() || !current.name.trim()) { setMessage({ tone: "error", text: t("Username and full name are required.") }); return; } setBusy("profile"); setMessage({ tone: "info", text: t("Saving account profile...") }); try { const nextProfile = await apiClient.updateAccountProfile({ username: current.username, name: current.name, company: emptyToNull(current.company), title: emptyToNull(current.title), bio: emptyToNull(bio), profilePublic: current.profilePublic, publicEmail: emptyToNull(current.publicEmail), xAccount: emptyToNull(current.xAccount), sharedPortfolioId: emptyToNull(current.sharedPortfolioId), acceptUnknownDms: current.acceptUnknownDms, chatEmailNotificationsEnabled: current.chatEmailNotificationsEnabled, weeklyRoundupEnabled: current.weeklyRoundupEnabled, positionAlertsEnabled: current.positionAlertsEnabled, }); setProfile(nextProfile); setDraft(profileToDraft(nextProfile)); await chatController.refreshSession().catch(() => {}); setMessage({ tone: "success", text: t("Account profile saved.") }); } catch (error) { setMessage({ tone: "error", text: error instanceof Error ? error.message : t("Failed to save account profile."), }); } finally { setBusy(null); } }, []); const turnOffEmailAlerts = useCallback(async () => { setActiveField("emailAlertsOffAction"); setBusy("alerts"); setMessage({ tone: "info", text: t("Turning off email alerts...") }); try { const nextProfile = await apiClient.updateAccountProfile({ chatEmailNotificationsEnabled: false, weeklyRoundupEnabled: false, positionAlertsEnabled: false, }); setDraft((current) => ({ ...current, chatEmailNotificationsEnabled: nextProfile.chatEmailNotificationsEnabled, weeklyRoundupEnabled: nextProfile.weeklyRoundupEnabled, positionAlertsEnabled: nextProfile.positionAlertsEnabled, })); setProfile(nextProfile); setMessage({ tone: "success", text: t("Email alerts are off.") }); } catch (error) { setMessage({ tone: "error", text: error instanceof Error ? error.message : t("Failed to update alert settings."), }); } finally { setBusy(null); } }, []); const openUpgrade = useCallback(() => { setActiveField("upgradeAction"); setBusy("billing"); setMessage({ tone: "info", text: t("Opening Pro upgrade...") }); void openCloudUpgrade(renderer) .then(() => setMessage(null)) .catch((error) => { setMessage({ tone: "error", text: error instanceof Error ? error.message : t("Failed to open upgrade page."), }); }) .finally(() => setBusy(null)); }, [renderer]); const deleteAccount = useCallback(async () => { setActiveField("deleteAccountAction"); if (busy) return; const confirmed = await dialog.prompt({ closeOnClickOutside: false, content: (context: PromptContext) => ( ), }).catch(() => false); if (!confirmed) return; setBusy("delete"); setMessage({ tone: "info", text: t("Deleting account...") }); try { await apiClient.deleteAccount(); setProfile(null); setDraft(profileToDraft(null)); setHasSession(false); await chatController.refreshSession().catch(() => {}); setMessage({ tone: "success", text: t("Account deleted.") }); } catch (error) { setMessage({ tone: "error", text: error instanceof Error ? error.message : t("Failed to delete account."), }); } finally { setBusy(null); } }, [busy, dialog]); useAccountManagementFooter({ busy, hasSession, message, saveProfile, }); useAccountManagementKeyboard({ activeField, cycleField, cyclePortfolio, deleteAccount, draftRef, fieldOrder, // Behind the sign-in wall the fields are not there, and Enter is the wall's. focused: focused && activeTab !== "teams" && (hasSession || apiClient.isSignedIn()), openPasswordDialog, openPortfolioDialog: openPortfolioPicker, openUpgrade, saveProfile, setDraftValue, turnOffEmailAlerts, }); if (!hasSession && !apiClient.isSignedIn()) { return ; } return ( {!tabsInHeader && ( )} {activeTab === "profile" ? ( <> setActiveField("profilePublic")} onChange={(checked) => setDraftValue("profilePublic", checked)} /> setActiveField("acceptUnknownDms")} onChange={(checked) => setDraftValue("acceptUnknownDms", checked)} /> setDraftValue("username", value)} onSubmit={() => { void saveProfile(); }} /> setDraftValue("name", value)} onSubmit={() => { void saveProfile(); }} /> setDraftValue("company", value)} onSubmit={() => { void saveProfile(); }} /> setDraftValue("title", value)} onSubmit={() => { void saveProfile(); }} /> setDraftValue("publicEmail", value)} onSubmit={() => { void saveProfile(); }} /> setDraftValue("xAccount", value)} onSubmit={() => { void saveProfile(); }} /> setActiveField("bio")} > {nativePaneChrome ? t("Bio") : `${activeField === "bio" ? "> " : " "}${t("Bio")}`}