Skip to content

Commit c761e7c

Browse files
feat: handle guides live preview content (#730)
1 parent 3558784 commit c761e7c

File tree

6 files changed

+217
-61
lines changed

6 files changed

+217
-61
lines changed

.changeset/cruel-sides-strive.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@knocklabs/client": minor
3+
"@knocklabs/react": minor
4+
---
5+
6+
feat: support live previewing guides content

packages/client/src/clients/guide/client.ts

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
GuideData,
2929
GuideGroupAddedEvent,
3030
GuideGroupUpdatedEvent,
31+
GuideLivePreviewUpdatedEvent,
3132
GuideRemovedEvent,
3233
GuideSocketEvent,
3334
GuideStepData,
@@ -58,7 +59,10 @@ const DEFAULT_COUNTER_INCREMENT_INTERVAL = 30 * 1000; // in milliseconds
5859
const SUBSCRIBE_RETRY_LIMIT = 3;
5960

6061
// Debug query param keys
61-
const DEBUG_GUIDE_KEY_PARAM = "knock_guide_key";
62+
export const DEBUG_QUERY_PARAMS = {
63+
GUIDE_KEY: "knock_guide_key",
64+
PREVIEW_SESSION_ID: "knock_preview_session_id",
65+
};
6266

6367
// Return the global window object if defined, so to safely guard against SSR.
6468
const checkForWindow = () => {
@@ -74,13 +78,14 @@ export const guidesApiRootPath = (userId: string | undefined | null) =>
7478
const detectDebugParams = (): DebugState => {
7579
const win = checkForWindow();
7680
if (!win) {
77-
return { forcedGuideKey: null };
81+
return { forcedGuideKey: null, previewSessionId: null };
7882
}
7983

8084
const urlParams = new URLSearchParams(win.location.search);
81-
const forcedGuideKey = urlParams.get(DEBUG_GUIDE_KEY_PARAM);
85+
const forcedGuideKey = urlParams.get(DEBUG_QUERY_PARAMS.GUIDE_KEY);
86+
const previewSessionId = urlParams.get(DEBUG_QUERY_PARAMS.PREVIEW_SESSION_ID);
8287

83-
return { forcedGuideKey };
88+
return { forcedGuideKey, previewSessionId };
8489
};
8590

8691
const select = (state: StoreState, filters: SelectFilterParams = {}) => {
@@ -103,7 +108,7 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
103108
}
104109

105110
for (const [index, guideKey] of displaySequence.entries()) {
106-
const guide = state.guides[guideKey];
111+
let guide = state.guides[guideKey];
107112
if (!guide) continue;
108113

109114
const affirmed = predicate(guide, {
@@ -113,6 +118,14 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
113118
});
114119
if (!affirmed) continue;
115120

121+
// Use preview guide if it exists and matches the forced guide key
122+
if (
123+
state.debug.forcedGuideKey === guideKey &&
124+
state.previewGuides[guideKey]
125+
) {
126+
guide = state.previewGuides[guideKey];
127+
}
128+
116129
result.set(index, guide);
117130
}
118131

@@ -177,6 +190,7 @@ export class KnockGuideClient {
177190
"guide.removed",
178191
"guide_group.added",
179192
"guide_group.updated",
193+
"guide.live_preview_updated",
180194
];
181195
private subscribeRetryCount = 0;
182196

@@ -208,6 +222,7 @@ export class KnockGuideClient {
208222
guideGroups: [],
209223
guideGroupDisplayLogs: {},
210224
guides: {},
225+
previewGuides: {},
211226
queries: {},
212227
location,
213228
// Increment to update the state store and trigger re-selection.
@@ -331,6 +346,7 @@ export class KnockGuideClient {
331346
...this.targetParams,
332347
user_id: this.knock.userId,
333348
force_all_guides: debugState.forcedGuideKey ? true : undefined,
349+
preview_session_id: debugState.previewSessionId || undefined,
334350
};
335351

336352
const newChannel = this.socket.channel(this.socketChannelTopic, params);
@@ -411,6 +427,9 @@ export class KnockGuideClient {
411427
case "guide_group.updated":
412428
return this.addOrReplaceGuideGroup(payload);
413429

430+
case "guide.live_preview_updated":
431+
return this.updatePreviewGuide(payload);
432+
414433
default:
415434
return;
416435
}
@@ -420,11 +439,19 @@ export class KnockGuideClient {
420439
// Make sure to clear out the stage.
421440
this.clearGroupStage();
422441

423-
this.store.setState((state) => ({
424-
...state,
425-
...additionalParams,
426-
location: href,
427-
}));
442+
this.store.setState((state) => {
443+
// Clear preview guides if no longer in preview mode
444+
const previewGuides = additionalParams?.debug?.previewSessionId
445+
? state.previewGuides
446+
: {};
447+
448+
return {
449+
...state,
450+
...additionalParams,
451+
previewGuides,
452+
location: href,
453+
};
454+
});
428455
}
429456

430457
//
@@ -965,6 +992,15 @@ export class KnockGuideClient {
965992
});
966993
}
967994

995+
private updatePreviewGuide({ data }: GuideLivePreviewUpdatedEvent) {
996+
const guide = this.localCopy(data.guide);
997+
998+
this.store.setState((state) => {
999+
const previewGuides = { ...state.previewGuides, [guide.key]: guide };
1000+
return { ...state, previewGuides };
1001+
});
1002+
}
1003+
9681004
// Define as an arrow func property to always bind this to the class instance.
9691005
private handleLocationChange = () => {
9701006
const win = checkForWindow();
@@ -980,20 +1016,30 @@ export class KnockGuideClient {
9801016
const newDebugParams = detectDebugParams();
9811017
this.setLocation(href, { debug: newDebugParams });
9821018

983-
// If entering/exiting debug mode, refetch guides and resubscribe to the
984-
// websocket channel.
985-
if (
986-
Boolean(currentDebugParams.forcedGuideKey) !==
987-
Boolean(newDebugParams.forcedGuideKey)
988-
) {
1019+
// If debug state has changed, refetch guides and resubscribe to the websocket channel
1020+
const debugStateChanged = this.checkDebugStateChanged(
1021+
currentDebugParams,
1022+
newDebugParams,
1023+
);
1024+
1025+
if (debugStateChanged) {
9891026
this.knock.log(
990-
`[Guide] ${newDebugParams.forcedGuideKey ? "Entering" : "Exiting"} debug mode`,
1027+
`[Guide] Debug state changed, refetching guides and resubscribing to the websocket channel`,
9911028
);
9921029
this.fetch();
9931030
this.subscribe();
9941031
}
9951032
};
9961033

1034+
// Returns whether debug params have changed. For guide key, we only check
1035+
// presence since the exact value has no impact on fetch/subscribe
1036+
private checkDebugStateChanged(a: DebugState, b: DebugState): boolean {
1037+
return (
1038+
Boolean(a.forcedGuideKey) !== Boolean(b.forcedGuideKey) ||
1039+
a.previewSessionId !== b.previewSessionId
1040+
);
1041+
}
1042+
9971043
private listenForLocationChangesFromWindow() {
9981044
const win = checkForWindow();
9991045
if (win?.history) {

packages/client/src/clients/guide/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { KnockGuideClient } from "./client";
1+
export { KnockGuideClient, DEBUG_QUERY_PARAMS } from "./client";
22
export type {
33
KnockGuide,
44
KnockGuideStep,

packages/client/src/clients/guide/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ type SocketEventType =
114114
| "guide.updated"
115115
| "guide.removed"
116116
| "guide_group.added"
117-
| "guide_group.updated";
117+
| "guide_group.updated"
118+
| "guide.live_preview_updated";
118119

119120
type SocketEventPayload<E extends SocketEventType, D> = {
120121
topic: string;
@@ -147,12 +148,18 @@ export type GuideGroupUpdatedEvent = SocketEventPayload<
147148
{ guide_group: GuideGroupData }
148149
>;
149150

151+
export type GuideLivePreviewUpdatedEvent = SocketEventPayload<
152+
"guide.live_preview_updated",
153+
{ guide: GuideData; eligible: boolean }
154+
>;
155+
150156
export type GuideSocketEvent =
151157
| GuideAddedEvent
152158
| GuideUpdatedEvent
153159
| GuideRemovedEvent
154160
| GuideGroupAddedEvent
155-
| GuideGroupUpdatedEvent;
161+
| GuideGroupUpdatedEvent
162+
| GuideLivePreviewUpdatedEvent;
156163

157164
//
158165
// Guide client
@@ -185,12 +192,14 @@ export type QueryStatus = {
185192

186193
export type DebugState = {
187194
forcedGuideKey?: string | null;
195+
previewSessionId?: string | null;
188196
};
189197

190198
export type StoreState = {
191199
guideGroups: GuideGroupData[];
192200
guideGroupDisplayLogs: Record<GuideGroupData["key"], string>;
193201
guides: Record<KnockGuide["key"], KnockGuide>;
202+
previewGuides: Record<KnockGuide["key"], KnockGuide>;
194203
queries: Record<QueryKey, QueryStatus>;
195204
location: string | undefined;
196205
counter: number;

0 commit comments

Comments
 (0)