Authorize onboarding (#1603)

* Tweak onboarding + authorize

* Authorize/Onboarding pimp

* More tweaks

* Comments
This commit is contained in:
Paulus Schoutsen 2018-08-27 22:10:15 +02:00 committed by GitHub
parent 772208ba22
commit f443942e03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 514 additions and 241 deletions

View File

@ -22,7 +22,10 @@ const TRANSLATION_FRAGMENTS = [
'history',
'logbook',
'mailbox',
'profile',
'shopping-list',
'page-authorize',
'page-onboarding',
];
const tasks = [];

View File

@ -117,6 +117,7 @@
"polymer-analyzer": "^3.0.1",
"polymer-bundler": "^4.0.1",
"polymer-cli": "^1.7.4",
"raw-loader": "^0.5.1",
"reify": "^0.16.2",
"require-dir": "^1.0.0",
"sinon": "^6.0.0",

View File

@ -2,46 +2,64 @@ import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '@polymer/paper-button/paper-button.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import '../components/ha-form.js';
import EventsMixin from '../mixins/events-mixin.js';
import LocalizeLiteMixin from '../mixins/localize-lite-mixin.js';
/*
* @appliesMixin EventsMixin
*/
class HaAuthFlow extends EventsMixin(PolymerElement) {
class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
/* So we can set min-height to avoid jumping during loading */
display: block;
}
.action {
margin: 32px 0;
margin: 24px 0 8px;
text-align: center;
}
</style>
<template is="dom-if" if="[[_equals(_state, &quot;loading&quot;)]]">
Please wait
</template>
<template is="dom-if" if="[[_equals(_state, &quot;error&quot;)]]">
Something went wrong
</template>
<template is="dom-if" if="[[_equals(_state, &quot;step&quot;)]]">
<template is="dom-if" if="[[_equals(_step.type, &quot;abort&quot;)]]">
Aborted
<form>
<template is="dom-if" if="[[_equals(_state, &quot;loading&quot;)]]">
[[localize('ui.panel.page-authorize.form.working')]]:
</template>
<template is="dom-if" if="[[_equals(_step.type, &quot;create_entry&quot;)]]">
Success!
<template is="dom-if" if="[[_equals(_state, &quot;error&quot;)]]">
[[localize('ui.panel.page-authorize.form.unknown_error')]]:
</template>
<template is="dom-if" if="[[_equals(_step.type, &quot;form&quot;)]]">
<ha-form data="{{_stepData}}" schema="[[_step.data_schema]]" error="[[_step.errors]]"></ha-form>
<template is="dom-if" if="[[_equals(_state, &quot;step&quot;)]]">
<template is="dom-if" if="[[_equals(_step.type, &quot;abort&quot;)]]">
[[localize('ui.panel.page-authorize.abort_intro')]]:
<ha-markdown content="[[_computeStepAbortedReason(localize, _step)]]"></ha-markdown>
</template>
<template is="dom-if" if="[[_equals(_step.type, &quot;form&quot;)]]">
<template is="dom-if" if="[[_computeStepDescription(localize, _step)]]">
<ha-markdown content="[[_computeStepDescription(localize, _step)]]" allow-svg></ha-markdown>
</template>
<ha-form
data="{{_stepData}}"
schema="[[_step.data_schema]]"
error="[[_step.errors]]"
compute-label="[[_computeLabelCallback(localize, _step)]]"
compute-error="[[_computeErrorCallback(localize, _step)]]"
></ha-form>
</template>
<div class='action'>
<paper-button
raised
on-click='_handleSubmit'
>[[_computeSubmitCaption(_step.type)]]</paper-button>
</div>
</template>
<div class='action'>
<paper-button raised on-click="_handleSubmit">[[_computeSubmitCaption(_step.type)]]</paper-button>
</div>
</template>
</form>
`;
}
static get properties() {
return {
authProvider: Object,
authProvider: {
type: Object,
observer: '_providerChanged',
},
clientId: String,
redirectUri: String,
oauth2State: String,
@ -53,11 +71,14 @@ class HaAuthFlow extends EventsMixin(PolymerElement) {
type: Object,
value: () => ({}),
},
_step: Object,
_step: {
type: Object,
notify: true,
}
};
}
async ready() {
ready() {
super.ready();
this.addEventListener('keypress', (ev) => {
@ -67,28 +88,45 @@ class HaAuthFlow extends EventsMixin(PolymerElement) {
});
}
connectedCallback() {
super.connectedCallback();
async _providerChanged(newProvider, oldProvider) {
if (oldProvider && this._step && this._step.type === 'form') {
fetch(`/auth/login_flow/${this._step.flow_id}`, {
method: 'DELETE',
credentials: 'same-origin',
}).catch(() => {});
}
fetch('/auth/login_flow', {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
client_id: this.clientId,
handler: [this.authProvider.type, this.authProvider.id],
redirect_uri: this.redirectUri,
})
}).then((response) => {
if (!response.ok) throw new Error();
return response.json();
}).then(step => this.setProperties({
_step: step,
_state: 'step',
})).catch((err) => {
try {
const response = await fetch('/auth/login_flow', {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
client_id: this.clientId,
handler: [newProvider.type, newProvider.id],
redirect_uri: this.redirectUri,
})
});
const step = await response.json();
this._updateStep(step);
} catch (err) {
// eslint-disable-next-line
console.error('Error starting auth flow', err);
this._state = 'error';
});
}
}
_updateStep(step) {
const props = {
_step: step,
_state: 'step',
};
if (this._step && step.step_id !== this._step.step_id) {
props._stepData = {};
}
this.setProperties(props);
}
_equals(a, b) {
@ -99,25 +137,52 @@ class HaAuthFlow extends EventsMixin(PolymerElement) {
return stepType === 'form' ? 'Next' : 'Start over';
}
_handleSubmit() {
_computeStepAbortedReason(localize, step) {
return localize(`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${step.reason}`);
}
_computeStepDescription(localize, step) {
const args = [`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.description`];
const placeholders = step.description_placeholders || {};
Object.keys(placeholders).forEach((key) => {
args.push(key);
args.push(placeholders[key]);
});
return localize(...args);
}
_computeLabelCallback(localize, step) {
// Returns a callback for ha-form to calculate labels per schema object
return schema => localize(`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${step.step_id}.data.${schema.name}`);
}
_computeErrorCallback(localize, step) {
// Returns a callback for ha-form to calculate error messages
return error => localize(`ui.panel.page-authorize.form.providers.${step.handler[0]}.error.${error}`);
}
async _handleSubmit() {
if (this._step.type !== 'form') {
this.fire('reset');
this._providerChanged(this.authProvider, null);
return;
}
this._state = 'loading';
// To avoid a jumping UI.
this.style.setProperty('min-height', `${this.offsetHeight}px`);
const postData = Object.assign({}, this._stepData, {
client_id: this.clientId,
});
fetch(`/auth/login_flow/${this._step.flow_id}`, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(postData)
}).then((response) => {
if (!response.ok) throw new Error();
return response.json();
}).then((newStep) => {
try {
const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(postData)
});
const newStep = await response.json();
if (newStep.type === 'create_entry') {
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
let url = this.redirectUri;
@ -136,20 +201,14 @@ class HaAuthFlow extends EventsMixin(PolymerElement) {
document.location = url;
return;
}
const props = {
_step: newStep,
_state: 'step',
};
if (newStep.step_id !== this._step.step_id) {
props._stepData = {};
}
this.setProperties(props);
}).catch((err) => {
this._updateStep(newStep);
} catch (err) {
// eslint-disable-next-line
console.error('Error loading auth providers', err);
console.error('Error submitting step', err);
this._state = 'error-loading';
});
} finally {
this.style.setProperty('min-height', '');
}
}
}
customElements.define('ha-auth-flow', HaAuthFlow);

View File

@ -1,76 +1,73 @@
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import '@polymer/polymer/lib/elements/dom-if.js';
import '@polymer/polymer/lib/elements/dom-repeat.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../auth/ha-auth-flow.js';
import '../auth/ha-pick-auth-provider.js';
import '../components/ha-markdown.js';
class HaAuthorize extends PolymerElement {
import LocalizeLiteMixin from '../mixins/localize-lite-mixin.js';
import '../auth/ha-auth-flow.js';
class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-positioning"></style>
<style>
.content {
padding: 20px 16px;
max-width: 360px;
margin: 0 auto;
ha-markdown a {
color: var(--primary-color);
}
.header {
text-align: center;
font-size: 1.96em;
display: flex;
align-items: center;
justify-content: center;
font-weight: 300;
ha-markdown p:last-child{
margin-bottom: 0;
}
.header img {
margin-right: 16px;
ha-pick-auth-provider {
display: block;
margin-top: 48px;
}
</style>
<div class="content layout vertical fit">
<div class='header'>
<img src="/static/icons/favicon-192x192.png" height="52">
Home Assistant
</div>
<p>Logging in to <b>[[clientId]]</b>.</p>
<template is="dom-if" if="[[!_authProviders]]">
<p>[[localize('ui.panel.page-authorize.initializing')]]</p>
</template>
<template is="dom-if" if="[[_authProvider]]">
<ha-auth-flow
client-id="[[clientId]]"
redirect-uri="[[redirectUri]]"
oauth2-state="[[oauth2State]]"
auth-provider="[[_authProvider]]"
on-reset="_handleReset"
></ha-auth-flow>
</template>
<template is="dom-if" if="[[!_authProvider]]">
<template is="dom-if" if="[[_authProviders]]">
<ha-markdown content='[[_computeIntro(localize, clientId, _authProvider)]]'></ha-markdown>
<ha-auth-flow
resources="[[resources]]"
client-id="[[clientId]]"
redirect-uri="[[redirectUri]]"
oauth2-state="[[oauth2State]]"
auth-provider="[[_authProvider]]"
step="{{step}}"
></ha-auth-flow>
<template is="dom-if" if="[[_computeMultiple(_authProviders)]]">
<ha-pick-auth-provider
resources="[[resources]]"
client-id="[[clientId]]"
auth-providers="[[_computeInactiveProvders(_authProvider, _authProviders)]]"
on-pick="_handleAuthProviderPick"
></ha-pick-auth-provider>
</template>
</div>
</template>
`;
}
static get properties() {
return {
_authProvider: {
type: String,
value: null,
},
_authProvider: String,
_authProviders: Array,
clientId: String,
redirectUri: String,
oauth2State: String,
translationFragment: {
type: String,
value: 'page-authorize',
}
};
}
ready() {
async ready() {
super.ready();
const query = {};
const values = location.search.substr(1).split('&');
@ -85,12 +82,49 @@ class HaAuthorize extends PolymerElement {
if (query.redirect_uri) props.redirectUri = query.redirect_uri;
if (query.state) props.oauth2State = query.state;
this.setProperties(props);
import(/* webpackChunkName: "pick-auth-provider" */ '../auth/ha-pick-auth-provider.js');
// Fetch auth providers
try {
const response = await window.providersPromise;
const authProviders = await response.json();
if (authProviders.length === 0) {
alert('No auth providers returned. Unable to finish login.');
return;
}
this.setProperties({
_authProviders: authProviders,
_authProvider: authProviders[0],
});
} catch (err) {
// eslint-disable-next-line
console.error('Error loading auth providers', err);
this._state = 'error-loading';
}
}
_handleAuthProviderPick(ev) {
_computeMultiple(array) {
return array && array.length > 1;
}
async _handleAuthProviderPick(ev) {
this._authProvider = ev.detail;
}
_handleReset() {
this._authProvider = null;
_computeInactiveProvders(curProvider, providers) {
return providers.filter(prv =>
prv.type !== curProvider.type || prv.id !== curProvider.id);
}
_computeIntro(localize, clientId, authProvider) {
return (
localize('ui.panel.page-authorize.authorizing_client', 'clientId', clientId) +
'\n\n' +
localize('ui.panel.page-authorize.logging_in_with', 'authProviderName', authProvider.name)
);
}
}
customElements.define('ha-authorize', HaAuthorize);

View File

@ -4,11 +4,12 @@ import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import EventsMixin from '../mixins/events-mixin.js';
import LocalizeLiteMixin from '../mixins/localize-lite-mixin.js';
/*
* @appliesMixin EventsMixin
*/
class HaPickAuthProvider extends EventsMixin(PolymerElement) {
class HaPickAuthProvider extends EventsMixin(LocalizeLiteMixin(PolymerElement)) {
static get template() {
return html`
<style>
@ -19,23 +20,12 @@ class HaPickAuthProvider extends EventsMixin(PolymerElement) {
margin-top: 0;
}
</style>
<template is="dom-if" if="[[_equal(_state, &quot;loading&quot;)]]">
Loading auth providers.
</template>
<template is="dom-if" if="[[_equal(_state, &quot;no-results&quot;)]]">
No auth providers found.
</template>
<template is="dom-if" if="[[_equal(_state, &quot;error-loading&quot;)]]">
Error loading
</template>
<template is="dom-if" if="[[_equal(_state, &quot;pick&quot;)]]">
<p>Pick an auth provider to log in with:</p>
<template is="dom-repeat" items="[[authProviders]]">
<paper-item on-click="_handlePick">
<paper-item-body>[[item.name]]</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>
</template>
<p>[[localize('ui.panel.page-authorize.pick_auth_provider')]]:</p>
<template is="dom-repeat" items="[[authProviders]]">
<paper-item on-click="_handlePick">
<paper-item-body>[[item.name]]</paper-item-body>
<iron-icon icon="hass:chevron-right"></iron-icon>
</paper-item>
</template>
`;
}
@ -47,29 +37,8 @@ class HaPickAuthProvider extends EventsMixin(PolymerElement) {
value: 'loading'
},
authProviders: Array,
clientId: String,
};
}
connectedCallback() {
super.connectedCallback();
fetch('/auth/providers', { credentials: 'same-origin' }).then((response) => {
if (!response.ok) throw new Error();
return response.json();
}).then((authProviders) => {
this.setProperties({
authProviders,
_state: 'pick',
});
if (authProviders.length === 1) {
this.fire('pick', authProviders[0]);
}
}).catch((err) => {
// eslint-disable-next-line
console.error('Error loading auth providers', err);
this._state = 'error-loading';
});
}
_handlePick(ev) {
this.fire('pick', ev.model.item);

View File

@ -23,7 +23,7 @@ class HaForm extends EventsMixin(PolymerElement) {
</style>
<template is="dom-if" if="[[_isArray(schema)]]" restamp="">
<template is="dom-if" if="[[error.base]]">
[[computeError(error.base, schema)]]
<div class='error'>[[computeError(error.base, schema)]]</div>
</template>
<template is="dom-repeat" items="[[schema]]">

View File

@ -17,7 +17,11 @@ import '../components/ha-iconset-svg.js';
import '../layouts/app/home-assistant.js';
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js');
setTimeout(
() =>
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js'),
2000
);
setPassiveTouchGestures(true);
/* LastPass createElement workaround. See #428 */

View File

@ -8,4 +8,5 @@ import '../resources/roboto.js';
import '../auth/ha-authorize.js';
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js');
setTimeout(() =>
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js'), 2000);

View File

@ -0,0 +1,15 @@
<meta charset="utf-8">
<link rel='manifest' href='/manifest.json' crossorigin="use-credentials">
<link rel='icon' href='/static/icons/favicon.ico'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<style>
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
}
</style>

View File

@ -1,30 +1,45 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name='viewport' content='width=device-width, user-scalable=no'>
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin />
<title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template') %>
<style>
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
.content {
padding: 20px 16px;
max-width: 360px;
margin: 0 auto;
}
.header {
font-size: 1.96em;
display: flex;
align-items: center;
justify-content: center;
font-weight: 300;
}
.header img {
margin-right: 16px;
}
</style>
</head>
<body>
<<%= tag %>>Loading</<%= tag %>>
<div class="content">
<div class='header'>
<img src="/static/icons/favicon-192x192.png" height="52">
Home Assistant
</div>
<ha-authorize><p>Initializing</p></ha-authorize>
</div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
window.providersPromise = fetch('/auth/providers', { credentials: 'same-origin' });
var webComponentsSupported = (
'customElements' in window &&
'content' in document.createElement('template'));

View File

@ -1,17 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home Assistant</title>
<link rel='manifest' href='/manifest.json' crossorigin="use-credentials">
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#3fbbf4">
<link rel='preload' href='<%= coreJS %>' as='script'/>
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Medium.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template') %>
<title>Home Assistant</title>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#3fbbf4">
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
@ -20,19 +17,8 @@
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='referrer' content='same-origin'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='{{ theme_color }}'>
<style>
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
}
#ha-init-skeleton::before {
display: block;
content: "";

View File

@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<title>Home Assistant</title>
<link rel='preload' href='/static/fonts/roboto/Roboto-Light.ttf' as='font' crossorigin />
<link rel='preload' href='/static/fonts/roboto/Roboto-Regular.ttf' as='font' crossorigin />
<%= require('raw-loader!./_header.html.template') %>
<style>
.content {
padding: 20px 16px;
max-width: 400px;
margin: 0 auto;
}
.header {
text-align: center;
font-size: 1.96em;
display: flex;
align-items: center;
justify-content: center;
font-weight: 300;
}
.header img {
margin-right: 16px;
}
</style>
</head>
<body>
<div class="content">
<div class='header'>
<img src="/static/icons/favicon-192x192.png" height="52">
Home Assistant
</div>
<ha-onboarding>Initializing…</ha-onboarding>
</div>
<% if (!latestBuild) { %>
<script src="/static/custom-elements-es5-adapter.js"></script>
<script src="<%= compatibility %>"></script>
<% } %>
<script>
window.stepsPromise = fetch('/api/onboarding', { credentials: 'same-origin' });
var webComponentsSupported = (
'customElements' in window &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var e = document.createElement('script');
e.src = '/static/webcomponents-bundle.js';
document.write(e.outerHTML);
}
</script>
<script src="<%= entrypoint %>"></script>
<script src='<%= hassIconsJS %>' async></script>
</body>
</html>

View File

@ -0,0 +1,52 @@
/**
* Lite mixin to add localization without depending on the Hass object.
*/
import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class.js';
import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
import { AppLocalizeBehavior } from '../util/app-localize-behavior.js';
import { getActiveTranslation, getTranslation } from '../util/hass-translation.js';
/**
* @polymerMixin
* @appliesMixin AppLocalizeBehavior
*/
export default dedupingMixin(superClass =>
class extends mixinBehaviors([AppLocalizeBehavior], superClass) {
static get properties() {
return {
language: {
type: String,
value: getActiveTranslation(),
},
resources: Object,
// The fragment to load.
translationFragment: String,
};
}
async ready() {
super.ready();
if (this.resources) {
return;
}
if (!this.translationFragment) {
// In dev mode, we will issue a warning if after a second we are still
// not configured correctly.
if (__DEV__) {
// eslint-disable-next-line
setTimeout(() => !this.resources && console.error(
'Forgot to pass in resources or set translationFragment for',
this.nodeName
), 1000);
}
return;
}
const { language, data } = await getTranslation(this.translationFragment);
this.resources = {
[language]: data
};
}
});

View File

@ -1,4 +1,3 @@
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import '@polymer/polymer/lib/elements/dom-if.js';
import '@polymer/polymer/lib/elements/dom-repeat.js';
import '@polymer/paper-input/paper-input.js';
@ -6,40 +5,16 @@ import '@polymer/paper-button/paper-button.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import hassCallApi from '../util/hass-call-api.js';
import localizeLiteMixin from '../mixins/localize-lite-mixin.js';
const callApi = (method, path, data) => hassCallApi('', {}, method, path, data);
class HaOnboarding extends PolymerElement {
class HaOnboarding extends localizeLiteMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-positioning"></style>
<style>
.content {
padding: 20px 16px;
max-width: 400px;
margin: 0 auto;
}
.header {
text-align: center;
font-size: 1.96em;
display: flex;
align-items: center;
justify-content: center;
font-weight: 300;
}
.header img {
margin-right: 16px;
}
h1 {
font-size: 1.2em;
}
.error {
color: red;
font-weight: bold;
}
.action {
@ -47,54 +22,56 @@ class HaOnboarding extends PolymerElement {
text-align: center;
}
</style>
<div class="content layout vertical fit">
<div class='header'>
<img src="/static/icons/favicon-192x192.png" height="52">
Home Assistant
</div>
<div>
<p>Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?</p>
<p>Let's get started by creating a user account.</p>
</div>
<p>
[[localize('ui.panel.page-onboarding.intro')]]
</p>
<template is='dom-if' if='[[_error]]'>
<p class='error'>[[_error]]</p>
</template>
<p>
[[localize('ui.panel.page-onboarding.user.intro')]]
</p>
<template is='dom-if' if='[[_errorMsg]]'>
<p class='error'>[[_computeErrorMsg(localize, _errorMsg)]]</p>
</template>
<form>
<paper-input
autofocus
label='Name'
label="[[localize('ui.panel.page-onboarding.user.data.name')]]"
value='{{_name}}'
required
auto-validate
error-message='Required'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
on-blur='_maybePopulateUsername'
></paper-input>
<paper-input
label='Username'
label="[[localize('ui.panel.page-onboarding.user.data.username')]]"
value='{{_username}}'
required
auto-validate
error-message='Required'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
></paper-input>
<paper-input
label='Password'
label="[[localize('ui.panel.page-onboarding.user.data.password')]]"
value='{{_password}}'
required
type='password'
auto-validate
error-message='Required'
error-message="[[localize('ui.panel.page-onboarding.user.required_field')]]"
></paper-input>
<template is='dom-if' if='[[!_loading]]'>
<p class='action'>
<paper-button raised on-click='_submitForm'>Create Account</paper-button>
<paper-button raised on-click='_submitForm'>
[[localize('ui.panel.page-onboarding.user.create_account')]]
</paper-button>
</p>
</template>
</div>
</form>
`;
}
@ -106,7 +83,12 @@ class HaOnboarding extends PolymerElement {
_loading: {
type: Boolean,
value: false,
}
},
translationFragment: {
type: String,
value: 'page-onboarding',
},
_errorMsg: String,
};
}
@ -117,10 +99,24 @@ class HaOnboarding extends PolymerElement {
this._submitForm();
}
});
const steps = await callApi('get', 'onboarding');
if (steps.every(step => step.done)) {
// Onboarding is done!
document.location = '/';
try {
const response = await window.stepsPromise;
if (response.status === 404) {
// We don't load the component when onboarding is done
document.location = '/';
return;
}
const steps = await response.json();
if (steps.every(step => step.done)) {
// Onboarding is done!
document.location = '/';
}
} catch (err) {
alert('Something went wrong loading loading onboarding, try refreshing');
}
}
@ -135,7 +131,12 @@ class HaOnboarding extends PolymerElement {
}
async _submitForm() {
if (!this._name || !this._username || !this._password) return;
if (!this._name || !this._username || !this._password) {
this._errorMsg = 'required_fields';
return;
}
this._errorMsg = '';
try {
await callApi('post', 'onboarding/users', {
@ -150,9 +151,13 @@ class HaOnboarding extends PolymerElement {
console.error(err);
this.setProperties({
_loading: false,
_error: err.message,
_errorMsg: err.message,
});
}
}
_computeErrorMsg(localize, errorMsg) {
return localize(`ui.panel.page-onboarding.user.error.${errorMsg}`) || errorMsg;
}
}
customElements.define('ha-onboarding', HaOnboarding);

View File

@ -752,6 +752,75 @@
"clear_completed": "Clear completed",
"add_item": "Add item",
"microphone_tip": "Tap the microphone on the top right and say “Add candy to my shopping list”"
},
"page-authorize": {
"initializing": "Initializing",
"authorizing_client": "You're about to give {clientId} access to your Home Assistant instance.",
"logging_in_with": "Logging in with **{authProviderName}**.",
"pick_auth_provider": "Or log in with",
"abort_intro": "Login aborted",
"form": {
"working": "Please wait",
"unknown_error": "Something went wrong",
"providers": {
"homeassistant": {
"step": {
"init": {
"data": {
"username": "Username",
"password": "Password"
}
}
},
"error": {
"invalid_auth": "Invalid username or password"
}
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API Password"
}
}
},
"error": {
"invalid_auth": "Invalid API password"
},
"abort": {
"no_api_password_set": "You don't have an API password configured."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "User"
}
}
},
"abort": {
"not_whitelisted": "Your computer is not whitelisted."
}
}
}
}
},
"page-onboarding": {
"intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?",
"user": {
"intro": "Let's get started by creating a user account.",
"required_field": "Required",
"data": {
"name": "Name",
"username": "Username",
"password": "Password"
},
"create_account": "Create Account",
"error": {
"required_fields": "Fill in all required fields"
}
}
}
}
}

View File

@ -17,12 +17,11 @@ const VERSION = version[0];
const generateJSPage = (entrypoint, latestBuild) => {
return new HtmlWebpackPlugin({
inject: false,
template: './src/html/extra_page.html.template',
template: `./src/html/${entrypoint}.html.template`,
// Default templateParameterGenerator code
// https://github.com/jantimon/html-webpack-plugin/blob/master/index.js#L719
templateParameters: (compilation, assets, option) => ({
latestBuild,
tag: `ha-${entrypoint}`,
compatibility: assets.chunks.compatibility.entry,
entrypoint: assets.chunks[entrypoint].entry,
hassIconsJS: assets.chunks['hass-icons'].entry,

View File

@ -9790,6 +9790,10 @@ raw-body@2.3.2:
iconv-lite "0.4.19"
unpipe "1.0.0"
raw-loader@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.7.tgz#8a10ca30d588d00464360372b890d06dacd02297"