"use client"; import { createContext, useContext, useEffect, useState } from "react"; import { init, mountMiniAppSync, mountThemeParamsSync, bindThemeParamsCssVars, mountViewport, bindViewportCssVars, unmountViewport, } from "@telegram-apps/sdk-react"; import { fixSurfaceContrast, useTelegramTheme } from "@/hooks/use-telegram-theme"; import { applyAndroidPerformanceClass } from "@/lib/telegram-android-perf"; import { useAppStore } from "@/store/app-store"; import { getLang } from "@/i18n/messages"; import { useTranslation } from "@/i18n/use-translation"; const EVENT_CONFIG_LOADED = "dt-config-loaded"; export interface TelegramSdkContextValue { /** True after init() and sync mounts have run; safe to use backButton etc. */ sdkReady: boolean; } const TelegramSdkContext = createContext({ sdkReady: false, }); export function useTelegramSdkReady(): TelegramSdkContextValue { return useContext(TelegramSdkContext); } /** * Wraps the app with Telegram Mini App SDK initialization. * Calls init(acceptCustomStyles), mounts theme params (binds --tg-theme-* CSS vars), * mounts the mini app, then mounts viewport and binds viewport CSS vars * (--tg-viewport-stable-height, --tg-viewport-content-safe-area-inset-*, etc.). * Does not call ready() here — the app calls callMiniAppReadyOnce() from * lib/telegram-ready when the first visible screen has finished loading. * Theme is set before first paint by the inline script in layout.tsx (URL hash); * useTelegramTheme() in the app handles ongoing theme changes. * Syncs lang from window.__DT_LANG on mount and when config.js fires dt-config-loaded. */ /** * Live theme sync: provider-owned so every route (/, /admin, not-found, error) gets * data-theme and Mini App chrome color updates when Telegram theme changes. */ function ThemeSync() { useTelegramTheme(); return null; } export function TelegramProvider({ children, }: { children: React.ReactNode; }) { const [sdkReady, setSdkReady] = useState(false); const setLang = useAppStore((s) => s.setLang); const lang = useAppStore((s) => s.lang); const { t } = useTranslation(); // Sync lang from backend config: on mount and when config.js has loaded (all routes, including admin). useEffect(() => { if (typeof window === "undefined") return; setLang(getLang()); const onConfigLoaded = () => setLang(getLang()); window.addEventListener(EVENT_CONFIG_LOADED, onConfigLoaded); return () => window.removeEventListener(EVENT_CONFIG_LOADED, onConfigLoaded); }, [setLang]); // Apply lang to document (title and html lang) so all routes including admin get correct title. useEffect(() => { if (typeof document === "undefined") return; document.documentElement.lang = lang; document.title = t("app.title"); }, [lang, t]); useEffect(() => { const cleanup = init({ acceptCustomStyles: true }); if (mountThemeParamsSync.isAvailable()) { mountThemeParamsSync(); } if (bindThemeParamsCssVars.isAvailable()) { bindThemeParamsCssVars(); } fixSurfaceContrast(); void document.documentElement.offsetHeight; if (mountMiniAppSync.isAvailable()) { mountMiniAppSync(); } applyAndroidPerformanceClass(); setSdkReady(true); let unbindViewportCssVars: (() => void) | undefined; if (mountViewport.isAvailable()) { mountViewport() .then(() => { if (bindViewportCssVars.isAvailable()) { unbindViewportCssVars = bindViewportCssVars(); } }) .catch(() => { // Viewport not supported (e.g. not in Mini App); ignore. }); } return () => { setSdkReady(false); unbindViewportCssVars?.(); unmountViewport(); cleanup(); }; }, []); return ( {children} ); }