diff --git a/client/src/components/PopupBlockedBanner.tsx b/client/src/components/PopupBlockedBanner.tsx new file mode 100644 index 00000000..06025bdc --- /dev/null +++ b/client/src/components/PopupBlockedBanner.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from "react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { AlertCircle, ArrowUp, X } from "lucide-react"; + +// Remembers a passed check so the test window doesn't flash on every reload. +const POPUP_OK_KEY = "keylessh:popup-ok"; + +/** + * Tide approvals run in a popup window (the approval enclave), so a browser + * that blocks popups breaks them. Probe once after login, outside a user + * gesture: that only succeeds when popups are allowed for this site. + */ +function popupsAllowed(): Promise { + let w: Window | null = null; + try { + w = window.open("about:blank", "_blank", "width=1,height=1,left=-10000,top=-10000"); + } catch { + return Promise.resolve(false); + } + if (!w || w.closed) return Promise.resolve(false); + // Some blockers hand back a window and then close it straight away. + return new Promise((resolve) => { + setTimeout(() => { + const ok = !w!.closed; + try { + w!.close(); + } catch { + // ignore + } + resolve(ok); + }, 250); + }); +} + +type Browser = "edge" | "chrome" | "firefox" | "safari" | "other"; + +function detectBrowser(): Browser { + const ua = navigator.userAgent; + if (/Edg\//.test(ua)) return "edge"; + if (/Firefox\//.test(ua)) return "firefox"; + if (/Chrome\/|Chromium\/|CriOS\//.test(ua)) return "chrome"; + if (/Safari\//.test(ua)) return "safari"; + return "other"; +} + +// Where the browser surfaces the blocked popup, and how to allow it from there. +const UNBLOCK_STEPS: Record = { + chrome: [ + "Click the blocked-popup icon at the right end of the address bar (↑ top right).", + "Choose “Always allow pop-ups and redirects from this site”, then click Done.", + "Click Reload below.", + ], + edge: [ + "Click the blocked-popup icon at the right end of the address bar (↑ top right).", + "Choose “Always allow pop-ups and redirects from this site”, then click Done.", + "Click Reload below.", + ], + firefox: [ + "Find the yellow “Firefox prevented this site from opening a pop-up window” bar just below the address bar.", + "Click its Options (or Preferences) button on the right (↑ top right).", + "Choose “Allow pop-ups for this site”, then click Reload below.", + ], + safari: [ + "Click the blocked-popup icon in the address bar (↑ top), or open Safari › Settings › Websites › Pop-up Windows.", + "Set this website to “Allow”.", + "Click Reload below.", + ], + other: [ + "Look for a blocked-popup icon or notice near the address bar (↑ top right).", + "Allow pop-ups for this site.", + "Click Reload below.", + ], +}; + +export function PopupBlockedBanner() { + const [blocked, setBlocked] = useState(false); + const [browser] = useState(detectBrowser); + + useEffect(() => { + try { + if (sessionStorage.getItem(POPUP_OK_KEY) === "1") return; + } catch { + // storage unavailable; just run the check + } + let cancelled = false; + popupsAllowed().then((ok) => { + if (cancelled) return; + if (ok) { + try { + sessionStorage.setItem(POPUP_OK_KEY, "1"); + } catch { + // ignore + } + } + setBlocked(!ok); + }); + return () => { + cancelled = true; + }; + }, []); + + if (!blocked) return null; + + return ( + + + +

Pop-ups are blocked: please unblock them to use KeyleSSH.

+

+ Tide approvals open in a pop-up window and won’t work until pop-ups are allowed for this site. +

+
    + {UNBLOCK_STEPS[browser].map((step) => ( +
  1. {step}
  2. + ))} +
+
+ + +
+
+ + {/* Points up at the address bar, where the browser shows its blocked-popup icon. */} + +
+ ); +} diff --git a/client/src/components/layout/AppLayout.tsx b/client/src/components/layout/AppLayout.tsx index 34782a17..38ce03b0 100644 --- a/client/src/components/layout/AppLayout.tsx +++ b/client/src/components/layout/AppLayout.tsx @@ -1,5 +1,6 @@ import { Link, useLocation } from "wouter"; import { useAuth } from "@/contexts/AuthContext"; +import { PopupBlockedBanner } from "@/components/PopupBlockedBanner"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -411,6 +412,8 @@ export function AppLayout({ children }: AppLayoutProps) {
+ +