"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" && }
);
}