@knocklabs/client 0.16.5 → 0.17.0
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 +11 -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 +206 -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 +171 -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.0",
|
|
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,16 +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];
|
|
77
112
|
if (!guide) continue;
|
|
78
113
|
|
|
79
|
-
const affirmed = predicate(guide, {
|
|
114
|
+
const affirmed = predicate(guide, {
|
|
115
|
+
location,
|
|
116
|
+
filters,
|
|
117
|
+
debug: state.debug,
|
|
118
|
+
});
|
|
80
119
|
if (!affirmed) continue;
|
|
81
120
|
|
|
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
|
+
|
|
82
129
|
result.set(index, guide);
|
|
83
130
|
}
|
|
84
131
|
|
|
@@ -89,12 +136,18 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
89
136
|
type PredicateOpts = {
|
|
90
137
|
location?: string | undefined;
|
|
91
138
|
filters?: SelectFilterParams | undefined;
|
|
139
|
+
debug: DebugState;
|
|
92
140
|
};
|
|
93
141
|
|
|
94
142
|
const predicate = (
|
|
95
143
|
guide: KnockGuide,
|
|
96
|
-
{ location, filters = {} }: PredicateOpts,
|
|
144
|
+
{ location, filters = {}, debug = {} }: PredicateOpts,
|
|
97
145
|
) => {
|
|
146
|
+
// Bypass filtering if debugging the current guide.
|
|
147
|
+
if (debug.forcedGuideKey === guide.key) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
if (filters.type && filters.type !== guide.type) {
|
|
99
152
|
return false;
|
|
100
153
|
}
|
|
@@ -107,36 +160,17 @@ const predicate = (
|
|
|
107
160
|
return false;
|
|
108
161
|
}
|
|
109
162
|
|
|
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).
|
|
163
|
+
const url = location ? newUrl(location) : undefined;
|
|
120
164
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
}
|
|
130
|
-
|
|
131
|
-
case "block": {
|
|
132
|
-
// Always test block rules (unless already matched to block)
|
|
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);
|
|
165
|
+
const urlRules = guide.activation_url_rules || [];
|
|
166
|
+
const urlPatterns = guide.activation_url_patterns || [];
|
|
139
167
|
|
|
168
|
+
// A guide can have either activation url rules XOR url patterns, but not both.
|
|
169
|
+
if (url && urlRules.length > 0) {
|
|
170
|
+
const allowed = predicateUrlRules(url, urlRules);
|
|
171
|
+
if (!allowed) return false;
|
|
172
|
+
} else if (url && urlPatterns.length > 0) {
|
|
173
|
+
const allowed = predicateUrlPatterns(url, urlPatterns);
|
|
140
174
|
if (!allowed) return false;
|
|
141
175
|
}
|
|
142
176
|
|
|
@@ -156,6 +190,7 @@ export class KnockGuideClient {
|
|
|
156
190
|
"guide.removed",
|
|
157
191
|
"guide_group.added",
|
|
158
192
|
"guide_group.updated",
|
|
193
|
+
"guide.live_preview_updated",
|
|
159
194
|
];
|
|
160
195
|
private subscribeRetryCount = 0;
|
|
161
196
|
|
|
@@ -181,14 +216,18 @@ export class KnockGuideClient {
|
|
|
181
216
|
|
|
182
217
|
const location = trackLocationFromWindow ? win?.location.href : undefined;
|
|
183
218
|
|
|
219
|
+
const debug = detectDebugParams();
|
|
220
|
+
|
|
184
221
|
this.store = new Store<StoreState>({
|
|
185
222
|
guideGroups: [],
|
|
186
223
|
guideGroupDisplayLogs: {},
|
|
187
224
|
guides: {},
|
|
225
|
+
previewGuides: {},
|
|
188
226
|
queries: {},
|
|
189
227
|
location,
|
|
190
228
|
// Increment to update the state store and trigger re-selection.
|
|
191
229
|
counter: 0,
|
|
230
|
+
debug,
|
|
192
231
|
});
|
|
193
232
|
|
|
194
233
|
// In server environments we might not have a socket connection.
|
|
@@ -302,7 +341,14 @@ export class KnockGuideClient {
|
|
|
302
341
|
}
|
|
303
342
|
|
|
304
343
|
// Join the channel topic and subscribe to supported events.
|
|
305
|
-
const
|
|
344
|
+
const debugState = this.store.state.debug;
|
|
345
|
+
const params = {
|
|
346
|
+
...this.targetParams,
|
|
347
|
+
user_id: this.knock.userId,
|
|
348
|
+
force_all_guides: debugState.forcedGuideKey ? true : undefined,
|
|
349
|
+
preview_session_id: debugState.previewSessionId || undefined,
|
|
350
|
+
};
|
|
351
|
+
|
|
306
352
|
const newChannel = this.socket.channel(this.socketChannelTopic, params);
|
|
307
353
|
|
|
308
354
|
for (const eventType of this.socketEventTypes) {
|
|
@@ -381,23 +427,41 @@ export class KnockGuideClient {
|
|
|
381
427
|
case "guide_group.updated":
|
|
382
428
|
return this.addOrReplaceGuideGroup(payload);
|
|
383
429
|
|
|
430
|
+
case "guide.live_preview_updated":
|
|
431
|
+
return this.updatePreviewGuide(payload);
|
|
432
|
+
|
|
384
433
|
default:
|
|
385
434
|
return;
|
|
386
435
|
}
|
|
387
436
|
}
|
|
388
437
|
|
|
389
|
-
setLocation(href: string) {
|
|
438
|
+
setLocation(href: string, additionalParams: Partial<StoreState> = {}) {
|
|
390
439
|
// Make sure to clear out the stage.
|
|
391
440
|
this.clearGroupStage();
|
|
392
441
|
|
|
393
|
-
this.store.setState((state) =>
|
|
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
|
+
});
|
|
394
455
|
}
|
|
395
456
|
|
|
396
457
|
//
|
|
397
458
|
// Store selector
|
|
398
459
|
//
|
|
399
460
|
|
|
400
|
-
selectGuides
|
|
461
|
+
selectGuides<C = Any>(
|
|
462
|
+
state: StoreState,
|
|
463
|
+
filters: SelectFilterParams = {},
|
|
464
|
+
): KnockGuide<C>[] {
|
|
401
465
|
if (Object.keys(state.guides).length === 0) {
|
|
402
466
|
return [];
|
|
403
467
|
}
|
|
@@ -416,7 +480,10 @@ export class KnockGuideClient {
|
|
|
416
480
|
return [...result.values()];
|
|
417
481
|
}
|
|
418
482
|
|
|
419
|
-
selectGuide
|
|
483
|
+
selectGuide<C = Any>(
|
|
484
|
+
state: StoreState,
|
|
485
|
+
filters: SelectFilterParams = {},
|
|
486
|
+
): KnockGuide<C> | undefined {
|
|
420
487
|
if (Object.keys(state.guides).length === 0) {
|
|
421
488
|
return undefined;
|
|
422
489
|
}
|
|
@@ -539,10 +606,22 @@ export class KnockGuideClient {
|
|
|
539
606
|
// callback to a setTimeout, but just to be safe.
|
|
540
607
|
this.ensureClearTimeout();
|
|
541
608
|
|
|
609
|
+
// If in debug mode, try to resolve the forced guide, otherwise return the first non-undefined guide.
|
|
610
|
+
let resolved = undefined;
|
|
611
|
+
if (this.store.state.debug.forcedGuideKey) {
|
|
612
|
+
resolved = this.stage.ordered.find(
|
|
613
|
+
(x) => x === this.store.state.debug.forcedGuideKey,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!resolved) {
|
|
618
|
+
resolved = this.stage.ordered.find((x) => x !== undefined);
|
|
619
|
+
}
|
|
620
|
+
|
|
542
621
|
this.stage = {
|
|
543
622
|
...this.stage,
|
|
544
623
|
status: "closed",
|
|
545
|
-
resolved
|
|
624
|
+
resolved,
|
|
546
625
|
timeoutId: null,
|
|
547
626
|
};
|
|
548
627
|
|
|
@@ -706,6 +785,11 @@ export class KnockGuideClient {
|
|
|
706
785
|
...remoteGuide,
|
|
707
786
|
// Get the next unarchived step.
|
|
708
787
|
getStep() {
|
|
788
|
+
// If debugging this guide, return the first step regardless of archive status
|
|
789
|
+
if (self.store.state.debug.forcedGuideKey === this.key) {
|
|
790
|
+
return this.steps[0];
|
|
791
|
+
}
|
|
792
|
+
|
|
709
793
|
return this.steps.find((s) => !s.message.archived_at);
|
|
710
794
|
},
|
|
711
795
|
} as KnockGuide;
|
|
@@ -741,8 +825,8 @@ export class KnockGuideClient {
|
|
|
741
825
|
return localStep;
|
|
742
826
|
});
|
|
743
827
|
|
|
744
|
-
localGuide.
|
|
745
|
-
remoteGuide.
|
|
828
|
+
localGuide.activation_url_patterns =
|
|
829
|
+
remoteGuide.activation_url_patterns.map((rule) => {
|
|
746
830
|
return {
|
|
747
831
|
...rule,
|
|
748
832
|
pattern: new URLPattern({ pathname: rule.pathname }),
|
|
@@ -754,7 +838,16 @@ export class KnockGuideClient {
|
|
|
754
838
|
|
|
755
839
|
private buildQueryParams(filterParams: QueryFilterParams = {}) {
|
|
756
840
|
// Combine the target params with the given filter params.
|
|
757
|
-
const combinedParams = {
|
|
841
|
+
const combinedParams: GenericData = {
|
|
842
|
+
...this.targetParams,
|
|
843
|
+
...filterParams,
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// Append debug params
|
|
847
|
+
const debugState = this.store.state.debug;
|
|
848
|
+
if (debugState.forcedGuideKey) {
|
|
849
|
+
combinedParams.force_all_guides = true;
|
|
850
|
+
}
|
|
758
851
|
|
|
759
852
|
// Prune out any keys that have an undefined or null value.
|
|
760
853
|
let params = Object.fromEntries(
|
|
@@ -899,6 +992,15 @@ export class KnockGuideClient {
|
|
|
899
992
|
});
|
|
900
993
|
}
|
|
901
994
|
|
|
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
|
+
|
|
902
1004
|
// Define as an arrow func property to always bind this to the class instance.
|
|
903
1005
|
private handleLocationChange = () => {
|
|
904
1006
|
const win = checkForWindow();
|
|
@@ -908,9 +1010,36 @@ export class KnockGuideClient {
|
|
|
908
1010
|
if (this.store.state.location === href) return;
|
|
909
1011
|
|
|
910
1012
|
this.knock.log(`[Guide] Handle Location change: ${href}`);
|
|
911
|
-
|
|
1013
|
+
|
|
1014
|
+
// If entering debug mode, fetch all guides.
|
|
1015
|
+
const currentDebugParams = this.store.state.debug;
|
|
1016
|
+
const newDebugParams = detectDebugParams();
|
|
1017
|
+
this.setLocation(href, { debug: newDebugParams });
|
|
1018
|
+
|
|
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) {
|
|
1026
|
+
this.knock.log(
|
|
1027
|
+
`[Guide] Debug state changed, refetching guides and resubscribing to the websocket channel`,
|
|
1028
|
+
);
|
|
1029
|
+
this.fetch();
|
|
1030
|
+
this.subscribe();
|
|
1031
|
+
}
|
|
912
1032
|
};
|
|
913
1033
|
|
|
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
|
+
|
|
914
1043
|
private listenForLocationChangesFromWindow() {
|
|
915
1044
|
const win = checkForWindow();
|
|
916
1045
|
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">;
|