Merge pull request #362 from danditomaso/update_ble_serial_wording_https
fix: update connect dialog messaging to describe requirement for https
This commit is contained in:
@@ -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:
|
|
||||||
<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>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,8 +58,7 @@ 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user