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`
+

+
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`
+

+
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`