mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): implement conditional Telegram SDK loading
Add lazy loading of Telegram SDK only when running inside Telegram Mini App environment instead of unconditionally loading in all browsers. Includes: - New environment detection functions: isTelegramEnvironment(), isTelegramApp() - Dynamic SDK loading with 3s timeout via loadTelegramSdk() - Removed static script tag from index.html - Made haptic feedback lazy (SDK check on each call) - Made theme listeners lazy (attached in initializeTheme()) - Updated all components to use unified detection functions This prevents SDK load time overhead in regular browser environments while ensuring the app works correctly both in Telegram and browser contexts.
This commit is contained in:
@@ -33,7 +33,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { getTelegramWebApp } from '@/hooks/useTelegram'
|
||||
import { getTelegramWebApp, isTelegramApp } from '@/hooks/useTelegram'
|
||||
import { initializeTheme } from '@/hooks/useTheme'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { useAuthSource } from '@/hooks/useAuthSource'
|
||||
@@ -138,7 +138,7 @@ export function App() {
|
||||
// Navigate and sync browser history (browser only, not Telegram)
|
||||
const navigateTo = useCallback((newScreen: Screen) => {
|
||||
setScreen(newScreen)
|
||||
if (!getTelegramWebApp()?.initData) {
|
||||
if (!isTelegramApp()) {
|
||||
history.pushState({ screen: newScreen }, '')
|
||||
}
|
||||
}, [])
|
||||
@@ -209,7 +209,7 @@ export function App() {
|
||||
|
||||
// Handle browser back button (browser only, not Telegram)
|
||||
useEffect(() => {
|
||||
if (getTelegramWebApp()?.initData) return
|
||||
if (isTelegramApp()) return
|
||||
|
||||
const handlePopState = (event: PopStateEvent) => {
|
||||
if (event.state?.screen) {
|
||||
@@ -227,7 +227,7 @@ export function App() {
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
// Browser: use native back (triggers popstate)
|
||||
if (!getTelegramWebApp()?.initData) {
|
||||
if (!isTelegramApp()) {
|
||||
history.back()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { Session } from '@/types/api'
|
||||
import { getTelegramWebApp } from '@/hooks/useTelegram'
|
||||
import { isTelegramApp } from '@/hooks/useTelegram'
|
||||
|
||||
function getSessionTitle(session: Session): string {
|
||||
if (session.metadata?.name) {
|
||||
@@ -20,11 +20,10 @@ export function SessionHeader(props: {
|
||||
session: Session
|
||||
onBack: () => void
|
||||
}) {
|
||||
const isTelegram = getTelegramWebApp() !== null
|
||||
const title = useMemo(() => getSessionTitle(props.session), [props.session])
|
||||
|
||||
// In Telegram, don't render header (Telegram provides its own)
|
||||
if (isTelegram) {
|
||||
if (isTelegramApp()) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { getTelegramWebApp } from './useTelegram'
|
||||
import { getTelegramWebApp, isTelegramEnvironment } from './useTelegram'
|
||||
import type { AuthSource } from './useAuth'
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'hapi_access_token'
|
||||
@@ -77,15 +77,8 @@ export function useAuthSource(): {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're likely in a Telegram environment before polling
|
||||
// Only use URL params (tgWebApp) as reliable indicator
|
||||
const hasTelegramHint =
|
||||
typeof window !== 'undefined' && (
|
||||
window.location.search.includes('tgWebApp') ||
|
||||
window.location.hash.includes('tgWebApp')
|
||||
)
|
||||
|
||||
if (!hasTelegramHint) {
|
||||
// Check if we're in a Telegram environment before polling
|
||||
if (!isTelegramEnvironment()) {
|
||||
// Plain browser - show login prompt immediately
|
||||
setIsLoading(false)
|
||||
return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { getTelegramWebApp } from './useTelegram'
|
||||
import { getTelegramWebApp, isTelegramApp } from './useTelegram'
|
||||
|
||||
export type HapticStyle = 'light' | 'medium' | 'heavy' | 'rigid' | 'soft'
|
||||
export type HapticNotification = 'error' | 'success' | 'warning'
|
||||
@@ -20,62 +20,53 @@ export type Platform = {
|
||||
haptic: PlatformHaptic
|
||||
}
|
||||
|
||||
function createHaptic(): PlatformHaptic {
|
||||
const tg = getTelegramWebApp()
|
||||
// Vibration patterns for web fallback (in ms)
|
||||
const vibrationPatterns = {
|
||||
light: 10,
|
||||
medium: 20,
|
||||
heavy: 30,
|
||||
rigid: 15,
|
||||
soft: 10,
|
||||
success: 20,
|
||||
warning: [20, 50, 20] as number | number[],
|
||||
error: [30, 50, 30] as number | number[],
|
||||
selection: 5,
|
||||
}
|
||||
|
||||
// Vibration patterns for web fallback (in ms)
|
||||
const vibrationPatterns = {
|
||||
light: 10,
|
||||
medium: 20,
|
||||
heavy: 30,
|
||||
rigid: 15,
|
||||
soft: 10,
|
||||
success: 20,
|
||||
warning: [20, 50, 20] as number | number[],
|
||||
error: [30, 50, 30] as number | number[],
|
||||
selection: 5,
|
||||
}
|
||||
function vibrate(pattern: number | number[]) {
|
||||
navigator.vibrate?.(pattern)
|
||||
}
|
||||
|
||||
const vibrate = (pattern: number | number[]) => {
|
||||
navigator.vibrate?.(pattern)
|
||||
}
|
||||
|
||||
return {
|
||||
impact: (style: HapticStyle) => {
|
||||
if (tg?.HapticFeedback) {
|
||||
tg.HapticFeedback.impactOccurred(style)
|
||||
} else {
|
||||
vibrate(vibrationPatterns[style])
|
||||
}
|
||||
},
|
||||
notification: (type: HapticNotification) => {
|
||||
if (tg?.HapticFeedback) {
|
||||
tg.HapticFeedback.notificationOccurred(type)
|
||||
} else {
|
||||
vibrate(vibrationPatterns[type])
|
||||
}
|
||||
},
|
||||
selection: () => {
|
||||
if (tg?.HapticFeedback) {
|
||||
tg.HapticFeedback.selectionChanged()
|
||||
} else {
|
||||
vibrate(vibrationPatterns.selection)
|
||||
}
|
||||
// Lazy haptic - checks for Telegram SDK on each call
|
||||
const haptic: PlatformHaptic = {
|
||||
impact: (style: HapticStyle) => {
|
||||
const tg = getTelegramWebApp()
|
||||
if (tg?.HapticFeedback) {
|
||||
tg.HapticFeedback.impactOccurred(style)
|
||||
} else {
|
||||
vibrate(vibrationPatterns[style])
|
||||
}
|
||||
},
|
||||
notification: (type: HapticNotification) => {
|
||||
const tg = getTelegramWebApp()
|
||||
if (tg?.HapticFeedback) {
|
||||
tg.HapticFeedback.notificationOccurred(type)
|
||||
} else {
|
||||
vibrate(vibrationPatterns[type])
|
||||
}
|
||||
},
|
||||
selection: () => {
|
||||
const tg = getTelegramWebApp()
|
||||
if (tg?.HapticFeedback) {
|
||||
tg.HapticFeedback.selectionChanged()
|
||||
} else {
|
||||
vibrate(vibrationPatterns.selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton haptic instance (functions are stable)
|
||||
const haptic = createHaptic()
|
||||
|
||||
function checkIsTelegram(): boolean {
|
||||
const tg = getTelegramWebApp()
|
||||
// SDK is always loaded, but initData is only present in actual Telegram environment
|
||||
return tg !== null && Boolean(tg.initData)
|
||||
}
|
||||
|
||||
export function usePlatform(): Platform {
|
||||
const isTelegram = useMemo(() => checkIsTelegram(), [])
|
||||
const isTelegram = useMemo(() => isTelegramApp(), [])
|
||||
|
||||
return {
|
||||
isTelegram,
|
||||
@@ -86,7 +77,7 @@ export function usePlatform(): Platform {
|
||||
// Non-hook version for use outside React components
|
||||
export function getPlatform(): Platform {
|
||||
return {
|
||||
isTelegram: checkIsTelegram(),
|
||||
isTelegram: isTelegramApp(),
|
||||
haptic
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
/**
|
||||
* Detects if the current environment is Telegram Mini App
|
||||
* by checking URL hash/query parameters that Telegram passes.
|
||||
* This works BEFORE the SDK is loaded.
|
||||
*/
|
||||
export function isTelegramEnvironment(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
|
||||
// Telegram passes launch params via window.location.hash
|
||||
// Format: #tgWebAppVersion=...&tgWebAppData=...&tgWebAppPlatform=...
|
||||
const hash = window.location.hash.slice(1)
|
||||
const hashParams = new URLSearchParams(hash)
|
||||
|
||||
// Primary detection: check hash parameters
|
||||
if (hashParams.has('tgWebAppVersion') || hashParams.has('tgWebAppData')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback: check query parameters (alternative flow)
|
||||
const search = window.location.search
|
||||
if (search.includes('tgWebApp') || search.includes('initData')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export type TelegramWebAppThemeParams = {
|
||||
bg_color?: string
|
||||
text_color?: string
|
||||
@@ -75,3 +102,43 @@ declare global {
|
||||
export function getTelegramWebApp(): TelegramWebApp | null {
|
||||
return window.Telegram?.WebApp ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running inside a real Telegram Mini App.
|
||||
* Requires SDK to be loaded. Returns true only if initData is present.
|
||||
*/
|
||||
export function isTelegramApp(): boolean {
|
||||
const tg = getTelegramWebApp()
|
||||
return tg !== null && Boolean(tg.initData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically loads the Telegram Web App SDK with timeout.
|
||||
* Only call this if isTelegramEnvironment() returns true.
|
||||
*/
|
||||
export function loadTelegramSdk(timeoutMs = 3000): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (window.Telegram?.WebApp) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let settled = false
|
||||
const settle = () => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout - don't block app indefinitely
|
||||
setTimeout(settle, timeoutMs)
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://telegram.org/js/telegram-web-app.js'
|
||||
script.async = true
|
||||
script.onload = settle
|
||||
script.onerror = settle
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
+17
-14
@@ -53,19 +53,8 @@ function updateScheme(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme on module load
|
||||
applyTheme(currentScheme)
|
||||
|
||||
// Listen for theme changes
|
||||
const tg = getTelegramWebApp()
|
||||
if (tg?.onEvent) {
|
||||
// Telegram theme changes
|
||||
tg.onEvent('themeChanged', updateScheme)
|
||||
} else if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
// Browser system preference changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
mediaQuery.addEventListener('change', updateScheme)
|
||||
}
|
||||
// Track if theme listeners have been set up
|
||||
let listenersInitialized = false
|
||||
|
||||
export function useTheme(): { colorScheme: ColorScheme; isDark: boolean } {
|
||||
const colorScheme = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
||||
@@ -76,8 +65,22 @@ export function useTheme(): { colorScheme: ColorScheme; isDark: boolean } {
|
||||
}
|
||||
}
|
||||
|
||||
// Call this once at app startup to ensure theme is applied
|
||||
// Call this once at app startup to ensure theme is applied and listeners attached
|
||||
export function initializeTheme(): void {
|
||||
currentScheme = getColorScheme()
|
||||
applyTheme(currentScheme)
|
||||
|
||||
// Set up listeners only once (after SDK may have loaded)
|
||||
if (!listenersInitialized) {
|
||||
listenersInitialized = true
|
||||
const tg = getTelegramWebApp()
|
||||
if (tg?.onEvent) {
|
||||
// Telegram theme changes
|
||||
tg.onEvent('themeChanged', updateScheme)
|
||||
} else if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
// Browser system preference changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
mediaQuery.addEventListener('change', updateScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-22
@@ -3,28 +3,38 @@ import ReactDOM from 'react-dom/client'
|
||||
import { App } from './App'
|
||||
import './index.css'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { isTelegramEnvironment, loadTelegramSdk } from './hooks/useTelegram'
|
||||
|
||||
registerSW({
|
||||
immediate: true,
|
||||
onOfflineReady() {
|
||||
console.log('App ready for offline use')
|
||||
},
|
||||
onRegistered(registration) {
|
||||
if (registration) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
registration.update()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
onRegisterError(error) {
|
||||
console.error('SW registration error:', error)
|
||||
async function bootstrap() {
|
||||
// Only load Telegram SDK in Telegram environment (with 3s timeout)
|
||||
if (isTelegramEnvironment()) {
|
||||
await loadTelegramSdk()
|
||||
}
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
registerSW({
|
||||
immediate: true,
|
||||
onOfflineReady() {
|
||||
console.log('App ready for offline use')
|
||||
},
|
||||
onRegistered(registration) {
|
||||
if (registration) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
registration.update()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
onRegisterError(error) {
|
||||
console.error('SW registration error:', error)
|
||||
}
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
Reference in New Issue
Block a user