@product7/feedback-sdk 1.1.8 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@product7/feedback-sdk",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "JavaScript SDK for integrating Product7 feedback widgets into any website",
5
5
  "main": "dist/feedback-sdk.js",
6
6
  "module": "src/index.js",
@@ -9,6 +9,39 @@ const MOCK_CONFIG = {
9
9
  displayMode: 'modal',
10
10
  };
11
11
 
12
+ // Mock surveys for development
13
+ const MOCK_SURVEYS = [
14
+ {
15
+ id: 'mock_nps_survey',
16
+ type: 'nps',
17
+ title: 'How likely are you to recommend us?',
18
+ description: 'Your feedback helps us improve',
19
+ low_label: 'Not likely',
20
+ high_label: 'Very likely',
21
+ trigger: 'manual',
22
+ status: 'active',
23
+ },
24
+ {
25
+ id: 'mock_csat_survey',
26
+ type: 'csat',
27
+ title: 'How satisfied are you?',
28
+ description: 'Rate your experience with our product',
29
+ trigger: 'manual',
30
+ status: 'active',
31
+ },
32
+ {
33
+ id: 'mock_ces_survey',
34
+ type: 'ces',
35
+ title: 'How easy was it?',
36
+ description: 'Rate the ease of completing your task',
37
+ low_label: 'Very difficult',
38
+ high_label: 'Very easy',
39
+ trigger: 'manual',
40
+ status: 'active',
41
+ },
42
+ ];
43
+
44
+ // Environment URLs
12
45
  const ENV_URLS = {
13
46
  production: {
14
47
  base: 'https://api.product7.io/api/v1',
@@ -158,6 +191,245 @@ export class APIService {
158
191
  }
159
192
  }
160
193
 
194
+ /**
195
+ * Get active surveys for the current user/context
196
+ * @param {Object} context - Optional context overrides for targeting
197
+ * @param {string} context.url - Override current URL
198
+ * @param {Object} context.userProperties - Additional user properties
199
+ * @returns {Promise<Array>} Array of active surveys matching targeting rules
200
+ */
201
+ async getActiveSurveys(context = {}) {
202
+ if (!this.isSessionValid()) {
203
+ await this.init();
204
+ }
205
+
206
+ if (!this.sessionToken) {
207
+ throw new APIError(401, 'No valid session token available');
208
+ }
209
+
210
+ // Mock mode - return mock surveys
211
+ if (this.mock) {
212
+ await new Promise((resolve) => setTimeout(resolve, 200));
213
+ return {
214
+ success: true,
215
+ data: MOCK_SURVEYS,
216
+ };
217
+ }
218
+
219
+ try {
220
+ const queryParams = new URLSearchParams();
221
+
222
+ // Auto-detect current URL or use provided override
223
+ const currentUrl =
224
+ context.url ||
225
+ (typeof window !== 'undefined' ? window.location.href : '');
226
+ if (currentUrl) {
227
+ queryParams.append('url', currentUrl);
228
+ }
229
+
230
+ // Auto-detect device info
231
+ const deviceInfo = this._getDeviceInfo();
232
+ if (deviceInfo.device) queryParams.append('device', deviceInfo.device);
233
+ if (deviceInfo.browser) queryParams.append('browser', deviceInfo.browser);
234
+ if (deviceInfo.os) queryParams.append('os', deviceInfo.os);
235
+
236
+ // Add user properties if provided
237
+ if (context.userProperties) {
238
+ queryParams.append(
239
+ 'user_properties',
240
+ JSON.stringify(context.userProperties)
241
+ );
242
+ }
243
+
244
+ const endpoint = `/widget/surveys/active${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
245
+
246
+ const response = await this._makeRequest(endpoint, {
247
+ method: 'GET',
248
+ headers: {
249
+ Authorization: `Bearer ${this.sessionToken}`,
250
+ },
251
+ });
252
+
253
+ return response;
254
+ } catch (error) {
255
+ if (error.status === 401) {
256
+ this.sessionToken = null;
257
+ this.sessionExpiry = null;
258
+ await this.init();
259
+ return this.getActiveSurveys(context);
260
+ }
261
+
262
+ throw new APIError(
263
+ error.status || 500,
264
+ `Failed to get active surveys: ${error.message}`,
265
+ error.response
266
+ );
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Auto-detect device, browser, and OS info
272
+ * @returns {Object} Device info object
273
+ */
274
+ _getDeviceInfo() {
275
+ if (typeof navigator === 'undefined') {
276
+ return { device: null, browser: null, os: null };
277
+ }
278
+
279
+ const ua = navigator.userAgent;
280
+
281
+ // Detect device type
282
+ let device = 'desktop';
283
+ if (/Mobi|Android/i.test(ua)) {
284
+ device = /Tablet|iPad/i.test(ua) ? 'tablet' : 'mobile';
285
+ }
286
+
287
+ // Detect browser
288
+ let browser = 'unknown';
289
+ if (ua.includes('Firefox')) browser = 'firefox';
290
+ else if (ua.includes('Edg')) browser = 'edge';
291
+ else if (ua.includes('Chrome')) browser = 'chrome';
292
+ else if (ua.includes('Safari')) browser = 'safari';
293
+ else if (ua.includes('Opera') || ua.includes('OPR')) browser = 'opera';
294
+
295
+ // Detect OS
296
+ let os = 'unknown';
297
+ if (ua.includes('Windows')) os = 'windows';
298
+ else if (ua.includes('Mac')) os = 'macos';
299
+ else if (ua.includes('Linux')) os = 'linux';
300
+ else if (ua.includes('Android')) os = 'android';
301
+ else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'ios';
302
+
303
+ return { device, browser, os };
304
+ }
305
+
306
+ /**
307
+ * Submit a survey response
308
+ * @param {string} surveyId - The survey ID
309
+ * @param {Object} responseData - The survey response data
310
+ * @param {number|string} responseData.rating - The rating/score value
311
+ * @param {string} responseData.feedback - Optional text feedback
312
+ * @param {Object} responseData.answers - For custom surveys, key-value pairs
313
+ * @returns {Promise<Object>} Submission result
314
+ */
315
+ async submitSurveyResponse(surveyId, responseData) {
316
+ if (!this.isSessionValid()) {
317
+ await this.init();
318
+ }
319
+
320
+ if (!this.sessionToken) {
321
+ throw new APIError(401, 'No valid session token available');
322
+ }
323
+
324
+ if (!surveyId) {
325
+ throw new APIError(400, 'Survey ID is required');
326
+ }
327
+
328
+ // Mock mode - simulate success
329
+ if (this.mock) {
330
+ await new Promise((resolve) => setTimeout(resolve, 300));
331
+ return {
332
+ success: true,
333
+ data: {
334
+ id: 'mock_response_' + Date.now(),
335
+ survey_id: surveyId,
336
+ ...responseData,
337
+ },
338
+ message: 'Survey response submitted successfully!',
339
+ };
340
+ }
341
+
342
+ const payload = {
343
+ rating: responseData.rating,
344
+ feedback: responseData.feedback || '',
345
+ answers: responseData.answers || {},
346
+ };
347
+
348
+ try {
349
+ const response = await this._makeRequest(
350
+ `/widget/surveys/${surveyId}/responses`,
351
+ {
352
+ method: 'POST',
353
+ body: JSON.stringify(payload),
354
+ headers: {
355
+ 'Content-Type': 'application/json',
356
+ Authorization: `Bearer ${this.sessionToken}`,
357
+ },
358
+ }
359
+ );
360
+
361
+ return response;
362
+ } catch (error) {
363
+ if (error.status === 401) {
364
+ this.sessionToken = null;
365
+ this.sessionExpiry = null;
366
+ await this.init();
367
+ return this.submitSurveyResponse(surveyId, responseData);
368
+ }
369
+
370
+ throw new APIError(
371
+ error.status || 500,
372
+ `Failed to submit survey response: ${error.message}`,
373
+ error.response
374
+ );
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Dismiss a survey (mark as seen but not completed)
380
+ * @param {string} surveyId - The survey ID to dismiss
381
+ * @returns {Promise<Object>} Dismissal result
382
+ */
383
+ async dismissSurvey(surveyId) {
384
+ if (!this.isSessionValid()) {
385
+ await this.init();
386
+ }
387
+
388
+ if (!this.sessionToken) {
389
+ throw new APIError(401, 'No valid session token available');
390
+ }
391
+
392
+ if (!surveyId) {
393
+ throw new APIError(400, 'Survey ID is required');
394
+ }
395
+
396
+ // Mock mode - simulate success
397
+ if (this.mock) {
398
+ await new Promise((resolve) => setTimeout(resolve, 100));
399
+ return {
400
+ success: true,
401
+ message: 'Survey dismissed successfully',
402
+ };
403
+ }
404
+
405
+ try {
406
+ const response = await this._makeRequest(
407
+ `/widget/surveys/${surveyId}/dismiss`,
408
+ {
409
+ method: 'POST',
410
+ headers: {
411
+ Authorization: `Bearer ${this.sessionToken}`,
412
+ },
413
+ }
414
+ );
415
+
416
+ return response;
417
+ } catch (error) {
418
+ if (error.status === 401) {
419
+ this.sessionToken = null;
420
+ this.sessionExpiry = null;
421
+ await this.init();
422
+ return this.dismissSurvey(surveyId);
423
+ }
424
+
425
+ throw new APIError(
426
+ error.status || 500,
427
+ `Failed to dismiss survey: ${error.message}`,
428
+ error.response
429
+ );
430
+ }
431
+ }
432
+
161
433
  isSessionValid() {
162
434
  return (
163
435
  this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
@@ -84,8 +84,76 @@ export class FeedbackSDK {
84
84
  }
85
85
 
86
86
  /**
87
- * Show a survey widget
87
+ * Fetch active surveys from the backend
88
+ * @param {Object} context - Optional context for targeting
89
+ * @param {string} context.page - Current page/route
90
+ * @param {string} context.event - Event trigger name
91
+ * @returns {Promise<Array>} Array of active survey configurations
92
+ */
93
+ async getActiveSurveys(context = {}) {
94
+ if (!this.initialized) {
95
+ throw new SDKError(
96
+ 'SDK must be initialized before fetching surveys. Call init() first.'
97
+ );
98
+ }
99
+
100
+ try {
101
+ const result = await this.apiService.getActiveSurveys(context);
102
+ return result.data || [];
103
+ } catch (error) {
104
+ this.eventBus.emit('sdk:error', { error });
105
+ throw new SDKError(
106
+ `Failed to fetch active surveys: ${error.message}`,
107
+ error
108
+ );
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Show a survey by its backend ID
114
+ * Fetches survey configuration from the backend and displays it
115
+ * @param {string} surveyId - The backend survey ID
116
+ * @param {Object} options - Additional display options
117
+ * @param {string} options.position - Position override
118
+ * @param {string} options.theme - Theme override
119
+ * @param {Function} options.onSubmit - Callback when survey is submitted
120
+ * @param {Function} options.onDismiss - Callback when survey is dismissed
121
+ * @returns {Promise<SurveyWidget>} The survey widget instance
122
+ */
123
+ async showSurveyById(surveyId, options = {}) {
124
+ if (!this.initialized) {
125
+ throw new SDKError(
126
+ 'SDK must be initialized before showing surveys. Call init() first.'
127
+ );
128
+ }
129
+
130
+ // Fetch active surveys to find the one with matching ID
131
+ const surveys = await this.getActiveSurveys();
132
+ const surveyConfig = surveys.find((s) => s.id === surveyId);
133
+
134
+ if (!surveyConfig) {
135
+ throw new SDKError(
136
+ `Survey with ID '${surveyId}' not found or not active`
137
+ );
138
+ }
139
+
140
+ return this.showSurvey({
141
+ surveyId: surveyConfig.id,
142
+ surveyType: surveyConfig.type,
143
+ title: surveyConfig.title,
144
+ description: surveyConfig.description,
145
+ lowLabel: surveyConfig.low_label,
146
+ highLabel: surveyConfig.high_label,
147
+ customQuestions: surveyConfig.questions,
148
+ ...options,
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Show a survey widget (local/manual mode)
154
+ * For backend-driven surveys, use showSurveyById() instead
88
155
  * @param {Object} options - Survey options
156
+ * @param {string} options.surveyId - Backend survey ID (for API tracking)
89
157
  * @param {string} options.surveyType - Type of survey: 'nps', 'csat', 'ces', 'custom'
90
158
  * @param {string} options.position - Position: 'bottom-right', 'bottom-left', 'center', 'bottom'
91
159
  * @param {string} options.theme - Theme: 'light', 'dark'
@@ -105,6 +173,7 @@ export class FeedbackSDK {
105
173
  }
106
174
 
107
175
  const surveyWidget = this.createWidget('survey', {
176
+ surveyId: options.surveyId,
108
177
  surveyType: options.surveyType || options.type || 'nps',
109
178
  position: options.position || 'bottom-right',
110
179
  theme: options.theme || this.config.theme || 'light',
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { APIService } from './core/APIService.js';
2
2
  import { EventBus } from './core/EventBus.js';
3
3
  import { FeedbackSDK } from './core/FeedbackSDK.js';
4
+ import { MESSENGER_STYLES } from './styles/messengerStyles.js';
4
5
  import { CSS_STYLES } from './styles/styles.js';
5
6
  import {
6
7
  APIError,
@@ -13,6 +14,7 @@ import * as helpers from './utils/helpers.js';
13
14
  import { BaseWidget } from './widgets/BaseWidget.js';
14
15
  import { ButtonWidget } from './widgets/ButtonWidget.js';
15
16
  import { InlineWidget } from './widgets/InlineWidget.js';
17
+ import { MessengerWidget } from './widgets/MessengerWidget.js';
16
18
  import { SurveyWidget } from './widgets/SurveyWidget.js';
17
19
  import { TabWidget } from './widgets/TabWidget.js';
18
20
  import { WidgetFactory } from './widgets/WidgetFactory.js';
@@ -24,7 +26,7 @@ function injectStyles() {
24
26
  ) {
25
27
  const style = document.createElement('style');
26
28
  style.id = 'feedback-sdk-styles';
27
- style.textContent = CSS_STYLES;
29
+ style.textContent = CSS_STYLES + MESSENGER_STYLES;
28
30
  document.head.appendChild(style);
29
31
  }
30
32
  }
@@ -96,6 +98,7 @@ const FeedbackSDKExport = {
96
98
  TabWidget,
97
99
  InlineWidget,
98
100
  SurveyWidget,
101
+ MessengerWidget,
99
102
  WidgetFactory,
100
103
  EventBus,
101
104
  APIService,
@@ -182,6 +185,7 @@ export {
182
185
  FeedbackSDK,
183
186
  helpers,
184
187
  InlineWidget,
188
+ MessengerWidget,
185
189
  SDKError,
186
190
  SurveyWidget,
187
191
  TabWidget,