mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-09 10:26:35 +00:00
Authorize onboarding (#1603)
* Tweak onboarding + authorize * Authorize/Onboarding pimp * More tweaks * Comments
This commit is contained in:
parent
772208ba22
commit
f443942e03
@ -22,7 +22,10 @@ const TRANSLATION_FRAGMENTS = [
|
||||
'history',
|
||||
'logbook',
|
||||
'mailbox',
|
||||
'profile',
|
||||
'shopping-list',
|
||||
'page-authorize',
|
||||
'page-onboarding',
|
||||
];
|
||||
|
||||
const tasks = [];
|
||||
|
@ -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",
|
||||
|
@ -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, "loading")]]">
|
||||
Please wait
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "error")]]">
|
||||
Something went wrong
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "step")]]">
|
||||
<template is="dom-if" if="[[_equals(_step.type, "abort")]]">
|
||||
Aborted
|
||||
<form>
|
||||
<template is="dom-if" if="[[_equals(_state, "loading")]]">
|
||||
[[localize('ui.panel.page-authorize.form.working')]]:
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_step.type, "create_entry")]]">
|
||||
Success!
|
||||
<template is="dom-if" if="[[_equals(_state, "error")]]">
|
||||
[[localize('ui.panel.page-authorize.form.unknown_error')]]:
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_step.type, "form")]]">
|
||||
<ha-form data="{{_stepData}}" schema="[[_step.data_schema]]" error="[[_step.errors]]"></ha-form>
|
||||
<template is="dom-if" if="[[_equals(_state, "step")]]">
|
||||
<template is="dom-if" if="[[_equals(_step.type, "abort")]]">
|
||||
[[localize('ui.panel.page-authorize.abort_intro')]]:
|
||||
<ha-markdown content="[[_computeStepAbortedReason(localize, _step)]]"></ha-markdown>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_equals(_step.type, "form")]]">
|
||||
<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);
|
||||
|
@ -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);
|
||||
|
@ -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, "loading")]]">
|
||||
Loading auth providers.
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equal(_state, "no-results")]]">
|
||||
No auth providers found.
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equal(_state, "error-loading")]]">
|
||||
Error loading
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equal(_state, "pick")]]">
|
||||
<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);
|
||||
|
@ -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]]">
|
||||
|
@ -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 */
|
||||
|
@ -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);
|
||||
|
15
src/html/_header.html.template
Normal file
15
src/html/_header.html.template
Normal 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>
|
@ -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'));
|
@ -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: "";
|
||||
|
57
src/html/onboarding.html.template
Normal file
57
src/html/onboarding.html.template
Normal 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>
|
52
src/mixins/localize-lite-mixin.js
Normal file
52
src/mixins/localize-lite-mixin.js
Normal 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
|
||||
};
|
||||
}
|
||||
});
|
@ -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);
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user