mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* feat(web): in-app PWA update prompt when new service worker is available (closes #938) User-controlled reload with a persistent banner, visibility-triggered SW checks, and an expandable rationale. Switches registerType to prompt. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align vite.config with soup layers for clean driver merge Keeps registerType prompt while matching garden IWER stubs and PWA share_target shape expected by feat/pwa-share-target in the manifest. Co-authored-by: Cursor <cursoragent@cursor.com> * Revert "fix(web): align vite.config with soup layers for clean driver merge" This reverts commit 6f0915b0884d029a2413d8819a4dfe81d7c4e595. * fix(web): make PWA reload apply waiting service worker updates Handle SKIP_WAITING in injectManifest sw.ts and reload via controllerchange with a timed fallback when vite-plugin-pwa prompt mode does not navigate. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): satisfy setTimeout mock typing in PWA reload tests Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): register PWA service worker before auth gates Mount PwaUpdateProvider at app root and show the update banner on login and error screens so registerSW runs for logged-out users too. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): offset PWA update banner below top status banners Reserve top-12 when syncing or reconnecting so the reload prompt stays visible above SyncingBanner and ReconnectingBanner. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): offset PWA update banner below voice error banner Use PwaUpdateBannerWithStatusOffset inside VoiceProvider so voice errors share the same top-12 reservation as sync and reconnect banners. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
import React from 'react'
|
|
import ReactDOM from 'react-dom/client'
|
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
import { RouterProvider, createMemoryHistory } from '@tanstack/react-router'
|
|
import './index.css'
|
|
import { initializeFontScale } from '@/hooks/useFontScale'
|
|
import { getTelegramWebApp, isTelegramEnvironment, loadTelegramSdk } from './hooks/useTelegram'
|
|
import { queryClient } from './lib/query-client'
|
|
import { createAppRouter } from './router'
|
|
import { I18nProvider } from './lib/i18n-context'
|
|
import { restoreSpaRedirect } from './lib/spaRedirect'
|
|
import { installScrollRestorationGuard } from './lib/scrollStorageGuard'
|
|
|
|
function getStartParam(): string | null {
|
|
const query = new URLSearchParams(window.location.search)
|
|
const fromQuery = query.get('startapp') || query.get('tgWebAppStartParam')
|
|
if (fromQuery) return fromQuery
|
|
|
|
return getTelegramWebApp()?.initDataUnsafe?.start_param ?? null
|
|
}
|
|
|
|
function getDeepLinkedSessionId(): string | null {
|
|
const startParam = getStartParam()
|
|
if (startParam?.startsWith('session_')) {
|
|
return startParam.slice('session_'.length)
|
|
}
|
|
return null
|
|
}
|
|
|
|
function getInitialPath(): string {
|
|
const sessionId = getDeepLinkedSessionId()
|
|
return sessionId ? `/sessions/${sessionId}` : '/sessions'
|
|
}
|
|
|
|
async function bootstrap() {
|
|
installScrollRestorationGuard()
|
|
initializeFontScale()
|
|
|
|
// Only load Telegram SDK in Telegram environment (with 3s timeout)
|
|
const isTelegram = isTelegramEnvironment()
|
|
document.documentElement.dataset.telegramApp = isTelegram ? 'true' : 'false'
|
|
if (isTelegram) {
|
|
await loadTelegramSdk()
|
|
}
|
|
|
|
// Handle GitHub Pages 404 redirect for SPA routing
|
|
// When GitHub Pages can't find a path (e.g. /sessions/xxx), it serves 404.html
|
|
// which stores the path in sessionStorage and redirects to /
|
|
if (!isTelegram) {
|
|
restoreSpaRedirect()
|
|
}
|
|
|
|
const history = isTelegram
|
|
? createMemoryHistory({ initialEntries: [getInitialPath()] })
|
|
: undefined
|
|
const router = createAppRouter(history)
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
<React.StrictMode>
|
|
<I18nProvider>
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} />
|
|
{import.meta.env.DEV ? <ReactQueryDevtools initialIsOpen={false} /> : null}
|
|
</QueryClientProvider>
|
|
</I18nProvider>
|
|
</React.StrictMode>
|
|
)
|
|
}
|
|
|
|
bootstrap()
|