mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): in-app PWA update prompt when new service worker is available (#946)
* 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>
This commit is contained in:
+31
-14
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import { Outlet, useLocation, useMatchRoute, useRouter } from '@tanstack/react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { getTelegramWebApp, isTelegramApp } from '@/hooks/useTelegram'
|
||||
@@ -23,11 +23,13 @@ import { getAppGlobalSseSubscription, getAppSessionSseSubscription } from '@/lib
|
||||
import { LoginPrompt } from '@/components/LoginPrompt'
|
||||
import { InstallPrompt } from '@/components/InstallPrompt'
|
||||
import { OfflineBanner } from '@/components/OfflineBanner'
|
||||
import { PwaUpdateBanner, PwaUpdateBannerWithStatusOffset } from '@/components/PwaUpdateBanner'
|
||||
import { SyncingBanner } from '@/components/SyncingBanner'
|
||||
import { ReconnectingBanner } from '@/components/ReconnectingBanner'
|
||||
import { VoiceErrorBanner } from '@/components/VoiceErrorBanner'
|
||||
import { LoadingState } from '@/components/LoadingState'
|
||||
import { ToastContainer } from '@/components/ToastContainer'
|
||||
import { PwaUpdateProvider } from '@/lib/pwa-update-context'
|
||||
import { ToastProvider, useToast } from '@/lib/toast-context'
|
||||
import type { SyncEvent } from '@/types/api'
|
||||
|
||||
@@ -35,10 +37,21 @@ type ToastEvent = Extract<SyncEvent, { type: 'toast' }>
|
||||
|
||||
const REQUIRE_SERVER_URL = requireHubUrlForLogin()
|
||||
|
||||
function withPwaBanner(content: ReactNode) {
|
||||
return (
|
||||
<>
|
||||
<PwaUpdateBanner />
|
||||
{content}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<AppInner />
|
||||
<PwaUpdateProvider>
|
||||
<AppInner />
|
||||
</PwaUpdateProvider>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
@@ -341,16 +354,16 @@ function AppInner() {
|
||||
|
||||
// Loading auth source
|
||||
if (isAuthSourceLoading) {
|
||||
return (
|
||||
return withPwaBanner(
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<LoadingState label={t('loading')} className="text-sm" />
|
||||
</div>
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
|
||||
// No auth source (browser environment, not logged in)
|
||||
if (!authSource) {
|
||||
return (
|
||||
return withPwaBanner(
|
||||
<LoginPrompt
|
||||
onLogin={setAccessToken}
|
||||
baseUrl={baseUrl}
|
||||
@@ -358,12 +371,12 @@ function AppInner() {
|
||||
setServerUrl={setServerUrl}
|
||||
clearServerUrl={clearServerUrl}
|
||||
requireServerUrl={REQUIRE_SERVER_URL}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
if (needsBinding) {
|
||||
return (
|
||||
return withPwaBanner(
|
||||
<LoginPrompt
|
||||
mode="bind"
|
||||
onBind={bind}
|
||||
@@ -373,16 +386,16 @@ function AppInner() {
|
||||
clearServerUrl={clearServerUrl}
|
||||
requireServerUrl={REQUIRE_SERVER_URL}
|
||||
error={authError ?? undefined}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
// Authenticating (also covers the gap before useAuth effect starts)
|
||||
if (isAuthLoading || (authSource && !token && !authError)) {
|
||||
return (
|
||||
return withPwaBanner(
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<LoadingState label={t('authorizing')} className="text-sm" />
|
||||
</div>
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -390,7 +403,7 @@ function AppInner() {
|
||||
if (authError || !token || !api) {
|
||||
// If using access token and auth failed, show login again
|
||||
if (authSource.type === 'accessToken') {
|
||||
return (
|
||||
return withPwaBanner(
|
||||
<LoginPrompt
|
||||
onLogin={setAccessToken}
|
||||
baseUrl={baseUrl}
|
||||
@@ -399,12 +412,12 @@ function AppInner() {
|
||||
clearServerUrl={clearServerUrl}
|
||||
requireServerUrl={REQUIRE_SERVER_URL}
|
||||
error={authError ?? t('login.error.authFailed')}
|
||||
/>
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
// Telegram auth failed
|
||||
return (
|
||||
return withPwaBanner(
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="text-base font-semibold">{t('login.title')}</div>
|
||||
<div className="text-sm text-red-600">
|
||||
@@ -413,13 +426,17 @@ function AppInner() {
|
||||
<div className="text-xs text-[var(--app-hint)]">
|
||||
Open this page from Telegram using the bot's "Open App" button (not "Open in browser").
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContextProvider value={{ api, token, baseUrl }}>
|
||||
<VoiceProvider>
|
||||
<PwaUpdateBannerWithStatusOffset
|
||||
isSyncing={isSyncing}
|
||||
isReconnecting={sseDisconnected && !isSyncing}
|
||||
/>
|
||||
<SyncingBanner isSyncing={isSyncing} />
|
||||
<ReconnectingBanner
|
||||
isReconnecting={sseDisconnected && !isSyncing}
|
||||
|
||||
Reference in New Issue
Block a user