feat(client): PWA install — client route, manifest start_url=/client, SW app-shell, invitation links → /client/:token

This commit is contained in:
Hermes 2026-08-11 13:37:29 +02:00
parent 65b42c8482
commit 15132f56e8
4 changed files with 49 additions and 15 deletions

View File

@ -1,8 +1,8 @@
{ {
"name": "СуперСам Доставка", "name": "СуперСам Доставка",
"short_name": "СуперСам", "short_name": "СуперСам",
"description": "Панель управления доставкой стройматериалов с уведомлениями.", "description": "Согласование доставки заказов и уведомления для клиентов СуперСам.",
"start_url": "/delivery/showcase", "start_url": "/client",
"scope": "/", "scope": "/",
"display": "standalone", "display": "standalone",
"background_color": "#f4f7f5", "background_color": "#f4f7f5",

View File

@ -1,9 +1,9 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1"; const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) { if (!isLocalhost) {
const STATIC_CACHE = "construction-delivery-static-v106"; const STATIC_CACHE = "construction-delivery-static-v107";
const RUNTIME_CACHE = "construction-delivery-runtime-v104"; const RUNTIME_CACHE = "construction-delivery-runtime-v107";
const APP_SHELL_URLS = ["/", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"]; const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(
@ -90,13 +90,27 @@ self.addEventListener("push", (event) => {
data = { title: "Уведомление", body: "" }; data = { title: "Уведомление", body: "" };
} }
const { title, body, url, order_group_id } = data; const { title, body, url } = data;
const targetUrl = (() => {
if (!url) return "/client";
try {
const parsed = new URL(url, self.location.origin);
if (parsed.origin !== self.location.origin) return "/client";
const legacyMatch = parsed.pathname.match(/^\/delivery\/([^/]+)$/);
if (legacyMatch) return `/client/${legacyMatch[1]}${parsed.search}${parsed.hash}`;
return parsed.pathname.startsWith("/client")
? `${parsed.pathname}${parsed.search}${parsed.hash}`
: "/client";
} catch {
return "/client";
}
})();
event.waitUntil( event.waitUntil(
self.registration.showNotification(title || "Уведомление", { self.registration.showNotification(title || "Уведомление", {
body: body || "", body: body || "",
icon: "/icons/icon-512.png", icon: "/icons/icon-512.png",
data: { url: url || "/dashboard" }, data: { url: targetUrl },
}), }),
); );
}); });
@ -105,14 +119,21 @@ self.addEventListener("push", (event) => {
self.addEventListener("notificationclick", (event) => { self.addEventListener("notificationclick", (event) => {
event.notification.close(); event.notification.close();
const targetUrl = (event.notification.data && event.notification.data.url) || "/dashboard"; const targetPath = (event.notification.data && event.notification.data.url) || "/client";
const targetUrl = new URL(targetPath, self.location.origin).href;
event.waitUntil( event.waitUntil(
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(async (clientList) => {
for (const client of clientList) { const sameOriginClient = clientList.find((client) => {
if ("focus" in client) { try {
return client.focus(); return new URL(client.url).origin === self.location.origin;
} catch {
return false;
} }
});
if (sameOriginClient) {
if ("navigate" in sameOriginClient) await sameOriginClient.navigate(targetUrl);
return "focus" in sameOriginClient ? sameOriginClient.focus() : undefined;
} }
return self.clients.openWindow(targetUrl); return self.clients.openWindow(targetUrl);
}), }),

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Navigate, createBrowserRouter, Outlet } from "react-router-dom"; import { Navigate, createBrowserRouter, useParams } from "react-router-dom";
import App from "./App"; import App from "./App";
import { ClientDeliveryPage } from "./pages/ClientDeliveryPage"; import { ClientDeliveryPage } from "./pages/ClientDeliveryPage";
import { DashboardPage } from "./pages/DashboardPage"; import { DashboardPage } from "./pages/DashboardPage";
@ -44,9 +44,17 @@ export const router = createBrowserRouter([
element: <LoginPage />, element: <LoginPage />,
}, },
{ {
path: "delivery/:token", path: "client",
element: <ClientDeliveryPage />, element: <ClientDeliveryPage />,
}, },
{
path: "client/:token",
element: <ClientDeliveryPage />,
},
{
path: "delivery/:token",
element: <LegacyDeliveryRedirect />,
},
{ {
path: "forbidden", path: "forbidden",
element: <ForbiddenPage />, element: <ForbiddenPage />,
@ -82,3 +90,8 @@ export const router = createBrowserRouter([
], ],
}, },
]); ]);
function LegacyDeliveryRedirect() {
const { token } = useParams();
return <Navigate to={`/client/${encodeURIComponent(token || "")}`} replace />;
}

View File

@ -197,7 +197,7 @@ export const resolvePublicAppUrl = (
}; };
export const buildInvitationUrl = (baseUrl: string, token: string) => export const buildInvitationUrl = (baseUrl: string, token: string) =>
`${baseUrl.replace(/\/$/, "")}/delivery/${token}`; `${baseUrl.replace(/\/$/, "")}/client/${token}`;
export type DeliveryInvitationRecord = { export type DeliveryInvitationRecord = {
id?: string; id?: string;