diff --git a/README.md b/README.md index b99bc1f..6b53b02 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,16 @@ It is possible to customize the button and the message. You do this by putting y ``` +## Events + +When the state of provisioning changes, a `state-changed` event is fired. + +A `state-changed` event contains the following information: + +Field | Description +-- | -- +state | The current state (`CONNECTING`, `AUTHORIZATION_REQUIRED`, `AUTHORIZED`, `PROVISIONING`, `PROVISIONED`, `ERROR`, `UNKNOWN`) + ## Browser Support This SDK requires a browser with support for WebBluetooth. Currently this is supported by Google Chrome, Microsoft Edge and other browsers based on the Blink engine. diff --git a/example.html b/example.html index 06bdd44..a69f0eb 100644 --- a/example.html +++ b/example.html @@ -13,6 +13,16 @@ ⚠️ Browser set-up not supported +

Events:

+

     
+    
   
 
diff --git a/src/const.ts b/src/const.ts
index 3f5cd5f..716167e 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -11,7 +11,16 @@ export const IMPROV_BLE_RPC_RESULT_CHARACTERISTIC =
 export const IMPROV_BLE_CAPABILITIES_CHARACTERISTIC =
   "00467768-6228-2272-4663-277478268005";
 
-export const enum ImprovCurrentState {
+export type State = "CONNECTING" | "IMPROV-STATE" | "ERROR";
+
+export interface ImprovState {
+  state:
+    | Omit
+    | keyof typeof ImprovCurrentState
+    | "UNKNOWN";
+}
+
+export enum ImprovCurrentState {
   AUTHORIZATION_REQUIRED = 0x01,
   AUTHORIZED = 0x02,
   PROVISIONING = 0x03,
@@ -39,3 +48,9 @@ export interface ImprovRPCResult {
 
 export const hasIdentifyCapability = (capabilities: number) =>
   (capabilities & 1) === 1;
+
+declare global {
+  interface HTMLElementEventMap {
+    "state-changed": CustomEvent;
+  }
+}
diff --git a/src/launch-button.ts b/src/launch-button.ts
index ff5381b..4922e64 100644
--- a/src/launch-button.ts
+++ b/src/launch-button.ts
@@ -75,7 +75,7 @@ export class LaunchButton extends HTMLElement {
     slot.addEventListener("click", async (ev) => {
       ev.preventDefault();
       const mod = await import("./provision");
-      mod.startProvisioning();
+      mod.startProvisioning(this);
     });
 
     if (
diff --git a/src/provision-dialog.ts b/src/provision-dialog.ts
index 0bb5531..0ee5575 100644
--- a/src/provision-dialog.ts
+++ b/src/provision-dialog.ts
@@ -1,4 +1,4 @@
-import { LitElement, html, PropertyValues, css } from "lit";
+import { LitElement, html, PropertyValues, css, TemplateResult } from "lit";
 import { customElement, query, state } from "lit/decorators.js";
 import "@material/mwc-dialog";
 import "@material/mwc-textfield";
@@ -16,6 +16,8 @@ import {
   IMPROV_BLE_RPC_RESULT_CHARACTERISTIC,
   IMPROV_BLE_SERVICE,
   ImprovRPCResult,
+  State,
+  ImprovState,
 } from "./const";
 
 const ERROR_ICON = "⚠️";
@@ -27,8 +29,9 @@ const DEBUG = false;
 class ProvisionDialog extends LitElement {
   public device!: BluetoothDevice;
 
-  @state() private _state: "connecting" | "improv-state" | "error" =
-    "connecting";
+  public stateUpdateCallback!: (state: ImprovState) => void;
+
+  @state() private _state: State = "CONNECTING";
 
   @state() private _improvCurrentState?: ImprovCurrentState | undefined;
   @state() private _improvErrorState = ImprovErrorState.NO_ERROR;
@@ -52,14 +55,14 @@ class ProvisionDialog extends LitElement {
   @query("mwc-textfield[name=password]") private _inputPassword!: TextField;
 
   protected render() {
-    let heading;
-    let content;
+    let heading: string = "";
+    let content: TemplateResult;
     let hideActions = false;
 
-    if (this._state === "connecting") {
+    if (this._state === "CONNECTING") {
       content = this._renderProgress("Connecting");
       hideActions = true;
-    } else if (this._state === "error") {
+    } else if (this._state === "ERROR") {
       content = this._renderMessage(
         ERROR_ICON,
         `An error occurred. ${this._error}`,
@@ -227,12 +230,12 @@ class ProvisionDialog extends LitElement {
     this.device.addEventListener("gattserverdisconnected", () => {
       // If we're provisioned, we expect to be disconnected.
       if (
-        this._state === "improv-state" &&
+        this._state === "IMPROV-STATE" &&
         this._improvCurrentState === ImprovCurrentState.PROVISIONED
       ) {
         return;
       }
-      this._state = "error";
+      this._state = "ERROR";
       this._error = "Device disconnected.";
     });
     this._connect();
@@ -241,9 +244,23 @@ class ProvisionDialog extends LitElement {
   protected updated(changedProps: PropertyValues) {
     super.updated(changedProps);
 
+    if (
+      changedProps.has("_state") ||
+      (this._state === "IMPROV-STATE" &&
+        changedProps.has("_improvCurrentState"))
+    ) {
+      const state =
+        this._state === "IMPROV-STATE"
+          ? (ImprovCurrentState[
+              this._improvCurrentState!
+            ] as keyof typeof ImprovCurrentState) || "UNKNOWN"
+          : this._state;
+      this.stateUpdateCallback({ state });
+    }
+
     if (
       (changedProps.has("_improvCurrentState") || changedProps.has("_state")) &&
-      this._state === "improv-state" &&
+      this._state === "IMPROV-STATE" &&
       this._improvCurrentState === ImprovCurrentState.AUTHORIZED
     ) {
       const input = this._inputSSID;
@@ -309,9 +326,9 @@ class ProvisionDialog extends LitElement {
 
       this._handleImprovCurrentStateChange(curState);
       this._handleImprovErrorStateChange(errorState);
-      this._state = "improv-state";
+      this._state = "IMPROV-STATE";
     } catch (err) {
-      this._state = "error";
+      this._state = "ERROR";
       this._error = `Unable to establish a connection: ${err}`;
     }
   }
diff --git a/src/provision.ts b/src/provision.ts
index 4e80cf6..239320e 100644
--- a/src/provision.ts
+++ b/src/provision.ts
@@ -1,7 +1,9 @@
 import { IMPROV_BLE_SERVICE } from "./const";
+import { LaunchButton } from "./launch-button";
 import "./provision-dialog";
+import { fireEvent } from "./util";
 
-export const startProvisioning = async () => {
+export const startProvisioning = async (button: LaunchButton) => {
   let device: BluetoothDevice | undefined;
   try {
     device = await navigator.bluetooth.requestDevice({
@@ -17,5 +19,8 @@ export const startProvisioning = async () => {
 
   const el = document.createElement("improv-wifi-provision-dialog");
   el.device = device;
+  el.stateUpdateCallback = (state) => {
+    fireEvent(button, "state-changed", state);
+  };
   document.body.appendChild(el);
 };
diff --git a/src/util.ts b/src/util.ts
new file mode 100644
index 0000000..184a38b
--- /dev/null
+++ b/src/util.ts
@@ -0,0 +1,20 @@
+export const fireEvent = (
+  eventTarget: EventTarget,
+  type: Event,
+  // @ts-ignore
+  detail?: HTMLElementEventMap[Event]["detail"],
+  options?: {
+    bubbles?: boolean;
+    cancelable?: boolean;
+    composed?: boolean;
+  }
+): void => {
+  options = options || {};
+  const event = new CustomEvent(type, {
+    bubbles: options.bubbles === undefined ? true : options.bubbles,
+    cancelable: Boolean(options.cancelable),
+    composed: options.composed === undefined ? true : options.composed,
+    detail,
+  });
+  eventTarget.dispatchEvent(event);
+};