From 80618cd5c53c950d185cba1903c76c5f44a195ad Mon Sep 17 00:00:00 2001 From: Ryan Pope Date: Mon, 3 Jun 2024 15:29:16 -0400 Subject: [PATCH] chat-headless: remove session storage support in favor of local storage Removes all references and usages of session storage in favor of local storage. This means that conversation state will now persist across pages as well as refreshes, so long as all pages are on the same hostname. TEST=manual,auto Updated unit tests, saw them pass. Ran local test page and refreshed, closed tabs, reopened. State persisted always. --- packages/chat-headless-react/package.json | 4 ++-- .../src/ChatHeadlessProvider.tsx | 20 +++++++++------- .../tests/headlessProvider.test.tsx | 8 +++---- packages/chat-headless/THIRD-PARTY-NOTICES | 2 +- ...-headless.chatheadless.initlocalstorage.md | 21 ++++++++++++++++ ...eadless.chatheadless.initsessionstorage.md | 21 ---------------- .../docs/chat-headless.chatheadless.md | 2 +- .../docs/chat-headless.headlessconfig.md | 2 +- ...less.headlessconfig.savetolocalstorage.md} | 8 +++---- .../chat-headless/etc/chat-headless.api.md | 4 ++-- packages/chat-headless/package.json | 2 +- .../chat-headless/src/ChatHeadlessImpl.ts | 8 +++---- .../chat-headless/src/models/ChatHeadless.ts | 6 ++--- .../src/models/HeadlessConfig.ts | 4 ++-- .../chat-headless/src/slices/conversation.ts | 24 +++++++++---------- .../tests/chatheadless.chatapi.test.ts | 2 +- .../chat-headless/tests/chatheadless.test.ts | 20 +++++++--------- 17 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 packages/chat-headless/docs/chat-headless.chatheadless.initlocalstorage.md delete mode 100644 packages/chat-headless/docs/chat-headless.chatheadless.initsessionstorage.md rename packages/chat-headless/docs/{chat-headless.headlessconfig.savetosessionstorage.md => chat-headless.headlessconfig.savetolocalstorage.md} (56%) diff --git a/packages/chat-headless-react/package.json b/packages/chat-headless-react/package.json index 23246fe..dc625e1 100644 --- a/packages/chat-headless-react/package.json +++ b/packages/chat-headless-react/package.json @@ -1,6 +1,6 @@ { "name": "@yext/chat-headless-react", - "version": "0.7.0", + "version": "0.7.1", "description": "the official React UI Bindings layer for Chat Headless", "main": "./dist/commonjs/src/index.js", "module": "./dist/esm/src/index.mjs", @@ -39,7 +39,7 @@ "homepage": "https://github.com/yext/chat-headless#readme", "dependencies": { "@reduxjs/toolkit": "^1.9.5", - "@yext/chat-headless": "^0.8.0", + "@yext/chat-headless": "^0.8.1", "react-redux": "^8.0.5" }, "peerDependencies": { diff --git a/packages/chat-headless-react/src/ChatHeadlessProvider.tsx b/packages/chat-headless-react/src/ChatHeadlessProvider.tsx index 1f64b3c..8699b55 100644 --- a/packages/chat-headless-react/src/ChatHeadlessProvider.tsx +++ b/packages/chat-headless-react/src/ChatHeadlessProvider.tsx @@ -31,14 +31,16 @@ export function ChatHeadlessProvider( const { children, config } = props; const headless = useMemo(() => { - const configWithoutSession = { ...config, saveToSessionStorage: false }; - const headless = provideChatHeadless(updateClientSdk(configWithoutSession)); + const configWithoutLocalStorage = { ...config, saveToLocalStorage: false }; + const headless = provideChatHeadless( + updateClientSdk(configWithoutLocalStorage) + ); return headless; }, [config]); return ( {children} @@ -53,7 +55,7 @@ export function ChatHeadlessProvider( */ export type ChatHeadlessInstanceProviderProps = PropsWithChildren<{ // Set this to true when using server-side rendering in conjunction with - // browser-specific APIs like session storage. + // browser-specific APIs like local storage. deferRender?: boolean; headless: ChatHeadless; }>; @@ -69,19 +71,19 @@ export function ChatHeadlessInstanceProvider( props: ChatHeadlessInstanceProviderProps ): JSX.Element { const { children, deferRender, headless } = props; - // deferLoad is typically used with sessionStorage so that the children won't be + // deferLoad is typically used with localStorage so that the children won't be // immediately rendered and trigger the "load initial message" flow before - // the state can be loaded from session. + // the state can be loaded from local storage. const [deferLoad, setDeferLoad] = useState(deferRender); - // sessionStorage is overridden here so that it is compatible with server- - // side rendering, which cannot have browser api calls like session storage + // localStorage is overridden here so that it is compatible with server- + // side rendering, which cannot have browser api calls like local storage // outside of hooks. useEffect(() => { if (!deferRender || !headless) { return; } - headless.initSessionStorage(); + headless.initLocalStorage(); setDeferLoad(false); }, [headless, deferRender]); diff --git a/packages/chat-headless-react/tests/headlessProvider.test.tsx b/packages/chat-headless-react/tests/headlessProvider.test.tsx index 09e1947..e35e189 100644 --- a/packages/chat-headless-react/tests/headlessProvider.test.tsx +++ b/packages/chat-headless-react/tests/headlessProvider.test.tsx @@ -7,11 +7,11 @@ import { import { renderToString } from "react-dom/server"; import { useChatState } from "../src/useChatState"; -it("only fetches session storage on client-side render", async () => { +it("only fetches local storage on client-side render", async () => { const win = window; - Object.defineProperty(win, "sessionStorage", { + Object.defineProperty(win, "localStorage", { value: { - ...win.sessionStorage, + ...win.localStorage, getItem: (_: string): string => { return JSON.stringify({ messages: [{ text: "foobar", source: "BOT" }], @@ -27,7 +27,7 @@ it("only fetches session storage on client-side render", async () => { const config: HeadlessConfig = { botId: "123", apiKey: "1234", - saveToSessionStorage: true, + saveToLocalStorage: true, }; const str = () => renderToString( diff --git a/packages/chat-headless/THIRD-PARTY-NOTICES b/packages/chat-headless/THIRD-PARTY-NOTICES index 53446c5..fa1d98f 100644 --- a/packages/chat-headless/THIRD-PARTY-NOTICES +++ b/packages/chat-headless/THIRD-PARTY-NOTICES @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file The following npm package may be included in this product: - - @babel/runtime@7.24.5 + - @babel/runtime@7.24.6 This package contains the following license and notice below: diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.initlocalstorage.md b/packages/chat-headless/docs/chat-headless.chatheadless.initlocalstorage.md new file mode 100644 index 0000000..8a309f1 --- /dev/null +++ b/packages/chat-headless/docs/chat-headless.chatheadless.initlocalstorage.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [ChatHeadless](./chat-headless.chatheadless.md) > [initLocalStorage](./chat-headless.chatheadless.initlocalstorage.md) + +## ChatHeadless.initLocalStorage() method + +Loads the [ConversationState](./chat-headless.conversationstate.md) from local storage, if present, and adds a listener to keep the conversation state in sync with the stored state + +**Signature:** + +```typescript +initLocalStorage(): void; +``` +**Returns:** + +void + +## Remarks + +This is called by default if [HeadlessConfig.saveToLocalStorage](./chat-headless.headlessconfig.savetolocalstorage.md) is true. + diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.initsessionstorage.md b/packages/chat-headless/docs/chat-headless.chatheadless.initsessionstorage.md deleted file mode 100644 index 653b536..0000000 --- a/packages/chat-headless/docs/chat-headless.chatheadless.initsessionstorage.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [ChatHeadless](./chat-headless.chatheadless.md) > [initSessionStorage](./chat-headless.chatheadless.initsessionstorage.md) - -## ChatHeadless.initSessionStorage() method - -Loads the [ConversationState](./chat-headless.conversationstate.md) from session storage, if present, and adds a listener to keep the conversation state in sync with the stored state - -**Signature:** - -```typescript -initSessionStorage(): void; -``` -**Returns:** - -void - -## Remarks - -This is called by default if [HeadlessConfig.saveToSessionStorage](./chat-headless.headlessconfig.savetosessionstorage.md) is true. - diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.md b/packages/chat-headless/docs/chat-headless.chatheadless.md index e5275c9..75f8748 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless.md @@ -19,7 +19,7 @@ export interface ChatHeadless | [addListener(listener)](./chat-headless.chatheadless.addlistener.md) | Adds a listener for a specific state value of type T. | | [addMessage(message)](./chat-headless.chatheadless.addmessage.md) | Adds a new message to [ConversationState.messages](./chat-headless.conversationstate.messages.md) | | [getNextMessage(text, source)](./chat-headless.chatheadless.getnextmessage.md) | Performs a Chat API request for the next message generated by chat bot using the conversation state (e.g. message history and notes). Update the state with the response data. | -| [initSessionStorage()](./chat-headless.chatheadless.initsessionstorage.md) | Loads the [ConversationState](./chat-headless.conversationstate.md) from session storage, if present, and adds a listener to keep the conversation state in sync with the stored state | +| [initLocalStorage()](./chat-headless.chatheadless.initlocalstorage.md) | Loads the [ConversationState](./chat-headless.conversationstate.md) from local storage, if present, and adds a listener to keep the conversation state in sync with the stored state | | [report(eventPayload)](./chat-headless.chatheadless.report.md) | Send Chat related analytics event to Yext Analytics API. | | [restartConversation()](./chat-headless.chatheadless.restartconversation.md) | Resets all fields within [ConversationState](./chat-headless.conversationstate.md) | | [setCanSendMessage(canSendMessage)](./chat-headless.chatheadless.setcansendmessage.md) | Sets [ConversationState.canSendMessage](./chat-headless.conversationstate.cansendmessage.md) to the specified state | diff --git a/packages/chat-headless/docs/chat-headless.headlessconfig.md b/packages/chat-headless/docs/chat-headless.headlessconfig.md index d786c89..218aab1 100644 --- a/packages/chat-headless/docs/chat-headless.headlessconfig.md +++ b/packages/chat-headless/docs/chat-headless.headlessconfig.md @@ -18,5 +18,5 @@ export interface HeadlessConfig extends ChatConfig | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [analyticsConfig?](./chat-headless.headlessconfig.analyticsconfig.md) | | Omit<ChatAnalyticsConfig, "apiKey" \| "env" \| "region"> & { baseEventPayload?: DeepPartial<ChatEventPayLoad>; } | _(Optional)_ Configurations for Chat analytics. | -| [saveToSessionStorage?](./chat-headless.headlessconfig.savetosessionstorage.md) | | boolean | _(Optional)_ Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to session storage. Defaults to true. | +| [saveToLocalStorage?](./chat-headless.headlessconfig.savetolocalstorage.md) | | boolean | _(Optional)_ Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to local storage. Defaults to true. | diff --git a/packages/chat-headless/docs/chat-headless.headlessconfig.savetosessionstorage.md b/packages/chat-headless/docs/chat-headless.headlessconfig.savetolocalstorage.md similarity index 56% rename from packages/chat-headless/docs/chat-headless.headlessconfig.savetosessionstorage.md rename to packages/chat-headless/docs/chat-headless.headlessconfig.savetolocalstorage.md index 08cba97..8c19dd9 100644 --- a/packages/chat-headless/docs/chat-headless.headlessconfig.savetosessionstorage.md +++ b/packages/chat-headless/docs/chat-headless.headlessconfig.savetolocalstorage.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [HeadlessConfig](./chat-headless.headlessconfig.md) > [saveToSessionStorage](./chat-headless.headlessconfig.savetosessionstorage.md) +[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [HeadlessConfig](./chat-headless.headlessconfig.md) > [saveToLocalStorage](./chat-headless.headlessconfig.savetolocalstorage.md) -## HeadlessConfig.saveToSessionStorage property +## HeadlessConfig.saveToLocalStorage property -Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to session storage. Defaults to true. +Whether to save the instance's [ConversationState](./chat-headless.conversationstate.md) to local storage. Defaults to true. **Signature:** ```typescript -saveToSessionStorage?: boolean; +saveToLocalStorage?: boolean; ``` diff --git a/packages/chat-headless/etc/chat-headless.api.md b/packages/chat-headless/etc/chat-headless.api.md index 4dfdc73..6ed7f3c 100644 --- a/packages/chat-headless/etc/chat-headless.api.md +++ b/packages/chat-headless/etc/chat-headless.api.md @@ -47,7 +47,7 @@ export interface ChatHeadless { addListener(listener: StateListener): Unsubscribe; addMessage(message: Message): void; getNextMessage(text?: string, source?: MessageSource): Promise; - initSessionStorage(): void; + initLocalStorage(): void; report(eventPayload: Omit & DeepPartial>): Promise; restartConversation(): void; setCanSendMessage(canSendMessage: boolean): void; @@ -84,7 +84,7 @@ export interface HeadlessConfig extends ChatConfig { analyticsConfig?: Omit & { baseEventPayload?: DeepPartial; }; - saveToSessionStorage?: boolean; + saveToLocalStorage?: boolean; } export { InternalConfig } diff --git a/packages/chat-headless/package.json b/packages/chat-headless/package.json index f4ca229..2862e20 100644 --- a/packages/chat-headless/package.json +++ b/packages/chat-headless/package.json @@ -1,6 +1,6 @@ { "name": "@yext/chat-headless", - "version": "0.8.0", + "version": "0.8.1", "description": "A state manager library powered by Redux for Yext Chat integrations", "main": "./dist/commonjs/src/index.js", "module": "./dist/esm/src/index.mjs", diff --git a/packages/chat-headless/src/ChatHeadlessImpl.ts b/packages/chat-headless/src/ChatHeadlessImpl.ts index eb95439..5eadb2a 100644 --- a/packages/chat-headless/src/ChatHeadlessImpl.ts +++ b/packages/chat-headless/src/ChatHeadlessImpl.ts @@ -55,7 +55,7 @@ export class ChatHeadlessImpl implements ChatHeadless { */ constructor(config: HeadlessConfig, chatClient?: ChatClient) { const defaultConfig: Partial = { - saveToSessionStorage: true, + saveToLocalStorage: true, }; this.config = { ...defaultConfig, ...config }; this.chatClient = chatClient ?? provideChatCore(this.config); @@ -66,8 +66,8 @@ export class ChatHeadlessImpl implements ChatHeadless { region: this.config.region, ...this.config.analyticsConfig, }); - if (this.config.saveToSessionStorage) { - this.initSessionStorage(); + if (this.config.saveToLocalStorage) { + this.initLocalStorage(); } } @@ -100,7 +100,7 @@ export class ChatHeadlessImpl implements ChatHeadless { }; } - initSessionStorage() { + initLocalStorage() { this.setState({ ...this.state, conversation: loadSessionState(this.config.botId), diff --git a/packages/chat-headless/src/models/ChatHeadless.ts b/packages/chat-headless/src/models/ChatHeadless.ts index 0a6c2b0..443c691 100644 --- a/packages/chat-headless/src/models/ChatHeadless.ts +++ b/packages/chat-headless/src/models/ChatHeadless.ts @@ -98,17 +98,17 @@ export interface ChatHeadless { */ addClientSdk(additionalClientSdk: Record): void; /** - * Loads the {@link ConversationState} from session storage, if present, + * Loads the {@link ConversationState} from local storage, if present, * and adds a listener to keep the conversation state in sync with the stored * state * * @remarks - * This is called by default if {@link HeadlessConfig.saveToSessionStorage} is + * This is called by default if {@link HeadlessConfig.saveToLocalStorage} is * true. * * @public */ - initSessionStorage(): void; + initLocalStorage(): void; /** * Resets all fields within {@link ConversationState} * diff --git a/packages/chat-headless/src/models/HeadlessConfig.ts b/packages/chat-headless/src/models/HeadlessConfig.ts index 00aeb7c..7f3b7f7 100644 --- a/packages/chat-headless/src/models/HeadlessConfig.ts +++ b/packages/chat-headless/src/models/HeadlessConfig.ts @@ -8,8 +8,8 @@ import { ChatConfig } from "@yext/chat-core"; * @public */ export interface HeadlessConfig extends ChatConfig { - /** Whether to save the instance's {@link ConversationState} to session storage. Defaults to true. */ - saveToSessionStorage?: boolean; + /** Whether to save the instance's {@link ConversationState} to local storage. Defaults to true. */ + saveToLocalStorage?: boolean; /** Configurations for Chat analytics. */ analyticsConfig?: Omit & { /** Base payload to include for requests to the Analytics Events API. */ diff --git a/packages/chat-headless/src/slices/conversation.ts b/packages/chat-headless/src/slices/conversation.ts index 401a494..5a887a1 100644 --- a/packages/chat-headless/src/slices/conversation.ts +++ b/packages/chat-headless/src/slices/conversation.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { ConversationState } from "../models/slices/ConversationState"; import { Message, MessageNotes } from "@yext/chat-core"; -const BASE_STATE_SESSION_STORAGE_KEY = "yext_chat_state"; +const BASE_STATE_LOCAL_STORAGE_KEY = "yext_chat_state"; export const initialState: ConversationState = { messages: [], @@ -10,20 +10,20 @@ export const initialState: ConversationState = { canSendMessage: true, }; -export function getStateSessionStorageKey( +export function getStateLocalStorageKey( hostname: string, botId: string ): string { - return `${BASE_STATE_SESSION_STORAGE_KEY}__${hostname}__${botId}`; + return `${BASE_STATE_LOCAL_STORAGE_KEY}__${hostname}__${botId}`; } /** - * Loads the {@link ConversationState} from session storage. + * Loads the {@link ConversationState} from local storage. */ export const loadSessionState = (botId: string): ConversationState => { - if (!sessionStorage) { + if (!localStorage) { console.warn( - "Session storage is not available. State will not be persisted across page refreshes." + "Local storage is not available. State will not be persisted while navigating across pages." ); return initialState; } @@ -34,16 +34,16 @@ export const loadSessionState = (botId: string): ConversationState => { ); return initialState; } - const savedState = sessionStorage.getItem( - getStateSessionStorageKey(hostname, botId) + const savedState = localStorage.getItem( + getStateLocalStorageKey(hostname, botId) ); return savedState ? JSON.parse(savedState) : initialState; }; export const saveSessionState = (botId: string, state: ConversationState) => { - if (!sessionStorage) { + if (!localStorage) { console.warn( - "Session storage is not available. State will not be persisted across page refreshes." + "Local storage is not available. State will not be persisted while navigating across pages." ); return initialState; } @@ -54,8 +54,8 @@ export const saveSessionState = (botId: string, state: ConversationState) => { ); return initialState; } - sessionStorage.setItem( - getStateSessionStorageKey(hostname, botId), + localStorage.setItem( + getStateLocalStorageKey(hostname, botId), JSON.stringify(state) ); }; diff --git a/packages/chat-headless/tests/chatheadless.chatapi.test.ts b/packages/chat-headless/tests/chatheadless.chatapi.test.ts index 54f7496..b73ceec 100644 --- a/packages/chat-headless/tests/chatheadless.chatapi.test.ts +++ b/packages/chat-headless/tests/chatheadless.chatapi.test.ts @@ -38,7 +38,7 @@ function mockChatCore(spy?: jest.Mock) { } beforeEach(() => { - sessionStorage.clear(); + localStorage.clear(); jest.spyOn(analyticsLib, "provideChatAnalytics").mockReturnValue({ report: jest.fn(), }); diff --git a/packages/chat-headless/tests/chatheadless.test.ts b/packages/chat-headless/tests/chatheadless.test.ts index c4344b6..232ebcc 100644 --- a/packages/chat-headless/tests/chatheadless.test.ts +++ b/packages/chat-headless/tests/chatheadless.test.ts @@ -11,7 +11,7 @@ import { import coreLib from "@yext/chat-core"; import { ReduxStateManager } from "../src/ReduxStateManager"; import { - getStateSessionStorageKey, + getStateLocalStorageKey, initialState, } from "../src/slices/conversation"; @@ -29,7 +29,7 @@ const mockedMetaState: MetaState = { beforeEach(() => { jest.spyOn(coreLib, "provideChatCore").mockImplementation(); - sessionStorage.clear(); + localStorage.clear(); }); describe("setters work as expected", () => { @@ -286,9 +286,9 @@ describe("loadSessionState works as expected", () => { isLoading: true, canSendMessage: true, }; - it("loads valid state from session storage", () => { - sessionStorage.setItem( - getStateSessionStorageKey(jestHostname, config.botId), + it("loads valid state from local storage", () => { + localStorage.setItem( + getStateLocalStorageKey(jestHostname, config.botId), JSON.stringify(expectedState) ); const chatHeadless = provideChatHeadless(config); @@ -299,13 +299,13 @@ describe("loadSessionState works as expected", () => { }); it("does not persist or load state when toggle is off", () => { - sessionStorage.setItem( - getStateSessionStorageKey(jestHostname, config.botId), + localStorage.setItem( + getStateLocalStorageKey(jestHostname, config.botId), JSON.stringify(expectedState) ); const chatHeadless = provideChatHeadless({ ...config, - saveToSessionStorage: false, + saveToLocalStorage: false, }); expect(chatHeadless.state).toEqual({ conversation: initialState, @@ -321,9 +321,7 @@ describe("loadSessionState works as expected", () => { ]; chatHeadless.setMessages(modifiedMessages); expect( - sessionStorage.getItem( - getStateSessionStorageKey(jestHostname, config.botId) - ) + localStorage.getItem(getStateLocalStorageKey(jestHostname, config.botId)) ).toEqual(JSON.stringify(expectedState)); }); });