diff --git a/package.json b/package.json index 0e3308960d..1e6bc6abf7 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "es6-object-assign": "^1.1.0", "eslint-import-resolver-webpack": "^0.10.1", "fecha": "^2.3.3", - "home-assistant-js-websocket": "^3.1.5", + "home-assistant-js-websocket": "^3.2.4", "intl-messageformat": "^2.2.0", "jquery": "^3.3.1", "js-yaml": "^3.12.0", diff --git a/src/common/auth/external_auth.js b/src/common/auth/external_auth.ts similarity index 60% rename from src/common/auth/external_auth.js rename to src/common/auth/external_auth.ts index 2a9d3f1483..4f5613af15 100644 --- a/src/common/auth/external_auth.js +++ b/src/common/auth/external_auth.ts @@ -6,6 +6,34 @@ import { Auth } from "home-assistant-js-websocket"; const CALLBACK_SET_TOKEN = "externalAuthSetToken"; const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken"; +interface BasePayload { + callback: string; +} + +interface RefreshTokenResponse { + access_token: string; + expires_in: number; +} + +declare global { + interface Window { + externalApp?: { + getExternalAuth(payload: BasePayload); + revokeExternalAuth(payload: BasePayload); + }; + webkit?: { + messageHandlers: { + getExternalAuth: { + postMessage(payload: BasePayload); + }; + revokeExternalAuth: { + postMessage(payload: BasePayload); + }; + }; + }; + } +} + if (!window.externalApp && !window.webkit) { throw new Error( "External auth requires either externalApp or webkit defined on Window object." @@ -14,21 +42,24 @@ if (!window.externalApp && !window.webkit) { export default class ExternalAuth extends Auth { constructor(hassUrl) { - super(); - - this.data = { + super({ hassUrl, + clientId: "", + refresh_token: "", access_token: "", + expires_in: 0, // This will trigger connection to do a refresh right away expires: 0, - }; + }); } - async refreshAccessToken() { - const responseProm = new Promise((resolve, reject) => { - window[CALLBACK_SET_TOKEN] = (success, data) => - success ? resolve(data) : reject(data); - }); + public async refreshAccessToken() { + const responseProm = new Promise( + (resolve, reject) => { + window[CALLBACK_SET_TOKEN] = (success, data) => + success ? resolve(data) : reject(data); + } + ); // Allow promise to set resolve on window object. await 0; @@ -38,23 +69,18 @@ export default class ExternalAuth extends Auth { if (window.externalApp) { window.externalApp.getExternalAuth(callbackPayload); } else { - window.webkit.messageHandlers.getExternalAuth.postMessage( + window.webkit!.messageHandlers.getExternalAuth.postMessage( callbackPayload ); } - // Response we expect back: - // { - // "access_token": "qwere", - // "expires_in": 1800 - // } const tokens = await responseProm; this.data.access_token = tokens.access_token; this.data.expires = tokens.expires_in * 1000 + Date.now(); } - async revoke() { + public async revoke() { const responseProm = new Promise((resolve, reject) => { window[CALLBACK_REVOKE_TOKEN] = (success, data) => success ? resolve(data) : reject(data); @@ -68,7 +94,7 @@ export default class ExternalAuth extends Auth { if (window.externalApp) { window.externalApp.revokeExternalAuth(callbackPayload); } else { - window.webkit.messageHandlers.revokeExternalAuth.postMessage( + window.webkit!.messageHandlers.revokeExternalAuth.postMessage( callbackPayload ); } diff --git a/src/common/auth/token_storage.js b/src/common/auth/token_storage.ts similarity index 67% rename from src/common/auth/token_storage.js rename to src/common/auth/token_storage.ts index 6483665720..335cb65075 100644 --- a/src/common/auth/token_storage.js +++ b/src/common/auth/token_storage.ts @@ -1,5 +1,18 @@ +import { AuthData } from "home-assistant-js-websocket"; + const storage = window.localStorage || {}; +declare global { + interface Window { + __tokenCache: { + // undefined: we haven't loaded yet + // null: none stored + tokens?: AuthData | null; + writeEnabled?: boolean; + }; + } +} + // So that core.js and main app hit same shared object. let tokenCache = window.__tokenCache; if (!tokenCache) { @@ -15,18 +28,22 @@ export function askWrite() { ); } -export function saveTokens(tokens) { +export function saveTokens(tokens: AuthData | null) { tokenCache.tokens = tokens; if (tokenCache.writeEnabled) { try { storage.hassTokens = JSON.stringify(tokens); - } catch (err) {} // eslint-disable-line + } catch (err) { + // write failed, ignore it. Happens if storage is full or private mode. + } } } export function enableWrite() { tokenCache.writeEnabled = true; - saveTokens(tokenCache.tokens); + if (tokenCache.tokens) { + saveTokens(tokenCache.tokens); + } } export function loadTokens() { diff --git a/src/data/ws-notifications.js b/src/data/ws-notifications.ts similarity index 62% rename from src/data/ws-notifications.js rename to src/data/ws-notifications.ts index 1fb3b5e07d..261a32aac6 100644 --- a/src/data/ws-notifications.js +++ b/src/data/ws-notifications.ts @@ -1,4 +1,4 @@ -import { createCollection } from "home-assistant-js-websocket"; +import { createCollection, Connection } from "home-assistant-js-websocket"; const fetchNotifications = (conn) => conn.sendMessagePromise({ @@ -11,8 +11,11 @@ const subscribeUpdates = (conn, store) => "persistent_notifications_updated" ); -export const subscribeNotifications = (conn, onChange) => - createCollection( +export const subscribeNotifications = ( + conn: Connection, + onChange: (notifications: Notification[]) => void +) => + createCollection( "_ntf", fetchNotifications, subscribeUpdates, diff --git a/src/data/ws-panels.js b/src/data/ws-panels.js deleted file mode 100644 index 515be6628d..0000000000 --- a/src/data/ws-panels.js +++ /dev/null @@ -1,10 +0,0 @@ -import { createCollection } from "home-assistant-js-websocket"; - -export const subscribePanels = (conn, onChange) => - createCollection( - "_pnl", - (conn_) => conn_.sendMessagePromise({ type: "get_panels" }), - null, - conn, - onChange - ); diff --git a/src/data/ws-panels.ts b/src/data/ws-panels.ts new file mode 100644 index 0000000000..88ec490d06 --- /dev/null +++ b/src/data/ws-panels.ts @@ -0,0 +1,14 @@ +import { createCollection, Connection } from "home-assistant-js-websocket"; +import { Panels } from "../types"; + +export const subscribePanels = ( + conn: Connection, + onChange: (panels: Panels) => void +) => + createCollection( + "_pnl", + () => conn.sendMessagePromise({ type: "get_panels" }), + undefined, + conn, + onChange + ); diff --git a/src/data/ws-themes.js b/src/data/ws-themes.js deleted file mode 100644 index d5e8cfba74..0000000000 --- a/src/data/ws-themes.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createCollection } from "home-assistant-js-websocket"; - -const fetchThemes = (conn) => - conn.sendMessagePromise({ - type: "frontend/get_themes", - }); - -const subscribeUpdates = (conn, store) => - conn.subscribeEvents( - (event) => store.setState(event.data, true), - "themes_updated" - ); - -export const subscribeThemes = (conn, onChange) => - createCollection("_thm", fetchThemes, subscribeUpdates, conn, onChange); diff --git a/src/data/ws-themes.ts b/src/data/ws-themes.ts new file mode 100644 index 0000000000..13f37844d4 --- /dev/null +++ b/src/data/ws-themes.ts @@ -0,0 +1,25 @@ +import { createCollection, Connection } from "home-assistant-js-websocket"; +import { Themes } from "../types"; + +const fetchThemes = (conn) => + conn.sendMessagePromise({ + type: "frontend/get_themes", + }); + +const subscribeUpdates = (conn, store) => + conn.subscribeEvents( + (event) => store.setState(event.data, true), + "themes_updated" + ); + +export const subscribeThemes = ( + conn: Connection, + onChange: (themes: Themes) => void +) => + createCollection( + "_thm", + fetchThemes, + subscribeUpdates, + conn, + onChange + ); diff --git a/src/data/ws-user.js b/src/data/ws-user.js deleted file mode 100644 index 9e33811a62..0000000000 --- a/src/data/ws-user.js +++ /dev/null @@ -1,4 +0,0 @@ -import { createCollection, getUser } from "home-assistant-js-websocket"; - -export const subscribeUser = (conn, onChange) => - createCollection("_usr", (conn_) => getUser(conn_), null, conn, onChange); diff --git a/src/data/ws-user.ts b/src/data/ws-user.ts new file mode 100644 index 0000000000..e627c20673 --- /dev/null +++ b/src/data/ws-user.ts @@ -0,0 +1,20 @@ +import { + createCollection, + getUser, + Connection, +} from "home-assistant-js-websocket"; +import { User } from "../types"; + +export const subscribeUser = ( + conn: Connection, + onChange: (user: User) => void +) => + createCollection( + "_usr", + // the getUser command is mistyped in current verrsion of HAWS. + // Fixed in 3.2.5 + () => (getUser(conn) as unknown) as Promise, + undefined, + conn, + onChange + ); diff --git a/src/entrypoints/core.js b/src/entrypoints/core.ts similarity index 81% rename from src/entrypoints/core.js rename to src/entrypoints/core.ts index 560fb8c96c..c75e9680a6 100644 --- a/src/entrypoints/core.js +++ b/src/entrypoints/core.ts @@ -5,12 +5,21 @@ import { subscribeEntities, subscribeServices, ERR_INVALID_AUTH, + Auth, + Connection, } from "home-assistant-js-websocket"; import { loadTokens, saveTokens } from "../common/auth/token_storage"; import { subscribePanels } from "../data/ws-panels"; import { subscribeThemes } from "../data/ws-themes"; import { subscribeUser } from "../data/ws-user"; +import { HomeAssistant } from "../types"; + +declare global { + interface Window { + hassConnection: Promise<{ auth: Auth; conn: Connection }>; + } +} const hassUrl = `${location.protocol}//${location.host}`; const isExternal = location.search.includes("external_auth=1"); @@ -33,7 +42,7 @@ const connProm = async (auth) => { // Clear url if we have been able to establish a connection if (location.search.includes("auth_callback=1")) { - history.replaceState(null, null, location.pathname); + history.replaceState(null, "", location.pathname); } return { auth, conn }; @@ -43,7 +52,9 @@ const connProm = async (auth) => { } // We can get invalid auth if auth tokens were stored that are no longer valid // Clear stored tokens. - if (!isExternal) saveTokens(null); + if (!isExternal) { + saveTokens(null); + } auth = await authProm(); const conn = await createConnection({ auth }); return { auth, conn }; @@ -54,7 +65,9 @@ window.hassConnection = authProm().then(connProm); // Start fetching some of the data that we will need. window.hassConnection.then(({ conn }) => { - const noop = () => {}; + const noop = () => { + // do nothing + }; subscribeEntities(conn, noop); subscribeConfig(conn, noop); subscribeServices(conn, noop); @@ -64,8 +77,12 @@ window.hassConnection.then(({ conn }) => { }); window.addEventListener("error", (e) => { - const homeAssistant = document.querySelector("home-assistant"); - if (homeAssistant && homeAssistant.hass && homeAssistant.hass.callService) { + const homeAssistant = document.querySelector("home-assistant") as any; + if ( + homeAssistant && + homeAssistant.hass && + (homeAssistant.hass as HomeAssistant).callService + ) { homeAssistant.hass.callService("system_log", "write", { logger: `frontend.${ __DEV__ ? "js_dev" : "js" diff --git a/src/types.ts b/src/types.ts index 6cc016d2ee..281eb63a6d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,12 @@ import { HassEntityAttributeBase, } from "home-assistant-js-websocket"; +declare global { + var __DEV__: boolean; + var __BUILD__: "latest" | "es5"; + var __VERSION__: string; +} + export interface Credential { auth_provider_type: string; auth_provider_id: string; @@ -34,6 +40,11 @@ export interface Theme { "accent-color": string; } +export interface Themes { + default_theme: string; + themes: { [key: string]: Theme }; +} + export interface Panel { component_name: string; config?: { [key: string]: any }; @@ -42,22 +53,31 @@ export interface Panel { url_path: string; } +export interface Panels { + [name: string]: Panel; +} + export interface Translation { nativeName: string; fingerprints: { [fragment: string]: string }; } +export interface Notification { + notification_id: string; + message: string; + title: string; + status: "read" | "unread"; + created_at: string; +} + export interface HomeAssistant { auth: Auth; connection: Connection; connected: boolean; states: HassEntities; config: HassConfig; - themes: { - default_theme: string; - themes: { [key: string]: Theme }; - }; - panels: { [key: string]: Panel }; + themes: Themes; + panels: Panels; panelUrl: string; language: string; resources: { [key: string]: any }; diff --git a/webpack.config.js b/webpack.config.js index 87b7105aed..4bc3fb1cc4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -41,7 +41,7 @@ function createConfig(isProdBuild, latestBuild) { app: "./src/entrypoints/app.js", authorize: "./src/entrypoints/authorize.js", onboarding: "./src/entrypoints/onboarding.js", - core: "./src/entrypoints/core.js", + core: "./src/entrypoints/core.ts", compatibility: "./src/entrypoints/compatibility.js", "custom-panel": "./src/entrypoints/custom-panel.js", "hass-icons": "./src/entrypoints/hass-icons.js", @@ -173,7 +173,7 @@ function createConfig(isProdBuild, latestBuild) { swDest: "service_worker.js", importWorkboxFrom: "local", include: [ - /core.js$/, + /core.ts$/, /app.js$/, /custom-panel.js$/, /hass-icons.js$/, diff --git a/yarn.lock b/yarn.lock index 38b4426f63..6bb4ab49fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7426,10 +7426,10 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== -home-assistant-js-websocket@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.1.5.tgz#56358660bbccb5f24e16f9197bbad12739f9b756" - integrity sha512-BlANSkA9ob6wlUJCYS26n1tfMla+aJzB2rhhXSFi0iiTtWWfuOlcSOw8pxjeWhh1I9oAcbQ4qxhB7Np1EXG+Og== +home-assistant-js-websocket@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-3.2.4.tgz#0c4212e6ac57b60ed939aa420253994e4f9f0bef" + integrity sha512-DaHpWIjJFLwTWNbHeGSCEUsbeyLUWAyWUgsYkiVWxzbfm+vqC5YaLNRu+Ma64SQYh5yGSYr7h25p2hip1GvyhQ== home-or-tmp@^2.0.0: version "2.0.0"