@product7/feedback-sdk 1.2.7 → 1.3.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/dist/feedback-sdk.js +1530 -1866
- package/dist/feedback-sdk.js.map +1 -1
- package/dist/feedback-sdk.min.js +1 -1
- package/dist/feedback-sdk.min.js.map +1 -1
- package/package.json +1 -1
- package/src/api/mock-data/index.js +202 -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 +197 -0
- package/src/api/services/SurveyService.js +99 -0
- package/src/api/utils/helpers.js +30 -0
- package/src/core/APIService.js +40 -1142
- package/src/core/BaseAPIService.js +245 -0
- package/src/core/FeedbackSDK.js +0 -68
- package/src/styles/base.js +75 -0
- package/src/styles/changelog.js +698 -0
- package/src/styles/feedback.js +574 -0
- package/src/styles/styles.js +5 -1379
- package/src/widgets/ChangelogWidget.js +0 -5
- package/src/widgets/MessengerWidget.js +1 -2
- package/src/widgets/messenger/components/MessengerLauncher.js +7 -7
- package/src/widgets/messenger/components/NavigationTabs.js +16 -4
- package/src/widgets/messenger/views/ChangelogView.js +9 -9
- package/src/widgets/messenger/views/ChatView.js +9 -20
- package/src/widgets/messenger/views/ConversationsView.js +9 -13
- package/src/widgets/messenger/views/HelpView.js +15 -14
- package/src/widgets/messenger/views/HomeView.js +6 -15
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { APIError } from '../utils/errors.js';
|
|
2
|
+
|
|
3
|
+
export class BaseAPIService {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.workspace = config.workspace;
|
|
6
|
+
this.sessionToken = null;
|
|
7
|
+
this.sessionExpiry = null;
|
|
8
|
+
this.userContext = config.userContext || null;
|
|
9
|
+
this.mock = config.mock || false;
|
|
10
|
+
this.env = config.env || 'production';
|
|
11
|
+
this.baseURL = this._getBaseURL(config);
|
|
12
|
+
|
|
13
|
+
this._loadStoredSession();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_getBaseURL(config) {
|
|
17
|
+
if (config.apiUrl) return config.apiUrl;
|
|
18
|
+
|
|
19
|
+
const ENV_URLS = {
|
|
20
|
+
production: {
|
|
21
|
+
base: 'https://api.product7.io/api/v1',
|
|
22
|
+
withWorkspace: (ws) => `https://${ws}.api.product7.io/api/v1`,
|
|
23
|
+
},
|
|
24
|
+
staging: {
|
|
25
|
+
base: 'https://staging.api.product7.io/api/v1',
|
|
26
|
+
withWorkspace: (ws) => `https://${ws}.staging.api.product7.io/api/v1`,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const envConfig = ENV_URLS[this.env] || ENV_URLS.production;
|
|
31
|
+
return this.workspace
|
|
32
|
+
? envConfig.withWorkspace(this.workspace)
|
|
33
|
+
: envConfig.base;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async init(userContext = null) {
|
|
37
|
+
if (userContext) {
|
|
38
|
+
this.userContext = userContext;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.isSessionValid()) {
|
|
42
|
+
return { sessionToken: this.sessionToken };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!this.workspace || !this.userContext) {
|
|
46
|
+
throw new APIError(
|
|
47
|
+
400,
|
|
48
|
+
`Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this.mock) {
|
|
53
|
+
return this._initMockSession();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this._initRealSession();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async _initMockSession() {
|
|
60
|
+
this.sessionToken = 'mock_session_' + Date.now();
|
|
61
|
+
this.sessionExpiry = new Date(Date.now() + 3600 * 1000);
|
|
62
|
+
this._storeSession();
|
|
63
|
+
return {
|
|
64
|
+
sessionToken: this.sessionToken,
|
|
65
|
+
config: {
|
|
66
|
+
primaryColor: '#21244A',
|
|
67
|
+
backgroundColor: '#ffffff',
|
|
68
|
+
textColor: '#1F2937',
|
|
69
|
+
boardId: 'feature-requests',
|
|
70
|
+
size: 'medium',
|
|
71
|
+
displayMode: 'modal',
|
|
72
|
+
},
|
|
73
|
+
expiresIn: 3600,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _initRealSession() {
|
|
78
|
+
const payload = {
|
|
79
|
+
workspace: this.workspace,
|
|
80
|
+
user: this.userContext,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await this._makeRequest('/widget/init', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify(payload),
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.sessionToken = response.session_token;
|
|
91
|
+
this.sessionExpiry = new Date(Date.now() + response.expires_in * 1000);
|
|
92
|
+
this._storeSession();
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
sessionToken: this.sessionToken,
|
|
96
|
+
config: response.config || {},
|
|
97
|
+
expiresIn: response.expires_in,
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new APIError(
|
|
101
|
+
error.status || 500,
|
|
102
|
+
`Failed to initialize widget: ${error.message}`,
|
|
103
|
+
error.response
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async _ensureSession() {
|
|
109
|
+
if (!this.isSessionValid()) {
|
|
110
|
+
await this.init();
|
|
111
|
+
}
|
|
112
|
+
if (!this.sessionToken) {
|
|
113
|
+
throw new APIError(401, 'No valid session token available');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async _handleAuthRetry(method, ...args) {
|
|
118
|
+
try {
|
|
119
|
+
return await method.apply(this, args);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.status === 401) {
|
|
122
|
+
this.sessionToken = null;
|
|
123
|
+
this.sessionExpiry = null;
|
|
124
|
+
await this.init();
|
|
125
|
+
return await method.apply(this, args);
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isSessionValid() {
|
|
132
|
+
return (
|
|
133
|
+
this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setUserContext(userContext) {
|
|
138
|
+
this.userContext = userContext;
|
|
139
|
+
this._storeData('feedbackSDK_userContext', userContext);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getUserContext() {
|
|
143
|
+
return this.userContext;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
clearSession() {
|
|
147
|
+
this.sessionToken = null;
|
|
148
|
+
this.sessionExpiry = null;
|
|
149
|
+
this._removeData('feedbackSDK_session');
|
|
150
|
+
this._removeData('feedbackSDK_userContext');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_storeSession() {
|
|
154
|
+
if (typeof localStorage === 'undefined') return;
|
|
155
|
+
try {
|
|
156
|
+
const sessionData = {
|
|
157
|
+
token: this.sessionToken,
|
|
158
|
+
expiry: this.sessionExpiry.toISOString(),
|
|
159
|
+
workspace: this.workspace,
|
|
160
|
+
};
|
|
161
|
+
this._storeData('feedbackSDK_session', sessionData);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// Silent fail
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_loadStoredSession() {
|
|
168
|
+
if (typeof localStorage === 'undefined') return false;
|
|
169
|
+
try {
|
|
170
|
+
const stored = localStorage.getItem('feedbackSDK_session');
|
|
171
|
+
if (!stored) return false;
|
|
172
|
+
|
|
173
|
+
const sessionData = JSON.parse(stored);
|
|
174
|
+
this.sessionToken = sessionData.token;
|
|
175
|
+
this.sessionExpiry = new Date(sessionData.expiry);
|
|
176
|
+
|
|
177
|
+
return this.isSessionValid();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_storeData(key, value) {
|
|
184
|
+
if (typeof localStorage !== 'undefined') {
|
|
185
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_removeData(key) {
|
|
190
|
+
if (typeof localStorage !== 'undefined') {
|
|
191
|
+
localStorage.removeItem(key);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async _makeRequest(endpoint, options = {}) {
|
|
196
|
+
const url = `${this.baseURL}${endpoint}`;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const response = await fetch(url, options);
|
|
200
|
+
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
203
|
+
let responseData = null;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
responseData = await response.json();
|
|
207
|
+
errorMessage =
|
|
208
|
+
responseData.message || responseData.error || errorMessage;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
errorMessage = (await response.text()) || errorMessage;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw new APIError(response.status, errorMessage, responseData);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const contentType = response.headers.get('content-type');
|
|
217
|
+
if (contentType && contentType.includes('application/json')) {
|
|
218
|
+
return await response.json();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return await response.text();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof APIError) throw error;
|
|
224
|
+
throw new APIError(0, error.message, null);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_buildQueryParams(params) {
|
|
229
|
+
const queryParams = new URLSearchParams();
|
|
230
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
231
|
+
if (value !== undefined && value !== null) {
|
|
232
|
+
queryParams.append(
|
|
233
|
+
key,
|
|
234
|
+
typeof value === 'object' ? JSON.stringify(value) : value
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return queryParams.toString();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_getEndpointWithParams(endpoint, params) {
|
|
242
|
+
const queryString = this._buildQueryParams(params);
|
|
243
|
+
return `${endpoint}${queryString ? '?' + queryString : ''}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/core/FeedbackSDK.js
CHANGED
|
@@ -30,7 +30,6 @@ export class FeedbackSDK {
|
|
|
30
30
|
try {
|
|
31
31
|
const initData = await this.apiService.init(this.config.userContext);
|
|
32
32
|
|
|
33
|
-
// Merge backend config as base, local config overrides
|
|
34
33
|
if (initData.config) {
|
|
35
34
|
this.config = deepMerge(initData.config, this.config);
|
|
36
35
|
}
|
|
@@ -83,13 +82,6 @@ export class FeedbackSDK {
|
|
|
83
82
|
return this.widgets.get(id);
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
/**
|
|
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
85
|
async getActiveSurveys(context = {}) {
|
|
94
86
|
if (!this.initialized) {
|
|
95
87
|
throw new SDKError(
|
|
@@ -109,17 +101,6 @@ export class FeedbackSDK {
|
|
|
109
101
|
}
|
|
110
102
|
}
|
|
111
103
|
|
|
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
104
|
async showSurveyById(surveyId, options = {}) {
|
|
124
105
|
if (!this.initialized) {
|
|
125
106
|
throw new SDKError(
|
|
@@ -127,7 +108,6 @@ export class FeedbackSDK {
|
|
|
127
108
|
);
|
|
128
109
|
}
|
|
129
110
|
|
|
130
|
-
// Fetch active surveys to find the one with matching ID
|
|
131
111
|
const surveys = await this.getActiveSurveys();
|
|
132
112
|
const surveyConfig = surveys.find((s) => s.id === surveyId);
|
|
133
113
|
|
|
@@ -149,22 +129,6 @@ export class FeedbackSDK {
|
|
|
149
129
|
});
|
|
150
130
|
}
|
|
151
131
|
|
|
152
|
-
/**
|
|
153
|
-
* Show a survey widget (local/manual mode)
|
|
154
|
-
* For backend-driven surveys, use showSurveyById() instead
|
|
155
|
-
* @param {Object} options - Survey options
|
|
156
|
-
* @param {string} options.surveyId - Backend survey ID (for API tracking)
|
|
157
|
-
* @param {string} options.surveyType - Type of survey: 'nps', 'csat', 'ces', 'custom'
|
|
158
|
-
* @param {string} options.position - Position: 'bottom-right', 'bottom-left', 'center', 'bottom'
|
|
159
|
-
* @param {string} options.theme - Theme: 'light', 'dark'
|
|
160
|
-
* @param {string} options.title - Custom title
|
|
161
|
-
* @param {string} options.description - Custom description
|
|
162
|
-
* @param {string} options.lowLabel - Low end label
|
|
163
|
-
* @param {string} options.highLabel - High end label
|
|
164
|
-
* @param {Function} options.onSubmit - Callback when survey is submitted
|
|
165
|
-
* @param {Function} options.onDismiss - Callback when survey is dismissed
|
|
166
|
-
* @returns {SurveyWidget} The survey widget instance
|
|
167
|
-
*/
|
|
168
132
|
showSurvey(options = {}) {
|
|
169
133
|
if (!this.initialized) {
|
|
170
134
|
throw new SDKError(
|
|
@@ -192,20 +156,6 @@ export class FeedbackSDK {
|
|
|
192
156
|
return surveyWidget;
|
|
193
157
|
}
|
|
194
158
|
|
|
195
|
-
/**
|
|
196
|
-
* Show a changelog widget with sidebar
|
|
197
|
-
* @param {Object} options - Changelog widget options
|
|
198
|
-
* @param {string} options.position - Position: 'bottom-right', 'bottom-left', 'top-right', 'top-left'
|
|
199
|
-
* @param {string} options.theme - Theme: 'light', 'dark'
|
|
200
|
-
* @param {string} options.title - Sidebar title
|
|
201
|
-
* @param {string} options.triggerText - Text on the trigger button
|
|
202
|
-
* @param {boolean} options.showBadge - Show notification badge
|
|
203
|
-
* @param {string} options.viewButtonText - Text for the view update button
|
|
204
|
-
* @param {string} options.changelogBaseUrl - Base URL for changelog links
|
|
205
|
-
* @param {boolean} options.openInNewTab - Open changelog links in new tab (default: true)
|
|
206
|
-
* @param {Function} options.onViewUpdate - Callback when user clicks view update
|
|
207
|
-
* @returns {ChangelogWidget} The changelog widget instance
|
|
208
|
-
*/
|
|
209
159
|
showChangelog(options = {}) {
|
|
210
160
|
if (!this.initialized) {
|
|
211
161
|
throw new SDKError(
|
|
@@ -231,13 +181,6 @@ export class FeedbackSDK {
|
|
|
231
181
|
return changelogWidget;
|
|
232
182
|
}
|
|
233
183
|
|
|
234
|
-
/**
|
|
235
|
-
* Get changelogs from the backend
|
|
236
|
-
* @param {Object} options - Optional query parameters
|
|
237
|
-
* @param {number} options.limit - Number of changelogs to fetch
|
|
238
|
-
* @param {number} options.offset - Offset for pagination
|
|
239
|
-
* @returns {Promise<Array>} Array of changelog entries
|
|
240
|
-
*/
|
|
241
184
|
async getChangelogs(options = {}) {
|
|
242
185
|
if (!this.initialized) {
|
|
243
186
|
throw new SDKError(
|
|
@@ -345,17 +288,10 @@ export class FeedbackSDK {
|
|
|
345
288
|
this.eventBus.emit('sdk:destroyed');
|
|
346
289
|
}
|
|
347
290
|
|
|
348
|
-
/**
|
|
349
|
-
* Check if feedback was recently submitted for this workspace
|
|
350
|
-
* Uses backend tracking (preferred) with localStorage as fallback
|
|
351
|
-
* @param {number} cooldownDays - Days to consider as "recently" (default: 30)
|
|
352
|
-
* @returns {boolean} true if feedback was submitted within the cooldown period
|
|
353
|
-
*/
|
|
354
291
|
hasFeedbackBeenSubmitted(cooldownDays = 30) {
|
|
355
292
|
const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000;
|
|
356
293
|
const now = Date.now();
|
|
357
294
|
|
|
358
|
-
// Check backend tracking first (from init response)
|
|
359
295
|
if (this.config.last_feedback_at) {
|
|
360
296
|
try {
|
|
361
297
|
const backendTimestamp = new Date(
|
|
@@ -369,7 +305,6 @@ export class FeedbackSDK {
|
|
|
369
305
|
}
|
|
370
306
|
}
|
|
371
307
|
|
|
372
|
-
// Fallback to localStorage
|
|
373
308
|
try {
|
|
374
309
|
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
375
310
|
const stored = localStorage.getItem(storageKey);
|
|
@@ -382,9 +317,6 @@ export class FeedbackSDK {
|
|
|
382
317
|
}
|
|
383
318
|
}
|
|
384
319
|
|
|
385
|
-
/**
|
|
386
|
-
* Clear the feedback submission tracking (allow showing widgets again)
|
|
387
|
-
*/
|
|
388
320
|
clearFeedbackSubmissionTracking() {
|
|
389
321
|
try {
|
|
390
322
|
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const baseStyles = `
|
|
2
|
+
.feedback-widget {
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
4
|
+
font-size: 14px;
|
|
5
|
+
line-height: 1.4;
|
|
6
|
+
z-index: 999999;
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.feedback-widget *,
|
|
11
|
+
.feedback-widget *::before,
|
|
12
|
+
.feedback-widget *::after {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Animations */
|
|
17
|
+
@keyframes fadeIn {
|
|
18
|
+
from { opacity: 0; }
|
|
19
|
+
to { opacity: 1; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@keyframes slideInRight {
|
|
23
|
+
from {
|
|
24
|
+
transform: translateX(400px);
|
|
25
|
+
opacity: 0;
|
|
26
|
+
}
|
|
27
|
+
to {
|
|
28
|
+
transform: translateX(0);
|
|
29
|
+
opacity: 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes confettiFall {
|
|
34
|
+
0% {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
transform: translateY(0) rotate(0deg) scale(1);
|
|
37
|
+
}
|
|
38
|
+
10% {
|
|
39
|
+
opacity: 1;
|
|
40
|
+
}
|
|
41
|
+
100% {
|
|
42
|
+
opacity: 0;
|
|
43
|
+
transform: translateY(100vh) rotate(720deg) scale(0.5);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@keyframes changelogSpin {
|
|
48
|
+
to {
|
|
49
|
+
transform: rotate(360deg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Accessibility */
|
|
54
|
+
@media (prefers-reduced-motion: reduce) {
|
|
55
|
+
* {
|
|
56
|
+
transition: none !important;
|
|
57
|
+
animation: none !important;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Print */
|
|
62
|
+
@media print {
|
|
63
|
+
.feedback-widget,
|
|
64
|
+
.feedback-panel,
|
|
65
|
+
.feedback-panel-backdrop,
|
|
66
|
+
.feedback-success-notification,
|
|
67
|
+
.changelog-widget,
|
|
68
|
+
.changelog-modal,
|
|
69
|
+
.changelog-modal-backdrop,
|
|
70
|
+
.changelog-list-modal,
|
|
71
|
+
.changelog-list-modal-backdrop {
|
|
72
|
+
display: none !important;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`;
|