Skip to content

Commit

Permalink
chat-headless: remove session storage support in favor of local storage
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
popestr committed Jun 3, 2024
1 parent 7be91bf commit 80618cd
Show file tree
Hide file tree
Showing 17 changed files with 79 additions and 79 deletions.
4 changes: 2 additions & 2 deletions packages/chat-headless-react/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
20 changes: 11 additions & 9 deletions packages/chat-headless-react/src/ChatHeadlessProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ChatHeadlessInstanceProvider
deferRender={config.saveToSessionStorage}
deferRender={config.saveToLocalStorage}
headless={headless}
>
{children}
Expand All @@ -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;
}>;
Expand All @@ -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]);

Expand Down
8 changes: 4 additions & 4 deletions packages/chat-headless-react/tests/headlessProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" }],
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion packages/chat-headless/THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file

The following npm package may be included in this product:

- @babel/[email protected].5
- @babel/[email protected].6

This package contains the following license and notice below:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/chat-headless](./chat-headless.md) &gt; [ChatHeadless](./chat-headless.chatheadless.md) &gt; [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.

This file was deleted.

2 changes: 1 addition & 1 deletion packages/chat-headless/docs/chat-headless.chatheadless.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export interface HeadlessConfig extends ChatConfig
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [analyticsConfig?](./chat-headless.headlessconfig.analyticsconfig.md) | | Omit&lt;ChatAnalyticsConfig, "apiKey" \| "env" \| "region"&gt; &amp; { baseEventPayload?: DeepPartial&lt;ChatEventPayLoad&gt;; } | _(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. |
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/chat-headless](./chat-headless.md) &gt; [HeadlessConfig](./chat-headless.headlessconfig.md) &gt; [saveToSessionStorage](./chat-headless.headlessconfig.savetosessionstorage.md)
[Home](./index.md) &gt; [@yext/chat-headless](./chat-headless.md) &gt; [HeadlessConfig](./chat-headless.headlessconfig.md) &gt; [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;
```
4 changes: 2 additions & 2 deletions packages/chat-headless/etc/chat-headless.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface ChatHeadless {
addListener<T>(listener: StateListener<T>): Unsubscribe;
addMessage(message: Message): void;
getNextMessage(text?: string, source?: MessageSource): Promise<MessageResponse | undefined>;
initSessionStorage(): void;
initLocalStorage(): void;
report(eventPayload: Omit<ChatEventPayLoad, "chat"> & DeepPartial<Pick<ChatEventPayLoad, "chat">>): Promise<void>;
restartConversation(): void;
setCanSendMessage(canSendMessage: boolean): void;
Expand Down Expand Up @@ -84,7 +84,7 @@ export interface HeadlessConfig extends ChatConfig {
analyticsConfig?: Omit<ChatAnalyticsConfig, "apiKey" | "env" | "region"> & {
baseEventPayload?: DeepPartial<ChatEventPayLoad>;
};
saveToSessionStorage?: boolean;
saveToLocalStorage?: boolean;
}

export { InternalConfig }
Expand Down
2 changes: 1 addition & 1 deletion packages/chat-headless/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 4 additions & 4 deletions packages/chat-headless/src/ChatHeadlessImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class ChatHeadlessImpl implements ChatHeadless {
*/
constructor(config: HeadlessConfig, chatClient?: ChatClient) {
const defaultConfig: Partial<HeadlessConfig> = {
saveToSessionStorage: true,
saveToLocalStorage: true,
};
this.config = { ...defaultConfig, ...config };
this.chatClient = chatClient ?? provideChatCore(this.config);
Expand All @@ -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();
}
}

Expand Down Expand Up @@ -100,7 +100,7 @@ export class ChatHeadlessImpl implements ChatHeadless {
};
}

initSessionStorage() {
initLocalStorage() {
this.setState({
...this.state,
conversation: loadSessionState(this.config.botId),
Expand Down
6 changes: 3 additions & 3 deletions packages/chat-headless/src/models/ChatHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,17 @@ export interface ChatHeadless {
*/
addClientSdk(additionalClientSdk: Record<string, string>): 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}
*
Expand Down
4 changes: 2 additions & 2 deletions packages/chat-headless/src/models/HeadlessConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatAnalyticsConfig, "apiKey" | "env" | "region"> & {
/** Base payload to include for requests to the Analytics Events API. */
Expand Down
24 changes: 12 additions & 12 deletions packages/chat-headless/src/slices/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@ 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: [],
isLoading: false,
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;
}
Expand All @@ -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;
}
Expand All @@ -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)
);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/chat-headless/tests/chatheadless.chatapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function mockChatCore(spy?: jest.Mock) {
}

beforeEach(() => {
sessionStorage.clear();
localStorage.clear();
jest.spyOn(analyticsLib, "provideChatAnalytics").mockReturnValue({
report: jest.fn(),
});
Expand Down
Loading

0 comments on commit 80618cd

Please sign in to comment.