From f8fb5d7bf2fb88d819c06bd490a3782b9fda3d68 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 22 Nov 2024 08:55:37 +0100 Subject: [PATCH] Add cloud signup to voice flow (#22941) --- .../cloud/cloud-step-intro.ts | 174 ++++++++++++++ .../cloud/cloud-step-signin.ts | 167 ++++++++++++++ .../cloud/cloud-step-signup.ts | 212 ++++++++++++++++++ .../voice-assistant-setup-step-cloud.ts | 191 +++------------- 4 files changed, 590 insertions(+), 154 deletions(-) create mode 100644 src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts create mode 100644 src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts create mode 100644 src/dialogs/voice-assistant-setup/cloud/cloud-step-signup.ts diff --git a/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts b/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts new file mode 100644 index 0000000000..ba3d4b0991 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts @@ -0,0 +1,174 @@ +import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js"; +import { LitElement, css, html } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button"; +import "../../../components/ha-svg-icon"; +import type { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { AssistantSetupStyles } from "../styles"; + +@customElement("cloud-step-intro") +export class CloudStepIntro extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + render() { + return html`
+ Nabu Casa logo +

The power of Home Assistant Cloud

+
+
+
+
+ +
+
+

+ ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.cloud.features.speech.title" + )} + +

+

+ ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.cloud.features.speech.text" + )} +

+
+
+
+
+ +
+
+

+ Remote access + +

+

+ Secure remote access to your system while supporting the + development of Home Assistant. +

+
+
+
+ Google Assistant + Amazon Alexa +
+

+ ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text" + )} +

+
+
+
+ `; + } + + private _signUp() { + fireEvent(this, "cloud-step", { step: "SIGNUP" }); + } + + static styles = [ + AssistantSetupStyles, + css` + :host { + display: flex; + } + .features { + display: flex; + flex-direction: column; + grid-gap: 16px; + padding: 16px; + } + .feature { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + margin-bottom: 16px; + } + .feature .logos { + margin-bottom: 16px; + } + .feature .logos > * { + width: 40px; + height: 40px; + margin: 0 4px; + } + .round-icon { + border-radius: 50%; + color: #6e41ab; + background-color: #e8dcf7; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + } + .access .round-icon { + color: #00aef8; + background-color: #cceffe; + } + .feature h2 { + font-weight: 500; + font-size: 16px; + line-height: 24px; + margin-top: 0; + margin-bottom: 8px; + } + .feature p { + font-weight: 400; + font-size: 14px; + line-height: 20px; + margin: 0; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "cloud-step-intro": CloudStepIntro; + } +} diff --git a/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts b/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts new file mode 100644 index 0000000000..4d17b25693 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/cloud/cloud-step-signin.ts @@ -0,0 +1,167 @@ +import { LitElement, css, html } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { navigate } from "../../../common/navigate"; +import "../../../components/ha-alert"; +import "../../../components/ha-button"; +import "../../../components/ha-password-field"; +import type { HaPasswordField } from "../../../components/ha-password-field"; +import "../../../components/ha-svg-icon"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { cloudLogin } from "../../../data/cloud"; +import type { HomeAssistant } from "../../../types"; +import { showAlertDialog } from "../../generic/show-dialog-box"; +import { AssistantSetupStyles } from "../styles"; + +@customElement("cloud-step-signin") +export class CloudStepSignin extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _requestInProgress = false; + + @state() private _error?: string; + + @query("#email", true) private _emailField!: HaTextField; + + @query("#password", true) private _passwordField!: HaPasswordField; + + render() { + return html`
+ Nabu Casa logo +

Sign in

+ ${this._error + ? html`${this._error}` + : ""} + + +
+ `; + } + + private _keyDown(ev: KeyboardEvent) { + if (ev.key === "Enter") { + this._handleLogin(); + } + } + + private async _handleLogin() { + const emailField = this._emailField; + const passwordField = this._passwordField; + + const email = emailField.value; + const password = passwordField.value; + + if (!emailField.reportValidity()) { + passwordField.reportValidity(); + emailField.focus(); + return; + } + + if (!passwordField.reportValidity()) { + passwordField.focus(); + return; + } + + this._requestInProgress = true; + + const doLogin = async (username: string) => { + try { + await cloudLogin(this.hass, username, password); + } catch (err: any) { + const errCode = err && err.body && err.body.code; + + if (errCode === "usernotfound" && username !== username.toLowerCase()) { + await doLogin(username.toLowerCase()); + return; + } + + if (errCode === "PasswordChangeRequired") { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.cloud.login.alert_password_change_required" + ), + }); + navigate("/config/cloud/forgot-password"); + fireEvent(this, "closed"); + return; + } + + this._requestInProgress = false; + + if (errCode === "UserNotConfirmed") { + this._error = this.hass.localize( + "ui.panel.config.cloud.login.alert_email_confirm_necessary" + ); + } else { + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } + + emailField.focus(); + } + }; + + await doLogin(email); + } + + static styles = [ + AssistantSetupStyles, + css` + :host { + display: block; + } + ha-textfield, + ha-password-field { + display: block; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "cloud-step-signin": CloudStepSignin; + } +} diff --git a/src/dialogs/voice-assistant-setup/cloud/cloud-step-signup.ts b/src/dialogs/voice-assistant-setup/cloud/cloud-step-signup.ts new file mode 100644 index 0000000000..0ec6ef0a69 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/cloud/cloud-step-signup.ts @@ -0,0 +1,212 @@ +import { LitElement, css, html } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-alert"; +import "../../../components/ha-button"; +import "../../../components/ha-password-field"; +import type { HaPasswordField } from "../../../components/ha-password-field"; +import "../../../components/ha-svg-icon"; +import "../../../components/ha-textfield"; +import type { HaTextField } from "../../../components/ha-textfield"; +import { + cloudLogin, + cloudRegister, + cloudResendVerification, +} from "../../../data/cloud"; +import type { HomeAssistant } from "../../../types"; +import { AssistantSetupStyles } from "../styles"; + +@customElement("cloud-step-signup") +export class CloudStepSignup extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _requestInProgress = false; + + @state() private _email?: string; + + @state() private _password?: string; + + @state() private _error?: string; + + @state() private _state?: "VERIFY"; + + @query("#email", true) private _emailField!: HaTextField; + + @query("#password", true) private _passwordField!: HaPasswordField; + + render() { + return html`
+ Nabu Casa logo +

Sign up

+ ${this._error + ? html`${this._error}` + : ""} + ${this._state === "VERIFY" + ? html`

+ Check the email we just sent to ${this._email} and click the + confirmation link to continue. +

` + : html` + `} +
+ `; + } + + private _signIn() { + fireEvent(this, "cloud-step", { step: "SIGNIN" }); + } + + private _keyDown(ev: KeyboardEvent) { + if (ev.key === "Enter") { + this._handleRegister(); + } + } + + private async _handleRegister() { + const emailField = this._emailField; + const passwordField = this._passwordField; + + if (!emailField.reportValidity()) { + passwordField.reportValidity(); + emailField.focus(); + return; + } + + if (!passwordField.reportValidity()) { + passwordField.focus(); + return; + } + + const email = emailField.value.toLowerCase(); + const password = passwordField.value; + + this._requestInProgress = true; + + try { + await cloudRegister(this.hass, email, password); + this._email = email; + this._password = password; + this._verificationEmailSent(); + } catch (err: any) { + this._password = ""; + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } finally { + this._requestInProgress = false; + } + } + + private async _handleResendVerifyEmail() { + if (!this._email) { + return; + } + try { + await cloudResendVerification(this.hass, this._email); + this._verificationEmailSent(); + } catch (err: any) { + this._error = + err && err.body && err.body.message + ? err.body.message + : "Unknown error"; + } + } + + private _verificationEmailSent() { + this._state = "VERIFY"; + + setTimeout(() => this._login(), 5000); + } + + private async _login() { + if (!this._email || !this._password) { + return; + } + + try { + await cloudLogin(this.hass, this._email, this._password); + fireEvent(this, "cloud-step", { step: "DONE" }); + } catch (e: any) { + if (e?.body?.code === "usernotconfirmed") { + this._verificationEmailSent(); + } else { + this._error = "Something went wrong. Please try again."; + } + } + } + + static styles = [ + AssistantSetupStyles, + css` + .content { + width: 100%; + } + ha-textfield, + ha-password-field { + display: block; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "cloud-step-signup": CloudStepSignup; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts index 5162ed97ee..793d2dd3ee 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts @@ -1,171 +1,54 @@ -import { mdiEarth, mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-button"; -import "../../components/ha-svg-icon"; +import { html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; import type { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; -import { AssistantSetupStyles } from "./styles"; +import "./cloud/cloud-step-intro"; +import "./cloud/cloud-step-signin"; +import "./cloud/cloud-step-signup"; +import { fireEvent } from "../../common/dom/fire_event"; +import { STEP } from "./voice-assistant-setup-dialog"; @customElement("ha-voice-assistant-setup-step-cloud") export class HaVoiceAssistantSetupStepCloud extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _state: "SIGNUP" | "SIGNIN" | "INTRO" = "INTRO"; + protected override render() { - return html`
- Nabu Casa logo -

The power of Home Assistant Cloud

-
-
-
-
- -
-
-

- ${this.hass.localize( - "ui.panel.config.voice_assistants.assistants.cloud.features.speech.title" - )} - -

-

- ${this.hass.localize( - "ui.panel.config.voice_assistants.assistants.cloud.features.speech.text" - )} -

-
-
-
-
- -
-
-

- Remote access - -

-

- Secure remote access to your system while supporting the - development of Home Assistant. -

-
-
-
- Google Assistant - Amazon Alexa -
-

- ${this.hass.localize( - "ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title" - )} -

-

- ${this.hass.localize( - "ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text" - )} -

-
-
-
- `; + if (this._state === "SIGNUP") { + return html``; + } + if (this._state === "SIGNIN") { + return html``; + } + return html``; } - private _close() { - fireEvent(this, "closed"); + private _cloudStep(ev) { + if (ev.detail.step === "DONE") { + fireEvent(this, "next-step", { + step: STEP.PIPELINE, + noPrevious: true, + }); + return; + } + this._state = ev.detail.step; } - - static styles = [ - AssistantSetupStyles, - css` - .features { - display: flex; - flex-direction: column; - grid-gap: 16px; - padding: 16px; - } - .feature { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - margin-bottom: 16px; - } - .feature .logos { - margin-bottom: 16px; - } - .feature .logos > * { - width: 40px; - height: 40px; - margin: 0 4px; - } - .round-icon { - border-radius: 50%; - color: #6e41ab; - background-color: #e8dcf7; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; - } - .access .round-icon { - color: #00aef8; - background-color: #cceffe; - } - .feature h2 { - font-weight: 500; - font-size: 16px; - line-height: 24px; - margin-top: 0; - margin-bottom: 8px; - } - .feature p { - font-weight: 400; - font-size: 14px; - line-height: 20px; - margin: 0; - } - `, - ]; } declare global { interface HTMLElementTagNameMap { "ha-voice-assistant-setup-step-cloud": HaVoiceAssistantSetupStepCloud; } + interface HASSDomEvents { + "cloud-step": { step: "SIGNUP" | "SIGNIN" | "DONE" }; + } }