diff --git a/src/alerts.ts b/src/alerts.ts
index f05a8134..4e372ef0 100644
--- a/src/alerts.ts
+++ b/src/alerts.ts
@@ -3,12 +3,14 @@
import { IToastProps } from '@blueprintjs/core';
import alerts from './alerts/alerts';
+import ble from './ble/alerts';
import explorer from './explorer/alerts';
import { CreateToast } from './i18nToaster';
/** This collects alerts from all of the subsystems of the app */
const alertDomains = {
alerts,
+ ble,
explorer,
};
diff --git a/src/ble/alerts/NoWebBluetooth.tsx b/src/ble/alerts/NoWebBluetooth.tsx
new file mode 100644
index 00000000..175cd784
--- /dev/null
+++ b/src/ble/alerts/NoWebBluetooth.tsx
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { Button, Intent } from '@blueprintjs/core';
+import React from 'react';
+import { CreateToast } from '../../i18nToaster';
+import { isIOS, isLinux } from '../../utils/os';
+import { I18nId, useI18n } from './i18n';
+
+const NoWebBluetooth: React.VoidFunctionComponent = () => {
+ const i18n = useI18n();
+ return (
+ <>
+
{i18n.translate(I18nId.NoWebBluetoothMessage)}
+ {!isLinux() && !isIOS() && (
+ {i18n.translate(I18nId.NoWebBluetoothSuggestion)}
+ )}
+ {isLinux() && (
+ <>
+ {i18n.translate(I18nId.NoWebBluetoothLinux)}
+
+
+ chrome://flags/#enable-experimental-web-platform-features
+
+
+ >
+ )}
+ >
+ );
+};
+
+export const noWebBluetooth: CreateToast = (onAction) => {
+ return {
+ message: ,
+ icon: 'error',
+ intent: Intent.DANGER,
+ onDismiss: () => onAction('dismiss'),
+ };
+};
diff --git a/src/ble/alerts/i18n.test.ts b/src/ble/alerts/i18n.test.ts
new file mode 100644
index 00000000..e706ba28
--- /dev/null
+++ b/src/ble/alerts/i18n.test.ts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { lookup } from '../../../test';
+import { I18nId } from './i18n';
+import en from './translations/en.json';
+
+describe('Ensure .json file has matches for I18nId', () => {
+ test.each(Object.values(I18nId))('%s', (id) => {
+ expect(lookup(en, id)).toBeDefined();
+ });
+});
diff --git a/src/ble/alerts/i18n.ts b/src/ble/alerts/i18n.ts
new file mode 100644
index 00000000..e488990f
--- /dev/null
+++ b/src/ble/alerts/i18n.ts
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { I18n, useI18n as useShopifyI18n } from '@shopify/react-i18n';
+
+export function useI18n(): I18n {
+ // istanbul ignore next: babel-loader rewrites this line
+ const [i18n] = useShopifyI18n();
+ return i18n;
+}
+
+export enum I18nId {
+ NoWebBluetoothMessage = 'noWebBluetooth.message',
+ NoWebBluetoothSuggestion = 'noWebBluetooth.suggestion',
+ NoWebBluetoothLinux = 'noWebBluetooth.linux',
+}
diff --git a/src/ble/alerts/index.ts b/src/ble/alerts/index.ts
new file mode 100644
index 00000000..9eda80a7
--- /dev/null
+++ b/src/ble/alerts/index.ts
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import { noWebBluetooth } from './NoWebBluetooth';
+
+// gathers all of the alert creation functions for passing up to the top level
+export default { noWebBluetooth };
diff --git a/src/ble/alerts/translations/en.json b/src/ble/alerts/translations/en.json
new file mode 100644
index 00000000..2ae6b9cf
--- /dev/null
+++ b/src/ble/alerts/translations/en.json
@@ -0,0 +1,8 @@
+{
+ "noWebBluetooth": {
+ "message": "This browser does not support Web Bluetooth or it is not enabled.",
+ "suggestion": "Use a supported browser such as Google Chrome or Microsoft Edge.",
+ "linux": "Web Bluetooth is experimental on Linux and must be manually enabled. Copy the link below and paste it in the address bar.",
+ "action": "More Info"
+ }
+}
diff --git a/src/ble/sagas.test.ts b/src/ble/sagas.test.ts
index 453c11ff..849b7a35 100644
--- a/src/ble/sagas.test.ts
+++ b/src/ble/sagas.test.ts
@@ -4,6 +4,7 @@
import { HubType } from '@pybricks/firmware';
import { MockProxy, mock } from 'jest-mock-extended';
import { AsyncSaga } from '../../test';
+import { alertsShowAlert } from '../alerts/actions';
import {
bleDIServiceDidReceiveFirmwareRevision,
bleDIServiceDidReceivePnPId,
@@ -241,6 +242,9 @@ describe('connect action is dispatched', () => {
it('should fail if no web bluetooth', async () => {
await runConnectUntil(saga, ConnectRunPoint.Connect);
+ await expect(saga.take()).resolves.toEqual(
+ alertsShowAlert('ble', 'noWebBluetooth'),
+ );
await expect(saga.take()).resolves.toEqual(
bleDidFailToConnectPybricks({
reason: BleDeviceFailToConnectReasonType.NoWebBluetooth,
diff --git a/src/ble/sagas.ts b/src/ble/sagas.ts
index 57fee43f..5e39ba37 100644
--- a/src/ble/sagas.ts
+++ b/src/ble/sagas.ts
@@ -15,6 +15,7 @@ import {
takeEvery,
takeMaybe,
} from 'typed-redux-saga/macro';
+import { alertsShowAlert } from '../alerts/actions';
import {
bleDIServiceDidReceiveFirmwareRevision,
bleDIServiceDidReceivePnPId,
@@ -101,6 +102,7 @@ function* handleWriteUart(
function* handleBleConnectPybricks(): Generator {
if (navigator.bluetooth === undefined) {
+ yield* put(alertsShowAlert('ble', 'noWebBluetooth'));
yield* put(bleDidFailToConnectPybricks({ reason: Reason.NoWebBluetooth }));
return;
}
diff --git a/src/lwp3-bootloader/sagas-ble.ts b/src/lwp3-bootloader/sagas-ble.ts
index 8b384736..1d5f8074 100644
--- a/src/lwp3-bootloader/sagas-ble.ts
+++ b/src/lwp3-bootloader/sagas-ble.ts
@@ -5,6 +5,7 @@
import { END, eventChannel } from 'redux-saga';
import { call, cancel, put, spawn, takeEvery, takeMaybe } from 'typed-redux-saga/macro';
+import { alertsShowAlert } from '../alerts/actions';
import { ensureError } from '../utils';
import {
BootloaderConnectionFailureReason as Reason,
@@ -42,6 +43,7 @@ function* write(
function* handleConnect(): Generator {
if (navigator.bluetooth === undefined) {
+ yield* put(alertsShowAlert('ble', 'noWebBluetooth'));
yield* put(didFailToConnect(Reason.NoWebBluetooth));
return;
}
diff --git a/src/notifications/i18n.ts b/src/notifications/i18n.ts
index d7dea7f7..b8977bc3 100644
--- a/src/notifications/i18n.ts
+++ b/src/notifications/i18n.ts
@@ -16,7 +16,6 @@ export enum I18nId {
BleUnexpectedError = 'ble.unexpectedError',
BleGattPermission = 'ble.gattPermission',
BleGattServiceNotFound = 'ble.gattServiceNotFound',
- BleNoWebBluetooth = 'ble.noWebBluetooth',
BleNoBluetooth = 'ble.noBluetooth',
EditorFailedToOpenFile = 'editor.failedToOpenFile',
EditorFailedToSaveFile = 'editor.failedToSaveFile',
diff --git a/src/notifications/sagas.test.ts b/src/notifications/sagas.test.ts
index edac4794..9bf92d8e 100644
--- a/src/notifications/sagas.test.ts
+++ b/src/notifications/sagas.test.ts
@@ -61,9 +61,6 @@ function createTestToasterSaga(): { toaster: IToaster; saga: AsyncSaga } {
}
test.each([
- bleDidFailToConnectPybricks({
- reason: BleDeviceFailToConnectReasonType.NoWebBluetooth,
- }),
bleDidFailToConnectPybricks({
reason: BleDeviceFailToConnectReasonType.NoBluetooth,
}),
@@ -81,7 +78,6 @@ test.each([
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Unknown, {
message: 'test',
}),
- bootloaderDidFailToConnect(BootloaderConnectionFailureReason.NoWebBluetooth),
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.NoBluetooth),
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.GattServiceNotFound),
didFailToCompile(['reason']),
@@ -135,7 +131,11 @@ test.each([
});
test.each([
+ bleDidFailToConnectPybricks({
+ reason: BleDeviceFailToConnectReasonType.NoWebBluetooth,
+ }),
bleDidFailToConnectPybricks({ reason: BleDeviceFailToConnectReasonType.Canceled }),
+ bootloaderDidFailToConnect(BootloaderConnectionFailureReason.NoWebBluetooth),
bootloaderDidFailToConnect(BootloaderConnectionFailureReason.Canceled),
didFailToFinish(FailToFinishReasonType.FailedToConnect),
serviceWorkerDidSucceed(),
diff --git a/src/notifications/sagas.ts b/src/notifications/sagas.ts
index 75728fac..b5871dd1 100644
--- a/src/notifications/sagas.ts
+++ b/src/notifications/sagas.ts
@@ -193,16 +193,6 @@ function* showBleDeviceDidFailToConnectError(
case BleDeviceFailToConnectReasonType.NoBluetooth:
yield* showSingleton(Level.Error, I18nId.BleNoBluetooth);
break;
- case BleDeviceFailToConnectReasonType.NoWebBluetooth:
- yield* showSingleton(
- Level.Error,
- I18nId.BleNoWebBluetooth,
- undefined,
- helpAction(
- 'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
- ),
- );
- break;
case BleDeviceFailToConnectReasonType.Unknown:
yield* showUnexpectedError(I18nId.BleUnexpectedError, action.err);
break;
@@ -219,16 +209,6 @@ function* showBootloaderDidFailToConnectError(
hubName: 'LEGO Bootloader',
});
break;
- case BootloaderConnectionFailureReason.NoWebBluetooth:
- yield* showSingleton(
- Level.Error,
- I18nId.BleNoWebBluetooth,
- undefined,
- helpAction(
- 'https://github.com/WebBluetoothCG/web-bluetooth/blob/master/implementation-status.md',
- ),
- );
- break;
case BootloaderConnectionFailureReason.NoBluetooth:
yield* showSingleton(Level.Error, I18nId.BleNoBluetooth);
break;
diff --git a/src/notifications/translations/en.json b/src/notifications/translations/en.json
index 15ed39f6..f03ca51a 100644
--- a/src/notifications/translations/en.json
+++ b/src/notifications/translations/en.json
@@ -5,7 +5,6 @@
"ble": {
"gattPermission": "The web browser did not give permission to use Bluetooth Low Energy",
"gattServiceNotFound": "Connected to hub but failed to get {serviceName} service.\nEnsure that you are using the most recent firmware.\nIf the problem persists, try removing the \"{hubName}\" device in your OS Bluetooth settings, then try connecting again.",
- "noWebBluetooth": "This web browser does not support Web Bluetooth or it is not enabled.",
"noBluetooth": "No Bluetooth adapter could be found. Please connect or enable a Bluetooth Low Energy adapter and restart the browser.",
"unexpectedError": "Unexpected error while trying to connect: {errorMessage}"
},