Split up onboarding code (#3158)

This commit is contained in:
Paulus Schoutsen 2019-05-04 11:59:43 -07:00 committed by GitHub
parent fcdb1b48a2
commit db65af9c22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 285 additions and 175 deletions

View File

@ -1,12 +1,19 @@
import { handleFetchPromise } from "../util/hass-call-api"; import { handleFetchPromise } from "../util/hass-call-api";
export interface OnboardingStep { export interface OnboardingUserStepResponse {
step: string; auth_code: string;
done: boolean;
} }
interface UserStepResponse { export interface OnboardingResponses {
auth_code: string; user: OnboardingUserStepResponse;
bla: number;
}
export type ValidOnboardingStep = keyof OnboardingResponses;
export interface OnboardingStep {
step: ValidOnboardingStep;
done: boolean;
} }
export const fetchOnboardingOverview = () => export const fetchOnboardingOverview = () =>
@ -18,7 +25,7 @@ export const onboardUserStep = (params: {
username: string; username: string;
password: string; password: string;
}) => }) =>
handleFetchPromise<UserStepResponse>( handleFetchPromise<OnboardingUserStepResponse>(
fetch("/api/onboarding/users", { fetch("/api/onboarding/users", {
method: "POST", method: "POST",
credentials: "same-origin", credentials: "same-origin",

View File

@ -1,137 +1,67 @@
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import { import {
LitElement, LitElement,
CSSResult,
css,
html, html,
PropertyValues, PropertyValues,
property,
customElement, customElement,
TemplateResult, TemplateResult,
property,
} from "lit-element"; } from "lit-element";
import { genClientId } from "home-assistant-js-websocket"; import { genClientId } from "home-assistant-js-websocket";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { OnboardingStep, onboardUserStep } from "../data/onboarding"; import {
import { PolymerChangedEvent } from "../polymer-types"; OnboardingStep,
ValidOnboardingStep,
OnboardingResponses,
} from "../data/onboarding";
import { registerServiceWorker } from "../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import { HASSDomEvent } from "../common/dom/fire_event";
import "./onboarding-create-user";
import "./onboarding-loading";
interface OnboardingEvent<T extends ValidOnboardingStep> {
type: T;
result: OnboardingResponses[T];
}
declare global {
interface HASSDomEvents {
"onboarding-step": OnboardingEvent<ValidOnboardingStep>;
}
interface GlobalEventHandlersEventMap {
"onboarding-step": HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>;
}
}
@customElement("ha-onboarding") @customElement("ha-onboarding")
class HaOnboarding extends litLocalizeLiteMixin(LitElement) { class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
public translationFragment = "page-onboarding"; public translationFragment = "page-onboarding";
@property() private _name = ""; @property() private _steps?: OnboardingStep[];
@property() private _username = "";
@property() private _password = "";
@property() private _passwordConfirm = "";
@property() private _loading = false;
@property() private _errorMsg?: string = undefined;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this._steps) {
return html` return html`
<p> <onboarding-loading></onboarding-loading>
${this.localize("ui.panel.page-onboarding.intro")} `;
</p>
<p>
${this.localize("ui.panel.page-onboarding.user.intro")}
</p>
${
this._errorMsg
? html`
<p class="error">
${this.localize(
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
) || this._errorMsg}
</p>
`
: ""
} }
const step = this._steps.find((stp) => !stp.done)!;
<form> if (step.step === "user") {
<paper-input return html`
autofocus <onboarding-create-user
name="name" .localize=${this.localize}
label="${this.localize("ui.panel.page-onboarding.user.data.name")}" ></onboarding-create-user>
.value=${this._name}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='on'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
@blur=${this._maybePopulateUsername}
></paper-input>
<paper-input
name="username"
label="${this.localize("ui.panel.page-onboarding.user.data.username")}"
value=${this._username}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='none'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="password"
label="${this.localize("ui.panel.page-onboarding.user.data.password")}"
value=${this._password}
@value-changed=${this._handleValueChanged}
required
type='password'
auto-validate
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="passwordConfirm"
label="${this.localize(
"ui.panel.page-onboarding.user.data.password_confirm"
)}"
value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type='password'
.invalid=${this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password}
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)}"
></paper-input>
<p class="action">
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`; `;
} }
}
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitForm();
}
});
this._fetchOnboardingSteps(); this._fetchOnboardingSteps();
registerServiceWorker(false); registerServiceWorker(false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
} }
private async _fetchOnboardingSteps() { private async _fetchOnboardingSteps() {
@ -150,82 +80,31 @@ class HaOnboarding extends litLocalizeLiteMixin(LitElement) {
// Onboarding is done! // Onboarding is done!
document.location.href = "/"; document.location.href = "/";
} }
this._steps = steps;
} catch (err) { } catch (err) {
alert("Something went wrong loading loading onboarding, try refreshing"); alert("Something went wrong loading loading onboarding, try refreshing");
} }
} }
private _handleValueChanged(ev: PolymerChangedEvent<string>): void { private async _handleStepDone(
const name = (ev.target as any).name; ev: HASSDomEvent<OnboardingEvent<ValidOnboardingStep>>
this[`_${name}`] = ev.detail.value; ) {
} const stepResult = ev.detail;
private _maybePopulateUsername(): void {
if (this._username) {
return;
}
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
private async _submitForm(): Promise<void> {
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = "password_not_match";
return;
}
this._loading = true;
this._errorMsg = "";
try {
const clientId = genClientId();
const { auth_code } = await onboardUserStep({
client_id: clientId,
name: this._name,
username: this._username,
password: this._password,
});
if (stepResult.type === "user") {
const result = stepResult.result as OnboardingResponses["user"];
const state = btoa( const state = btoa(
JSON.stringify({ JSON.stringify({
hassUrl: `${location.protocol}//${location.host}`, hassUrl: `${location.protocol}//${location.host}`,
clientId, clientId: genClientId(),
}) })
); );
document.location.href = `/?auth_callback=1&code=${encodeURIComponent( document.location.href = `/?auth_callback=1&code=${encodeURIComponent(
auth_code result.auth_code
)}&state=${state}`; )}&state=${state}`;
} catch (err) {
// tslint:disable-next-line
console.error(err);
this._loading = false;
this._errorMsg = err.message;
} }
} }
static get styles(): CSSResult {
return css`
.error {
color: red;
}
.action {
margin: 32px 0;
text-align: center;
}
`;
}
} }
declare global { declare global {

View File

@ -0,0 +1,208 @@
import "@polymer/paper-input/paper-input";
import "@material/mwc-button";
import {
LitElement,
CSSResult,
css,
html,
PropertyValues,
property,
customElement,
TemplateResult,
} from "lit-element";
import { genClientId } from "home-assistant-js-websocket";
import { onboardUserStep } from "../data/onboarding";
import { PolymerChangedEvent } from "../polymer-types";
import { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
@customElement("onboarding-create-user")
class OnboardingCreateUser extends LitElement {
@property() public localize!: LocalizeFunc;
@property() private _name = "";
@property() private _username = "";
@property() private _password = "";
@property() private _passwordConfirm = "";
@property() private _loading = false;
@property() private _errorMsg?: string = undefined;
protected render(): TemplateResult | void {
return html`
<p>
${this.localize("ui.panel.page-onboarding.intro")}
</p>
<p>
${this.localize("ui.panel.page-onboarding.user.intro")}
</p>
${
this._errorMsg
? html`
<p class="error">
${this.localize(
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
) || this._errorMsg}
</p>
`
: ""
}
<form>
<paper-input
name="name"
label="${this.localize("ui.panel.page-onboarding.user.data.name")}"
.value=${this._name}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='on'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
@blur=${this._maybePopulateUsername}
></paper-input>
<paper-input
name="username"
label="${this.localize("ui.panel.page-onboarding.user.data.username")}"
value=${this._username}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='none'
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="password"
label="${this.localize("ui.panel.page-onboarding.user.data.password")}"
value=${this._password}
@value-changed=${this._handleValueChanged}
required
type='password'
auto-validate
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}"
></paper-input>
<paper-input
name="passwordConfirm"
label="${this.localize(
"ui.panel.page-onboarding.user.data.password_confirm"
)}"
value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type='password'
.invalid=${this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password}
.errorMessage="${this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)}"
></paper-input>
<p class="action">
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
setTimeout(
() => this.shadowRoot!.querySelector("paper-input")!.focus(),
100
);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
this._submitForm();
}
});
}
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
const name = (ev.target as any).name;
this[`_${name}`] = ev.detail.value;
}
private _maybePopulateUsername(): void {
if (this._username) {
return;
}
const parts = this._name.split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
}
}
private async _submitForm(): Promise<void> {
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = "password_not_match";
return;
}
this._loading = true;
this._errorMsg = "";
try {
const clientId = genClientId();
const result = await onboardUserStep({
client_id: clientId,
name: this._name,
username: this._username,
password: this._password,
});
fireEvent(this, "onboarding-step", {
type: "user",
result,
});
} catch (err) {
// tslint:disable-next-line
console.error(err);
this._loading = false;
this._errorMsg = err.body.message;
}
}
static get styles(): CSSResult {
return css`
.error {
color: red;
}
.action {
margin: 32px 0;
text-align: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-create-user": OnboardingCreateUser;
}
}

View File

@ -0,0 +1,16 @@
import { LitElement, TemplateResult, html, customElement } from "lit-element";
@customElement("onboarding-loading")
class OnboardingLoading extends LitElement {
protected render(): TemplateResult | void {
return html`
Loading
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-loading": OnboardingLoading;
}
}