"use client"; import { useEffect, useState } from "react"; import api, { getErrorMessage } from "@/lib/api/client"; import { createDevMockWebApp, isDevMockWebApp, waitForTelegramWebApp, type TelegramWebApp, } from "@/lib/telegram/webapp"; import { startTelegramThemeSync } from "@/lib/telegram/theme"; import { TgWebAppProvider } from "@/lib/telegram/hooks"; import { TgTabBar, type TgTab } from "@/components/tg/tg-tab-bar"; import { TgApprovalsTab } from "@/components/tg/tg-approvals-tab"; import { TgInboxTab } from "@/components/tg/tg-inbox-tab"; import { TgBoardTab } from "@/components/tg/tg-board-tab"; import { TgChatTab } from "@/components/tg/tg-chat-tab"; import { Loader2, AlertTriangle, ExternalLink } from "lucide-react"; type BootstrapState = | { kind: "validating" } | { kind: "ready"; webApp: TelegramWebApp } | { kind: "not_in_telegram" } | { kind: "error"; message: string }; function CenteredMessage({ children }: { children: React.ReactNode }) { return (
{children}
); } /** * `/tg` — the CEO's phone cockpit. On mount: resolve the Telegram WebApp * bridge, then POST its initData to the auth route unconditionally (the * route is idempotent — it just re-mints the session cookie on every call) * before rendering the tabbed cockpit. There's no way to read the resulting * httponly session cookie client-side to skip this on a warm reload, so it * always runs; it's cheap and the backend contract says so explicitly. * * Outside Telegram, a development build falls back to the dev mock bridge * (skipping the auth POST — the regular panel session cookie authorizes the * API calls) so the shell is workable in a plain browser; production keeps * the "Open from Telegram" wall. */ export default function TelegramMiniAppPage() { const [state, setState] = useState({ kind: "validating" }); const [tab, setTab] = useState("approvals"); useEffect(() => { let cancelled = false; void (async () => { let webApp = await waitForTelegramWebApp(); if (cancelled) return; if (!webApp && process.env.NODE_ENV === "development") { webApp = createDevMockWebApp(); } if (!webApp) { setState({ kind: "not_in_telegram" }); return; } webApp.ready(); webApp.expand(); webApp.disableVerticalSwipes?.(); if (isDevMockWebApp(webApp)) { setState({ kind: "ready", webApp }); return; } try { await api.post("/telegram/webapp-auth", { init_data: webApp.initData ?? "", }); if (!cancelled) setState({ kind: "ready", webApp }); } catch (err) { if (!cancelled) { setState({ kind: "error", message: getErrorMessage(err) }); } } })(); return () => { cancelled = true; }; }, []); // Adopt the user's Telegram palette for the whole shell (and track live // theme switches). Scoped to #tg-shell so the desktop dashboard is // untouched; the dev mock carries empty themeParams, so a dev browser // keeps the panel's own theme. useEffect(() => { if (state.kind !== "ready") return; const shell = document.getElementById("tg-shell"); if (!shell) return; return startTelegramThemeSync(state.webApp, shell); }, [state]); if (state.kind === "validating") { return (

Connecting…

); } if (state.kind === "not_in_telegram") { return (

Open from Telegram

This cockpit only runs inside Telegram. Open it from the bot's menu button.

); } if (state.kind === "error") { return (

Couldn't sign in

{state.message}

); } return (
{tab === "approvals" && } {tab === "inbox" && } {tab === "board" && } {tab === "chat" && }
); }