Files
duty-teller/webapp-next/src/components/providers/TelegramProvider.tsx
Nikolay Tatarinov 40e2b5adc4 feat: enhance theme handling and layout components for Telegram Mini App
- Updated theme resolution logic to utilize a shared inline script for consistent theme application across routes.
- Introduced `AppShell` and `ReadyGate` components to manage app readiness and theme synchronization, improving user experience.
- Enhanced `GlobalError` and `NotFound` pages with a unified full-screen layout for better accessibility and visual consistency.
- Refactored CSS to implement safe area insets for sticky headers and content safety, ensuring proper layout on various devices.
- Added unit tests for new functionality and improved existing tests for better coverage and reliability.
2026-03-06 16:48:24 +03:00

128 lines
3.9 KiB
TypeScript

"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<TelegramSdkContextValue>({
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 (
<TelegramSdkContext.Provider value={{ sdkReady }}>
<ThemeSync />
{children}
</TelegramSdkContext.Provider>
);
}