@knocklabs/client 0.16.4 → 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 +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 +219 -138
- 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 +15 -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 +3 -3
- package/src/clients/guide/client.ts +207 -43
- 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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
|
51
51
|
"@babel/plugin-proposal-object-rest-spread": "^7.16.7",
|
|
52
52
|
"@babel/plugin-transform-runtime": "^7.28.0",
|
|
53
|
-
"@babel/preset-env": "^7.
|
|
53
|
+
"@babel/preset-env": "^7.28.3",
|
|
54
54
|
"@babel/preset-typescript": "^7.27.0",
|
|
55
55
|
"@codecov/vite-plugin": "^1.9.1",
|
|
56
56
|
"@types/jsonwebtoken": "^9.0.10",
|
|
@@ -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,
|
|
@@ -49,6 +55,15 @@ const DEFAULT_ORDER_RESOLUTION_DURATION = 50; // in milliseconds
|
|
|
49
55
|
// trigger subscribed callbacks.
|
|
50
56
|
const DEFAULT_COUNTER_INCREMENT_INTERVAL = 30 * 1000; // in milliseconds
|
|
51
57
|
|
|
58
|
+
// Maximum number of retry attempts for channel subscription
|
|
59
|
+
const SUBSCRIBE_RETRY_LIMIT = 3;
|
|
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
|
+
|
|
52
67
|
// Return the global window object if defined, so to safely guard against SSR.
|
|
53
68
|
const checkForWindow = () => {
|
|
54
69
|
if (typeof window !== "undefined") {
|
|
@@ -59,6 +74,20 @@ const checkForWindow = () => {
|
|
|
59
74
|
export const guidesApiRootPath = (userId: string | undefined | null) =>
|
|
60
75
|
`/v1/users/${userId}/guides`;
|
|
61
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
|
+
|
|
62
91
|
const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
63
92
|
// A map of selected guides as values, with its order index as keys.
|
|
64
93
|
const result = new SelectionResult();
|
|
@@ -66,16 +95,37 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
66
95
|
const defaultGroup = findDefaultGroup(state.guideGroups);
|
|
67
96
|
if (!defaultGroup) return result;
|
|
68
97
|
|
|
69
|
-
const displaySequence = defaultGroup.display_sequence;
|
|
98
|
+
const displaySequence = [...defaultGroup.display_sequence];
|
|
70
99
|
const location = state.location;
|
|
71
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
|
+
|
|
72
110
|
for (const [index, guideKey] of displaySequence.entries()) {
|
|
73
|
-
|
|
111
|
+
let guide = state.guides[guideKey];
|
|
74
112
|
if (!guide) continue;
|
|
75
113
|
|
|
76
|
-
const affirmed = predicate(guide, {
|
|
114
|
+
const affirmed = predicate(guide, {
|
|
115
|
+
location,
|
|
116
|
+
filters,
|
|
117
|
+
debug: state.debug,
|
|
118
|
+
});
|
|
77
119
|
if (!affirmed) continue;
|
|
78
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
|
+
|
|
79
129
|
result.set(index, guide);
|
|
80
130
|
}
|
|
81
131
|
|
|
@@ -86,12 +136,18 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
86
136
|
type PredicateOpts = {
|
|
87
137
|
location?: string | undefined;
|
|
88
138
|
filters?: SelectFilterParams | undefined;
|
|
139
|
+
debug: DebugState;
|
|
89
140
|
};
|
|
90
141
|
|
|
91
142
|
const predicate = (
|
|
92
143
|
guide: KnockGuide,
|
|
93
|
-
{ location, filters = {} }: PredicateOpts,
|
|
144
|
+
{ location, filters = {}, debug = {} }: PredicateOpts,
|
|
94
145
|
) => {
|
|
146
|
+
// Bypass filtering if debugging the current guide.
|
|
147
|
+
if (debug.forcedGuideKey === guide.key) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
95
151
|
if (filters.type && filters.type !== guide.type) {
|
|
96
152
|
return false;
|
|
97
153
|
}
|
|
@@ -104,36 +160,17 @@ const predicate = (
|
|
|
104
160
|
return false;
|
|
105
161
|
}
|
|
106
162
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (locationRules.length > 0 && location) {
|
|
110
|
-
const allowed = locationRules.reduce<boolean | undefined>((acc, rule) => {
|
|
111
|
-
// Any matched block rule prevails so no need to evaluate further
|
|
112
|
-
// as soon as there is one.
|
|
113
|
-
if (acc === false) return false;
|
|
114
|
-
|
|
115
|
-
// At this point we either have a matched allow rule (acc is true),
|
|
116
|
-
// or no matched rule found yet (acc is undefined).
|
|
163
|
+
const url = location ? newUrl(location) : undefined;
|
|
117
164
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// No need to evaluate more allow rules once we matched one
|
|
121
|
-
// since any matched allowed rule means allow.
|
|
122
|
-
if (acc === true) return true;
|
|
123
|
-
|
|
124
|
-
const matched = rule.pattern.test(location);
|
|
125
|
-
return matched ? true : undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
case "block": {
|
|
129
|
-
// Always test block rules (unless already matched to block)
|
|
130
|
-
// because they'd prevail over matched allow rules.
|
|
131
|
-
const matched = rule.pattern.test(location);
|
|
132
|
-
return matched ? false : acc;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}, undefined);
|
|
165
|
+
const urlRules = guide.activation_url_rules || [];
|
|
166
|
+
const urlPatterns = guide.activation_url_patterns || [];
|
|
136
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);
|
|
137
174
|
if (!allowed) return false;
|
|
138
175
|
}
|
|
139
176
|
|
|
@@ -153,7 +190,9 @@ export class KnockGuideClient {
|
|
|
153
190
|
"guide.removed",
|
|
154
191
|
"guide_group.added",
|
|
155
192
|
"guide_group.updated",
|
|
193
|
+
"guide.live_preview_updated",
|
|
156
194
|
];
|
|
195
|
+
private subscribeRetryCount = 0;
|
|
157
196
|
|
|
158
197
|
// Original history methods to monkey patch, or restore in cleanups.
|
|
159
198
|
private pushStateFn: History["pushState"] | undefined;
|
|
@@ -177,14 +216,18 @@ export class KnockGuideClient {
|
|
|
177
216
|
|
|
178
217
|
const location = trackLocationFromWindow ? win?.location.href : undefined;
|
|
179
218
|
|
|
219
|
+
const debug = detectDebugParams();
|
|
220
|
+
|
|
180
221
|
this.store = new Store<StoreState>({
|
|
181
222
|
guideGroups: [],
|
|
182
223
|
guideGroupDisplayLogs: {},
|
|
183
224
|
guides: {},
|
|
225
|
+
previewGuides: {},
|
|
184
226
|
queries: {},
|
|
185
227
|
location,
|
|
186
228
|
// Increment to update the state store and trigger re-selection.
|
|
187
229
|
counter: 0,
|
|
230
|
+
debug,
|
|
188
231
|
});
|
|
189
232
|
|
|
190
233
|
// In server environments we might not have a socket connection.
|
|
@@ -298,7 +341,14 @@ export class KnockGuideClient {
|
|
|
298
341
|
}
|
|
299
342
|
|
|
300
343
|
// Join the channel topic and subscribe to supported events.
|
|
301
|
-
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
|
+
|
|
302
352
|
const newChannel = this.socket.channel(this.socketChannelTopic, params);
|
|
303
353
|
|
|
304
354
|
for (const eventType of this.socketEventTypes) {
|
|
@@ -306,13 +356,44 @@ export class KnockGuideClient {
|
|
|
306
356
|
}
|
|
307
357
|
|
|
308
358
|
if (["closed", "errored"].includes(newChannel.state)) {
|
|
309
|
-
|
|
359
|
+
// Reset retry count for new subscription attempt
|
|
360
|
+
this.subscribeRetryCount = 0;
|
|
361
|
+
|
|
362
|
+
newChannel
|
|
363
|
+
.join()
|
|
364
|
+
.receive("ok", () => {
|
|
365
|
+
this.knock.log("[Guide] Successfully joined channel");
|
|
366
|
+
})
|
|
367
|
+
.receive("error", (resp) => {
|
|
368
|
+
this.knock.log(
|
|
369
|
+
`[Guide] Failed to join channel: ${JSON.stringify(resp)}`,
|
|
370
|
+
);
|
|
371
|
+
this.handleChannelJoinError();
|
|
372
|
+
})
|
|
373
|
+
.receive("timeout", () => {
|
|
374
|
+
this.knock.log("[Guide] Channel join timed out");
|
|
375
|
+
this.handleChannelJoinError();
|
|
376
|
+
});
|
|
310
377
|
}
|
|
311
378
|
|
|
312
379
|
// Track the joined channel.
|
|
313
380
|
this.socketChannel = newChannel;
|
|
314
381
|
}
|
|
315
382
|
|
|
383
|
+
private handleChannelJoinError() {
|
|
384
|
+
// Prevent phx channel from retrying forever in case of either network or
|
|
385
|
+
// other errors (e.g. auth error, invalid channel etc)
|
|
386
|
+
if (this.subscribeRetryCount >= SUBSCRIBE_RETRY_LIMIT) {
|
|
387
|
+
this.knock.log(
|
|
388
|
+
`[Guide] Channel join max retry limit reached: ${this.subscribeRetryCount}`,
|
|
389
|
+
);
|
|
390
|
+
this.unsubscribe();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.subscribeRetryCount++;
|
|
395
|
+
}
|
|
396
|
+
|
|
316
397
|
unsubscribe() {
|
|
317
398
|
if (!this.socketChannel) return;
|
|
318
399
|
this.knock.log("[Guide] Unsubscribing from real time updates");
|
|
@@ -346,23 +427,41 @@ export class KnockGuideClient {
|
|
|
346
427
|
case "guide_group.updated":
|
|
347
428
|
return this.addOrReplaceGuideGroup(payload);
|
|
348
429
|
|
|
430
|
+
case "guide.live_preview_updated":
|
|
431
|
+
return this.updatePreviewGuide(payload);
|
|
432
|
+
|
|
349
433
|
default:
|
|
350
434
|
return;
|
|
351
435
|
}
|
|
352
436
|
}
|
|
353
437
|
|
|
354
|
-
setLocation(href: string) {
|
|
438
|
+
setLocation(href: string, additionalParams: Partial<StoreState> = {}) {
|
|
355
439
|
// Make sure to clear out the stage.
|
|
356
440
|
this.clearGroupStage();
|
|
357
441
|
|
|
358
|
-
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
|
+
});
|
|
359
455
|
}
|
|
360
456
|
|
|
361
457
|
//
|
|
362
458
|
// Store selector
|
|
363
459
|
//
|
|
364
460
|
|
|
365
|
-
selectGuides
|
|
461
|
+
selectGuides<C = Any>(
|
|
462
|
+
state: StoreState,
|
|
463
|
+
filters: SelectFilterParams = {},
|
|
464
|
+
): KnockGuide<C>[] {
|
|
366
465
|
if (Object.keys(state.guides).length === 0) {
|
|
367
466
|
return [];
|
|
368
467
|
}
|
|
@@ -381,7 +480,10 @@ export class KnockGuideClient {
|
|
|
381
480
|
return [...result.values()];
|
|
382
481
|
}
|
|
383
482
|
|
|
384
|
-
selectGuide
|
|
483
|
+
selectGuide<C = Any>(
|
|
484
|
+
state: StoreState,
|
|
485
|
+
filters: SelectFilterParams = {},
|
|
486
|
+
): KnockGuide<C> | undefined {
|
|
385
487
|
if (Object.keys(state.guides).length === 0) {
|
|
386
488
|
return undefined;
|
|
387
489
|
}
|
|
@@ -504,10 +606,22 @@ export class KnockGuideClient {
|
|
|
504
606
|
// callback to a setTimeout, but just to be safe.
|
|
505
607
|
this.ensureClearTimeout();
|
|
506
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
|
+
|
|
507
621
|
this.stage = {
|
|
508
622
|
...this.stage,
|
|
509
623
|
status: "closed",
|
|
510
|
-
resolved
|
|
624
|
+
resolved,
|
|
511
625
|
timeoutId: null,
|
|
512
626
|
};
|
|
513
627
|
|
|
@@ -671,6 +785,11 @@ export class KnockGuideClient {
|
|
|
671
785
|
...remoteGuide,
|
|
672
786
|
// Get the next unarchived step.
|
|
673
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
|
+
|
|
674
793
|
return this.steps.find((s) => !s.message.archived_at);
|
|
675
794
|
},
|
|
676
795
|
} as KnockGuide;
|
|
@@ -706,8 +825,8 @@ export class KnockGuideClient {
|
|
|
706
825
|
return localStep;
|
|
707
826
|
});
|
|
708
827
|
|
|
709
|
-
localGuide.
|
|
710
|
-
remoteGuide.
|
|
828
|
+
localGuide.activation_url_patterns =
|
|
829
|
+
remoteGuide.activation_url_patterns.map((rule) => {
|
|
711
830
|
return {
|
|
712
831
|
...rule,
|
|
713
832
|
pattern: new URLPattern({ pathname: rule.pathname }),
|
|
@@ -719,7 +838,16 @@ export class KnockGuideClient {
|
|
|
719
838
|
|
|
720
839
|
private buildQueryParams(filterParams: QueryFilterParams = {}) {
|
|
721
840
|
// Combine the target params with the given filter params.
|
|
722
|
-
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
|
+
}
|
|
723
851
|
|
|
724
852
|
// Prune out any keys that have an undefined or null value.
|
|
725
853
|
let params = Object.fromEntries(
|
|
@@ -864,6 +992,15 @@ export class KnockGuideClient {
|
|
|
864
992
|
});
|
|
865
993
|
}
|
|
866
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
|
+
|
|
867
1004
|
// Define as an arrow func property to always bind this to the class instance.
|
|
868
1005
|
private handleLocationChange = () => {
|
|
869
1006
|
const win = checkForWindow();
|
|
@@ -873,9 +1010,36 @@ export class KnockGuideClient {
|
|
|
873
1010
|
if (this.store.state.location === href) return;
|
|
874
1011
|
|
|
875
1012
|
this.knock.log(`[Guide] Handle Location change: ${href}`);
|
|
876
|
-
|
|
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
|
+
}
|
|
877
1032
|
};
|
|
878
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
|
+
|
|
879
1043
|
private listenForLocationChangesFromWindow() {
|
|
880
1044
|
const win = checkForWindow();
|
|
881
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
|
+
};
|