Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use localStorage with Web Storage API #23172

Merged
merged 7 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demo/src/custom-cards/ha-demo-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {

@state() private _switching = false;

private _hidden = localStorage.hide_demo_card;
private _hidden = window.localStorage.getItem("hide_demo_card");

public getCardSize() {
return this._hidden ? 0 : 2;
Expand Down
5 changes: 4 additions & 1 deletion landing-page/src/ha-landing-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ class HaLandingPage extends LandingPageBaseElement {
if (language !== this.language && language) {
this.language = language;
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
window.localStorage.setItem(
"selectedLanguage",
JSON.stringify(language)
);
} catch (err: any) {
// Ignore
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
"gulp-rename": "2.0.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "25.0.1",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lit-analyzer": "2.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/auth/ha-authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this.language = language;

try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
window.localStorage.setItem("selectedLanguage", JSON.stringify(language));
} catch (err: any) {
// Ignore
}
Expand Down
15 changes: 9 additions & 6 deletions src/common/auth/token_storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";

const storage = window.localStorage || {};

declare global {
interface Window {
__tokenCache: {
Expand Down Expand Up @@ -38,9 +36,15 @@ export function saveTokens(tokens: AuthData | null) {

if (tokenCache.writeEnabled) {
try {
storage.hassTokens = JSON.stringify(tokens);
window.localStorage.setItem("hassTokens", JSON.stringify(tokens));
} catch (err: any) {
// write failed, ignore it. Happens if storage is full or private mode.
// eslint-disable-next-line no-console
console.warn(
"Failed to store tokens; Are you in private mode or is your storage full?"
);
// eslint-disable-next-line no-console
console.error("Error storing tokens:", err);
}
}
}
Expand All @@ -51,12 +55,11 @@ export function enableWrite() {
saveTokens(tokenCache.tokens);
}
}

export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
// Delete the old token cache.
delete storage.tokens;
wendevlin marked this conversation as resolved.
Show resolved Hide resolved
const tokens = storage.hassTokens;
const tokens = window.localStorage.getItem("hassTokens");
if (tokens) {
tokenCache.tokens = JSON.parse(tokens);
tokenCache.writeEnabled = true;
Expand Down
9 changes: 5 additions & 4 deletions src/data/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import type { HomeAssistant, PanelInfo } from "../types";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "lovelace";

export const getStorageDefaultPanelUrlPath = (): string =>
localStorage.defaultPanel
? JSON.parse(localStorage.defaultPanel)
: DEFAULT_PANEL;
export const getStorageDefaultPanelUrlPath = (): string => {
const defaultPanel = window.localStorage.getItem("defaultPanel");

return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
};

export const setDefaultPanel = (
element: HTMLElement,
Expand Down
5 changes: 4 additions & 1 deletion src/onboarding/ha-onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
storeState(this.hass!);
} else {
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
window.localStorage.setItem(
"selectedLanguage",
JSON.stringify(language)
);
} catch (err: any) {
// Ignore
}
Expand Down
7 changes: 4 additions & 3 deletions src/panels/config/automation/ha-automation-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,14 @@ export class HaAutomationTrace extends LitElement {
if (!traceText) {
return;
}
localStorage.devTrace = traceText;
window.localStorage.setItem("devTrace", traceText);
this._loadLocalTrace(traceText);
}

private _loadLocalStorageTrace() {
if (localStorage.devTrace) {
this._loadLocalTrace(localStorage.devTrace);
const devTrace = window.localStorage.getItem("devTrace");
if (devTrace) {
this._loadLocalTrace(devTrace);
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/panels/config/script/ha-script-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,13 +487,15 @@ export class HaScriptTrace extends LitElement {
if (!traceText) {
return;
}
localStorage.devTrace = traceText;

window.localStorage.setItem("devTrace", traceText);
this._loadLocalTrace(traceText);
}

private _loadLocalStorageTrace() {
if (localStorage.devTrace) {
this._loadLocalTrace(localStorage.devTrace);
const devTrace = window.localStorage.getItem("devTrace");
if (devTrace) {
this._loadLocalTrace(devTrace);
}
}

Expand Down
22 changes: 14 additions & 8 deletions src/util/ha-pref-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,34 @@ const STORED_STATE = [
"enableShortcuts",
"defaultPanel",
];
const STORAGE = window.localStorage || {};

export function storeState(hass: HomeAssistant) {
try {
STORED_STATE.forEach((key) => {
const value = hass[key];
STORAGE[key] = JSON.stringify(value === undefined ? null : value);
window.localStorage.setItem(
key,
JSON.stringify(value === undefined ? null : value)
);
});
} catch (err: any) {
// Safari throws exception in private mode
// eslint-disable-next-line no-console
console.warn(
"Cannot store state; Are you in private mode or is your storage full?"
);
// eslint-disable-next-line no-console
console.error(err);
}
}

export function getState() {
const state = {};

STORED_STATE.forEach((key) => {
if (key in STORAGE) {
let value = JSON.parse(STORAGE[key]);
const storageItem = window.localStorage.getItem(key);
if (storageItem !== null) {
let value = JSON.parse(storageItem);
// selectedTheme went from string to object on 20200718
if (key === "selectedTheme" && typeof value === "string") {
value = { theme: value };
Expand All @@ -44,8 +53,5 @@ export function getState() {
}

export function clearState() {
// STORAGE is an object if localStorage not available.
if (STORAGE.clear) {
STORAGE.clear();
}
window.localStorage.clear();
}
62 changes: 62 additions & 0 deletions test/common/auth/token_storage/askWrite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { afterEach, describe, expect, test, vi } from "vitest";

let askWrite;

describe("token_storage.askWrite", () => {
afterEach(() => {
vi.resetModules();
});

test("askWrite", async () => {
vi.stubGlobal(
"window.__tokenCache",
(window.__tokenCache = {
tokens: undefined,
writeEnabled: true,
})
);

({ askWrite } = await import("../../../../src/common/auth/token_storage"));
expect(askWrite()).toBe(false);
});

test("askWrite prefilled token", async () => {
vi.stubGlobal(
"window.__tokenCache",
(window.__tokenCache = {
tokens: {
access_token: "test",
expires: 1800,
expires_in: 1800,
hassUrl: "http://localhost",
refresh_token: "refresh",
clientId: "client",
},
writeEnabled: undefined,
})
);

({ askWrite } = await import("../../../../src/common/auth/token_storage"));
expect(askWrite()).toBe(true);
});

test("askWrite prefilled token, write enabled", async () => {
vi.stubGlobal(
"window.__tokenCache",
(window.__tokenCache = {
tokens: {
access_token: "test",
expires: 1800,
expires_in: 1800,
hassUrl: "http://localhost",
refresh_token: "refresh",
clientId: "client",
},
writeEnabled: true,
})
);

({ askWrite } = await import("../../../../src/common/auth/token_storage"));
expect(askWrite()).toBe(false);
});
});
140 changes: 140 additions & 0 deletions test/common/auth/token_storage/saveTokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import type { AuthData } from "home-assistant-js-websocket";
import { FallbackStorage } from "../../../test_helper/local-storage-fallback";

let saveTokens;

describe("token_storage.saveTokens", () => {
beforeEach(() => {
window.localStorage = new FallbackStorage();
});

afterEach(() => {
vi.resetModules();
vi.resetAllMocks();
});

test("saveTokens", async () => {
const tokens: AuthData = {
access_token: "test",
expires: 1800,
expires_in: 1800,
hassUrl: "http://localhost",
refresh_token: "refresh",
clientId: "client",
};

vi.stubGlobal(
"window.__tokenCache",
(window.__tokenCache = {
tokens: undefined,
writeEnabled: undefined,
})
);

({ saveTokens } = await import(
"../../../../src/common/auth/token_storage"
));
saveTokens(tokens);
expect(window.__tokenCache.tokens).toEqual(tokens);
});

test("saveTokens write enabled", async () => {
const tokens: AuthData = {
access_token: "test",
expires: 1800,
expires_in: 1800,
hassUrl: "http://localhost",
refresh_token: "refresh",
clientId: "client",
};

vi.stubGlobal(
"window.__tokenCache",
(window.__tokenCache = {
tokens: undefined,
writeEnabled: undefined,
})
);

const extractSearchParamSpy = vi.fn().mockReturnValue("true");

vi.doMock("../../../../src/common/url/search-params", () => ({
extractSearchParam: extractSearchParamSpy,
}));
const setItemSpy = vi.fn();
window.localStorage.setItem = setItemSpy;

({ saveTokens } = await import(
"../../../../src/common/auth/token_storage"
));
saveTokens(tokens);
expect(window.__tokenCache.tokens).toEqual(tokens);
expect(window.__tokenCache.writeEnabled).toBe(true);
expect(extractSearchParamSpy).toHaveBeenCalledOnce();
expect(extractSearchParamSpy).toHaveBeenCalledWith("storeToken");
expect(setItemSpy).toHaveBeenCalledOnce();
expect(setItemSpy).toHaveBeenCalledWith(
"hassTokens",
JSON.stringify(tokens)
);
});

test("saveTokens write enabled full storage", async () => {
const tokens: AuthData = {
access_token: "test",
expires: 1800,
expires_in: 1800,
hassUrl: "http://localhost",
refresh_token: "refresh",
clientId: "client",
};

vi.stubGlobal(
"window.__tokenCache",
(window.__tokenCache = {
tokens: undefined,
writeEnabled: true,
})
);

const extractSearchParamSpy = vi.fn();

vi.doMock("../../../../src/common/url/search-params", () => ({
extractSearchParam: extractSearchParamSpy,
}));

const setItemSpy = vi.fn(() => {
throw new Error("Full storage");
});

window.localStorage.setItem = setItemSpy;

// eslint-disable-next-line no-global-assign
console = {
warn: vi.fn(),
error: vi.fn(),
} as unknown as Console;

({ saveTokens } = await import(
"../../../../src/common/auth/token_storage"
));
saveTokens(tokens);
expect(window.__tokenCache.tokens).toEqual(tokens);
expect(window.__tokenCache.writeEnabled).toBe(true);
expect(extractSearchParamSpy).toBeCalledTimes(0);
expect(setItemSpy).toHaveBeenCalledOnce();
expect(setItemSpy).toHaveBeenCalledWith(
"hassTokens",
JSON.stringify(tokens)
);
// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledOnce();
// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledWith(
"Failed to store tokens; Are you in private mode or is your storage full?"
);
// eslint-disable-next-line no-console
expect(console.error).toHaveBeenCalledOnce();
});
});
Loading
Loading