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.
This commit is contained in:
60
webapp-next/src/lib/telegram-link.test.ts
Normal file
60
webapp-next/src/lib/telegram-link.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Unit tests for openTelegramProfile: Mini App–friendly Telegram link opening.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const openTelegramLinkMock = vi.fn();
|
||||
const isAvailableFn = vi.fn().mockReturnValue(true);
|
||||
|
||||
vi.mock("@telegram-apps/sdk-react", () => ({
|
||||
openTelegramLink: Object.assign(openTelegramLinkMock, {
|
||||
isAvailable: () => isAvailableFn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("openTelegramProfile", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isAvailableFn.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("calls openTelegramLink with t.me URL when SDK is available", async () => {
|
||||
const { openTelegramProfile } = await import("./telegram-link");
|
||||
|
||||
openTelegramProfile("alice");
|
||||
expect(openTelegramLinkMock).toHaveBeenCalledWith("https://t.me/alice");
|
||||
});
|
||||
|
||||
it("strips leading @ from username", async () => {
|
||||
const { openTelegramProfile } = await import("./telegram-link");
|
||||
|
||||
openTelegramProfile("@bob");
|
||||
expect(openTelegramLinkMock).toHaveBeenCalledWith("https://t.me/bob");
|
||||
});
|
||||
|
||||
it("does nothing when username is empty", async () => {
|
||||
const { openTelegramProfile } = await import("./telegram-link");
|
||||
|
||||
openTelegramProfile("");
|
||||
openTelegramProfile(null);
|
||||
openTelegramProfile(undefined);
|
||||
expect(openTelegramLinkMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to window.open when SDK is unavailable", async () => {
|
||||
isAvailableFn.mockReturnValue(false);
|
||||
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
vi.resetModules();
|
||||
const { openTelegramProfile } = await import("./telegram-link");
|
||||
|
||||
openTelegramProfile("alice");
|
||||
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
"https://t.me/alice",
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
28
webapp-next/src/lib/telegram-link.ts
Normal file
28
webapp-next/src/lib/telegram-link.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Opens a Telegram profile (t.me/username) in a Mini App–friendly way.
|
||||
* Uses SDK openTelegramLink() when available so the link opens inside Telegram;
|
||||
* otherwise falls back to window.open.
|
||||
*/
|
||||
|
||||
import { openTelegramLink } from "@telegram-apps/sdk-react";
|
||||
|
||||
/**
|
||||
* Opens the given Telegram username profile. When running inside the Mini App,
|
||||
* uses openTelegramLink() so the link opens in-app; otherwise opens in a new tab.
|
||||
* Safe to call from click handlers; does not throw.
|
||||
*/
|
||||
export function openTelegramProfile(username: string | null | undefined): void {
|
||||
const clean = username ? String(username).trim().replace(/^@+/, "") : "";
|
||||
if (!clean) return;
|
||||
if (typeof window === "undefined") return;
|
||||
const url = `https://t.me/${encodeURIComponent(clean)}`;
|
||||
try {
|
||||
if (openTelegramLink.isAvailable()) {
|
||||
openTelegramLink(url);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// SDK not available or not in Mini App context.
|
||||
}
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
7
webapp-next/src/lib/theme-bootstrap-script.ts
Normal file
7
webapp-next/src/lib/theme-bootstrap-script.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Inline script for first-paint theme: hash (tgWebAppColorScheme + themeParams),
|
||||
* then Telegram WebApp, then CSS --tg-color-scheme, then prefers-color-scheme.
|
||||
* Sets data-theme and Mini App bg/header colors. Shared by layout and global-error.
|
||||
*/
|
||||
export const THEME_BOOTSTRAP_SCRIPT =
|
||||
"(function(){var scheme='dark';var root=document.documentElement;var hash=typeof location!=='undefined'&&location.hash?location.hash.slice(1):'';if(hash){var params={};hash.split('&').forEach(function(p){var i=p.indexOf('=');if(i===-1)return;var k=decodeURIComponent(p.slice(0,i));var v=p.slice(i+1);params[k]=v;});if(params.tgWebAppColorScheme==='light'||params.tgWebAppColorScheme==='dark')scheme=params.tgWebAppColorScheme;if(params.tgWebAppThemeParams){try{var theme=JSON.parse(decodeURIComponent(params.tgWebAppThemeParams));var tgKeys=['bg_color','text_color','hint_color','link_color','button_color','button_text_color','secondary_bg_color','header_bg_color','accent_text_color','destructive_text_color','section_bg_color','section_header_text_color','section_separator_color','subtitle_text_color','bottom_bar_bg_color'];tgKeys.forEach(function(k){var v=theme[k];if(v&&typeof v==='string')root.style.setProperty('--tg-theme-'+k.replace(/_/g,'-'),v);});Object.keys(theme).forEach(function(k){var v=theme[k];if(v&&typeof v==='string')root.style.setProperty('--tg-theme-'+k.replace(/_/g,'-'),v);});}catch(e){}}}var twa=typeof window!=='undefined'&&window.Telegram&&window.Telegram.WebApp;if(scheme!=='light'&&scheme!=='dark'&&twa&&(twa.colorScheme==='light'||twa.colorScheme==='dark'))scheme=twa.colorScheme;if(scheme!=='light'&&scheme!=='dark'){try{var s=getComputedStyle(root).getPropertyValue('--tg-color-scheme').trim();if(s==='light'||s==='dark')scheme=s;}catch(e){} }if(scheme!=='light'&&scheme!=='dark')scheme=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';root.setAttribute('data-theme',scheme);if(twa){if(twa.setBackgroundColor)twa.setBackgroundColor('bg_color');if(twa.setHeaderColor)twa.setHeaderColor('bg_color');}})();";
|
||||
Reference in New Issue
Block a user