From 225d6055d4c39cf258332058b6c4007854269695 Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Wed, 15 Jan 2025 20:11:46 -0500 Subject: [PATCH] Improved error messaging based on feature missing from browser --- src/components/Dialog/NewDeviceDialog.tsx | 138 ++++++++++++------- src/components/UI/Typography/Link.tsx | 7 +- src/core/hooks/useBrowserFeatureDetection.ts | 49 ++++--- 3 files changed, 117 insertions(+), 77 deletions(-) diff --git a/src/components/Dialog/NewDeviceDialog.tsx b/src/components/Dialog/NewDeviceDialog.tsx index 92d4c75a..e03668d7 100644 --- a/src/components/Dialog/NewDeviceDialog.tsx +++ b/src/components/Dialog/NewDeviceDialog.tsx @@ -1,4 +1,7 @@ -import { useBrowserFeatureDetection } from "@app/core/hooks/useBrowserFeatureDetection"; +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"; @@ -8,14 +11,16 @@ import { DialogHeader, DialogTitle, } from "@components/UI/Dialog.tsx"; +import { AlertCircle, } 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; @@ -29,19 +34,88 @@ export interface TabManifest { disabledLink?: string; } - - 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 { hasRequiredFeatures, isSecureContext, missingFeatures } = useBrowserFeatureDetection(); - console.log(missingFeatures); + const { unsupported } = useBrowserFeatureDetection(); const tabs: TabManifest[] = [ { @@ -53,23 +127,21 @@ export const NewDeviceDialog = ({ { label: "Bluetooth", element: BLE, - disabled: missingFeatures.includes("Web Bluetooth"), - disabledMessage: - "Web Bluetooth is currently only supported by Chromium-based browsers", - disabledLink: - "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility" + disabled: + unsupported.includes("Web Bluetooth") || + unsupported.includes("Secure Context"), + disabledMessage: "", }, { label: "Serial", element: Serial, - disabled: missingFeatures.includes("Web Serial"), - disabledMessage: - "Web Serial is currently only supported by Chromium based browsers", - disabledLink: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", + disabled: + unsupported.includes("Web Serial") || + unsupported.includes("Secure Context"), + disabledMessage: "", }, ]; - return ( @@ -99,40 +171,8 @@ export const NewDeviceDialog = ({ )} ))} + - - {!isSecureContext && ( - <> - - Web Bluetooth and Web Serial require using a HTTPS connection or to localhost. - - - Read more:  - - Secure Contexts - - - - )} - - {!hasRequiredFeatures && ( - <> - - Web Bluetooth and Web Serial are currently only supported by - Chromium-based browsers. - - - Read more:  - - Web Bluetooth - -   - - Web Serial - - - - )} ); 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 index df103b2c..83a90a7a 100644 --- a/src/core/hooks/useBrowserFeatureDetection.ts +++ b/src/core/hooks/useBrowserFeatureDetection.ts @@ -1,32 +1,29 @@ -type Feature = 'Web Bluetooth' | 'Web Serial'; -type FeatureKey = 'bluetooth' | 'serial'; +import { useMemo } from 'react'; -interface BrowserFeatureDetection { - hasRequiredFeatures: boolean; - missingFeatures: Feature[]; - isSecureContext: boolean; -} +export type BrowserFeature = 'Web Bluetooth' | 'Web Serial' | 'Secure Context'; -const featureLabels: Record = { - bluetooth: 'Web Bluetooth', - serial: 'Web Serial' -}; +interface BrowserSupport { + supported: BrowserFeature[]; + unsupported: BrowserFeature[]; +} -export function useBrowserFeatureDetection(): BrowserFeatureDetection { - const { bluetooth, serial } = navigator; - const isSecureContext = window.location.protocol === 'https:' || - window.location.hostname === 'localhost'; +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'] + ]; - const features = { - bluetooth, - serial - }; + return features.reduce( + (acc, [feature, isSupported]) => { + const list = isSupported ? acc.supported : acc.unsupported; + list.push(feature); + return acc; + }, + { supported: [], unsupported: [] } + ); + }, []); - return { - hasRequiredFeatures: Object.values(features).every(Boolean), - missingFeatures: Object.entries(features) - .filter(([_, supported]) => !supported) - .map(([feature]) => featureLabels[feature as FeatureKey]), - isSecureContext - }; + return support; } \ No newline at end of file