diff --git a/app/src/components/admin/ExtensionInstallSuggestion.js b/app/src/components/admin/ExtensionInstallSuggestion.js index bd83df8c..acc6ffd0 100644 --- a/app/src/components/admin/ExtensionInstallSuggestion.js +++ b/app/src/components/admin/ExtensionInstallSuggestion.js @@ -3,10 +3,13 @@ import {Link} from "react-router"; import {Button, Icon, Message} from "semantic-ui-react"; import {useLocalStorage} from "../Common"; import {useExtensionInstalled} from "../../hooks/customHooks"; +import {isExtensionSupportedBrowser} from "./browserDetect"; // Top-of-dashboard nudge for users who don't yet have the WROLPi browser // extension installed (or installed but haven't added this WROLPi as a // destination). Hidden once the user installs+configures, or dismisses it. +// Only shown on Chromium / Firefox — Safari and other browsers cannot run +// the extension. // // Detection is via a meta tag injected by the extension's content script. // The extension only injects on configured destination origins, so seeing @@ -19,7 +22,8 @@ export function ExtensionInstallSuggestion() { // installed === null means "still checking" — render nothing rather // than flashing the banner before the content-script race resolves. - if (installed !== false || dismissed) return null; + // Also skip on browsers that cannot install the extension (e.g. Safari). + if (installed !== false || dismissed || !isExtensionSupportedBrowser()) return null; return setDismissed(true)}> diff --git a/app/src/components/admin/ExtensionInstallSuggestion.test.js b/app/src/components/admin/ExtensionInstallSuggestion.test.js index 13d44ead..7d9f2744 100644 --- a/app/src/components/admin/ExtensionInstallSuggestion.test.js +++ b/app/src/components/admin/ExtensionInstallSuggestion.test.js @@ -2,6 +2,14 @@ import React from 'react'; import {render, screen, waitFor} from '../../test-utils'; import {ExtensionInstallSuggestion} from './ExtensionInstallSuggestion'; +const CHROME_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36'; +const FIREFOX_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0'; +const SAFARI_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'; + +const setUserAgent = (ua) => { + Object.defineProperty(window.navigator, 'userAgent', {value: ua, configurable: true}); +}; + const setMeta = (version) => { const existing = document.querySelector('meta[name="wrolpi-extension"]'); if (existing) existing.remove(); @@ -15,6 +23,7 @@ const setMeta = (version) => { describe('ExtensionInstallSuggestion', () => { beforeEach(() => { setMeta(undefined); + setUserAgent(CHROME_UA); window.localStorage.removeItem('wrolpi-extension-banner-dismissed'); }); @@ -39,6 +48,22 @@ describe('ExtensionInstallSuggestion', () => { expect(screen.getByText(/Open install page/i)).toBeInTheDocument(); }); + it('renders the install message on Firefox when the marker is absent', async () => { + setUserAgent(FIREFOX_UA); + render(); + await waitFor( + () => expect(screen.getByText(/Install the WROLPi browser extension/i)).toBeInTheDocument(), + {timeout: 1500}, + ); + }); + + it('renders nothing on Safari even when the marker is absent', async () => { + setUserAgent(SAFARI_UA); + render(); + await new Promise(r => setTimeout(r, 600)); + expect(screen.queryByText(/Install the WROLPi browser extension/i)).toBeNull(); + }); + it('renders nothing when the user has dismissed the banner', async () => { window.localStorage.setItem('wrolpi-extension-banner-dismissed', 'true'); render(); diff --git a/app/src/components/admin/ExtensionPage.js b/app/src/components/admin/ExtensionPage.js index 828dd1a5..859863d0 100644 --- a/app/src/components/admin/ExtensionPage.js +++ b/app/src/components/admin/ExtensionPage.js @@ -2,6 +2,7 @@ import React from "react"; import {Button, Header, Icon, Segment} from "../Theme"; import {Input, Message} from "semantic-ui-react"; import {ThemeContext} from "../../contexts/contexts"; +import {detectBrowser} from "./browserDetect"; // The WROLPi browser extension is distributed off-store: Chrome users // sideload the unpacked .zip; Firefox users get a Mozilla-signed .xpi that @@ -11,17 +12,6 @@ import {ThemeContext} from "../../contexts/contexts"; const CHROME_ZIP_PATH = '/api/extensions/wrolpi-chrome.zip'; const FIREFOX_XPI_PATH = '/api/extensions/wrolpi-firefox.xpi'; -function detectBrowser() { - if (typeof navigator === 'undefined') return 'unknown'; - const ua = navigator.userAgent || ''; - // Order matters: Edge / Brave / Opera all include 'Chrome' in their UA. - if (ua.includes('Firefox')) return 'firefox'; - if (ua.includes('Edg/')) return 'chromium'; - if (ua.includes('OPR/') || ua.includes('Opera')) return 'chromium'; - if (ua.includes('Chrome')) return 'chromium'; - return 'unknown'; -} - function formatSize(bytes) { if (!bytes) return ''; const kb = bytes / 1024; diff --git a/app/src/components/admin/browserDetect.js b/app/src/components/admin/browserDetect.js new file mode 100644 index 00000000..e259bdf8 --- /dev/null +++ b/app/src/components/admin/browserDetect.js @@ -0,0 +1,23 @@ +// Shared UA sniffing for the WROLPi browser extension UI. +// The extension only ships for Chromium (Chrome/Brave/Edge/Opera) and Firefox; +// Safari and other browsers should not be nudged to install it. + +/** + * @returns {'firefox' | 'chromium' | 'unknown'} + */ +export function detectBrowser() { + if (typeof navigator === 'undefined') return 'unknown'; + const ua = navigator.userAgent || ''; + // Order matters: Edge / Brave / Opera all include 'Chrome' in their UA. + if (ua.includes('Firefox')) return 'firefox'; + if (ua.includes('Edg/')) return 'chromium'; + if (ua.includes('OPR/') || ua.includes('Opera')) return 'chromium'; + if (ua.includes('Chrome')) return 'chromium'; + return 'unknown'; +} + +/** True when the current browser can run the WROLPi extension. */ +export function isExtensionSupportedBrowser() { + const browser = detectBrowser(); + return browser === 'firefox' || browser === 'chromium'; +}