diff --git a/src/components/Dialog/NewDeviceDialog.tsx b/src/components/Dialog/NewDeviceDialog.tsx index 6062d69c..e3330cbf 100644 --- a/src/components/Dialog/NewDeviceDialog.tsx +++ b/src/components/Dialog/NewDeviceDialog.tsx @@ -1,3 +1,7 @@ +import { + type BrowserFeature, + useBrowserFeatureDetection, +} from "@app/core/hooks/useBrowserFeatureDetection"; import { BLE } from "@components/PageComponents/Connect/BLE.tsx"; import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx"; import { Serial } from "@components/PageComponents/Connect/Serial.tsx"; @@ -7,14 +11,16 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; +import { AlertCircle, InfoIcon, } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@components/UI/Tabs.tsx"; -import { Link } from "@components/UI/Typography/Link.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx"; +import { Link } from "../UI/Typography/Link"; +import { Fragment } from "react/jsx-runtime"; export interface TabElementProps { closeDialog: () => void; @@ -23,44 +29,114 @@ export interface TabElementProps { export interface TabManifest { label: string; element: React.FC; - disabled: boolean; - disabledMessage: string; - disabledLink?: string; + isDisabled: boolean; } -const tabs: TabManifest[] = [ - { - label: "HTTP", - element: HTTP, - disabled: false, - disabledMessage: "Unsuported connection method", - }, - { - label: "Bluetooth", - element: BLE, - disabled: !navigator.bluetooth, - disabledMessage: - "Web Bluetooth is currently only supported by Chromium-based browsers", - disabledLink: - "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", - }, - { - label: "Serial", - element: Serial, - disabled: !navigator.serial, - disabledMessage: - "WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", - }, -]; export interface NewDeviceProps { open: boolean; onOpenChange: (open: boolean) => void; } +interface FeatureErrorProps { + missingFeatures: BrowserFeature[]; +} + +const links: { [key: string]: string } = { + "Web Bluetooth": + "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", + "Web Serial": + "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", + "Secure Context": + "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", +}; + +const listFormatter = new Intl.ListFormat('en', { + style: 'long', + type: 'conjunction' +}); + +const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { + if (missingFeatures.length === 0) return null; + + const browserFeatures = missingFeatures.filter(feature => feature !== "Secure Context"); + const needsSecureContext = missingFeatures.includes("Secure Context"); + + const formatFeatureList = (features: string[]) => { + const parts = listFormatter.formatToParts(features); + return parts.map((part) => { + if (part.type === 'element') { + return ( + + {part.value} + + ); + } + return {part.value}; + }); + }; + + return ( + +
+ +
+

+ {browserFeatures.length > 0 && ( + <> + This application requires {formatFeatureList(browserFeatures)}. + Please use a Chromium-based browser like Chrome or Edge. + + )} + {needsSecureContext && ( + <> + {browserFeatures.length > 0 && " Additionally, it"} + {browserFeatures.length === 0 && "This application"} requires a{" "} + + secure context + + . Please connect using HTTPS or localhost. + + )} +

+
+
+
+ ); +}; + export const NewDeviceDialog = ({ open, onOpenChange, }: NewDeviceProps): JSX.Element => { + const { unsupported } = useBrowserFeatureDetection(); + + const tabs: TabManifest[] = [ + { + label: "HTTP", + element: HTTP, + isDisabled: false, + }, + { + label: "Bluetooth", + element: BLE, + isDisabled: + unsupported.includes("Web Bluetooth") || + unsupported.includes("Secure Context"), + }, + { + label: "Serial", + element: Serial, + isDisabled: + unsupported.includes("Web Serial") || + unsupported.includes("Secure Context"), + }, + ]; + return ( @@ -73,7 +149,6 @@ export const NewDeviceDialog = ({ {tab.label} @@ -81,35 +156,13 @@ export const NewDeviceDialog = ({ {tabs.map((tab) => ( - {tab.disabled ? ( -

- {tab.disabledMessage} -

- ) : ( +
+ {tab.isDisabled ? : null} onOpenChange(false)} /> - )} +
))} - - {(!navigator.bluetooth || !navigator.serial) && ( - <> - - Web Bluetooth and Web Serial are currently only supported by - Chromium-based browsers. - - - Read more:  - - Web Bluetooth - -   - - Web Serial - - - - )}
); diff --git a/src/components/PageComponents/Connect/Serial.tsx b/src/components/PageComponents/Connect/Serial.tsx index 38aab8fb..53c623cb 100644 --- a/src/components/PageComponents/Connect/Serial.tsx +++ b/src/components/PageComponents/Connect/Serial.tsx @@ -14,13 +14,13 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => { const { setSelectedDevice } = useAppStore(); const updateSerialPortList = useCallback(async () => { - setSerialPorts(await navigator.serial.getPorts()); + setSerialPorts(await navigator?.serial.getPorts()); }, []); - navigator.serial.addEventListener("connect", () => { + navigator?.serial?.addEventListener("connect", () => { updateSerialPortList(); }); - navigator.serial.addEventListener("disconnect", () => { + navigator?.serial?.addEventListener("disconnect", () => { updateSerialPortList(); }); useEffect(() => { @@ -58,9 +58,8 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => { await onConnect(port); }} > - {`# ${index} - ${usbVendorId ?? "UNK"} - ${ - usbProductId ?? "UNK" - }`} + {`# ${index} - ${usbVendorId ?? "UNK"} - ${usbProductId ?? "UNK" + }`} ); })} diff --git a/src/components/UI/Typography/Link.tsx b/src/components/UI/Typography/Link.tsx index af1a90ac..efcfbdf6 100644 --- a/src/components/UI/Typography/Link.tsx +++ b/src/components/UI/Typography/Link.tsx @@ -1,14 +1,17 @@ +import { cn } from "@app/core/utils/cn"; + export interface LinkProps { href: string; children: React.ReactNode; + className?: string; } -export const Link = ({ href, children }: LinkProps): JSX.Element => ( +export const Link = ({ href, children, className }: LinkProps): JSX.Element => ( {children} diff --git a/src/core/hooks/useBrowserFeatureDetection.ts b/src/core/hooks/useBrowserFeatureDetection.ts new file mode 100644 index 00000000..a31b7b5a --- /dev/null +++ b/src/core/hooks/useBrowserFeatureDetection.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; + +export type BrowserFeature = 'Web Bluetooth' | 'Web Serial' | 'Secure Context'; + +interface BrowserSupport { + supported: BrowserFeature[]; + unsupported: BrowserFeature[]; +} + +export function useBrowserFeatureDetection(): BrowserSupport { + const support = useMemo(() => { + const features: [BrowserFeature, boolean][] = [ + ['Web Bluetooth', !!navigator?.bluetooth], + ['Web Serial', !!navigator?.serial], + ['Secure Context', window.location.protocol === 'https:' || window.location.hostname === 'localhost'] + ]; + + return features.reduce( + (acc, [feature, isSupported]) => { + const list = isSupported ? acc.supported : acc.unsupported; + list.push(feature); + return acc; + }, + { supported: [], unsupported: [] } + ); + }, []); + + return support; +} \ No newline at end of file