Реализована проверка версионности и требование soft\force обновлений приложения
This commit is contained in:
Binary file not shown.
@@ -10,9 +10,11 @@ import { AuthOtp } from './pages/AuthOtp';
|
||||
import { AuthLoading } from './pages/AuthLoading';
|
||||
import { AuthConsent } from './pages/AuthConsent';
|
||||
import { Root } from './pages/Root';
|
||||
import { UpdateProvider } from './contexts/UpdateContext';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<UpdateProvider>
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Root />}>
|
||||
@@ -30,5 +32,6 @@ export default function App() {
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</UpdateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { ArrowUpCircle, AlertTriangle, X } from "lucide-react";
|
||||
import { useUpdate } from "../contexts/UpdateContext";
|
||||
|
||||
export function UpdateModal() {
|
||||
const { config, dismissUpdate } = useUpdate();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const isStrict = config.type === "strict";
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (config.updateUrl) {
|
||||
window.open(config.updateUrl, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
// В strict-режиме не закрываем — пользователь обязан обновиться
|
||||
if (!isStrict) {
|
||||
dismissUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open && !isStrict) {
|
||||
dismissUpdate();
|
||||
}
|
||||
// В strict-режиме игнорируем попытки закрыть
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={true} onOpenChange={handleOpenChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
{/* Оверлей: в strict-режиме pointer-events-none отключаем клик по фону */}
|
||||
<DialogPrimitive.Overlay
|
||||
className={[
|
||||
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
isStrict ? "pointer-events-none" : "",
|
||||
].join(" ")}
|
||||
/>
|
||||
|
||||
<DialogPrimitive.Content
|
||||
// В strict-режиме блокируем закрытие по Escape и клику вне
|
||||
onEscapeKeyDown={(e) => isStrict && e.preventDefault()}
|
||||
onPointerDownOutside={(e) => isStrict && e.preventDefault()}
|
||||
onInteractOutside={(e) => isStrict && e.preventDefault()}
|
||||
className={[
|
||||
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
|
||||
"w-full max-w-[360px] rounded-2xl border border-border bg-background shadow-2xl",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"focus:outline-none",
|
||||
].join(" ")}
|
||||
>
|
||||
{/* Иконка и шапка */}
|
||||
<div
|
||||
className={[
|
||||
"flex flex-col items-center gap-3 rounded-t-2xl px-6 pt-7 pb-5",
|
||||
isStrict
|
||||
? "bg-destructive/10"
|
||||
: "bg-primary/5",
|
||||
].join(" ")}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
"flex items-center justify-center w-14 h-14 rounded-full",
|
||||
isStrict
|
||||
? "bg-destructive/15 text-destructive"
|
||||
: "bg-primary/10 text-primary",
|
||||
].join(" ")}
|
||||
>
|
||||
{isStrict ? (
|
||||
<AlertTriangle className="w-7 h-7" strokeWidth={2} />
|
||||
) : (
|
||||
<ArrowUpCircle className="w-7 h-7" strokeWidth={2} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<DialogPrimitive.Title className="text-lg font-semibold text-foreground leading-tight">
|
||||
{isStrict ? "Обновление обязательно" : "Доступно обновление"}
|
||||
</DialogPrimitive.Title>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Версия{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{config.newVersion}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Тело */}
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
<DialogPrimitive.Description className="text-sm text-muted-foreground leading-relaxed text-center">
|
||||
{isStrict ? (
|
||||
<>
|
||||
Версия <strong>{config.currentVersion}</strong> больше не
|
||||
поддерживается. Для продолжения работы необходимо установить
|
||||
обновление.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Вышла новая версия приложения. Вы можете обновить его сейчас
|
||||
или сделать это позже.
|
||||
</>
|
||||
)}
|
||||
</DialogPrimitive.Description>
|
||||
|
||||
{config.releaseNotes && (
|
||||
<div className="rounded-xl bg-muted/60 px-4 py-3 text-xs text-muted-foreground leading-relaxed">
|
||||
{config.releaseNotes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Плашка текущей / новой версии */}
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="rounded-md bg-muted px-2 py-1">
|
||||
{config.currentVersion}
|
||||
</span>
|
||||
<span>→</span>
|
||||
<span
|
||||
className={[
|
||||
"rounded-md px-2 py-1 font-medium",
|
||||
isStrict
|
||||
? "bg-destructive/10 text-destructive"
|
||||
: "bg-primary/10 text-primary",
|
||||
].join(" ")}
|
||||
>
|
||||
{config.newVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопки */}
|
||||
<div className="px-6 pb-6 flex flex-col gap-2">
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className={[
|
||||
"w-full rounded-xl py-3 text-sm font-semibold transition-opacity active:opacity-80",
|
||||
isStrict
|
||||
? "bg-destructive text-destructive-foreground"
|
||||
: "bg-primary text-primary-foreground",
|
||||
].join(" ")}
|
||||
>
|
||||
Обновить приложение
|
||||
</button>
|
||||
|
||||
{!isStrict && (
|
||||
<button
|
||||
onClick={dismissUpdate}
|
||||
className="w-full rounded-xl py-3 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60"
|
||||
>
|
||||
Позже
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Крестик только в soft-режиме */}
|
||||
{!isStrict && (
|
||||
<DialogPrimitive.Close
|
||||
onClick={dismissUpdate}
|
||||
className="absolute right-4 top-4 rounded-full p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from "react";
|
||||
|
||||
export type UpdateType = "soft" | "strict";
|
||||
|
||||
export interface UpdateConfig {
|
||||
type: UpdateType;
|
||||
currentVersion: string;
|
||||
newVersion: string;
|
||||
updateUrl?: string;
|
||||
releaseNotes?: string;
|
||||
}
|
||||
|
||||
interface UpdateContextValue {
|
||||
config: UpdateConfig | null;
|
||||
showUpdate: (cfg: UpdateConfig) => void;
|
||||
dismissUpdate: () => void;
|
||||
}
|
||||
|
||||
const UpdateContext = createContext<UpdateContextValue | null>(null);
|
||||
|
||||
export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
const [config, setConfig] = useState<UpdateConfig | null>(null);
|
||||
|
||||
const showUpdate = useCallback((cfg: UpdateConfig) => {
|
||||
setConfig(cfg);
|
||||
}, []);
|
||||
|
||||
const dismissUpdate = useCallback(() => {
|
||||
setConfig(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UpdateContext.Provider value={{ config, showUpdate, dismissUpdate }}>
|
||||
{children}
|
||||
</UpdateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdate() {
|
||||
const ctx = useContext(UpdateContext);
|
||||
if (!ctx) throw new Error("useUpdate must be used inside UpdateProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -3,13 +3,44 @@ import { Notifications } from '../components/Notifications';
|
||||
import { PopularQuestions } from '../components/PopularQuestions';
|
||||
import { ChatInput } from '../components/ChatInput';
|
||||
import { QuickActions } from '../components/QuickActions';
|
||||
import { useUpdate } from '../contexts/UpdateContext';
|
||||
|
||||
export function Home() {
|
||||
const { showUpdate } = useUpdate();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Компактный логотип HAN */}
|
||||
<HanLogo />
|
||||
|
||||
{/* Demo: триггеры для тестирования диалогов обновления */}
|
||||
<div className="px-4 py-2 flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
showUpdate({
|
||||
type: "soft",
|
||||
currentVersion: "2.4.1",
|
||||
newVersion: "2.5.0",
|
||||
})
|
||||
}
|
||||
className="flex-1 rounded-xl border border-border bg-muted/40 py-2 text-xs font-medium text-muted-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
Soft update
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
showUpdate({
|
||||
type: "strict",
|
||||
currentVersion: "1.8.0",
|
||||
newVersion: "2.5.0",
|
||||
})
|
||||
}
|
||||
className="flex-1 rounded-xl border border-destructive/30 bg-destructive/5 py-2 text-xs font-medium text-destructive hover:bg-destructive/10 transition-colors"
|
||||
>
|
||||
Strict update
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Уведомления от компании */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Notifications />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Outlet, useLocation } from 'react-router';
|
||||
import { Header } from '../components/Header';
|
||||
import { UpdateModal } from '../components/UpdateModal';
|
||||
|
||||
export function Root() {
|
||||
const location = useLocation();
|
||||
@@ -14,6 +15,8 @@ export function Root() {
|
||||
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
<UpdateModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user