@product7/product7-js 0.1.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/README.md +1025 -0
- package/dist/README.md +1025 -0
- package/dist/product7-js.js +14658 -0
- package/dist/product7-js.js.map +1 -0
- package/dist/product7-js.min.js +2 -0
- package/dist/product7-js.min.js.map +1 -0
- package/package.json +114 -0
- package/src/api/mock-data/index.js +360 -0
- package/src/api/services/ChangelogService.js +28 -0
- package/src/api/services/FeedbackService.js +44 -0
- package/src/api/services/HelpService.js +50 -0
- package/src/api/services/MessengerService.js +279 -0
- package/src/api/services/SurveyService.js +127 -0
- package/src/api/utils/helpers.js +30 -0
- package/src/core/APIService.js +303 -0
- package/src/core/BaseAPIService.js +298 -0
- package/src/core/EventBus.js +54 -0
- package/src/core/Product7.js +812 -0
- package/src/core/WebSocketService.js +275 -0
- package/src/docs/api.md +226 -0
- package/src/docs/example.md +461 -0
- package/src/docs/framework-integrations.md +714 -0
- package/src/docs/installation.md +281 -0
- package/src/index.js +894 -0
- package/src/styles/base.js +50 -0
- package/src/styles/changelog.js +665 -0
- package/src/styles/components.js +553 -0
- package/src/styles/design-tokens.js +124 -0
- package/src/styles/feedback.js +325 -0
- package/src/styles/messenger-components.js +632 -0
- package/src/styles/messenger-core.js +233 -0
- package/src/styles/messenger-features.js +169 -0
- package/src/styles/messenger-views.js +877 -0
- package/src/styles/messenger.js +17 -0
- package/src/styles/messengerCustomStyles.js +114 -0
- package/src/styles/styles.js +26 -0
- package/src/styles/survey.js +894 -0
- package/src/utils/errors.js +142 -0
- package/src/utils/helpers.js +219 -0
- package/src/widgets/BaseWidget.js +548 -0
- package/src/widgets/ButtonWidget.js +104 -0
- package/src/widgets/ChangelogWidget.js +615 -0
- package/src/widgets/InlineWidget.js +148 -0
- package/src/widgets/MessengerWidget.js +979 -0
- package/src/widgets/SurveyWidget.js +1325 -0
- package/src/widgets/TabWidget.js +45 -0
- package/src/widgets/WidgetFactory.js +70 -0
- package/src/widgets/messenger/MessengerState.js +323 -0
- package/src/widgets/messenger/components/MessengerLauncher.js +124 -0
- package/src/widgets/messenger/components/MessengerPanel.js +111 -0
- package/src/widgets/messenger/components/NavigationTabs.js +130 -0
- package/src/widgets/messenger/views/ChangelogView.js +167 -0
- package/src/widgets/messenger/views/ChatView.js +592 -0
- package/src/widgets/messenger/views/ConversationsView.js +244 -0
- package/src/widgets/messenger/views/HelpView.js +239 -0
- package/src/widgets/messenger/views/HomeView.js +300 -0
- package/src/widgets/messenger/views/PreChatFormView.js +109 -0
- package/types/index.d.ts +341 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { ConfigError, SDKError } from '../utils/errors.js';
|
|
2
|
+
import { deepMerge, generateId } from '../utils/helpers.js';
|
|
3
|
+
import { WidgetFactory } from '../widgets/WidgetFactory.js';
|
|
4
|
+
import { APIService } from './APIService.js';
|
|
5
|
+
import { EventBus } from './EventBus.js';
|
|
6
|
+
|
|
7
|
+
export class Product7 {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.config = this._validateAndMergeConfig(config);
|
|
10
|
+
this.initialized = false;
|
|
11
|
+
this.widgets = new Map();
|
|
12
|
+
this.eventBus = new EventBus();
|
|
13
|
+
|
|
14
|
+
this.apiService = new APIService({
|
|
15
|
+
apiUrl: this.config.apiUrl,
|
|
16
|
+
workspace: this.config.workspace,
|
|
17
|
+
siteId: this.config.siteId,
|
|
18
|
+
sessionToken: this.config.sessionToken,
|
|
19
|
+
metadata: this.config.metadata,
|
|
20
|
+
mock: this.config.mock,
|
|
21
|
+
debug: this.config.debug,
|
|
22
|
+
env: this.config.env,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this._bindMethods();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async init() {
|
|
29
|
+
if (this.initialized) {
|
|
30
|
+
return { alreadyInitialized: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const initData = await this.apiService.init(this.config.metadata);
|
|
35
|
+
|
|
36
|
+
if (initData.config) {
|
|
37
|
+
this.config = deepMerge(initData.config, this.config);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.initialized = true;
|
|
41
|
+
this.eventBus.emit('sdk:initialized', {
|
|
42
|
+
config: this.config,
|
|
43
|
+
sessionToken: initData.sessionToken,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
initialized: true,
|
|
48
|
+
config: initData.config || {},
|
|
49
|
+
sessionToken: initData.sessionToken,
|
|
50
|
+
expiresIn: initData.expiresIn,
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.eventBus.emit('sdk:error', { error });
|
|
54
|
+
throw new SDKError(`Failed to initialize SDK: ${error.message}`, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createWidget(type = 'button', options = {}) {
|
|
59
|
+
if (!this.initialized) {
|
|
60
|
+
throw new SDKError(
|
|
61
|
+
'SDK must be initialized before creating widgets. Call init() first.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const widgetId = generateId('widget');
|
|
66
|
+
const widgetConfig = this._getWidgetTypeConfig(type);
|
|
67
|
+
const explicitOptions = this._omitUndefined(options);
|
|
68
|
+
const widgetEnabled = this._isWidgetEnabled(type, {
|
|
69
|
+
...widgetConfig,
|
|
70
|
+
...explicitOptions,
|
|
71
|
+
});
|
|
72
|
+
const widgetOptions = {
|
|
73
|
+
id: widgetId,
|
|
74
|
+
sdk: this,
|
|
75
|
+
apiService: this.apiService,
|
|
76
|
+
...this.config,
|
|
77
|
+
...widgetConfig,
|
|
78
|
+
...explicitOptions,
|
|
79
|
+
enabled: widgetEnabled,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const widget = WidgetFactory.create(type, widgetOptions);
|
|
84
|
+
this.widgets.set(widgetId, widget);
|
|
85
|
+
this.eventBus.emit('widget:created', { widget, type });
|
|
86
|
+
return widget;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new SDKError(`Failed to create widget: ${error.message}`, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getWidget(id) {
|
|
93
|
+
return this.widgets.get(id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getActiveSurveys(context = {}) {
|
|
97
|
+
if (!this.initialized) {
|
|
98
|
+
throw new SDKError(
|
|
99
|
+
'SDK must be initialized before fetching surveys. Call init() first.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.apiService.getActiveSurveys(context);
|
|
105
|
+
const surveys = (result.data || []).map((survey) =>
|
|
106
|
+
this._normalizeSurveyConfig(survey)
|
|
107
|
+
);
|
|
108
|
+
if (context.includeIneligible) {
|
|
109
|
+
return surveys;
|
|
110
|
+
}
|
|
111
|
+
return surveys.filter((survey) => this._isSurveyEligible(survey));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.eventBus.emit('sdk:error', { error });
|
|
114
|
+
throw new SDKError(
|
|
115
|
+
`Failed to fetch active surveys: ${error.message}`,
|
|
116
|
+
error
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async showSurveyById(surveyId, options = {}) {
|
|
122
|
+
if (!this.initialized) {
|
|
123
|
+
throw new SDKError(
|
|
124
|
+
'SDK must be initialized before showing surveys. Call init() first.'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { context = {}, ...displayOptions } = options;
|
|
129
|
+
const surveys = await this.getActiveSurveys({
|
|
130
|
+
...context,
|
|
131
|
+
includeEligibility: true,
|
|
132
|
+
includeIneligible: true,
|
|
133
|
+
});
|
|
134
|
+
const surveyConfig = surveys.find((s) => s.id === surveyId);
|
|
135
|
+
|
|
136
|
+
if (!surveyConfig) {
|
|
137
|
+
throw new SDKError(
|
|
138
|
+
`Survey with ID '${surveyId}' not found or not active`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!this._isSurveyEligible(surveyConfig)) {
|
|
143
|
+
this.eventBus.emit('survey:suppressed', {
|
|
144
|
+
surveyId,
|
|
145
|
+
reason: this._getSurveyIneligibilityReason(surveyConfig),
|
|
146
|
+
survey: surveyConfig,
|
|
147
|
+
});
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return this.showSurvey({
|
|
152
|
+
surveyId: surveyConfig.id,
|
|
153
|
+
surveyType: surveyConfig.surveyType || surveyConfig.type,
|
|
154
|
+
title: surveyConfig.title,
|
|
155
|
+
description: surveyConfig.description,
|
|
156
|
+
lowLabel: surveyConfig.lowLabel || surveyConfig.low_label,
|
|
157
|
+
highLabel: surveyConfig.highLabel || surveyConfig.high_label,
|
|
158
|
+
ratingScale: surveyConfig.ratingScale ?? surveyConfig.rating_scale,
|
|
159
|
+
showFeedbackInput:
|
|
160
|
+
surveyConfig.showFeedbackInput ?? surveyConfig.show_feedback_input,
|
|
161
|
+
showSubmitButton:
|
|
162
|
+
surveyConfig.showSubmitButton ?? surveyConfig.show_submit_button,
|
|
163
|
+
autoSubmitOnSelect:
|
|
164
|
+
surveyConfig.autoSubmitOnSelect ?? surveyConfig.auto_submit_on_select,
|
|
165
|
+
showTitle: surveyConfig.showTitle ?? surveyConfig.show_title,
|
|
166
|
+
showDescription:
|
|
167
|
+
surveyConfig.showDescription ?? surveyConfig.show_description,
|
|
168
|
+
customQuestions: surveyConfig.customQuestions || surveyConfig.questions,
|
|
169
|
+
pages: surveyConfig.pages,
|
|
170
|
+
enabled: surveyConfig.enabled,
|
|
171
|
+
...displayOptions,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
showSurvey(options = {}) {
|
|
176
|
+
if (!this.initialized) {
|
|
177
|
+
throw new SDKError(
|
|
178
|
+
'SDK must be initialized before showing surveys. Call init() first.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!this._isSurveyEligible(options)) {
|
|
183
|
+
this.eventBus.emit('survey:suppressed', {
|
|
184
|
+
surveyId: options.surveyId || options.id || null,
|
|
185
|
+
reason: this._getSurveyIneligibilityReason(options),
|
|
186
|
+
survey: options,
|
|
187
|
+
});
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const normalizedOptions = this._normalizeSurveyConfig(options);
|
|
192
|
+
const surveyConfigDefaults = this._getWidgetTypeConfig('survey');
|
|
193
|
+
const surveyEnabled = this._isWidgetEnabled('survey', normalizedOptions);
|
|
194
|
+
|
|
195
|
+
if (!surveyEnabled) {
|
|
196
|
+
this.eventBus.emit('survey:suppressed', {
|
|
197
|
+
surveyId:
|
|
198
|
+
normalizedOptions.surveyId ||
|
|
199
|
+
normalizedOptions.id ||
|
|
200
|
+
options.id ||
|
|
201
|
+
null,
|
|
202
|
+
reason: 'disabled',
|
|
203
|
+
survey: normalizedOptions,
|
|
204
|
+
});
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const surveyWidget = this.createWidget('survey', {
|
|
209
|
+
surveyId: normalizedOptions.surveyId,
|
|
210
|
+
surveyType:
|
|
211
|
+
normalizedOptions.surveyType || normalizedOptions.type || 'nps',
|
|
212
|
+
position:
|
|
213
|
+
normalizedOptions.position ??
|
|
214
|
+
surveyConfigDefaults.position ??
|
|
215
|
+
this.config.position ??
|
|
216
|
+
'right',
|
|
217
|
+
theme:
|
|
218
|
+
normalizedOptions.theme ??
|
|
219
|
+
surveyConfigDefaults.theme ??
|
|
220
|
+
this.config.theme ??
|
|
221
|
+
'light',
|
|
222
|
+
title: normalizedOptions.title,
|
|
223
|
+
description: normalizedOptions.description,
|
|
224
|
+
lowLabel: normalizedOptions.lowLabel,
|
|
225
|
+
highLabel: normalizedOptions.highLabel,
|
|
226
|
+
ratingScale: normalizedOptions.ratingScale ?? normalizedOptions.scale,
|
|
227
|
+
showFeedbackInput: normalizedOptions.showFeedbackInput,
|
|
228
|
+
showSubmitButton: normalizedOptions.showSubmitButton,
|
|
229
|
+
autoSubmitOnSelect: normalizedOptions.autoSubmitOnSelect,
|
|
230
|
+
showTitle: normalizedOptions.showTitle,
|
|
231
|
+
showDescription: normalizedOptions.showDescription,
|
|
232
|
+
customQuestions: normalizedOptions.customQuestions,
|
|
233
|
+
pages: normalizedOptions.pages,
|
|
234
|
+
respondentId: normalizedOptions.respondentId,
|
|
235
|
+
email: normalizedOptions.email,
|
|
236
|
+
onSubmit: normalizedOptions.onSubmit,
|
|
237
|
+
onDismiss: normalizedOptions.onDismiss,
|
|
238
|
+
enabled: surveyEnabled,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
surveyWidget.mount();
|
|
242
|
+
surveyWidget.show();
|
|
243
|
+
|
|
244
|
+
return surveyWidget;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_isSurveyEligible(survey = {}) {
|
|
248
|
+
const shouldShow = this._getSurveyField(survey, [
|
|
249
|
+
'shouldShow',
|
|
250
|
+
'should_show',
|
|
251
|
+
]);
|
|
252
|
+
if (typeof shouldShow === 'boolean') {
|
|
253
|
+
return shouldShow;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const eligibilityShouldShow = this._getSurveyField(
|
|
257
|
+
survey.eligibility || {},
|
|
258
|
+
['shouldShow', 'should_show']
|
|
259
|
+
);
|
|
260
|
+
if (typeof eligibilityShouldShow === 'boolean') {
|
|
261
|
+
return eligibilityShouldShow;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const eligible = this._getSurveyField(survey, [
|
|
265
|
+
'eligible',
|
|
266
|
+
'isEligible',
|
|
267
|
+
'is_eligible',
|
|
268
|
+
]);
|
|
269
|
+
if (typeof eligible === 'boolean') {
|
|
270
|
+
return eligible;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const isAnswered = this._getSurveyField(survey, [
|
|
274
|
+
'isAnswered',
|
|
275
|
+
'is_answered',
|
|
276
|
+
]);
|
|
277
|
+
if (typeof isAnswered === 'boolean') {
|
|
278
|
+
return !isAnswered;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const eligibilityAnswered = this._getSurveyField(survey.eligibility || {}, [
|
|
282
|
+
'isAnswered',
|
|
283
|
+
'is_answered',
|
|
284
|
+
]);
|
|
285
|
+
if (typeof eligibilityAnswered === 'boolean') {
|
|
286
|
+
return !eligibilityAnswered;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_getSurveyIneligibilityReason(survey = {}) {
|
|
293
|
+
const explicitReason = this._getSurveyField(survey, [
|
|
294
|
+
'reason',
|
|
295
|
+
'suppressionReason',
|
|
296
|
+
'suppression_reason',
|
|
297
|
+
]);
|
|
298
|
+
if (explicitReason) {
|
|
299
|
+
return explicitReason;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const eligibilityReason = this._getSurveyField(survey.eligibility || {}, [
|
|
303
|
+
'reason',
|
|
304
|
+
'suppressionReason',
|
|
305
|
+
'suppression_reason',
|
|
306
|
+
]);
|
|
307
|
+
if (eligibilityReason) {
|
|
308
|
+
return eligibilityReason;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const isAnswered = this._getSurveyField(survey, [
|
|
312
|
+
'isAnswered',
|
|
313
|
+
'is_answered',
|
|
314
|
+
]);
|
|
315
|
+
if (isAnswered === true) {
|
|
316
|
+
return 'already_answered';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return 'ineligible';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_normalizeSurveyConfig(survey = {}) {
|
|
323
|
+
const firstPage =
|
|
324
|
+
Array.isArray(survey.pages) && survey.pages.length > 0
|
|
325
|
+
? survey.pages[0]
|
|
326
|
+
: null;
|
|
327
|
+
const ratingConfig = firstPage
|
|
328
|
+
? firstPage.rating_config || firstPage.ratingConfig || {}
|
|
329
|
+
: {};
|
|
330
|
+
|
|
331
|
+
const inferredType =
|
|
332
|
+
survey.surveyType ||
|
|
333
|
+
survey.survey_type ||
|
|
334
|
+
survey.type ||
|
|
335
|
+
this._inferSurveyTypeFromPage(firstPage) ||
|
|
336
|
+
'nps';
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
...survey,
|
|
340
|
+
surveyId: survey.surveyId || survey.survey_id || survey.id || null,
|
|
341
|
+
surveyType: survey.surveyType || survey.survey_type || inferredType,
|
|
342
|
+
type: survey.type || survey.survey_type || inferredType,
|
|
343
|
+
enabled: typeof survey.enabled === 'boolean' ? survey.enabled : undefined,
|
|
344
|
+
should_show:
|
|
345
|
+
survey.should_show ??
|
|
346
|
+
(survey.eligibility ? survey.eligibility.should_show : undefined),
|
|
347
|
+
reason:
|
|
348
|
+
survey.reason ||
|
|
349
|
+
(survey.eligibility ? survey.eligibility.reason : undefined),
|
|
350
|
+
title:
|
|
351
|
+
survey.title || survey.name || (firstPage ? firstPage.title : null),
|
|
352
|
+
description:
|
|
353
|
+
survey.description || (firstPage ? firstPage.description : null),
|
|
354
|
+
lowLabel:
|
|
355
|
+
survey.lowLabel || survey.low_label || ratingConfig.low_label || null,
|
|
356
|
+
highLabel:
|
|
357
|
+
survey.highLabel ||
|
|
358
|
+
survey.high_label ||
|
|
359
|
+
ratingConfig.high_label ||
|
|
360
|
+
null,
|
|
361
|
+
ratingScale:
|
|
362
|
+
survey.ratingScale ?? survey.rating_scale ?? ratingConfig.scale ?? null,
|
|
363
|
+
showFeedbackInput:
|
|
364
|
+
survey.showFeedbackInput ?? survey.show_feedback_input ?? null,
|
|
365
|
+
showSubmitButton:
|
|
366
|
+
survey.showSubmitButton ?? survey.show_submit_button ?? null,
|
|
367
|
+
autoSubmitOnSelect:
|
|
368
|
+
survey.autoSubmitOnSelect ?? survey.auto_submit_on_select ?? null,
|
|
369
|
+
showTitle: survey.showTitle ?? survey.show_title ?? null,
|
|
370
|
+
showDescription:
|
|
371
|
+
survey.showDescription ?? survey.show_description ?? null,
|
|
372
|
+
customQuestions:
|
|
373
|
+
survey.customQuestions ||
|
|
374
|
+
survey.custom_questions ||
|
|
375
|
+
survey.questions ||
|
|
376
|
+
[],
|
|
377
|
+
pages: this._normalizeSurveyPages(survey.pages || []),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_normalizeSurveyPages(pages = []) {
|
|
382
|
+
if (!Array.isArray(pages)) {
|
|
383
|
+
return [];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return pages
|
|
387
|
+
.map((page, index) => ({
|
|
388
|
+
id: page.id || `page_${index}`,
|
|
389
|
+
type: page.type || 'rating',
|
|
390
|
+
title: page.title || '',
|
|
391
|
+
description: page.description || '',
|
|
392
|
+
placeholder: page.placeholder || '',
|
|
393
|
+
required: page.required !== false,
|
|
394
|
+
position: page.position ?? index,
|
|
395
|
+
ratingConfig: page.ratingConfig || page.rating_config || null,
|
|
396
|
+
multipleChoiceConfig:
|
|
397
|
+
page.multipleChoiceConfig || page.multiple_choice_config || null,
|
|
398
|
+
linkConfig: page.linkConfig || page.link_config || null,
|
|
399
|
+
afterThisPage: page.afterThisPage || page.after_this_page || null,
|
|
400
|
+
}))
|
|
401
|
+
.sort((a, b) => a.position - b.position);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_inferSurveyTypeFromPage(page) {
|
|
405
|
+
if (!page) return null;
|
|
406
|
+
const ratingConfig = page.rating_config || page.ratingConfig || {};
|
|
407
|
+
const scale = ratingConfig.scale;
|
|
408
|
+
const surveyType = ratingConfig.survey_type;
|
|
409
|
+
const title = (page.title || '').toLowerCase();
|
|
410
|
+
|
|
411
|
+
if (scale === 11 || surveyType === 'nps') {
|
|
412
|
+
return 'nps';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const lowLabel = (ratingConfig.low_label || '').toLowerCase();
|
|
416
|
+
if (
|
|
417
|
+
surveyType === 'ces' ||
|
|
418
|
+
title.includes('effort') ||
|
|
419
|
+
title.includes('easy') ||
|
|
420
|
+
lowLabel.includes('difficult')
|
|
421
|
+
) {
|
|
422
|
+
return 'ces';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return 'csat';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_getSurveyField(survey, fields) {
|
|
429
|
+
for (const field of fields) {
|
|
430
|
+
if (survey[field] !== undefined && survey[field] !== null) {
|
|
431
|
+
return survey[field];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
_getWidgetTypeConfig(type) {
|
|
438
|
+
const widgetsConfig = this._isPlainObject(this.config?.widgets)
|
|
439
|
+
? this.config.widgets
|
|
440
|
+
: {};
|
|
441
|
+
|
|
442
|
+
const legacyTypeConfig = this._isPlainObject(this.config?.[type])
|
|
443
|
+
? this.config[type]
|
|
444
|
+
: {};
|
|
445
|
+
|
|
446
|
+
const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[type])
|
|
447
|
+
? widgetsConfig[type]
|
|
448
|
+
: {};
|
|
449
|
+
|
|
450
|
+
const mergedTypeConfig = deepMerge(legacyTypeConfig, namespacedTypeConfig);
|
|
451
|
+
return this._toCamelCaseObject(mergedTypeConfig);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
_isWidgetEnabled(type, options = {}) {
|
|
455
|
+
const typeConfig = this._getWidgetTypeConfig(type);
|
|
456
|
+
if (typeConfig.enabled === false) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (typeof options.enabled === 'boolean') {
|
|
461
|
+
return options.enabled;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof typeConfig.enabled === 'boolean') {
|
|
465
|
+
return typeConfig.enabled;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_isPlainObject(value) {
|
|
472
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_toCamelCaseKey(key) {
|
|
476
|
+
return key.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
_toCamelCaseObject(value) {
|
|
480
|
+
if (Array.isArray(value)) {
|
|
481
|
+
return value.map((item) => this._toCamelCaseObject(item));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!this._isPlainObject(value)) {
|
|
485
|
+
return value;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const normalized = {};
|
|
489
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
490
|
+
normalized[this._toCamelCaseKey(key)] =
|
|
491
|
+
this._toCamelCaseObject(nestedValue);
|
|
492
|
+
}
|
|
493
|
+
return normalized;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_omitUndefined(value) {
|
|
497
|
+
if (!this._isPlainObject(value)) {
|
|
498
|
+
return value;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return Object.fromEntries(
|
|
502
|
+
Object.entries(value).filter(
|
|
503
|
+
([, nestedValue]) => nestedValue !== undefined
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
showChangelog(options = {}) {
|
|
509
|
+
if (!this.initialized) {
|
|
510
|
+
throw new SDKError(
|
|
511
|
+
'SDK must be initialized before showing changelog. Call init() first.'
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const defaults = {
|
|
516
|
+
position: this.config.position || 'right',
|
|
517
|
+
theme: this.config.theme || 'light',
|
|
518
|
+
title: "What's New",
|
|
519
|
+
triggerText: "What's New",
|
|
520
|
+
showBadge: true,
|
|
521
|
+
viewButtonText: 'View Update',
|
|
522
|
+
showBackdrop: true,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const configDefaults = this._getWidgetTypeConfig('changelog');
|
|
526
|
+
const explicitOptions = this._omitUndefined(options);
|
|
527
|
+
const changelogEnabled = this._isWidgetEnabled(
|
|
528
|
+
'changelog',
|
|
529
|
+
explicitOptions
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (!changelogEnabled) {
|
|
533
|
+
this.eventBus.emit('widget:suppressed', {
|
|
534
|
+
type: 'changelog',
|
|
535
|
+
reason: 'disabled',
|
|
536
|
+
});
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const changelogWidget = this.createWidget('changelog', {
|
|
541
|
+
...defaults,
|
|
542
|
+
...configDefaults,
|
|
543
|
+
...explicitOptions,
|
|
544
|
+
enabled: changelogEnabled,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
changelogWidget.mount();
|
|
548
|
+
changelogWidget.show();
|
|
549
|
+
|
|
550
|
+
return changelogWidget;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async getChangelogs(options = {}) {
|
|
554
|
+
if (!this.initialized) {
|
|
555
|
+
throw new SDKError(
|
|
556
|
+
'SDK must be initialized before fetching changelogs. Call init() first.'
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const result = await this.apiService.getChangelogs(options);
|
|
562
|
+
return result.data || [];
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.eventBus.emit('sdk:error', { error });
|
|
565
|
+
throw new SDKError(`Failed to fetch changelogs: ${error.message}`, error);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getAllWidgets() {
|
|
570
|
+
return Array.from(this.widgets.values());
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
destroyWidget(id) {
|
|
574
|
+
const widget = this.widgets.get(id);
|
|
575
|
+
if (widget) {
|
|
576
|
+
widget.destroy();
|
|
577
|
+
this.widgets.delete(id);
|
|
578
|
+
this.eventBus.emit('widget:removed', { widgetId: id });
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
destroyAllWidgets() {
|
|
585
|
+
for (const widget of this.widgets.values()) {
|
|
586
|
+
widget.destroy();
|
|
587
|
+
}
|
|
588
|
+
this.widgets.clear();
|
|
589
|
+
this.eventBus.emit('widgets:cleared');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
updateConfig(newConfig) {
|
|
593
|
+
const oldConfig = { ...this.config };
|
|
594
|
+
this.config = this._validateAndMergeConfig(newConfig, this.config);
|
|
595
|
+
|
|
596
|
+
for (const widget of this.widgets.values()) {
|
|
597
|
+
widget.handleConfigUpdate(this.config);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this.eventBus.emit('config:updated', {
|
|
601
|
+
oldConfig,
|
|
602
|
+
newConfig: this.config,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
setMetadata(metadata) {
|
|
607
|
+
this.config.metadata = metadata;
|
|
608
|
+
if (this.apiService) {
|
|
609
|
+
this.apiService.setMetadata(metadata);
|
|
610
|
+
}
|
|
611
|
+
this.eventBus.emit('metadata:updated', { metadata });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
getMetadata() {
|
|
615
|
+
return (
|
|
616
|
+
this.config.metadata ||
|
|
617
|
+
(this.apiService ? this.apiService.getMetadata() : null)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async reinitialize(newMetadata = null) {
|
|
622
|
+
this.apiService.clearSession();
|
|
623
|
+
this.initialized = false;
|
|
624
|
+
|
|
625
|
+
if (newMetadata) {
|
|
626
|
+
this.setMetadata(newMetadata);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return this.init();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
on(event, callback) {
|
|
633
|
+
this.eventBus.on(event, callback);
|
|
634
|
+
return this;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
off(event, callback) {
|
|
638
|
+
this.eventBus.off(event, callback);
|
|
639
|
+
return this;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
once(event, callback) {
|
|
643
|
+
this.eventBus.once(event, callback);
|
|
644
|
+
return this;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
emit(event, data) {
|
|
648
|
+
this.eventBus.emit(event, data);
|
|
649
|
+
return this;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
destroy() {
|
|
653
|
+
this.destroyAllWidgets();
|
|
654
|
+
this.eventBus.removeAllListeners();
|
|
655
|
+
this.apiService.clearSession();
|
|
656
|
+
this.initialized = false;
|
|
657
|
+
this.eventBus.emit('sdk:destroyed');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
hasFeedbackBeenSubmitted(cooldownDays = 30) {
|
|
661
|
+
const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000;
|
|
662
|
+
const now = Date.now();
|
|
663
|
+
|
|
664
|
+
if (this.config.last_feedback_at) {
|
|
665
|
+
try {
|
|
666
|
+
const backendTimestamp = new Date(
|
|
667
|
+
this.config.last_feedback_at
|
|
668
|
+
).getTime();
|
|
669
|
+
if (now - backendTimestamp < cooldownMs) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
} catch (e) {
|
|
673
|
+
// Invalid date format, continue to localStorage check
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
679
|
+
const stored = localStorage.getItem(storageKey);
|
|
680
|
+
if (!stored) return false;
|
|
681
|
+
|
|
682
|
+
const data = JSON.parse(stored);
|
|
683
|
+
return now - data.submittedAt < cooldownMs;
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
clearFeedbackSubmissionTracking() {
|
|
690
|
+
try {
|
|
691
|
+
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
692
|
+
localStorage.removeItem(storageKey);
|
|
693
|
+
this.eventBus.emit('feedback:trackingCleared');
|
|
694
|
+
} catch (e) {
|
|
695
|
+
console.warn('Failed to clear feedback tracking:', e);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
_detectEnvironment() {
|
|
700
|
+
if (typeof window === 'undefined') {
|
|
701
|
+
return 'production';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
705
|
+
|
|
706
|
+
if (
|
|
707
|
+
hostname.includes('staging') ||
|
|
708
|
+
hostname.includes('localhost') ||
|
|
709
|
+
hostname.includes('127.0.0.1') ||
|
|
710
|
+
hostname.includes('.local')
|
|
711
|
+
) {
|
|
712
|
+
return 'staging';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return 'production';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
_validateAndMergeConfig(newConfig, existingConfig = {}) {
|
|
719
|
+
const defaultConfig = {
|
|
720
|
+
apiUrl: null,
|
|
721
|
+
workspace: null,
|
|
722
|
+
metadata: null,
|
|
723
|
+
position: 'right',
|
|
724
|
+
theme: 'light',
|
|
725
|
+
boardName: 'general',
|
|
726
|
+
autoShow: true,
|
|
727
|
+
debug: false,
|
|
728
|
+
mock: false,
|
|
729
|
+
env: this._detectEnvironment(),
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const mergedConfig = deepMerge(
|
|
733
|
+
deepMerge(defaultConfig, existingConfig),
|
|
734
|
+
newConfig
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
if (!newConfig.env && !existingConfig.env) {
|
|
738
|
+
mergedConfig.env = this._detectEnvironment();
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!mergedConfig.workspace) {
|
|
742
|
+
throw new ConfigError('Missing required configuration: workspace');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (mergedConfig.metadata) {
|
|
746
|
+
this._validateMetadata(mergedConfig.metadata);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return mergedConfig;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
_validateMetadata(metadata) {
|
|
753
|
+
if (!metadata.user_id && !metadata.email) {
|
|
754
|
+
throw new ConfigError('Metadata must include at least user_id or email');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const validStructure = {
|
|
758
|
+
user_id: 'string',
|
|
759
|
+
email: 'string',
|
|
760
|
+
name: 'string',
|
|
761
|
+
custom_fields: 'object',
|
|
762
|
+
company: 'object',
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
for (const [key, expectedType] of Object.entries(validStructure)) {
|
|
766
|
+
if (metadata[key] && typeof metadata[key] !== expectedType) {
|
|
767
|
+
throw new ConfigError(
|
|
768
|
+
`Metadata field '${key}' must be of type '${expectedType}'`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
_bindMethods() {
|
|
775
|
+
this.createWidget = this.createWidget.bind(this);
|
|
776
|
+
this.destroyWidget = this.destroyWidget.bind(this);
|
|
777
|
+
this.updateConfig = this.updateConfig.bind(this);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
static create(config) {
|
|
781
|
+
return new Product7(config);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
static async createAndInit(config) {
|
|
785
|
+
const sdk = new Product7(config);
|
|
786
|
+
await sdk.init();
|
|
787
|
+
return sdk;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
static extractMetadataFromAuth(authData) {
|
|
791
|
+
if (!authData) return null;
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
user_id: authData.sub || authData.id || authData.user_id,
|
|
795
|
+
email: authData.email,
|
|
796
|
+
name: authData.name || authData.display_name || authData.full_name,
|
|
797
|
+
custom_fields: {
|
|
798
|
+
role: authData.role,
|
|
799
|
+
plan: authData.plan || authData.subscription?.plan,
|
|
800
|
+
...(authData.custom_fields || {}),
|
|
801
|
+
},
|
|
802
|
+
company:
|
|
803
|
+
authData.company || authData.organization
|
|
804
|
+
? {
|
|
805
|
+
id: authData.company?.id || authData.organization?.id,
|
|
806
|
+
name: authData.company?.name || authData.organization?.name,
|
|
807
|
+
monthly_spend: authData.company?.monthly_spend,
|
|
808
|
+
}
|
|
809
|
+
: undefined,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|