import { LitElement, html, PropertyValues, css, TemplateResult } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import "./components/ib-dialog";
import "./components/ib-textfield";
import "./components/ib-button";
import "./components/ib-circular-progress";
import type { IbTextfield } from "./components/ib-textfield";
import {
hasIdentifyCapability,
ImprovCurrentState,
ImprovErrorState,
State,
ImprovState,
} from "./const";
import { ImprovBluetoothLE } from "./ble";
const ERROR_ICON = "⚠️";
const OK_ICON = "🎉";
const AUTHORIZE_ICON = "👉";
@customElement("improv-wifi-provision-dialog")
class ProvisionDialog extends LitElement {
public device!: BluetoothDevice;
public stateUpdateCallback!: (state: ImprovState) => void;
@state() private _state: State = "CONNECTING";
@state() private _improvCurrentState?: ImprovCurrentState | undefined;
@state() private _improvErrorState = ImprovErrorState.NO_ERROR;
@state() private _improvCapabilities = 0;
@state() private _busy = false;
private _error?: string;
@query("ib-textfield[name=ssid]") private _inputSSID!: IbTextfield;
@query("ib-textfield[name=password]") private _inputPassword!: IbTextfield;
private __client?: ImprovBluetoothLE;
private get _client(): ImprovBluetoothLE {
if (!this.__client) {
this.__client = new ImprovBluetoothLE(this.device, console);
}
return this.__client;
}
protected render() {
let heading: string = "";
let content: TemplateResult;
let hideActions = false;
if (this._state === "CONNECTING") {
content = this._renderProgress("Connecting");
hideActions = true;
} else if (this._state === "ERROR") {
content = this._renderMessage(
ERROR_ICON,
`An error occurred. ${this._error}`,
true
);
} else if (
this._improvCurrentState === ImprovCurrentState.AUTHORIZATION_REQUIRED
) {
content = this._renderMessage(
AUTHORIZE_ICON,
"Press the authorize button on the device",
false
);
} else if (this._improvCurrentState === ImprovCurrentState.AUTHORIZED) {
if (this._busy) {
content = this._renderProgress("Provisioning");
hideActions = true;
} else {
heading = "Configure Wi-Fi";
content = this._renderImprovAuthorized();
}
} else if (this._improvCurrentState === ImprovCurrentState.PROVISIONING) {
content = this._renderProgress("Provisioning");
hideActions = true;
} else if (this._improvCurrentState === ImprovCurrentState.PROVISIONED) {
content = this._renderImprovProvisioned();
} else {
content = this._renderMessage(
ERROR_ICON,
`Unexpected state: ${this._state} - ${this._improvCurrentState}`,
true
);
}
return html`
${content}
`;
}
_renderProgress(label: string) {
return html`
`;
}
_renderMessage(icon: string, label: string, showClose: boolean) {
return html`
${showClose
? html`
`
: ``}
`;
}
private _renderImprovAuthorized() {
let error: string | undefined;
switch (this._improvErrorState) {
case ImprovErrorState.UNABLE_TO_CONNECT:
error = "Unable to connect";
break;
case ImprovErrorState.NO_ERROR:
break;
default:
error = `Unknown error (${this._improvErrorState})`;
}
return html`
Enter the credentials of the Wi-Fi network that you want
${this._client.name || "your device"} to connect to.
${hasIdentifyCapability(this._improvCapabilities)
? html`
Identify the device.
`
: ""}
${error ? html`${error}
` : ""}
`;
}
private _renderImprovProvisioned() {
return html`
${this._client.nextUrl === undefined
? html`
`
: html`
`}
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._client.addEventListener("state-changed", () => {
this._state = "IMPROV-STATE";
this._busy = false;
this._improvCurrentState = this._client.currentState;
});
this._client.addEventListener("error-changed", () => {
this._improvErrorState = this._client.errorState;
// Sending an RPC command sets error to no error.
// If we get a real error it means the RPC command is done.
if (this._improvErrorState !== ImprovErrorState.NO_ERROR) {
this._busy = false;
}
});
this._client.addEventListener("disconnect", () => {
// If we're provisioned, we expect to be disconnected.
if (
this._state === "IMPROV-STATE" &&
this._improvCurrentState === ImprovCurrentState.PROVISIONED
) {
return;
}
this._state = "ERROR";
this._error = "Device disconnected.";
});
this._connect();
}
private async _connect() {
try {
await this._client.initialize();
this._improvCurrentState = this._client.currentState;
this._improvErrorState = this._client.errorState;
this._improvCapabilities = this._client.capabilities;
this._state = "IMPROV-STATE";
} catch (err: any) {
this._state = "ERROR";
this._error = err.message;
}
}
private async _provision() {
this._busy = true;
try {
await this._client.provision(
this._inputSSID.value,
this._inputPassword.value
);
} catch (err) {
// Ignore, error state takes care of this.
} finally {
this._busy = false;
}
}
private _identify() {
this._client.identify();
}
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._improvCurrentState === ImprovCurrentState.AUTHORIZED
) {
const input = this._inputSSID;
input.updateComplete.then(() => input.focus());
}
}
private _handleClose() {
this._client.close();
this.parentNode!.removeChild(this);
}
static styles = css`
:host {
--mdc-dialog-max-width: 390px;
--mdc-theme-primary: var(--improv-primary-color, #03a9f4);
--mdc-theme-on-primary: var(--improv-on-primary-color, #fff);
}
ib-textfield {
display: block;
}
ib-textfield {
margin-top: 16px;
}
.center {
text-align: center;
}
ib-circular-progress {
margin-bottom: 16px;
}
a.has-button {
text-decoration: none;
}
.icon {
font-size: 50px;
line-height: 80px;
color: black;
}
.error {
color: #db4437;
}
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"improv-wifi-provision-dialog": ProvisionDialog;
}
}