app: only show extension install banner on Chromium/Firefox

The dashboard nudge is useless on Safari and other browsers that cannot
run the WROLPi extension. Share detectBrowser and gate the banner with
isExtensionSupportedBrowser.
This commit is contained in:
Roland 2026-07-18 13:47:43 -06:00 • committed by lrnselfreliance
parent 46ba804a1b
commit db7603012e
4 changed files with 54 additions and 12 deletions

View file

@ -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 <Message info icon onDismiss={() => setDismissed(true)}>
<Icon name='puzzle piece'/>

View file

@ -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(<ExtensionInstallSuggestion/>);
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(<ExtensionInstallSuggestion/>);
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(<ExtensionInstallSuggestion/>);

View file

@ -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;

View file

@ -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';
}