@croct/plug 0.11.0-alpha → 0.11.0-alpha.3
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/README.md +1 -1
- package/constants.d.ts +1 -1
- package/constants.js +2 -1
- package/constants.js.map +1 -0
- package/eap.js +1 -0
- package/eap.js.map +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -0
- package/package.json +4 -3
- package/playground.js +1 -0
- package/playground.js.map +1 -0
- package/plug.js +2 -1
- package/plug.js.map +1 -0
- package/plugin.js +1 -0
- package/plugin.js.map +1 -0
- package/preview.d.ts +1 -0
- package/preview.js +15 -3
- package/preview.js.map +1 -0
- package/sdk/evaluation.js +1 -0
- package/sdk/evaluation.js.map +1 -0
- package/sdk/index.js +1 -0
- package/sdk/index.js.map +1 -0
- package/sdk/json.js +1 -0
- package/sdk/json.js.map +1 -0
- package/sdk/sdkEvents.js +1 -0
- package/sdk/sdkEvents.js.map +1 -0
- package/sdk/token.js +1 -0
- package/sdk/token.js.map +1 -0
- package/sdk/tracking.js +1 -0
- package/sdk/tracking.js.map +1 -0
- package/sdk/validation.js +1 -0
- package/sdk/validation.js.map +1 -0
- package/slot.js +1 -0
- package/slot.js.map +1 -0
- package/src/constants.ts +5 -0
- package/src/eap.ts +17 -0
- package/src/index.ts +6 -0
- package/src/playground.ts +247 -0
- package/src/plug.ts +470 -0
- package/src/plugin.ts +39 -0
- package/src/preview.ts +187 -0
- package/src/sdk/evaluation.ts +2 -0
- package/src/sdk/index.ts +14 -0
- package/src/sdk/json.ts +4 -0
- package/src/sdk/sdkEvents.ts +1 -0
- package/src/sdk/token.ts +1 -0
- package/src/sdk/tracking.ts +14 -0
- package/src/sdk/validation.ts +2 -0
- package/src/slot.ts +37 -0
package/src/plug.ts
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {Logger} from '@croct/sdk/logging';
|
|
2
|
+
import {SessionFacade} from '@croct/sdk/facade/sessionFacade';
|
|
3
|
+
import {UserFacade} from '@croct/sdk/facade/userFacade';
|
|
4
|
+
import {TrackerFacade} from '@croct/sdk/facade/trackerFacade';
|
|
5
|
+
import {EvaluationOptions, EvaluatorFacade} from '@croct/sdk/facade/evaluatorFacade';
|
|
6
|
+
import {Configuration as SdkFacadeConfiguration, SdkFacade} from '@croct/sdk/facade/sdkFacade';
|
|
7
|
+
import {formatCause} from '@croct/sdk/error';
|
|
8
|
+
import {describe} from '@croct/sdk/validation';
|
|
9
|
+
import {Optional} from '@croct/sdk/utilityTypes';
|
|
10
|
+
import {Token} from '@croct/sdk/token';
|
|
11
|
+
import {
|
|
12
|
+
ExternalTrackingEvent as ExternalEvent,
|
|
13
|
+
ExternalTrackingEventPayload as ExternalEventPayload,
|
|
14
|
+
ExternalTrackingEventType as ExternalEventType,
|
|
15
|
+
} from '@croct/sdk/trackingEvents';
|
|
16
|
+
import {VERSION} from '@croct/sdk';
|
|
17
|
+
import {FetchOptions as BaseFetchOptions} from '@croct/sdk/facade/contentFetcherFacade';
|
|
18
|
+
import {Plugin, PluginArguments, PluginFactory} from './plugin';
|
|
19
|
+
import {CDN_URL} from './constants';
|
|
20
|
+
import {factory as playgroundPluginFactory} from './playground';
|
|
21
|
+
import {factory as previewPluginFactory} from './preview';
|
|
22
|
+
import {EapFeatures} from './eap';
|
|
23
|
+
import {VersionedSlotId, SlotContent} from './slot';
|
|
24
|
+
import {JsonValue, JsonObject} from './sdk/json';
|
|
25
|
+
|
|
26
|
+
export interface PluginConfigurations {
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Configuration = Optional<SdkFacadeConfiguration, 'appId'> & {
|
|
31
|
+
plugins?: PluginConfigurations,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type FetchOptions = Omit<BaseFetchOptions, 'version'>;
|
|
35
|
+
|
|
36
|
+
export type FetchResponse<I extends VersionedSlotId, C extends JsonObject = JsonObject> = {
|
|
37
|
+
content: SlotContent<I, C>,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export type LegacyFetchResponse<I extends VersionedSlotId, C extends JsonObject = JsonObject> = FetchResponse<I, C> & {
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated Use `content` instead.
|
|
46
|
+
*/
|
|
47
|
+
payload: SlotContent<I, C>,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface Plug extends EapFeatures {
|
|
51
|
+
readonly tracker: TrackerFacade;
|
|
52
|
+
readonly user: UserFacade;
|
|
53
|
+
readonly session: SessionFacade;
|
|
54
|
+
readonly initialized: boolean;
|
|
55
|
+
readonly flushed: Promise<this>;
|
|
56
|
+
readonly plugged: Promise<this>;
|
|
57
|
+
|
|
58
|
+
plug(configuration: Configuration): void;
|
|
59
|
+
|
|
60
|
+
isAnonymous(): boolean;
|
|
61
|
+
|
|
62
|
+
getUserId(): string | null;
|
|
63
|
+
|
|
64
|
+
identify(userId: string): void;
|
|
65
|
+
|
|
66
|
+
anonymize(): void;
|
|
67
|
+
|
|
68
|
+
setToken(token: string): void;
|
|
69
|
+
|
|
70
|
+
unsetToken(): void;
|
|
71
|
+
|
|
72
|
+
track<T extends ExternalEventType>(type: T, payload: ExternalEventPayload<T>): Promise<ExternalEvent<T>>;
|
|
73
|
+
|
|
74
|
+
evaluate<T extends JsonValue>(expression: string, options?: EvaluationOptions): Promise<T>;
|
|
75
|
+
|
|
76
|
+
fetch<P extends JsonObject, I extends VersionedSlotId>(
|
|
77
|
+
slotId: I,
|
|
78
|
+
options?: FetchOptions
|
|
79
|
+
): Promise<LegacyFetchResponse<I, P>>;
|
|
80
|
+
|
|
81
|
+
unplug(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const PLUGIN_NAMESPACE = 'Plugin';
|
|
85
|
+
|
|
86
|
+
function detectAppId(): string | null {
|
|
87
|
+
const script = window.document.querySelector(`script[src^='${CDN_URL}']`);
|
|
88
|
+
|
|
89
|
+
if (!(script instanceof HTMLScriptElement)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (new URL(script.src)).searchParams.get('appId');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class GlobalPlug implements Plug {
|
|
97
|
+
private pluginFactories: {[key: string]: PluginFactory} = {
|
|
98
|
+
playground: playgroundPluginFactory,
|
|
99
|
+
preview: previewPluginFactory,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
private instance?: SdkFacade;
|
|
103
|
+
|
|
104
|
+
private plugins: {[key: string]: Plugin} = {};
|
|
105
|
+
|
|
106
|
+
private initialize: {(): void};
|
|
107
|
+
|
|
108
|
+
private ready: Promise<void>;
|
|
109
|
+
|
|
110
|
+
public constructor() {
|
|
111
|
+
this.ready = new Promise(resolve => {
|
|
112
|
+
this.initialize = resolve;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public extend(name: string, plugin: PluginFactory): void {
|
|
117
|
+
if (this.pluginFactories[name] !== undefined) {
|
|
118
|
+
throw new Error(`Another plugin is already registered with name "${name}".`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.pluginFactories[name] = plugin;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public plug(configuration: Configuration = {}): void {
|
|
125
|
+
if (this.instance !== undefined) {
|
|
126
|
+
const logger = this.instance.getLogger();
|
|
127
|
+
|
|
128
|
+
logger.info('Croct is already plugged in.');
|
|
129
|
+
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const detectedAppId = detectAppId();
|
|
134
|
+
const configuredAppId = configuration.appId ?? null;
|
|
135
|
+
|
|
136
|
+
if (detectedAppId !== null && configuredAppId !== null && detectedAppId !== configuredAppId) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'The specified app ID and the auto-detected app ID are conflicting. '
|
|
139
|
+
+ 'There is no need to specify an app ID when using an application-specific tag. '
|
|
140
|
+
+ 'Please try again omitting the "appId" option.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const appId = detectedAppId ?? configuredAppId;
|
|
145
|
+
|
|
146
|
+
if (appId === null) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
'The app ID must be specified when it cannot be auto-detected. '
|
|
149
|
+
+ 'Please try again specifying the "appId" option.',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const {plugins, test, ...sdkConfiguration} = configuration;
|
|
154
|
+
|
|
155
|
+
const sdk = SdkFacade.init({
|
|
156
|
+
...sdkConfiguration,
|
|
157
|
+
appId: appId,
|
|
158
|
+
test: test ?? (typeof process === 'object' && (
|
|
159
|
+
process.env?.CROCT_TEST_MODE !== undefined
|
|
160
|
+
? process.env.CROCT_TEST_MODE === 'true'
|
|
161
|
+
: process.env?.NODE_ENV === 'test'
|
|
162
|
+
)),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.instance = sdk;
|
|
166
|
+
|
|
167
|
+
const logger = this.instance.getLogger();
|
|
168
|
+
|
|
169
|
+
if (detectedAppId === configuredAppId) {
|
|
170
|
+
logger.warn(
|
|
171
|
+
'It is strongly recommended omitting the "appId" option when using '
|
|
172
|
+
+ 'the application-specific tag as it is detected automatically.',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pending: Array<Promise<void>> = [];
|
|
177
|
+
|
|
178
|
+
const defaultEnabledPlugins = Object.fromEntries(
|
|
179
|
+
Object.keys(this.pluginFactories)
|
|
180
|
+
.map(name => [name, true]),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
for (const [name, options] of Object.entries({...defaultEnabledPlugins, ...plugins})) {
|
|
184
|
+
logger.debug(`Initializing plugin "${name}"...`);
|
|
185
|
+
|
|
186
|
+
const factory = this.pluginFactories[name];
|
|
187
|
+
|
|
188
|
+
if (factory === undefined) {
|
|
189
|
+
logger.error(`Plugin "${name}" is not registered.`);
|
|
190
|
+
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (typeof options !== 'boolean' && (options === null || typeof options !== 'object')) {
|
|
195
|
+
logger.error(
|
|
196
|
+
`Invalid options for plugin "${name}", `
|
|
197
|
+
+ `expected either boolean or object but got ${describe(options)}`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options === false) {
|
|
204
|
+
logger.warn(`Plugin "${name}" is declared but not enabled`);
|
|
205
|
+
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const args: PluginArguments = {
|
|
210
|
+
options: options === true ? {} : options,
|
|
211
|
+
sdk: {
|
|
212
|
+
version: VERSION,
|
|
213
|
+
appId: appId,
|
|
214
|
+
tracker: sdk.tracker,
|
|
215
|
+
evaluator: sdk.evaluator,
|
|
216
|
+
user: sdk.user,
|
|
217
|
+
session: sdk.session,
|
|
218
|
+
tab: sdk.context.getTab(),
|
|
219
|
+
userTokenStore: {
|
|
220
|
+
getToken: sdk.getToken.bind(sdk),
|
|
221
|
+
setToken: sdk.setToken.bind(sdk),
|
|
222
|
+
},
|
|
223
|
+
previewTokenStore: sdk.previewTokenStore,
|
|
224
|
+
cidAssigner: sdk.cidAssigner,
|
|
225
|
+
eventManager: sdk.eventManager,
|
|
226
|
+
getLogger: (...namespace: string[]): Logger => sdk.getLogger(PLUGIN_NAMESPACE, name, ...namespace),
|
|
227
|
+
getTabStorage: (...namespace: string[]): Storage => (
|
|
228
|
+
sdk.getTabStorage(PLUGIN_NAMESPACE, name, ...namespace)
|
|
229
|
+
),
|
|
230
|
+
getBrowserStorage: (...namespace: string[]): Storage => (
|
|
231
|
+
sdk.getBrowserStorage(PLUGIN_NAMESPACE, name, ...namespace)
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
let plugin;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
plugin = factory(args);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error(`Failed to initialize plugin "${name}": ${formatCause(error)}`);
|
|
242
|
+
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logger.debug(`Plugin "${name}" initialized`);
|
|
247
|
+
|
|
248
|
+
if (typeof plugin !== 'object') {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.plugins[name] = plugin;
|
|
253
|
+
|
|
254
|
+
const promise = plugin.enable();
|
|
255
|
+
|
|
256
|
+
if (!(promise instanceof Promise)) {
|
|
257
|
+
logger.debug(`Plugin "${name}" enabled`);
|
|
258
|
+
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pending.push(
|
|
263
|
+
promise.then(() => logger.debug(`Plugin "${name}" enabled`))
|
|
264
|
+
.catch(error => logger.error(`Failed to enable plugin "${name}": ${formatCause(error)}`)),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const initializeEap = window.croctEap?.initialize;
|
|
269
|
+
|
|
270
|
+
if (typeof initializeEap === 'function') {
|
|
271
|
+
initializeEap.call(this);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
Promise.all(pending)
|
|
275
|
+
.then(() => {
|
|
276
|
+
this.initialize();
|
|
277
|
+
|
|
278
|
+
logger.debug('Initialization complete');
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public get initialized(): boolean {
|
|
283
|
+
return this.instance !== undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public get plugged(): Promise<this> {
|
|
287
|
+
return this.ready.then(() => this);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public get flushed(): Promise<this> {
|
|
291
|
+
return this.tracker
|
|
292
|
+
.flushed
|
|
293
|
+
.then(() => this);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private get sdk(): SdkFacade {
|
|
297
|
+
if (this.instance === undefined) {
|
|
298
|
+
throw new Error('Croct is not plugged in.');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return this.instance;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public get tracker(): TrackerFacade {
|
|
305
|
+
return this.sdk.tracker;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public get evaluator(): EvaluatorFacade {
|
|
309
|
+
return this.sdk.evaluator;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public get user(): UserFacade {
|
|
313
|
+
return this.sdk.user;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public get session(): SessionFacade {
|
|
317
|
+
return this.sdk.session;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public isAnonymous(): boolean {
|
|
321
|
+
return this.sdk
|
|
322
|
+
.context
|
|
323
|
+
.isAnonymous();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
public getUserId(): string | null {
|
|
327
|
+
return this.sdk
|
|
328
|
+
.context
|
|
329
|
+
.getUser();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public identify(userId: string): void {
|
|
333
|
+
if (typeof userId !== 'string') {
|
|
334
|
+
throw new Error('The user ID must be a string. Read more on https://croct.help/plug-js/id-conversion');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.sdk.identify(userId);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
public anonymize(): void {
|
|
341
|
+
this.sdk.anonymize();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
public setToken(token: string): void {
|
|
345
|
+
this.sdk.setToken(Token.parse(token));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
public unsetToken(): void {
|
|
349
|
+
this.sdk.unsetToken();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public track<T extends ExternalEventType>(type: T, payload: ExternalEventPayload<T>): Promise<ExternalEvent<T>> {
|
|
353
|
+
return this.sdk
|
|
354
|
+
.tracker
|
|
355
|
+
.track(type, payload);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
public evaluate<T extends JsonValue>(expression: string, options: EvaluationOptions = {}): Promise<T> {
|
|
359
|
+
return this.sdk
|
|
360
|
+
.evaluator
|
|
361
|
+
.evaluate(expression, options) as Promise<T>;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public test(expression: string, options: EvaluationOptions = {}): Promise<boolean> {
|
|
365
|
+
return this.evaluate(expression, options)
|
|
366
|
+
.then(result => result === true);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public get fetch(): Plug['fetch'] {
|
|
370
|
+
return this.eap(
|
|
371
|
+
'fetch',
|
|
372
|
+
<C extends JsonObject, I extends VersionedSlotId = VersionedSlotId>(
|
|
373
|
+
slotId: I,
|
|
374
|
+
options: FetchOptions = {},
|
|
375
|
+
): Promise<LegacyFetchResponse<I, C>> => {
|
|
376
|
+
const [id, version] = slotId.split('@') as [string, `${number}` | 'latest' | undefined];
|
|
377
|
+
const logger = this.sdk.getLogger();
|
|
378
|
+
|
|
379
|
+
return this.sdk
|
|
380
|
+
.contentFetcher
|
|
381
|
+
.fetch<SlotContent<I, C>>(id, {
|
|
382
|
+
...options,
|
|
383
|
+
version: version === 'latest' ? undefined : version,
|
|
384
|
+
})
|
|
385
|
+
.then(
|
|
386
|
+
response => ({
|
|
387
|
+
get payload(): SlotContent<I, C> {
|
|
388
|
+
logger.warn(
|
|
389
|
+
'Accessing the "payload" property of the fetch response is deprecated'
|
|
390
|
+
+ ' and will be removed in a future version. Use the "content" property instead.',
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
return response.content;
|
|
394
|
+
},
|
|
395
|
+
content: response.content,
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
public async unplug(): Promise<void> {
|
|
403
|
+
if (this.instance === undefined) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const {instance, plugins} = this;
|
|
408
|
+
|
|
409
|
+
const logger = this.sdk.getLogger();
|
|
410
|
+
const pending: Array<Promise<void>> = [];
|
|
411
|
+
|
|
412
|
+
for (const [pluginName, controller] of Object.entries(plugins)) {
|
|
413
|
+
if (typeof controller.disable !== 'function') {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
logger.debug(`Disabling plugin "${pluginName}"...`);
|
|
418
|
+
|
|
419
|
+
const promise = controller.disable();
|
|
420
|
+
|
|
421
|
+
if (!(promise instanceof Promise)) {
|
|
422
|
+
logger.debug(`Plugin "${pluginName}" disabled`);
|
|
423
|
+
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
pending.push(
|
|
428
|
+
promise.then(() => logger.debug(`Plugin "${pluginName}" disabled`))
|
|
429
|
+
.catch(error => logger.error(`Failed to disable "${pluginName}": ${formatCause(error)}`)),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Reset
|
|
434
|
+
delete this.instance;
|
|
435
|
+
|
|
436
|
+
this.plugins = {};
|
|
437
|
+
this.ready = new Promise(resolve => {
|
|
438
|
+
this.initialize = resolve;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await Promise.all(pending);
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await instance.close();
|
|
445
|
+
} finally {
|
|
446
|
+
logger.info('🔌 Croct has been unplugged.');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private eap<T extends keyof EapFeatures>(feature: T, api: EapFeatures[T]): EapFeatures[T] {
|
|
451
|
+
const eap = window.croctEap;
|
|
452
|
+
const method: EapFeatures[T] | undefined = typeof eap === 'object' ? eap[feature] : undefined;
|
|
453
|
+
|
|
454
|
+
if (typeof method !== 'function') {
|
|
455
|
+
return api;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return method.bind(
|
|
459
|
+
new Proxy(this, {
|
|
460
|
+
get: (plug, property): any => {
|
|
461
|
+
if (property === feature) {
|
|
462
|
+
return api;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return plug[property as keyof GlobalPlug];
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {TokenStore} from './sdk/token';
|
|
2
|
+
import {EvaluatorFacade} from './sdk/evaluation';
|
|
3
|
+
import {TrackerFacade} from './sdk/tracking';
|
|
4
|
+
import {Tab, Logger, SdkEventManager, SessionFacade, UserFacade, CidAssigner} from './sdk';
|
|
5
|
+
|
|
6
|
+
export interface PluginSdk {
|
|
7
|
+
readonly version: string;
|
|
8
|
+
readonly appId: string;
|
|
9
|
+
readonly tracker: TrackerFacade;
|
|
10
|
+
readonly evaluator: EvaluatorFacade;
|
|
11
|
+
readonly user: UserFacade;
|
|
12
|
+
readonly session: SessionFacade;
|
|
13
|
+
readonly tab: Tab;
|
|
14
|
+
readonly userTokenStore: TokenStore;
|
|
15
|
+
readonly previewTokenStore: TokenStore;
|
|
16
|
+
readonly cidAssigner: CidAssigner;
|
|
17
|
+
readonly eventManager: SdkEventManager;
|
|
18
|
+
|
|
19
|
+
getLogger(...namespace: string[]): Logger;
|
|
20
|
+
|
|
21
|
+
getTabStorage(...namespace: string[]): Storage;
|
|
22
|
+
|
|
23
|
+
getBrowserStorage(...namespace: string[]): Storage;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PluginArguments<T = any> {
|
|
27
|
+
options: T;
|
|
28
|
+
sdk: PluginSdk;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PluginFactory<T = any> {
|
|
32
|
+
(args: PluginArguments<T>): Plugin;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Plugin {
|
|
36
|
+
enable(): Promise<void>|void;
|
|
37
|
+
|
|
38
|
+
disable?(): Promise<void>|void;
|
|
39
|
+
}
|
package/src/preview.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {formatCause} from '@croct/sdk/error';
|
|
2
|
+
import {uuid4} from '@croct/sdk/uuid';
|
|
3
|
+
import {Logger} from './sdk';
|
|
4
|
+
import {Plugin, PluginFactory} from './plugin';
|
|
5
|
+
import {Token, TokenStore} from './sdk/token';
|
|
6
|
+
import {PREVIEW_WIDGET_ORIGIN, PREVIEW_WIDGET_URL} from './constants';
|
|
7
|
+
|
|
8
|
+
const PREVIEW_PARAMETER = 'croct-preview';
|
|
9
|
+
const PREVIEW_EXIT = 'exit';
|
|
10
|
+
|
|
11
|
+
export type Configuration = {
|
|
12
|
+
tokenStore: TokenStore,
|
|
13
|
+
logger: Logger,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class PreviewPlugin implements Plugin {
|
|
17
|
+
private static readonly PREVIEW_PARAMS = {
|
|
18
|
+
experienceName: 'experience',
|
|
19
|
+
experimentName: 'experiment',
|
|
20
|
+
audienceName: 'audience',
|
|
21
|
+
variantName: 'variant',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
private readonly tokenStore: TokenStore;
|
|
25
|
+
|
|
26
|
+
private readonly logger: Logger;
|
|
27
|
+
|
|
28
|
+
private readonly widgetId = `croct-preview:${uuid4()}`;
|
|
29
|
+
|
|
30
|
+
public constructor(configuration: Configuration) {
|
|
31
|
+
this.tokenStore = configuration.tokenStore;
|
|
32
|
+
this.logger = configuration.logger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public enable(): void {
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
const previewData = url.searchParams.get(PREVIEW_PARAMETER);
|
|
38
|
+
|
|
39
|
+
if (previewData !== null) {
|
|
40
|
+
this.updateToken(previewData);
|
|
41
|
+
|
|
42
|
+
this.updateUrl();
|
|
43
|
+
|
|
44
|
+
// Some frameworks (e.g. Next) may revert the URL change
|
|
45
|
+
// after the page is loaded, so ensure the token is removed
|
|
46
|
+
// from the URL after a short delay.
|
|
47
|
+
setTimeout(this.updateUrl, 500);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const token = this.tokenStore.getToken();
|
|
51
|
+
|
|
52
|
+
if (token !== null) {
|
|
53
|
+
this.insertWidget(this.getWidgetUrl(token));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public disable(): void {
|
|
58
|
+
document.getElementById(this.widgetId)?.remove();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private updateToken(data: string): void {
|
|
62
|
+
if (data === PREVIEW_EXIT) {
|
|
63
|
+
this.logger.debug('Exiting preview mode.');
|
|
64
|
+
|
|
65
|
+
this.tokenStore.setToken(null);
|
|
66
|
+
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
let token: Token|null = Token.parse(data);
|
|
72
|
+
const {exp} = token.getPayload();
|
|
73
|
+
|
|
74
|
+
if (exp !== undefined && exp <= Date.now() / 1000) {
|
|
75
|
+
this.logger.debug('Preview token expired.');
|
|
76
|
+
|
|
77
|
+
token = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.tokenStore.setToken(token);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.tokenStore.setToken(null);
|
|
83
|
+
|
|
84
|
+
this.logger.warn(`Invalid preview token: ${formatCause(error)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private getWidgetUrl(token: Token): string {
|
|
89
|
+
const params = this.getWidgetParams(token);
|
|
90
|
+
let queryString = params.toString();
|
|
91
|
+
|
|
92
|
+
if (queryString !== '') {
|
|
93
|
+
queryString = `?${queryString}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `${PREVIEW_WIDGET_URL}${queryString}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private getWidgetParams(token: Token): URLSearchParams {
|
|
100
|
+
const {metadata = {}} = token.getPayload();
|
|
101
|
+
const params = new URLSearchParams();
|
|
102
|
+
|
|
103
|
+
if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
104
|
+
return params;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const [key, param] of Object.entries(PreviewPlugin.PREVIEW_PARAMS)) {
|
|
108
|
+
const value = metadata[key];
|
|
109
|
+
|
|
110
|
+
if (typeof value === 'string') {
|
|
111
|
+
params.set(param, value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return params;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private insertWidget(url: string): void {
|
|
119
|
+
const widget = this.createWidget(url);
|
|
120
|
+
|
|
121
|
+
window.addEventListener('message', event => {
|
|
122
|
+
if (event.origin !== PREVIEW_WIDGET_ORIGIN) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (event.data.type) {
|
|
127
|
+
case 'croct:preview:leave': {
|
|
128
|
+
this.tokenStore.setToken(null);
|
|
129
|
+
|
|
130
|
+
const exitUrl = new URL(window.location.href);
|
|
131
|
+
|
|
132
|
+
exitUrl.searchParams.set(PREVIEW_PARAMETER, PREVIEW_EXIT);
|
|
133
|
+
|
|
134
|
+
window.location.replace(exitUrl.toString());
|
|
135
|
+
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case 'croct:preview:resize':
|
|
140
|
+
widget.style.width = `${event.data.width}px`;
|
|
141
|
+
widget.style.height = `${event.data.height}px`;
|
|
142
|
+
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
document.body.prepend(widget);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private updateUrl(): void {
|
|
151
|
+
const url = new URL(window.location.href);
|
|
152
|
+
|
|
153
|
+
if (url.searchParams.has(PREVIEW_PARAMETER)) {
|
|
154
|
+
url.searchParams.delete(PREVIEW_PARAMETER);
|
|
155
|
+
|
|
156
|
+
window.history.replaceState({}, '', url.toString());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private createWidget(url: string): HTMLIFrameElement {
|
|
161
|
+
const widget = document.createElement('iframe');
|
|
162
|
+
|
|
163
|
+
widget.setAttribute('id', this.widgetId);
|
|
164
|
+
widget.setAttribute('src', url);
|
|
165
|
+
widget.setAttribute('sandbox', 'allow-scripts allow-same-origin');
|
|
166
|
+
|
|
167
|
+
widget.style.position = 'fixed';
|
|
168
|
+
widget.style.width = '0px';
|
|
169
|
+
widget.style.height = '0px';
|
|
170
|
+
widget.style.right = '0';
|
|
171
|
+
widget.style.bottom = '0';
|
|
172
|
+
widget.style.border = '0';
|
|
173
|
+
widget.style.overflow = 'hidden';
|
|
174
|
+
widget.style.zIndex = '2147483647';
|
|
175
|
+
|
|
176
|
+
return widget;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const factory: PluginFactory = (props): PreviewPlugin => {
|
|
181
|
+
const {sdk} = props;
|
|
182
|
+
|
|
183
|
+
return new PreviewPlugin({
|
|
184
|
+
tokenStore: props.sdk.previewTokenStore,
|
|
185
|
+
logger: sdk.getLogger(),
|
|
186
|
+
});
|
|
187
|
+
};
|