@knocklabs/client 0.16.5 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/clients/guide/client.js +1 -1
- package/dist/cjs/clients/guide/client.js.map +1 -1
- package/dist/cjs/clients/guide/helpers.js +1 -1
- package/dist/cjs/clients/guide/helpers.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/clients/guide/client.mjs +208 -143
- package/dist/esm/clients/guide/client.mjs.map +1 -1
- package/dist/esm/clients/guide/helpers.mjs +47 -21
- package/dist/esm/clients/guide/helpers.mjs.map +1 -1
- package/dist/esm/index.mjs +10 -9
- package/dist/types/clients/guide/client.d.ts +13 -7
- package/dist/types/clients/guide/client.d.ts.map +1 -1
- package/dist/types/clients/guide/helpers.d.ts +5 -1
- package/dist/types/clients/guide/helpers.d.ts.map +1 -1
- package/dist/types/clients/guide/index.d.ts +1 -1
- package/dist/types/clients/guide/index.d.ts.map +1 -1
- package/dist/types/clients/guide/types.d.ts +33 -14
- package/dist/types/clients/guide/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clients/guide/client.ts +175 -42
- package/src/clients/guide/helpers.ts +99 -0
- package/src/clients/guide/index.ts +1 -1
- package/src/clients/guide/types.ts +42 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/clients/guide/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/clients/guide/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knocklabs/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "The clientside library for interacting with Knock",
|
|
5
5
|
"homepage": "https://github.com/knocklabs/javascript/tree/main/packages/client",
|
|
6
6
|
"author": "@knocklabs",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"jsonwebtoken": "^9.0.2",
|
|
63
63
|
"prettier": "^3.5.3",
|
|
64
64
|
"rimraf": "^6.0.1",
|
|
65
|
-
"rollup": "^4.
|
|
65
|
+
"rollup": "^4.46.2",
|
|
66
66
|
"typescript": "^5.8.3",
|
|
67
67
|
"vite": "^5.4.19",
|
|
68
68
|
"vitest": "^3.1.1"
|
|
@@ -13,9 +13,14 @@ import {
|
|
|
13
13
|
findDefaultGroup,
|
|
14
14
|
formatFilters,
|
|
15
15
|
mockDefaultGroup,
|
|
16
|
+
newUrl,
|
|
17
|
+
predicateUrlPatterns,
|
|
18
|
+
predicateUrlRules,
|
|
16
19
|
} from "./helpers";
|
|
17
20
|
import {
|
|
21
|
+
Any,
|
|
18
22
|
ConstructorOpts,
|
|
23
|
+
DebugState,
|
|
19
24
|
GetGuidesQueryParams,
|
|
20
25
|
GetGuidesResponse,
|
|
21
26
|
GroupStage,
|
|
@@ -23,6 +28,7 @@ import {
|
|
|
23
28
|
GuideData,
|
|
24
29
|
GuideGroupAddedEvent,
|
|
25
30
|
GuideGroupUpdatedEvent,
|
|
31
|
+
GuideLivePreviewUpdatedEvent,
|
|
26
32
|
GuideRemovedEvent,
|
|
27
33
|
GuideSocketEvent,
|
|
28
34
|
GuideStepData,
|
|
@@ -52,6 +58,12 @@ const DEFAULT_COUNTER_INCREMENT_INTERVAL = 30 * 1000; // in milliseconds
|
|
|
52
58
|
// Maximum number of retry attempts for channel subscription
|
|
53
59
|
const SUBSCRIBE_RETRY_LIMIT = 3;
|
|
54
60
|
|
|
61
|
+
// Debug query param keys
|
|
62
|
+
export const DEBUG_QUERY_PARAMS = {
|
|
63
|
+
GUIDE_KEY: "knock_guide_key",
|
|
64
|
+
PREVIEW_SESSION_ID: "knock_preview_session_id",
|
|
65
|
+
};
|
|
66
|
+
|
|
55
67
|
// Return the global window object if defined, so to safely guard against SSR.
|
|
56
68
|
const checkForWindow = () => {
|
|
57
69
|
if (typeof window !== "undefined") {
|
|
@@ -62,6 +74,20 @@ const checkForWindow = () => {
|
|
|
62
74
|
export const guidesApiRootPath = (userId: string | undefined | null) =>
|
|
63
75
|
`/v1/users/${userId}/guides`;
|
|
64
76
|
|
|
77
|
+
// Detect debug params like "knock_guide_key" from URL.
|
|
78
|
+
const detectDebugParams = (): DebugState => {
|
|
79
|
+
const win = checkForWindow();
|
|
80
|
+
if (!win) {
|
|
81
|
+
return { forcedGuideKey: null, previewSessionId: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const urlParams = new URLSearchParams(win.location.search);
|
|
85
|
+
const forcedGuideKey = urlParams.get(DEBUG_QUERY_PARAMS.GUIDE_KEY);
|
|
86
|
+
const previewSessionId = urlParams.get(DEBUG_QUERY_PARAMS.PREVIEW_SESSION_ID);
|
|
87
|
+
|
|
88
|
+
return { forcedGuideKey, previewSessionId };
|
|
89
|
+
};
|
|
90
|
+
|
|
65
91
|
const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
66
92
|
// A map of selected guides as values, with its order index as keys.
|
|
67
93
|
const result = new SelectionResult();
|
|
@@ -69,14 +95,37 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
69
95
|
const defaultGroup = findDefaultGroup(state.guideGroups);
|
|
70
96
|
if (!defaultGroup) return result;
|
|
71
97
|
|
|
72
|
-
const displaySequence = defaultGroup.display_sequence;
|
|
98
|
+
const displaySequence = [...defaultGroup.display_sequence];
|
|
73
99
|
const location = state.location;
|
|
74
100
|
|
|
101
|
+
// If in debug mode, put the forced guide at the beginning of the display sequence.
|
|
102
|
+
if (state.debug.forcedGuideKey) {
|
|
103
|
+
const forcedKeyIndex = displaySequence.indexOf(state.debug.forcedGuideKey);
|
|
104
|
+
if (forcedKeyIndex > -1) {
|
|
105
|
+
displaySequence.splice(forcedKeyIndex, 1);
|
|
106
|
+
}
|
|
107
|
+
displaySequence.unshift(state.debug.forcedGuideKey);
|
|
108
|
+
}
|
|
109
|
+
|
|
75
110
|
for (const [index, guideKey] of displaySequence.entries()) {
|
|
76
|
-
|
|
111
|
+
let guide = state.guides[guideKey];
|
|
112
|
+
|
|
113
|
+
// Use preview guide if it exists and matches the forced guide key
|
|
114
|
+
if (
|
|
115
|
+
state.debug.forcedGuideKey === guideKey &&
|
|
116
|
+
state.previewGuides[guideKey]
|
|
117
|
+
) {
|
|
118
|
+
guide = state.previewGuides[guideKey];
|
|
119
|
+
}
|
|
120
|
+
|
|
77
121
|
if (!guide) continue;
|
|
78
122
|
|
|
79
|
-
const affirmed = predicate(guide, {
|
|
123
|
+
const affirmed = predicate(guide, {
|
|
124
|
+
location,
|
|
125
|
+
filters,
|
|
126
|
+
debug: state.debug,
|
|
127
|
+
});
|
|
128
|
+
|
|
80
129
|
if (!affirmed) continue;
|
|
81
130
|
|
|
82
131
|
result.set(index, guide);
|
|
@@ -89,11 +138,12 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
89
138
|
type PredicateOpts = {
|
|
90
139
|
location?: string | undefined;
|
|
91
140
|
filters?: SelectFilterParams | undefined;
|
|
141
|
+
debug: DebugState;
|
|
92
142
|
};
|
|
93
143
|
|
|
94
144
|
const predicate = (
|
|
95
145
|
guide: KnockGuide,
|
|
96
|
-
{ location, filters = {} }: PredicateOpts,
|
|
146
|
+
{ location, filters = {}, debug = {} }: PredicateOpts,
|
|
97
147
|
) => {
|
|
98
148
|
if (filters.type && filters.type !== guide.type) {
|
|
99
149
|
return false;
|
|
@@ -103,40 +153,28 @@ const predicate = (
|
|
|
103
153
|
return false;
|
|
104
154
|
}
|
|
105
155
|
|
|
156
|
+
// Bypass filtering if the debugged guide matches the given filters.
|
|
157
|
+
// This should always run AFTER checking the filters but BEFORE
|
|
158
|
+
// checking archived status and location rules.
|
|
159
|
+
if (debug.forcedGuideKey === guide.key) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
106
163
|
if (guide.steps.every((s) => !!s.message.archived_at)) {
|
|
107
164
|
return false;
|
|
108
165
|
}
|
|
109
166
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
if (locationRules.length > 0 && location) {
|
|
113
|
-
const allowed = locationRules.reduce<boolean | undefined>((acc, rule) => {
|
|
114
|
-
// Any matched block rule prevails so no need to evaluate further
|
|
115
|
-
// as soon as there is one.
|
|
116
|
-
if (acc === false) return false;
|
|
117
|
-
|
|
118
|
-
// At this point we either have a matched allow rule (acc is true),
|
|
119
|
-
// or no matched rule found yet (acc is undefined).
|
|
120
|
-
|
|
121
|
-
switch (rule.directive) {
|
|
122
|
-
case "allow": {
|
|
123
|
-
// No need to evaluate more allow rules once we matched one
|
|
124
|
-
// since any matched allowed rule means allow.
|
|
125
|
-
if (acc === true) return true;
|
|
126
|
-
|
|
127
|
-
const matched = rule.pattern.test(location);
|
|
128
|
-
return matched ? true : undefined;
|
|
129
|
-
}
|
|
167
|
+
const url = location ? newUrl(location) : undefined;
|
|
130
168
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// because they'd prevail over matched allow rules.
|
|
134
|
-
const matched = rule.pattern.test(location);
|
|
135
|
-
return matched ? false : acc;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}, undefined);
|
|
169
|
+
const urlRules = guide.activation_url_rules || [];
|
|
170
|
+
const urlPatterns = guide.activation_url_patterns || [];
|
|
139
171
|
|
|
172
|
+
// A guide can have either activation url rules XOR url patterns, but not both.
|
|
173
|
+
if (url && urlRules.length > 0) {
|
|
174
|
+
const allowed = predicateUrlRules(url, urlRules);
|
|
175
|
+
if (!allowed) return false;
|
|
176
|
+
} else if (url && urlPatterns.length > 0) {
|
|
177
|
+
const allowed = predicateUrlPatterns(url, urlPatterns);
|
|
140
178
|
if (!allowed) return false;
|
|
141
179
|
}
|
|
142
180
|
|
|
@@ -156,6 +194,7 @@ export class KnockGuideClient {
|
|
|
156
194
|
"guide.removed",
|
|
157
195
|
"guide_group.added",
|
|
158
196
|
"guide_group.updated",
|
|
197
|
+
"guide.live_preview_updated",
|
|
159
198
|
];
|
|
160
199
|
private subscribeRetryCount = 0;
|
|
161
200
|
|
|
@@ -181,14 +220,18 @@ export class KnockGuideClient {
|
|
|
181
220
|
|
|
182
221
|
const location = trackLocationFromWindow ? win?.location.href : undefined;
|
|
183
222
|
|
|
223
|
+
const debug = detectDebugParams();
|
|
224
|
+
|
|
184
225
|
this.store = new Store<StoreState>({
|
|
185
226
|
guideGroups: [],
|
|
186
227
|
guideGroupDisplayLogs: {},
|
|
187
228
|
guides: {},
|
|
229
|
+
previewGuides: {},
|
|
188
230
|
queries: {},
|
|
189
231
|
location,
|
|
190
232
|
// Increment to update the state store and trigger re-selection.
|
|
191
233
|
counter: 0,
|
|
234
|
+
debug,
|
|
192
235
|
});
|
|
193
236
|
|
|
194
237
|
// In server environments we might not have a socket connection.
|
|
@@ -302,7 +345,14 @@ export class KnockGuideClient {
|
|
|
302
345
|
}
|
|
303
346
|
|
|
304
347
|
// Join the channel topic and subscribe to supported events.
|
|
305
|
-
const
|
|
348
|
+
const debugState = this.store.state.debug;
|
|
349
|
+
const params = {
|
|
350
|
+
...this.targetParams,
|
|
351
|
+
user_id: this.knock.userId,
|
|
352
|
+
force_all_guides: debugState.forcedGuideKey ? true : undefined,
|
|
353
|
+
preview_session_id: debugState.previewSessionId || undefined,
|
|
354
|
+
};
|
|
355
|
+
|
|
306
356
|
const newChannel = this.socket.channel(this.socketChannelTopic, params);
|
|
307
357
|
|
|
308
358
|
for (const eventType of this.socketEventTypes) {
|
|
@@ -381,23 +431,41 @@ export class KnockGuideClient {
|
|
|
381
431
|
case "guide_group.updated":
|
|
382
432
|
return this.addOrReplaceGuideGroup(payload);
|
|
383
433
|
|
|
434
|
+
case "guide.live_preview_updated":
|
|
435
|
+
return this.updatePreviewGuide(payload);
|
|
436
|
+
|
|
384
437
|
default:
|
|
385
438
|
return;
|
|
386
439
|
}
|
|
387
440
|
}
|
|
388
441
|
|
|
389
|
-
setLocation(href: string) {
|
|
442
|
+
setLocation(href: string, additionalParams: Partial<StoreState> = {}) {
|
|
390
443
|
// Make sure to clear out the stage.
|
|
391
444
|
this.clearGroupStage();
|
|
392
445
|
|
|
393
|
-
this.store.setState((state) =>
|
|
446
|
+
this.store.setState((state) => {
|
|
447
|
+
// Clear preview guides if no longer in preview mode
|
|
448
|
+
const previewGuides = additionalParams?.debug?.previewSessionId
|
|
449
|
+
? state.previewGuides
|
|
450
|
+
: {};
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
...state,
|
|
454
|
+
...additionalParams,
|
|
455
|
+
previewGuides,
|
|
456
|
+
location: href,
|
|
457
|
+
};
|
|
458
|
+
});
|
|
394
459
|
}
|
|
395
460
|
|
|
396
461
|
//
|
|
397
462
|
// Store selector
|
|
398
463
|
//
|
|
399
464
|
|
|
400
|
-
selectGuides
|
|
465
|
+
selectGuides<C = Any>(
|
|
466
|
+
state: StoreState,
|
|
467
|
+
filters: SelectFilterParams = {},
|
|
468
|
+
): KnockGuide<C>[] {
|
|
401
469
|
if (Object.keys(state.guides).length === 0) {
|
|
402
470
|
return [];
|
|
403
471
|
}
|
|
@@ -416,7 +484,10 @@ export class KnockGuideClient {
|
|
|
416
484
|
return [...result.values()];
|
|
417
485
|
}
|
|
418
486
|
|
|
419
|
-
selectGuide
|
|
487
|
+
selectGuide<C = Any>(
|
|
488
|
+
state: StoreState,
|
|
489
|
+
filters: SelectFilterParams = {},
|
|
490
|
+
): KnockGuide<C> | undefined {
|
|
420
491
|
if (Object.keys(state.guides).length === 0) {
|
|
421
492
|
return undefined;
|
|
422
493
|
}
|
|
@@ -539,10 +610,22 @@ export class KnockGuideClient {
|
|
|
539
610
|
// callback to a setTimeout, but just to be safe.
|
|
540
611
|
this.ensureClearTimeout();
|
|
541
612
|
|
|
613
|
+
// If in debug mode, try to resolve the forced guide, otherwise return the first non-undefined guide.
|
|
614
|
+
let resolved = undefined;
|
|
615
|
+
if (this.store.state.debug.forcedGuideKey) {
|
|
616
|
+
resolved = this.stage.ordered.find(
|
|
617
|
+
(x) => x === this.store.state.debug.forcedGuideKey,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!resolved) {
|
|
622
|
+
resolved = this.stage.ordered.find((x) => x !== undefined);
|
|
623
|
+
}
|
|
624
|
+
|
|
542
625
|
this.stage = {
|
|
543
626
|
...this.stage,
|
|
544
627
|
status: "closed",
|
|
545
|
-
resolved
|
|
628
|
+
resolved,
|
|
546
629
|
timeoutId: null,
|
|
547
630
|
};
|
|
548
631
|
|
|
@@ -706,6 +789,11 @@ export class KnockGuideClient {
|
|
|
706
789
|
...remoteGuide,
|
|
707
790
|
// Get the next unarchived step.
|
|
708
791
|
getStep() {
|
|
792
|
+
// If debugging this guide, return the first step regardless of archive status
|
|
793
|
+
if (self.store.state.debug.forcedGuideKey === this.key) {
|
|
794
|
+
return this.steps[0];
|
|
795
|
+
}
|
|
796
|
+
|
|
709
797
|
return this.steps.find((s) => !s.message.archived_at);
|
|
710
798
|
},
|
|
711
799
|
} as KnockGuide;
|
|
@@ -741,8 +829,8 @@ export class KnockGuideClient {
|
|
|
741
829
|
return localStep;
|
|
742
830
|
});
|
|
743
831
|
|
|
744
|
-
localGuide.
|
|
745
|
-
remoteGuide.
|
|
832
|
+
localGuide.activation_url_patterns =
|
|
833
|
+
remoteGuide.activation_url_patterns.map((rule) => {
|
|
746
834
|
return {
|
|
747
835
|
...rule,
|
|
748
836
|
pattern: new URLPattern({ pathname: rule.pathname }),
|
|
@@ -754,7 +842,16 @@ export class KnockGuideClient {
|
|
|
754
842
|
|
|
755
843
|
private buildQueryParams(filterParams: QueryFilterParams = {}) {
|
|
756
844
|
// Combine the target params with the given filter params.
|
|
757
|
-
const combinedParams = {
|
|
845
|
+
const combinedParams: GenericData = {
|
|
846
|
+
...this.targetParams,
|
|
847
|
+
...filterParams,
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// Append debug params
|
|
851
|
+
const debugState = this.store.state.debug;
|
|
852
|
+
if (debugState.forcedGuideKey) {
|
|
853
|
+
combinedParams.force_all_guides = true;
|
|
854
|
+
}
|
|
758
855
|
|
|
759
856
|
// Prune out any keys that have an undefined or null value.
|
|
760
857
|
let params = Object.fromEntries(
|
|
@@ -899,6 +996,15 @@ export class KnockGuideClient {
|
|
|
899
996
|
});
|
|
900
997
|
}
|
|
901
998
|
|
|
999
|
+
private updatePreviewGuide({ data }: GuideLivePreviewUpdatedEvent) {
|
|
1000
|
+
const guide = this.localCopy(data.guide);
|
|
1001
|
+
|
|
1002
|
+
this.store.setState((state) => {
|
|
1003
|
+
const previewGuides = { ...state.previewGuides, [guide.key]: guide };
|
|
1004
|
+
return { ...state, previewGuides };
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
902
1008
|
// Define as an arrow func property to always bind this to the class instance.
|
|
903
1009
|
private handleLocationChange = () => {
|
|
904
1010
|
const win = checkForWindow();
|
|
@@ -908,9 +1014,36 @@ export class KnockGuideClient {
|
|
|
908
1014
|
if (this.store.state.location === href) return;
|
|
909
1015
|
|
|
910
1016
|
this.knock.log(`[Guide] Handle Location change: ${href}`);
|
|
911
|
-
|
|
1017
|
+
|
|
1018
|
+
// If entering debug mode, fetch all guides.
|
|
1019
|
+
const currentDebugParams = this.store.state.debug;
|
|
1020
|
+
const newDebugParams = detectDebugParams();
|
|
1021
|
+
this.setLocation(href, { debug: newDebugParams });
|
|
1022
|
+
|
|
1023
|
+
// If debug state has changed, refetch guides and resubscribe to the websocket channel
|
|
1024
|
+
const debugStateChanged = this.checkDebugStateChanged(
|
|
1025
|
+
currentDebugParams,
|
|
1026
|
+
newDebugParams,
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
if (debugStateChanged) {
|
|
1030
|
+
this.knock.log(
|
|
1031
|
+
`[Guide] Debug state changed, refetching guides and resubscribing to the websocket channel`,
|
|
1032
|
+
);
|
|
1033
|
+
this.fetch();
|
|
1034
|
+
this.subscribe();
|
|
1035
|
+
}
|
|
912
1036
|
};
|
|
913
1037
|
|
|
1038
|
+
// Returns whether debug params have changed. For guide key, we only check
|
|
1039
|
+
// presence since the exact value has no impact on fetch/subscribe
|
|
1040
|
+
private checkDebugStateChanged(a: DebugState, b: DebugState): boolean {
|
|
1041
|
+
return (
|
|
1042
|
+
Boolean(a.forcedGuideKey) !== Boolean(b.forcedGuideKey) ||
|
|
1043
|
+
a.previewSessionId !== b.previewSessionId
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
914
1047
|
private listenForLocationChangesFromWindow() {
|
|
915
1048
|
const win = checkForWindow();
|
|
916
1049
|
if (win?.history) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
GuideActivationUrlRuleData,
|
|
2
3
|
GuideData,
|
|
3
4
|
GuideGroupData,
|
|
4
5
|
KnockGuide,
|
|
6
|
+
KnockGuideActivationUrlPattern,
|
|
5
7
|
SelectFilterParams,
|
|
6
8
|
} from "./types";
|
|
7
9
|
|
|
@@ -96,3 +98,100 @@ export const checkIfThrottled = (
|
|
|
96
98
|
// accurate regardless of local timezones.
|
|
97
99
|
return currentTimeInMilliseconds <= throttleWindowEndInMilliseconds;
|
|
98
100
|
};
|
|
101
|
+
|
|
102
|
+
// Safely parse and build a new URL object.
|
|
103
|
+
export const newUrl = (location: string) => {
|
|
104
|
+
try {
|
|
105
|
+
return new URL(location);
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Evaluates whether the given location url satisfies the url rule.
|
|
112
|
+
export const evaluateUrlRule = (
|
|
113
|
+
url: URL,
|
|
114
|
+
urlRule: GuideActivationUrlRuleData,
|
|
115
|
+
) => {
|
|
116
|
+
if (urlRule.variable === "pathname") {
|
|
117
|
+
if (urlRule.operator === "equal_to") {
|
|
118
|
+
const argument = urlRule.argument.startsWith("/")
|
|
119
|
+
? urlRule.argument
|
|
120
|
+
: `/${urlRule.argument}`;
|
|
121
|
+
|
|
122
|
+
return argument === url.pathname;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (urlRule.operator === "contains") {
|
|
126
|
+
return url.pathname.includes(urlRule.argument);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const predicateUrlRules = (
|
|
136
|
+
url: URL,
|
|
137
|
+
urlRules: GuideActivationUrlRuleData[],
|
|
138
|
+
) => {
|
|
139
|
+
return urlRules.reduce<boolean | undefined>((acc, urlRule) => {
|
|
140
|
+
// Any matched block rule prevails so no need to evaluate further
|
|
141
|
+
// as soon as there is one.
|
|
142
|
+
if (acc === false) return false;
|
|
143
|
+
|
|
144
|
+
// At this point we either have a matched allow rule (acc is true),
|
|
145
|
+
// or no matched rule found yet (acc is undefined).
|
|
146
|
+
|
|
147
|
+
switch (urlRule.directive) {
|
|
148
|
+
case "allow": {
|
|
149
|
+
// No need to evaluate more allow rules once we matched one
|
|
150
|
+
// since any matched allowed rule means allow.
|
|
151
|
+
if (acc === true) return true;
|
|
152
|
+
|
|
153
|
+
const matched = evaluateUrlRule(url, urlRule);
|
|
154
|
+
return matched ? true : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "block": {
|
|
158
|
+
// Always test block rules (unless already matched to block)
|
|
159
|
+
// because they'd prevail over matched allow rules.
|
|
160
|
+
const matched = evaluateUrlRule(url, urlRule);
|
|
161
|
+
return matched ? false : acc;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}, undefined);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const predicateUrlPatterns = (
|
|
168
|
+
url: URL,
|
|
169
|
+
urlPatterns: KnockGuideActivationUrlPattern[],
|
|
170
|
+
) => {
|
|
171
|
+
return urlPatterns.reduce<boolean | undefined>((acc, urlPattern) => {
|
|
172
|
+
// Any matched block rule prevails so no need to evaluate further
|
|
173
|
+
// as soon as there is one.
|
|
174
|
+
if (acc === false) return false;
|
|
175
|
+
|
|
176
|
+
// At this point we either have a matched allow rule (acc is true),
|
|
177
|
+
// or no matched rule found yet (acc is undefined).
|
|
178
|
+
|
|
179
|
+
switch (urlPattern.directive) {
|
|
180
|
+
case "allow": {
|
|
181
|
+
// No need to evaluate more allow rules once we matched one
|
|
182
|
+
// since any matched allowed rule means allow.
|
|
183
|
+
if (acc === true) return true;
|
|
184
|
+
|
|
185
|
+
const matched = urlPattern.pattern.test(url);
|
|
186
|
+
return matched ? true : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case "block": {
|
|
190
|
+
// Always test block rules (unless already matched to block)
|
|
191
|
+
// because they'd prevail over matched allow rules.
|
|
192
|
+
const matched = urlPattern.pattern.test(url);
|
|
193
|
+
return matched ? false : acc;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, undefined);
|
|
197
|
+
};
|
|
@@ -4,6 +4,9 @@ import { GenericData } from "@knocklabs/types";
|
|
|
4
4
|
// Fetch guides API
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
export type Any = any;
|
|
9
|
+
|
|
7
10
|
export interface StepMessageState {
|
|
8
11
|
id: string;
|
|
9
12
|
seen_at: string | null;
|
|
@@ -13,30 +16,37 @@ export interface StepMessageState {
|
|
|
13
16
|
link_clicked_at: string | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export interface GuideStepData {
|
|
19
|
+
export interface GuideStepData<TContent = Any> {
|
|
17
20
|
ref: string;
|
|
18
21
|
schema_key: string;
|
|
19
22
|
schema_semver: string;
|
|
20
23
|
schema_variant_key: string;
|
|
21
24
|
message: StepMessageState;
|
|
22
|
-
|
|
23
|
-
content: any;
|
|
25
|
+
content: TContent;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
interface
|
|
28
|
+
export interface GuideActivationUrlRuleData {
|
|
29
|
+
directive: "allow" | "block";
|
|
30
|
+
variable: "pathname";
|
|
31
|
+
operator: "equal_to" | "contains";
|
|
32
|
+
argument: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface GuideActivationUrlPatternData {
|
|
27
36
|
directive: "allow" | "block";
|
|
28
37
|
pathname: string;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
export interface GuideData {
|
|
40
|
+
export interface GuideData<TContent = Any> {
|
|
32
41
|
__typename: "Guide";
|
|
33
42
|
channel_id: string;
|
|
34
43
|
id: string;
|
|
35
44
|
key: string;
|
|
36
45
|
type: string;
|
|
37
46
|
semver: string;
|
|
38
|
-
steps: GuideStepData[];
|
|
39
|
-
|
|
47
|
+
steps: GuideStepData<TContent>[];
|
|
48
|
+
activation_url_rules: GuideActivationUrlRuleData[];
|
|
49
|
+
activation_url_patterns: GuideActivationUrlPatternData[];
|
|
40
50
|
bypass_global_group_limit: boolean;
|
|
41
51
|
inserted_at: string;
|
|
42
52
|
updated_at: string;
|
|
@@ -57,6 +67,7 @@ export type GetGuidesQueryParams = {
|
|
|
57
67
|
data?: string;
|
|
58
68
|
tenant?: string;
|
|
59
69
|
type?: string;
|
|
70
|
+
force_all_guides?: boolean;
|
|
60
71
|
};
|
|
61
72
|
|
|
62
73
|
export type GetGuidesResponse = {
|
|
@@ -103,7 +114,8 @@ type SocketEventType =
|
|
|
103
114
|
| "guide.updated"
|
|
104
115
|
| "guide.removed"
|
|
105
116
|
| "guide_group.added"
|
|
106
|
-
| "guide_group.updated"
|
|
117
|
+
| "guide_group.updated"
|
|
118
|
+
| "guide.live_preview_updated";
|
|
107
119
|
|
|
108
120
|
type SocketEventPayload<E extends SocketEventType, D> = {
|
|
109
121
|
topic: string;
|
|
@@ -136,32 +148,39 @@ export type GuideGroupUpdatedEvent = SocketEventPayload<
|
|
|
136
148
|
{ guide_group: GuideGroupData }
|
|
137
149
|
>;
|
|
138
150
|
|
|
151
|
+
export type GuideLivePreviewUpdatedEvent = SocketEventPayload<
|
|
152
|
+
"guide.live_preview_updated",
|
|
153
|
+
{ guide: GuideData; eligible: boolean }
|
|
154
|
+
>;
|
|
155
|
+
|
|
139
156
|
export type GuideSocketEvent =
|
|
140
157
|
| GuideAddedEvent
|
|
141
158
|
| GuideUpdatedEvent
|
|
142
159
|
| GuideRemovedEvent
|
|
143
160
|
| GuideGroupAddedEvent
|
|
144
|
-
| GuideGroupUpdatedEvent
|
|
161
|
+
| GuideGroupUpdatedEvent
|
|
162
|
+
| GuideLivePreviewUpdatedEvent;
|
|
145
163
|
|
|
146
164
|
//
|
|
147
165
|
// Guide client
|
|
148
166
|
//
|
|
149
167
|
|
|
150
|
-
export interface KnockGuideStep
|
|
168
|
+
export interface KnockGuideStep<TContent = Any>
|
|
169
|
+
extends GuideStepData<TContent> {
|
|
151
170
|
markAsSeen: () => void;
|
|
152
171
|
markAsInteracted: (params?: { metadata?: GenericData }) => void;
|
|
153
172
|
markAsArchived: () => void;
|
|
154
173
|
}
|
|
155
174
|
|
|
156
|
-
interface
|
|
157
|
-
extends
|
|
175
|
+
export interface KnockGuideActivationUrlPattern
|
|
176
|
+
extends GuideActivationUrlPatternData {
|
|
158
177
|
pattern: URLPattern;
|
|
159
178
|
}
|
|
160
179
|
|
|
161
|
-
export interface KnockGuide extends GuideData {
|
|
162
|
-
steps: KnockGuideStep[];
|
|
163
|
-
|
|
164
|
-
getStep: () => KnockGuideStep | undefined;
|
|
180
|
+
export interface KnockGuide<TContent = Any> extends GuideData<TContent> {
|
|
181
|
+
steps: KnockGuideStep<TContent>[];
|
|
182
|
+
activation_url_patterns: KnockGuideActivationUrlPattern[];
|
|
183
|
+
getStep: () => KnockGuideStep<TContent> | undefined;
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
type QueryKey = string;
|
|
@@ -171,13 +190,20 @@ export type QueryStatus = {
|
|
|
171
190
|
error?: Error;
|
|
172
191
|
};
|
|
173
192
|
|
|
193
|
+
export type DebugState = {
|
|
194
|
+
forcedGuideKey?: string | null;
|
|
195
|
+
previewSessionId?: string | null;
|
|
196
|
+
};
|
|
197
|
+
|
|
174
198
|
export type StoreState = {
|
|
175
199
|
guideGroups: GuideGroupData[];
|
|
176
200
|
guideGroupDisplayLogs: Record<GuideGroupData["key"], string>;
|
|
177
201
|
guides: Record<KnockGuide["key"], KnockGuide>;
|
|
202
|
+
previewGuides: Record<KnockGuide["key"], KnockGuide>;
|
|
178
203
|
queries: Record<QueryKey, QueryStatus>;
|
|
179
204
|
location: string | undefined;
|
|
180
205
|
counter: number;
|
|
206
|
+
debug: DebugState;
|
|
181
207
|
};
|
|
182
208
|
|
|
183
209
|
export type QueryFilterParams = Pick<GetGuidesQueryParams, "type">;
|