Repository: electron/electron (language: cpp)

Issue:
[Bug]: Web Bluetooth PIN pairing fails on Windows
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a bug report that matches the one I want to file, without success.

### Electron Version

17.0.1

### What operating system are you using?

Windows

### Operating System Version

Windows 10 Pro 1909

### What arch are you using?

x64

### Last Known Working Electron version

_No response_

### Expected Behavior

When pairing a device using the Web Bluetooth API that requires a PIN, I expect the PIN pairing dialog box to appear. 

### Actual Behavior

PIN pairing dialog box does not appear, and operation fails with "GATT error: Not paired."

### Testcase Gist URL

_No response_

### Additional Information

Web Bluetooth PIN pairing has always worked on MacOS, but there was a Chromium bug that [prevented it from working on Windows](https://bugs.chromium.org/p/chromium/issues/detail?id=960258). This bug has since been fixed, at least since Chromium v96.0.4664.110. 


Discussion / hints:
@jkleinsc Anything else I can do to help with this?
@jkleinsc @dsanders11 @codebytere It looks like this also affects devices that pair without a PIN, and still exists on Electron v19. Please let me know if there's anything I can do to help move this forward or make it easier to debug.

Fix patch:
diff --git a/docs/api/session.md b/docs/api/session.md
index 4a578d6263c73..654fe3b751197 100644
--- a/docs/api/session.md
+++ b/docs/api/session.md
@@ -823,6 +823,71 @@ app.whenReady().then(() => {
 })
 ```
 
+#### `ses.setBluetoothPairingHandler(handler)` _Windows_ _Linux_
+
+* `handler` Function | null
+  * `details` Object
+    * `deviceId` string
+    * `pairingKind` string - The type of pairing prompt being requested.
+      One of the following values:
+      * `confirm`
+        This prompt is requesting confirmation that the Bluetooth device should
+        be paired.
+      * `confirmPin`
+        This prompt is requesting confirmation that the provided PIN matches the
+        pin displayed on the device.
+      * `providePin`
+        This prompt is requesting that a pin be provided for the device.
+    * `frame` [WebFrameMain](web-frame-main.md)
+    * `pin` string (optional) - The pin value to verify if `pairingKind` is `confirmPin`.
+  * `callback` Function
+    * `response` Object
+      * `confirmed` boolean - `false` should be passed in if the dialog is canceled.
+        If the `pairingKind` is `confirm` or `confirmPin`, this value should indicate
+        if the pairing is confirmed.  If the `pairingKind` is `providePin` the value
+        should be `true` when a value is provided.
+      * `pin` string | null (optional) - When the `pairingKind` is `providePin`
+        this value should be the required pin for the Bluetooth device.
+
+Sets a handler to respond to Bluetooth pairing requests. This handler
+allows developers to handle devices that require additional validation
+before pairing.  When a handler is not defined, any pairing on Linux or Windows
+that requires additional validation will be automatically cancelled.
+macOS does not require a handler because macOS handles the pairing
+automatically.  To clear the handler, call `setBluetoothPairingHandler(null)`.
+
+```javascript
+
+const { app, BrowserWindow, ipcMain, session } = require('electron')
+
+let bluetoothPinCallback = null
+
+function createWindow () {
+  const mainWindow = new BrowserWindow({
+    webPreferences: {
+      preload: path.join(__dirname, 'preload.js')
+    }
+  })
+}
+
+// Listen for an IPC message from the renderer to get the response for the Bluetooth pairing.
+ipcMain.on('bluetooth-pairing-response', (event, response) => {
+  bluetoothPinCallback(response)
+})
+
+mainWindow.webContents.session.setBluetoothPairingHandler((details, callback) => {
+  bluetoothPinCallback = callback
+  // Send a IPC message to the renderer to prompt the user to confirm the pairing.
+  // Note that this will require logic in the renderer to handle this message and
+  // display a prompt to the user.
+  mainWindow.webContents.send('bluetooth-pairing-request', details)
+})
+
+app.whenReady().then(() => {
+  createWindow()
+})
+```
+
 #### `ses.clearHostResolverCache()`
 
 Returns `Promise<void>` - Resolves when the operation is complete.
diff --git a/docs/fiddles/features/web-bluetooth/main.js b/docs/fiddles/features/web-bluetooth/main.js
index b3cc55a438198..5d560579cc05f 100644
--- a/docs/fiddles/features/web-bluetooth/main.js
+++ b/docs/fiddles/features/web-bluetooth/main.js
@@ -1,10 +1,13 @@
-const {app, BrowserWindow} = require('electron')
+const {app, BrowserWindow, ipcMain} = require('electron')
 const path = require('path')
 
 function createWindow () {
   const mainWindow = new BrowserWindow({
     width: 800,
-    height: 600
+    height: 600,
+    webPreferences: {
+      preload: path.join(__dirname, 'preload.js')
+    }    
   })
 
   mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
@@ -14,6 +17,18 @@ function createWindow () {
     } 
   })
 
+  // Listen for a message from the renderer to get the response for the Bluetooth pairing.
+  ipcMain.on('bluetooth-pairing-response', (event, response) => {
+    bluetoothPinCallback(response)
+  })
+
+  mainWindow.webContents.session.setBluetoothPairingHandler((details, callback) => {
+
+    bluetoothPinCallback = callback
+    // Send a message to the renderer to prompt the user to confirm the pairing.
+    mainWindow.webContents.send('bluetooth-pairing-request', details)
+  })  
+
   mainWindow.loadFile('index.html')
 }
 
diff --git a/docs/fiddles/features/web-bluetooth/preload.js b/docs/fiddles/features/web-bluetooth/preload.js
new file mode 100644
index 0000000000000..0732ea315ff7f
--- /dev/null
+++ b/docs/fiddles/features/web-bluetooth/preload.js
@@ -0,0 +1,6 @@
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI', {
+  bluetoothPairingRequest: (callback) => ipcRenderer.on('bluetooth-pairing-request', callback),
+  bluetoothPairingResponse: (response) => ipcRenderer.send('bluetooth-pairing-respnse', response)
+})
\ No newline at end of file
diff --git a/docs/fiddles/features/web-bluetooth/renderer.js b/docs/fiddles/features/web-bluetooth/renderer.js
index e5830955599af..080fb6105b287 100644
--- a/docs/fiddles/features/web-bluetooth/renderer.js
+++ b/docs/fiddles/features/web-bluetooth/renderer.js
@@ -5,4 +5,30 @@ async function testIt() {
   document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
 }
 
-document.getElementById('clickme').addEventListener('click',testIt)
\ No newline at end of file
+document.getElementById('clickme').addEventListener('click',testIt)
+
+window.electronAPI.bluetoothPairingRequest((event, details) => {
+  const response = {}
+  
+  switch (details.pairingKind) {
+    case 'confirm': {
+      response.confirmed = confirm(`Do you want to connect to device ${details.deviceId}?`)
+      break
+    }
+    case 'confirmPin': {
+      response.confirmed = confirm(`Does the pin ${details.pin} match the pin displayed on device ${details.deviceId}?`)
+      break
+    }
+    case 'providePin': {
+      const pin = prompt(`Please provide a pin for ${details.deviceId}.`)
+      if (pin) {
+        response.pin = pin
+        response.confirmed = true
+      } else {
+        response.confirmed = false
+      }
+    }
+  }
+
+  window.electronAPI.bluetoothPairingResponse(response)
+})
\ No newline at end of file
diff --git a/docs/tutorial/devices.md b/docs/tutorial/devices.md
index cba807891f07b..6f11fec760208 100644
--- a/docs/tutorial/devices.md
+++ b/docs/tutorial/devices.md
@@ -16,6 +16,10 @@ with bluetooth devices. In order to use this API in Electron, developers will
 need to handle the [`select-bluetooth-device` event on the webContents](../api/web-contents.md#event-select-bluetooth-device)
 associated with the device request.
 
+Additionally, [`ses.setBluetoothPairingHandler(handler)`](../api/session.md#sessetbluetoothpairinghandlerhandler-windows-linux)
+can be used to handle pairing to bluetooth devices on Windows or Linux when
+additional validation such as a pin is needed.
+
 ### Example
 
 This example demonstrates an Electron application that automatically selects
diff --git a/filenames.auto.gni b/filenames.auto.gni
index df8837bcf3162..a7c71081f4e84 100644
--- a/filenames.auto.gni
+++ b/filenames.auto.gni
@@ -140,6 +140,7 @@ auto_filenames = {
     "lib/common/define-properties.ts",
     "lib/common/ipc-messages.ts",
     "lib/common/web-view-methods.ts",
+    "lib/common/webpack-globals-provider.ts",
     "lib/renderer/api/context-bridge.ts",
     "lib/renderer/api/crash-reporter.ts",
     "lib/renderer/api/ipc-renderer.ts",
diff --git a/shell/browser/api/electron_api_session.cc b/shell/browser/api/electron_api_session.cc
index f03e00f537a4c..1319249ad84b4 100644
--- a/shell/browser/api/electron_api_session.cc
+++ b/shell/browser/api/electron_api_session.cc
@@ -674,6 +674,18 @@ void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
   permission_manager->SetDevicePermissionHandler(handler);
 }
 
+void Session::SetBluetoothPairingHandler(v8::Local<v8::Value> val,
+                                         gin::Arguments* args) {
+  ElectronPermissionManager::BluetoothPairingHandler handler;
+  if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
+    args->ThrowTypeError("Must pass null or function");
+    return;
+  }
+  auto* permission_manager = static_cast<ElectronPermissionManager*>(
+      browser_context()->GetPermissionControllerDelegate());
+  permission_manager->SetBluetoothPairingHandler(handler);
+}
+
 v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
   v8::Isolate* isolate = args->isolate();
   gin_helper::Promise<void> promise(isolate);
@@ -1225,6 +1237,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
                  &Session::SetDisplayMediaRequestHandler)
       .SetMethod("setDevicePermissionHandler",
                  &Session::SetDevicePermissionHandler)
+      .SetMethod("setBluetoothPairingHandler",
+                 &Session::SetBluetoothPairingHandler)
       .SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
       .SetMethod("clearAuthCache", &Session::ClearAuthCache)
       .SetMethod("allowNTLMCredentialsForDomains",
diff --git a/shell/browser/api/electron_api_session.h b/shell/browser/api/electron_api_session.h
index 164b4c292b644..45f3d82e4f908 100644
--- a/shell/browser/api/electron_api_session.h
+++ b/shell/browser/api/electron_api_session.h
@@ -105,6 +105,8 @@ class Session : public gin::Wrappable<Session>,
                                  gin::Arguments* args);
   void SetDevicePermissionHandler(v8::Local<v8::Value> val,
                                   gin::Arguments* args);
+  void SetBluetoothPairingHandler(v8::Local<v8::Value> val,
+                                  gin::Arguments* args);
   v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);
   v8::Local<v8::Promise> ClearAuthCache();
   void AllowNTLMCredentialsForDomains(const std::string& domains);
diff --git a/shell/browser/bluetooth/electron_bluetooth_delegate.cc b/shell/browser/bluetooth/electron_bluetooth_delegate.cc
index 340cb5a4a35f2..6cfc775041553 100644
--- a/shell/browser/bluetooth/electron_bluetooth_delegate.cc
+++ b/shell/browser/bluetooth/electron_bluetooth_delegate.cc
@@ -7,6 +7,7 @@
 #include <memory>
 #include <utility>
 
+#include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
 #include "content/public/browser/render_frame_host.h"
@@ -14,7 +15,10 @@
 #include "device/bluetooth/bluetooth_device.h"
 #include "device/bluetooth/public/cpp/bluetooth_uuid.h"
 #include "shell/browser/api/electron_api_web_contents.h"
+#include "shell/browser/electron_permission_manager.h"
 #include "shell/browser/lib/bluetooth_chooser.h"
+#include "shell/common/gin_converters/frame_converter.h"
+#include "shell/common/gin_helper/dictionary.h"
 #include "third_party/blink/public/common/bluetooth/web_bluetooth_device_id.h"
 #include "third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom.h"
 
@@ -23,6 +27,28 @@ using content::RenderFrameHost;
 using content::WebContents;
 using device::BluetoothUUID;
 
+namespace gin {
+
+template <>
+struct Converter<content::BluetoothDelegate::PairingKind> {
+  static v8::Local<v8::Value> ToV8(
+      v8::Isolate* isolate,
+      content::BluetoothDelegate::PairingKind pairing_kind) {
+    switch (pairing_kind) {
+      case content::BluetoothDelegate::PairingKind::kConfirmOnly:
+        return StringToV8(isolate, "confirm");
+      case content::BluetoothDelegate::PairingKind::kConfirmPinMatch:
+        return StringToV8(isolate, "confirmPin");
+      case content::BluetoothDelegate::PairingKind::kProvidePin:
+        return StringToV8(isolate, "providePin");
+      default:
+        return StringToV8(isolate, "unknown");
+    }
+  }
+};
+
+}  // namespace gin
+
 namespace electron {
 
 ElectronBluetoothDelegate::ElectronBluetoothDelegate() = default;
@@ -136,9 +162,46 @@ void ElectronBluetoothDelegate::ShowDevicePairPrompt(
     PairPromptCallback callback,
     PairingKind pairing_kind,
     const absl::optional<std::u16string>& pin) {
-  NOTIMPLEMENTED();
-  std::move(callback).Run(BluetoothDelegate::PairPromptResult(
-      BluetoothDelegate::PairPromptStatus::kCancelled));
+  auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
+  if (web_contents) {
+    auto* permission_manager = static_cast<ElectronPermissionManager*>(
+        web_contents->GetBrowserContext()->GetPermissionControllerDelegate());
+
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::HandleScope scope(isolate);
+    gin_helper::Dictionary details =
+        gin_helper::Dictionary::CreateEmpty(isolate);
+    details.Set("deviceId", device_identifier);
+    details.Set("pairingKind", pairing_kind);
+    details.SetGetter("frame", frame);
+    if (pin.has_value()) {
+      details.Set("pin", pin.value());
+    }
+
+    permission_manager->CheckBluetoothDevicePair(
+        details, base::AdaptCallbackForRepeating(base::BindOnce(
+                     &ElectronBluetoothDelegate::OnDevicePairPromptResponse,
+                     weak_factory_.GetWeakPtr(), std::move(callback))));
+  }
+}
+
+void ElectronBluetoothDelegate::OnDevicePairPromptResponse(
+    PairPromptCallback callback,
+    base::Value::Dict response) {
+  BluetoothDelegate::PairPromptResult result;
+  if (response.FindBool("confirmed").value_or(false)) {
+    result.result_code = BluetoothDelegate::PairPromptStatus::kSuccess;
+  } else {
+    result.result_code = BluetoothDelegate::PairPromptStatus::kCancelled;
+  }
+
+  const std::string* pin = response.FindString("pin");
+  if (pin) {
+    std::u16string trimmed_input = base::UTF8ToUTF16(*pin);
+    base::TrimWhitespace(trimmed_input, base::TRIM_ALL, &trimmed_input);
+    result.pin = base::UTF16ToUTF8(trimmed_input);
+  }
+  std::move(callback).Run(result);
 }
 
 }  // namespace electron
diff --git a/shell/browser/bluetooth/electron_bluetooth_delegate.h b/shell/browser/bluetooth/electron_bluetooth_delegate.h
index df4f07703e815..8328f45636ad3 100644
--- a/shell/browser/bluetooth/electron_bluetooth_delegate.h
+++ b/shell/browser/bluetooth/electron_bluetooth_delegate.h
@@ -9,6 +9,7 @@
 #include <string>
 #include <vector>
 
+#include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "base/scoped_observation.h"
 #include "content/public/browser/bluetooth_delegate.h"
@@ -91,6 +92,12 @@ class ElectronBluetoothDelegate : public content::BluetoothDelegate {
   void AddFramePermissionObserver(FramePermissionObserver* observer) override;
   void RemoveFramePermissionObserver(
       FramePermissionObserver* observer) override;
+
+ private:
+  void OnDevicePairPromptResponse(PairPromptCallback callback,
+                                  base::Value::Dict response);
+
+  base::WeakPtrFactory<ElectronBluetoothDelegate> weak_factory_{this};
 };
 
 }  // namespace electron
diff --git a/shell/browser/electron_permission_manager.cc b/shell/browser/electron_permission_manager.cc
index 8b3d4d50df02f..995a704bb6503 100644
--- a/shell/browser/electron_permission_manager.cc
+++ b/shell/browser/electron_permission_manager.cc
@@ -130,6 +130,11 @@ void ElectronPermissionManager::SetDevicePermissionHandler(
   device_permission_handler_ = handler;
 }
 
+void ElectronPermissionManager::SetBluetoothPairingHandler(
+    const BluetoothPairingHandler& handler) {
+  bluetooth_pairing_handler_ = handler;
+}
+
 void ElectronPermissionManager::RequestPermission(
     blink::PermissionType permission,
     content::RenderFrameHost* render_frame_host,
@@ -276,6 +281,18 @@ ElectronPermissionManager::SubscribePermissionStatusChange(
 void ElectronPermissionManager::UnsubscribePermissionStatusChange(
     SubscriptionId id) {}
 
+void ElectronPermissionManager::CheckBluetoothDevicePair(
+    gin_helper::Dictionary details,
+    PairCallback pair_callback) const {
+  if (bluetooth_pairing_handler_.is_null()) {
+    base::Value::Dict response;
+    response.Set("confirmed", false);
+    std::move(pair_callback).Run(std::move(response));
+  } else {
+    bluetooth_pairing_handler_.Run(details, std::move(pair_callback));
+  }
+}
+
 bool ElectronPermissionManager::CheckPermissionWithDetails(
     blink::PermissionType permission,
     content::RenderFrameHost* render_frame_host,
diff --git a/shell/browser/electron_permission_manager.h b/shell/browser/electron_permission_manager.h
index 00cbb81e9f733..3aa980ea22d71 100644
--- a/shell/browser/electron_permission_manager.h
+++ b/shell/browser/electron_permission_manager.h
@@ -13,6 +13,7 @@
 #include "content/public/browser/permission_controller_delegate.h"
 #include "gin/dictionary.h"
 #include "shell/browser/electron_browser_context.h"
+#include "shell/common/gin_helper/dictionary.h"
 
 namespace base {
 class Value;
@@ -38,6 +39,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
       base::OnceCallback<void(blink::mojom::PermissionStatus)>;
   using StatusesCallback = base::OnceCallback<void(
       const std::vector<blink::mojom::PermissionStatus>&)>;
+  using PairCallback = base::OnceCallback<void(base::Value::Dict)>;
   using RequestHandler = base::RepeatingCallback<void(content::WebContents*,
                                                       blink::PermissionType,
                                                       StatusCallback,
@@ -50,11 +52,14 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
 
   using DeviceCheckHandler =
       base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
+  using BluetoothPairingHandler =
+      base::RepeatingCallback<void(gin_helper::Dictionary, PairCallback)>;
 
   // Handler to dispatch permission requests in JS.
   void SetPermissionRequestHandler(const RequestHandler& handler);
   void SetPermissionCheckHandler(const CheckHandler& handler);
   void SetDevicePermissionHandler(const DeviceCheckHandler& handler);
+  void SetBluetoothPairingHandler(const BluetoothPairingHandler& handler);
 
   // content::PermissionControllerDelegate:
   void RequestPermission(blink::PermissionType permission,
@@ -81,6 +86,9 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
       base::Value::Dict details,
       StatusesCallback callback);
 
+  void CheckBluetoothDevicePair(gin_helper::Dictionary details,
+                                PairCallback pair_callback) const;
+
   bool CheckPermissionWithDetails(blink::PermissionType permission,
                                   content::RenderFrameHost* render_frame_host,
                                   const GURL& requesting_origin,
@@ -147,6 +155,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
   RequestHandler request_handler_;
   CheckHandler check_handler_;
   DeviceCheckHandler device_permission_handler_;
+  BluetoothPairingHandler bluetooth_pairing_handler_;
 
   PendingRequestsMap pending_requests_;
 };


Test patch: