Improved error messaging based on feature missing from browser
This commit is contained in:
@@ -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 (
|
||||
<Link
|
||||
key={part.value}
|
||||
href={links[part.value]}
|
||||
>
|
||||
{part.value}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <Fragment key={part.value}>{part.value}</Fragment>;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Subtle className="flex flex-col items-start gap-2 text-black bg-red-200/80 p-4 rounded-md">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<AlertCircle size={40} className="mr-2 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm">
|
||||
{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{" "}
|
||||
<Link
|
||||
href={links["Secure Context"]}
|
||||
>
|
||||
secure context
|
||||
</Link>
|
||||
. Please connect using HTTPS or localhost.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Subtle>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
@@ -99,40 +171,8 @@ export const NewDeviceDialog = ({
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
<ErrorMessage missingFeatures={unsupported} />
|
||||
</Tabs>
|
||||
|
||||
{!isSecureContext && (
|
||||
<>
|
||||
<Subtle>
|
||||
Web Bluetooth and Web Serial require using a HTTPS connection or to localhost.
|
||||
</Subtle>
|
||||
<Subtle>
|
||||
Read more:
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts">
|
||||
Secure Contexts
|
||||
</Link>
|
||||
</Subtle>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasRequiredFeatures && (
|
||||
<>
|
||||
<Subtle>
|
||||
Web Bluetooth and Web Serial are currently only supported by
|
||||
Chromium-based browsers.
|
||||
</Subtle>
|
||||
<Subtle>
|
||||
Read more:
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
|
||||
Web Bluetooth
|
||||
</Link>
|
||||
|
||||
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
|
||||
Web Serial
|
||||
</Link>
|
||||
</Subtle>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -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 => (
|
||||
<a
|
||||
href={href}
|
||||
target={"_blank"}
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50"
|
||||
className={cn("font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50", className)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
||||
@@ -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';
|
||||
|
||||
interface BrowserSupport {
|
||||
supported: BrowserFeature[];
|
||||
unsupported: BrowserFeature[];
|
||||
}
|
||||
|
||||
const featureLabels: Record<FeatureKey, Feature> = {
|
||||
bluetooth: 'Web Bluetooth',
|
||||
serial: 'Web Serial'
|
||||
};
|
||||
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']
|
||||
];
|
||||
|
||||
export function useBrowserFeatureDetection(): BrowserFeatureDetection {
|
||||
const { bluetooth, serial } = navigator;
|
||||
const isSecureContext = window.location.protocol === 'https:' ||
|
||||
window.location.hostname === 'localhost';
|
||||
return features.reduce<BrowserSupport>(
|
||||
(acc, [feature, isSupported]) => {
|
||||
const list = isSupported ? acc.supported : acc.unsupported;
|
||||
list.push(feature);
|
||||
return acc;
|
||||
},
|
||||
{ supported: [], unsupported: [] }
|
||||
);
|
||||
}, []);
|
||||
|
||||
const features = {
|
||||
bluetooth,
|
||||
serial
|
||||
};
|
||||
|
||||
return {
|
||||
hasRequiredFeatures: Object.values(features).every(Boolean),
|
||||
missingFeatures: Object.entries(features)
|
||||
.filter(([_, supported]) => !supported)
|
||||
.map(([feature]) => featureLabels[feature as FeatureKey]),
|
||||
isSecureContext
|
||||
};
|
||||
return support;
|
||||
}
|
||||
Reference in New Issue
Block a user