From 89d6a01b68e9186c5f12ea309dc15595396316ae Mon Sep 17 00:00:00 2001 From: root Date: Fri, 22 May 2026 14:10:08 +0000 Subject: [PATCH] feat: add PWA install button to header --- src/components/UI/PwaInstallButton.jsx | 76 ++++++++++++++++++++++++++ src/layouts/AppShell.jsx | 6 ++ src/pages/DashboardPage.jsx | 6 ++ 3 files changed, 88 insertions(+) create mode 100644 src/components/UI/PwaInstallButton.jsx diff --git a/src/components/UI/PwaInstallButton.jsx b/src/components/UI/PwaInstallButton.jsx new file mode 100644 index 0000000..319cdcb --- /dev/null +++ b/src/components/UI/PwaInstallButton.jsx @@ -0,0 +1,76 @@ +import React from "react"; + +/** + * Compact PWA install button for the header. + * - Shows πŸ“² icon when install prompt is available (Chrome/Edge on Android & desktop). + * - Shows iOS Safari instructions tooltip on click when on iOS. + * - Hidden when app is already installed (standalone mode). + * - After install: auto-hidden via appinstalled event. + */ +export const PwaInstallButton = ({ onInstall, isInstalled, isInstallAvailable }) => { + const [showTip, setShowTip] = React.useState(false); + const tipRef = React.useRef(null); + + // Detect iOS (no beforeinstallprompt, but can be added to home screen) + const isIOS = typeof navigator !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; + + const handleInstallClick = async () => { + if (isInstallAvailable && onInstall) { + await onInstall(); + } else { + // Show instruction tip for iOS or unsupported browsers + setShowTip((prev) => !prev); + } + }; + + // Close tip on outside click + React.useEffect(() => { + if (!showTip) return; + const handler = (e) => { + if (tipRef.current && !tipRef.current.contains(e.target)) { + setShowTip(false); + } + }; + document.addEventListener("click", handler); + return () => document.removeEventListener("click", handler); + }, [showTip]); + + // Don't render if already installed as PWA β€” AFTER all hooks + if (isInstalled) return null; + + return ( +
+ + + {showTip && ( +
+ {isIOS ? ( + <> +

Установка Π½Π° iOS

+
    +
  1. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Π² Safari
  2. +
  3. НаТмитС ΠŸΠΎΠ΄Π΅Π»ΠΈΡ‚ΡŒΡΡ ⬆️
  4. +
  5. Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ «На экран Π”ΠΎΠΌΠΎΠΉΒ»
  6. +
+ + ) : ( + <> +

Установка

+

+ НаТмитС Π·Π½Π°Ρ‡ΠΎΠΊ πŸ“² Π² адрСсной строкС Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π° ΠΈΠ»ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ мСню Β«Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅Β». +

+ + )} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/layouts/AppShell.jsx b/src/layouts/AppShell.jsx index d4d5d29..ae1fbd0 100644 --- a/src/layouts/AppShell.jsx +++ b/src/layouts/AppShell.jsx @@ -4,11 +4,15 @@ import { Badge } from "../components/UI/Badge"; import { Button } from "../components/UI/Button"; import { Panel } from "../components/UI/Panel"; import { ThemeToggle } from "../components/UI/ThemeToggle"; +import { PwaInstallButton } from "../components/UI/PwaInstallButton"; import { NotificationBell } from "../components/notifications/NotificationBell"; import { NotificationSettings } from "../components/notifications/NotificationSettings"; export const AppShell = ({ user, + onInstallApp, + isInstalled, + isInstallAvailable, onSignOut, onOpenGuide, isGuideOpen = false, @@ -108,6 +112,7 @@ export const AppShell = ({ {isGuideOpen ? "Назад" : "?"} ) : null} + ) : null} + diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index cd8948f..81c15e8 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -8,6 +8,7 @@ import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel"; import { useAuth } from "../context/AuthContext"; import { useNotifications } from "../hooks/useNotifications"; import { usePushNotifications } from "../hooks/usePushNotifications"; +import { usePwaStatus } from "../hooks/usePwaStatus"; import { useOrderGroups } from "../hooks/useOrderGroups"; import { AppShell } from "../layouts/AppShell"; @@ -53,6 +54,8 @@ export const DashboardPage = () => { } }, [isSupported, isSubscribed, user?.id, subscribe]); + const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus(); + const { orderGroups, allOrderGroups, @@ -143,6 +146,9 @@ export const DashboardPage = () => { setActiveSection((current) => (current === "guide" ? section.key : "guide"))} isGuideOpen={isGuideOpen} navItems={navItems}