@product7/feedback-sdk 1.0.1
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 +227 -0
- package/dist/README.md +227 -0
- package/dist/feedback-sdk.js +2483 -0
- package/dist/feedback-sdk.js.map +1 -0
- package/dist/feedback-sdk.min.js +2 -0
- package/dist/feedback-sdk.min.js.map +1 -0
- package/package.json +111 -0
- package/src/core/APIService.js +338 -0
- package/src/core/EventBus.js +54 -0
- package/src/core/FeedbackSDK.js +285 -0
- package/src/docs/api.md +686 -0
- package/src/docs/example.md +823 -0
- package/src/docs/installation.md +264 -0
- package/src/index.js +281 -0
- package/src/styles/styles.js +557 -0
- package/src/types/index.d.ts +12 -0
- package/src/utils/errors.js +142 -0
- package/src/utils/helpers.js +219 -0
- package/src/widgets/BaseWidget.js +334 -0
- package/src/widgets/ButtonWidget.js +62 -0
- package/src/widgets/InlineWidget.js +145 -0
- package/src/widgets/TabWidget.js +65 -0
- package/src/widgets/WidgetFactory.js +64 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { APIError } from '../utils/errors.js';
|
|
2
|
+
|
|
3
|
+
export class APIService {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.workspace = config.workspace;
|
|
6
|
+
this.sessionToken = null;
|
|
7
|
+
this.sessionExpiry = null;
|
|
8
|
+
this.userContext = config.userContext || null;
|
|
9
|
+
|
|
10
|
+
// Construct workspace-specific API URL
|
|
11
|
+
if (config.apiUrl) {
|
|
12
|
+
// If explicitly provided, use it
|
|
13
|
+
this.baseURL = config.apiUrl;
|
|
14
|
+
} else if (this.workspace) {
|
|
15
|
+
// Construct from workspace: workspace.api.staging.product7.io/api/v1
|
|
16
|
+
this.baseURL = `https://${this.workspace}.api.staging.product7.io/api/v1`;
|
|
17
|
+
} else {
|
|
18
|
+
// Fallback to default
|
|
19
|
+
this.baseURL = 'https://api.staging.product7.io/api/v1';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log('[APIService] Using API URL:', this.baseURL);
|
|
23
|
+
|
|
24
|
+
// Try to load existing session from localStorage
|
|
25
|
+
this._loadStoredSession();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async init(userContext = null) {
|
|
29
|
+
console.log('[APIService] Starting initialization...');
|
|
30
|
+
|
|
31
|
+
if (userContext) {
|
|
32
|
+
this.userContext = userContext;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if we have a valid session token
|
|
36
|
+
if (this.isSessionValid()) {
|
|
37
|
+
console.log('[APIService] Found valid existing session');
|
|
38
|
+
return { sessionToken: this.sessionToken };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Try to get user context from various sources if not provided
|
|
42
|
+
if (!this.userContext) {
|
|
43
|
+
console.log(
|
|
44
|
+
'[APIService] No user context provided, attempting auto-detection...'
|
|
45
|
+
);
|
|
46
|
+
this.userContext = this._getUserContextFromStorage();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!this.userContext || !this.workspace) {
|
|
50
|
+
const error = `Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`;
|
|
51
|
+
console.error('[APIService]', error);
|
|
52
|
+
throw new APIError(400, error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('[APIService] User context detected:', this.userContext);
|
|
56
|
+
|
|
57
|
+
const payload = {
|
|
58
|
+
workspace: this.workspace,
|
|
59
|
+
user: this.userContext,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
console.log(
|
|
63
|
+
'[APIService] Making init request to:',
|
|
64
|
+
`${this.baseURL}/widget/init`
|
|
65
|
+
);
|
|
66
|
+
console.log('[APIService] Payload:', payload);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await this._makeRequest('/widget/init', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: JSON.stringify(payload),
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log('[APIService] Init response:', response);
|
|
78
|
+
|
|
79
|
+
// Store session token and expiry
|
|
80
|
+
this.sessionToken = response.session_token;
|
|
81
|
+
this.sessionExpiry = new Date(Date.now() + response.expires_in * 1000);
|
|
82
|
+
|
|
83
|
+
// Store session in localStorage for persistence
|
|
84
|
+
this._storeSession();
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
sessionToken: this.sessionToken,
|
|
88
|
+
config: response.config || {},
|
|
89
|
+
expiresIn: response.expires_in,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('[APIService] Init failed:', error);
|
|
93
|
+
throw new APIError(
|
|
94
|
+
error.status || 500,
|
|
95
|
+
`Failed to initialize widget: ${error.message}`,
|
|
96
|
+
error.response
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async submitFeedback(feedbackData) {
|
|
102
|
+
// Ensure we have a valid session
|
|
103
|
+
if (!this.isSessionValid()) {
|
|
104
|
+
await this.init();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!this.sessionToken) {
|
|
108
|
+
throw new APIError(401, 'No valid session token available');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const payload = {
|
|
112
|
+
board:
|
|
113
|
+
feedbackData.board_id || feedbackData.board || feedbackData.boardId,
|
|
114
|
+
title: feedbackData.title,
|
|
115
|
+
content: feedbackData.content,
|
|
116
|
+
attachments: feedbackData.attachments || [],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await this._makeRequest('/widget/feedback', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: JSON.stringify(payload),
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return response;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// If session expired, try to reinitialize once
|
|
132
|
+
if (error.status === 401) {
|
|
133
|
+
this.sessionToken = null;
|
|
134
|
+
this.sessionExpiry = null;
|
|
135
|
+
await this.init();
|
|
136
|
+
|
|
137
|
+
// Retry the request with new session
|
|
138
|
+
return this.submitFeedback(feedbackData);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new APIError(
|
|
142
|
+
error.status || 500,
|
|
143
|
+
`Failed to submit feedback: ${error.message}`,
|
|
144
|
+
error.response
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isSessionValid() {
|
|
150
|
+
return (
|
|
151
|
+
this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setUserContext(userContext) {
|
|
156
|
+
this.userContext = userContext;
|
|
157
|
+
// Store in localStorage for persistence
|
|
158
|
+
if (typeof localStorage !== 'undefined') {
|
|
159
|
+
localStorage.setItem(
|
|
160
|
+
'feedbackSDK_userContext',
|
|
161
|
+
JSON.stringify(userContext)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getUserContext() {
|
|
167
|
+
return this.userContext;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
clearSession() {
|
|
171
|
+
this.sessionToken = null;
|
|
172
|
+
this.sessionExpiry = null;
|
|
173
|
+
if (typeof localStorage !== 'undefined') {
|
|
174
|
+
localStorage.removeItem('feedbackSDK_session');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_getUserContextFromStorage() {
|
|
179
|
+
if (typeof localStorage === 'undefined') return null;
|
|
180
|
+
|
|
181
|
+
console.log('[APIService] Attempting to detect user from storage...');
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Try to get from feedbackSDK specific storage first
|
|
185
|
+
const stored = localStorage.getItem('feedbackSDK_userContext');
|
|
186
|
+
if (stored) {
|
|
187
|
+
console.log('[APIService] Found user context in feedbackSDK storage');
|
|
188
|
+
return JSON.parse(stored);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Try to get from window object (if set by parent application)
|
|
192
|
+
if (typeof window !== 'undefined' && window.FeedbackSDKUserContext) {
|
|
193
|
+
console.log(
|
|
194
|
+
'[APIService] Found user context in window.FeedbackSDKUserContext'
|
|
195
|
+
);
|
|
196
|
+
return window.FeedbackSDKUserContext;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check window.currentUser
|
|
200
|
+
if (typeof window !== 'undefined' && window.currentUser) {
|
|
201
|
+
console.log('[APIService] Found user context in window.currentUser');
|
|
202
|
+
return this._mapUserData(window.currentUser);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Try to extract from existing session storage
|
|
206
|
+
const authSources = ['auth', 'currentUser', 'userSession', 'user'];
|
|
207
|
+
|
|
208
|
+
for (const key of authSources) {
|
|
209
|
+
const sessionData = localStorage.getItem(key);
|
|
210
|
+
if (sessionData) {
|
|
211
|
+
console.log(`[APIService] Found data in localStorage.${key}`);
|
|
212
|
+
const parsed = JSON.parse(sessionData);
|
|
213
|
+
|
|
214
|
+
// Handle nested user data (like {user: {...}, token: ...})
|
|
215
|
+
const userData = parsed.user || parsed;
|
|
216
|
+
|
|
217
|
+
if (userData && (userData.id || userData.user_id || userData.email)) {
|
|
218
|
+
console.log('[APIService] Successfully mapped user data');
|
|
219
|
+
return this._mapUserData(userData);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('[APIService] No user context found in any storage location');
|
|
225
|
+
return null;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.warn(
|
|
228
|
+
'[FeedbackSDK] Failed to load user context from storage:',
|
|
229
|
+
error
|
|
230
|
+
);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_mapUserData(userData) {
|
|
236
|
+
console.log('[APIService] Mapping user data:', userData);
|
|
237
|
+
|
|
238
|
+
const mapped = {
|
|
239
|
+
user_id: userData.id || userData.user_id || userData.userId,
|
|
240
|
+
email: userData.email,
|
|
241
|
+
name: userData.name || userData.displayName || userData.full_name,
|
|
242
|
+
custom_fields: {},
|
|
243
|
+
company: {},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Map company data if available
|
|
247
|
+
if (userData.company || userData.organization) {
|
|
248
|
+
const company = userData.company || userData.organization;
|
|
249
|
+
mapped.company = {
|
|
250
|
+
id: company.id || company.company_id,
|
|
251
|
+
name: company.name || company.company_name,
|
|
252
|
+
monthly_spend: company.monthly_spend || company.spend,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Map any additional custom fields
|
|
257
|
+
const customFieldKeys = ['plan', 'role', 'tier', 'subscription'];
|
|
258
|
+
customFieldKeys.forEach((key) => {
|
|
259
|
+
if (userData[key]) {
|
|
260
|
+
mapped.custom_fields[key] = userData[key];
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
console.log('[APIService] Mapped result:', mapped);
|
|
265
|
+
return mapped;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_storeSession() {
|
|
269
|
+
if (typeof localStorage === 'undefined') return;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const sessionData = {
|
|
273
|
+
token: this.sessionToken,
|
|
274
|
+
expiry: this.sessionExpiry.toISOString(),
|
|
275
|
+
workspace: this.workspace,
|
|
276
|
+
};
|
|
277
|
+
localStorage.setItem('feedbackSDK_session', JSON.stringify(sessionData));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.warn('[FeedbackSDK] Failed to store session:', error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
_loadStoredSession() {
|
|
284
|
+
if (typeof localStorage === 'undefined') return false;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const stored = localStorage.getItem('feedbackSDK_session');
|
|
288
|
+
if (!stored) return false;
|
|
289
|
+
|
|
290
|
+
const sessionData = JSON.parse(stored);
|
|
291
|
+
this.sessionToken = sessionData.token;
|
|
292
|
+
this.sessionExpiry = new Date(sessionData.expiry);
|
|
293
|
+
|
|
294
|
+
return this.isSessionValid();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.warn('[FeedbackSDK] Failed to load stored session:', error);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async _makeRequest(endpoint, options = {}) {
|
|
302
|
+
const url = `${this.baseURL}${endpoint}`;
|
|
303
|
+
console.log('[APIService] Making request to:', url);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch(url, options);
|
|
307
|
+
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
310
|
+
let responseData = null;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
responseData = await response.json();
|
|
314
|
+
errorMessage =
|
|
315
|
+
responseData.message || responseData.error || errorMessage;
|
|
316
|
+
} catch (e) {
|
|
317
|
+
errorMessage = (await response.text()) || errorMessage;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw new APIError(response.status, errorMessage, responseData);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const contentType = response.headers.get('content-type');
|
|
324
|
+
if (contentType && contentType.includes('application/json')) {
|
|
325
|
+
return await response.json();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return await response.text();
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error instanceof APIError) {
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Network or other errors
|
|
335
|
+
throw new APIError(0, error.message, null);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class EventBus {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.events = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
on(event, callback) {
|
|
7
|
+
if (!this.events.has(event)) {
|
|
8
|
+
this.events.set(event, []);
|
|
9
|
+
}
|
|
10
|
+
this.events.get(event).push(callback);
|
|
11
|
+
|
|
12
|
+
return () => this.off(event, callback);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
off(event, callback) {
|
|
16
|
+
const callbacks = this.events.get(event);
|
|
17
|
+
if (callbacks) {
|
|
18
|
+
const index = callbacks.indexOf(callback);
|
|
19
|
+
if (index > -1) {
|
|
20
|
+
callbacks.splice(index, 1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
emit(event, data) {
|
|
26
|
+
const callbacks = this.events.get(event);
|
|
27
|
+
if (callbacks) {
|
|
28
|
+
callbacks.forEach((callback) => {
|
|
29
|
+
try {
|
|
30
|
+
callback(data);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('[FeedbackSDK] Event callback error:', error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
once(event, callback) {
|
|
39
|
+
const unsubscribe = this.on(event, (data) => {
|
|
40
|
+
callback(data);
|
|
41
|
+
unsubscribe();
|
|
42
|
+
});
|
|
43
|
+
return unsubscribe;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clear() {
|
|
47
|
+
this.events.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getListenerCount(event) {
|
|
51
|
+
const callbacks = this.events.get(event);
|
|
52
|
+
return callbacks ? callbacks.length : 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
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 FeedbackSDK {
|
|
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
|
+
// Initialize API service
|
|
15
|
+
this.apiService = new APIService({
|
|
16
|
+
apiUrl: this.config.apiUrl,
|
|
17
|
+
workspace: this.config.workspace,
|
|
18
|
+
userContext: this.config.userContext,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
this._bindMethods();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async init() {
|
|
25
|
+
if (this.initialized) {
|
|
26
|
+
return { alreadyInitialized: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Initialize the API service (this will handle the /widget/init call)
|
|
31
|
+
const initData = await this.apiService.init(this.config.userContext);
|
|
32
|
+
|
|
33
|
+
// Merge any server-provided config with local config
|
|
34
|
+
if (initData.config) {
|
|
35
|
+
this.config = deepMerge(this.config, initData.config);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.initialized = true;
|
|
39
|
+
this.eventBus.emit('sdk:initialized', {
|
|
40
|
+
config: this.config,
|
|
41
|
+
sessionToken: initData.sessionToken,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
initialized: true,
|
|
46
|
+
config: initData.config || {},
|
|
47
|
+
sessionToken: initData.sessionToken,
|
|
48
|
+
expiresIn: initData.expiresIn,
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.eventBus.emit('sdk:error', { error });
|
|
52
|
+
throw new SDKError(`Failed to initialize SDK: ${error.message}`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createWidget(type = 'button', options = {}) {
|
|
57
|
+
if (!this.initialized) {
|
|
58
|
+
throw new SDKError(
|
|
59
|
+
'SDK must be initialized before creating widgets. Call init() first.'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const widgetId = generateId('widget');
|
|
64
|
+
const widgetOptions = {
|
|
65
|
+
id: widgetId,
|
|
66
|
+
sdk: this,
|
|
67
|
+
apiService: this.apiService,
|
|
68
|
+
...this.config,
|
|
69
|
+
...options,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const widget = WidgetFactory.create(type, widgetOptions);
|
|
74
|
+
this.widgets.set(widgetId, widget);
|
|
75
|
+
|
|
76
|
+
this.eventBus.emit('widget:created', { widget, type });
|
|
77
|
+
return widget;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new SDKError(`Failed to create widget: ${error.message}`, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getWidget(id) {
|
|
84
|
+
return this.widgets.get(id);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getAllWidgets() {
|
|
88
|
+
return Array.from(this.widgets.values());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
destroyWidget(id) {
|
|
92
|
+
const widget = this.widgets.get(id);
|
|
93
|
+
if (widget) {
|
|
94
|
+
widget.destroy();
|
|
95
|
+
this.widgets.delete(id);
|
|
96
|
+
this.eventBus.emit('widget:removed', { widgetId: id });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
destroyAllWidgets() {
|
|
103
|
+
for (const widget of this.widgets.values()) {
|
|
104
|
+
widget.destroy();
|
|
105
|
+
}
|
|
106
|
+
this.widgets.clear();
|
|
107
|
+
this.eventBus.emit('widgets:cleared');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
updateConfig(newConfig) {
|
|
111
|
+
const oldConfig = { ...this.config };
|
|
112
|
+
this.config = this._validateAndMergeConfig(newConfig, this.config);
|
|
113
|
+
|
|
114
|
+
// Update all existing widgets with new config
|
|
115
|
+
for (const widget of this.widgets.values()) {
|
|
116
|
+
widget.handleConfigUpdate(this.config);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.eventBus.emit('config:updated', {
|
|
120
|
+
oldConfig,
|
|
121
|
+
newConfig: this.config,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setUserContext(userContext) {
|
|
126
|
+
this.config.userContext = userContext;
|
|
127
|
+
if (this.apiService) {
|
|
128
|
+
this.apiService.setUserContext(userContext);
|
|
129
|
+
}
|
|
130
|
+
this.eventBus.emit('user:updated', { userContext });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getUserContext() {
|
|
134
|
+
return (
|
|
135
|
+
this.config.userContext ||
|
|
136
|
+
(this.apiService ? this.apiService.getUserContext() : null)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async reinitialize(newUserContext = null) {
|
|
141
|
+
// Clear current session
|
|
142
|
+
this.apiService.clearSession();
|
|
143
|
+
this.initialized = false;
|
|
144
|
+
|
|
145
|
+
// Update user context if provided
|
|
146
|
+
if (newUserContext) {
|
|
147
|
+
this.setUserContext(newUserContext);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Reinitialize
|
|
151
|
+
return this.init();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
on(event, callback) {
|
|
155
|
+
this.eventBus.on(event, callback);
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
off(event, callback) {
|
|
160
|
+
this.eventBus.off(event, callback);
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
once(event, callback) {
|
|
165
|
+
this.eventBus.once(event, callback);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
emit(event, data) {
|
|
170
|
+
this.eventBus.emit(event, data);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
destroy() {
|
|
175
|
+
this.destroyAllWidgets();
|
|
176
|
+
this.eventBus.removeAllListeners();
|
|
177
|
+
this.apiService.clearSession();
|
|
178
|
+
this.initialized = false;
|
|
179
|
+
this.eventBus.emit('sdk:destroyed');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_validateAndMergeConfig(newConfig, existingConfig = {}) {
|
|
183
|
+
const defaultConfig = {
|
|
184
|
+
apiUrl: null,
|
|
185
|
+
workspace: null,
|
|
186
|
+
userContext: null,
|
|
187
|
+
position: 'bottom-right',
|
|
188
|
+
theme: 'light',
|
|
189
|
+
boardId: 'general',
|
|
190
|
+
autoShow: true,
|
|
191
|
+
debug: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const mergedConfig = deepMerge(
|
|
195
|
+
deepMerge(defaultConfig, existingConfig),
|
|
196
|
+
newConfig
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Validate required config
|
|
200
|
+
const requiredFields = ['workspace'];
|
|
201
|
+
const missingFields = requiredFields.filter(
|
|
202
|
+
(field) => !mergedConfig[field]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (missingFields.length > 0) {
|
|
206
|
+
throw new ConfigError(
|
|
207
|
+
`Missing required configuration: ${missingFields.join(', ')}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Validate userContext structure if provided
|
|
212
|
+
if (mergedConfig.userContext) {
|
|
213
|
+
this._validateUserContext(mergedConfig.userContext);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return mergedConfig;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_validateUserContext(userContext) {
|
|
220
|
+
if (!userContext.user_id && !userContext.email) {
|
|
221
|
+
throw new ConfigError(
|
|
222
|
+
'User context must include at least user_id or email'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Validate structure matches expected API format
|
|
227
|
+
const validStructure = {
|
|
228
|
+
user_id: 'string',
|
|
229
|
+
email: 'string',
|
|
230
|
+
name: 'string',
|
|
231
|
+
custom_fields: 'object',
|
|
232
|
+
company: 'object',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const [key, expectedType] of Object.entries(validStructure)) {
|
|
236
|
+
if (userContext[key] && typeof userContext[key] !== expectedType) {
|
|
237
|
+
throw new ConfigError(
|
|
238
|
+
`User context field '${key}' must be of type '${expectedType}'`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_bindMethods() {
|
|
245
|
+
this.createWidget = this.createWidget.bind(this);
|
|
246
|
+
this.destroyWidget = this.destroyWidget.bind(this);
|
|
247
|
+
this.updateConfig = this.updateConfig.bind(this);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Static helper methods
|
|
251
|
+
static create(config) {
|
|
252
|
+
return new FeedbackSDK(config);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static async createAndInit(config) {
|
|
256
|
+
const sdk = new FeedbackSDK(config);
|
|
257
|
+
await sdk.init();
|
|
258
|
+
return sdk;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Utility methods for external integrations
|
|
262
|
+
static extractUserContextFromAuth(authData) {
|
|
263
|
+
// Helper method to extract user context from common auth structures
|
|
264
|
+
if (!authData) return null;
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
user_id: authData.sub || authData.id || authData.user_id,
|
|
268
|
+
email: authData.email,
|
|
269
|
+
name: authData.name || authData.display_name || authData.full_name,
|
|
270
|
+
custom_fields: {
|
|
271
|
+
role: authData.role,
|
|
272
|
+
plan: authData.plan || authData.subscription?.plan,
|
|
273
|
+
...(authData.custom_fields || {}),
|
|
274
|
+
},
|
|
275
|
+
company:
|
|
276
|
+
authData.company || authData.organization
|
|
277
|
+
? {
|
|
278
|
+
id: authData.company?.id || authData.organization?.id,
|
|
279
|
+
name: authData.company?.name || authData.organization?.name,
|
|
280
|
+
monthly_spend: authData.company?.monthly_spend,
|
|
281
|
+
}
|
|
282
|
+
: undefined,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|