Skip to content

Commit

Permalink
chat-headless: remove session storage support in favor of local stora…
Browse files Browse the repository at this point in the history
…ge (#49)

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 authored Jun 5, 2024
1 parent 7be91bf commit 0b2f4e9
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 70 deletions.
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/chat-headless-react/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].7

This package contains the following license and notice below:

Expand Down Expand Up @@ -67,7 +67,7 @@ The following npm packages may be included in this product:
- @types/[email protected]
- @types/[email protected]
- @types/[email protected]
- @types/[email protected].2
- @types/[email protected].3
- @types/[email protected]

These packages each contain the following license and notice below:
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].7

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.9.0",
"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
52 changes: 38 additions & 14 deletions packages/chat-headless/src/slices/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,72 @@ 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;
}
const hostname = window?.location?.hostname;
if (!hostname) {
console.warn(
"Unable to get hostname of current page. State will not be persisted across page refreshes."
"Unable to get hostname of current page. State will not be persisted while navigating across pages."
);
return initialState;
}
const savedState = sessionStorage.getItem(
getStateSessionStorageKey(hostname, botId)
const savedState = localStorage.getItem(
getStateLocalStorageKey(hostname, botId)
);
return savedState ? JSON.parse(savedState) : initialState;

if (savedState) {
try {
const parsedState: ConversationState = JSON.parse(savedState);
if (parsedState.messages.length > 0) {
const lastTimestamp =
parsedState.messages[parsedState.messages.length - 1].timestamp;
const currentDate = new Date();
const lastDate = new Date(lastTimestamp || 0);
const diff = currentDate.getTime() - lastDate.getTime();
// If the last message was sent within the last day, we consider the session to be active
if (diff < 24 * 60 * 60 * 1000) {
return parsedState;
}
localStorage.removeItem(getStateLocalStorageKey(hostname, botId));
}
} catch (e) {
console.warn(
"Unabled to load saved state: error parsing state. Starting with a fresh state."
);
localStorage.removeItem(getStateLocalStorageKey(hostname, botId));
}
}

return 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 +78,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 0b2f4e9

Please sign in to comment.