Browse Source

Merge pull request #362 from danditomaso/update_ble_serial_wording_https

fix: update connect dialog messaging to describe requirement for https
pull/384/head
Hunter Thornsberry 1 year ago
committed by GitHub
parent
commit
578405d5d3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 161
      src/components/Dialog/NewDeviceDialog.tsx
  2. 11
      src/components/PageComponents/Connect/Serial.tsx
  3. 7
      src/components/UI/Typography/Link.tsx
  4. 29
      src/core/hooks/useBrowserFeatureDetection.ts

161
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 { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx"; import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx"; import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
@ -7,14 +11,16 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { AlertCircle, InfoIcon, } from "lucide-react";
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "@components/UI/Tabs.tsx"; } from "@components/UI/Tabs.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { Link } from "../UI/Typography/Link";
import { Fragment } from "react/jsx-runtime";
export interface TabElementProps { export interface TabElementProps {
closeDialog: () => void; closeDialog: () => void;
@ -23,44 +29,114 @@ export interface TabElementProps {
export interface TabManifest { export interface TabManifest {
label: string; label: string;
element: React.FC<TabElementProps>; element: React.FC<TabElementProps>;
disabled: boolean; isDisabled: boolean;
disabledMessage: string;
disabledLink?: string;
} }
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 { export interface NewDeviceProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; 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 = ({ export const NewDeviceDialog = ({
open, open,
onOpenChange, onOpenChange,
}: NewDeviceProps): JSX.Element => { }: 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 ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent>
@ -73,7 +149,6 @@ export const NewDeviceDialog = ({
<TabsTrigger <TabsTrigger
key={tab.label} key={tab.label}
value={tab.label} value={tab.label}
disabled={tab.disabled}
> >
{tab.label} {tab.label}
</TabsTrigger> </TabsTrigger>
@ -81,35 +156,13 @@ export const NewDeviceDialog = ({
</TabsList> </TabsList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabsContent key={tab.label} value={tab.label}> <TabsContent key={tab.label} value={tab.label}>
{tab.disabled ? ( <fieldset disabled={tab.isDisabled}>
<p className="text-sm text-slate-500 dark:text-slate-400"> {tab.isDisabled ? <ErrorMessage missingFeatures={unsupported} /> : null}
{tab.disabledMessage}
</p>
) : (
<tab.element closeDialog={() => onOpenChange(false)} /> <tab.element closeDialog={() => onOpenChange(false)} />
)} </fieldset>
</TabsContent> </TabsContent>
))} ))}
</Tabs> </Tabs>
{(!navigator.bluetooth || !navigator.serial) && (
<>
<Subtle>
Web Bluetooth and Web Serial are currently only supported by
Chromium-based browsers.
</Subtle>
<Subtle>
Read more:&nbsp;
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility">
Web Bluetooth
</Link>
&nbsp;
<Link href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility">
Web Serial
</Link>
</Subtle>
</>
)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

11
src/components/PageComponents/Connect/Serial.tsx

@ -14,13 +14,13 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
const { setSelectedDevice } = useAppStore(); const { setSelectedDevice } = useAppStore();
const updateSerialPortList = useCallback(async () => { const updateSerialPortList = useCallback(async () => {
setSerialPorts(await navigator.serial.getPorts()); setSerialPorts(await navigator?.serial.getPorts());
}, []); }, []);
navigator.serial.addEventListener("connect", () => { navigator?.serial?.addEventListener("connect", () => {
updateSerialPortList(); updateSerialPortList();
}); });
navigator.serial.addEventListener("disconnect", () => { navigator?.serial?.addEventListener("disconnect", () => {
updateSerialPortList(); updateSerialPortList();
}); });
useEffect(() => { useEffect(() => {
@ -58,9 +58,8 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
await onConnect(port); await onConnect(port);
}} }}
> >
{`# ${index} - ${usbVendorId ?? "UNK"} - ${ {`# ${index} - ${usbVendorId ?? "UNK"} - ${usbProductId ?? "UNK"
usbProductId ?? "UNK" }`}
}`}
</Button> </Button>
); );
})} })}

7
src/components/UI/Typography/Link.tsx

@ -1,14 +1,17 @@
import { cn } from "@app/core/utils/cn";
export interface LinkProps { export interface LinkProps {
href: string; href: string;
children: React.ReactNode; children: React.ReactNode;
className?: string;
} }
export const Link = ({ href, children }: LinkProps): JSX.Element => ( export const Link = ({ href, children, className }: LinkProps): JSX.Element => (
<a <a
href={href} href={href}
target={"_blank"} target={"_blank"}
rel="noopener noreferrer" 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} {children}
</a> </a>

29
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<BrowserSupport>(
(acc, [feature, isSupported]) => {
const list = isSupported ? acc.supported : acc.unsupported;
list.push(feature);
return acc;
},
{ supported: [], unsupported: [] }
);
}, []);
return support;
}
Loading…
Cancel
Save